UNPKG

@babylonjs/viewer

Version:

The Babylon Viewer aims to simplify a specific but common Babylon.js use case: loading, viewing, and interacting with a 3D model.

455 lines (433 loc) 16.8 kB
import { p as U8, o as TU, B as BU, F as F32, t as targetSignatureKey, n as SS, b7 as CW, g as getSceneBindGroupLayout, b8 as getRenderTargetSize, b9 as getViewMatrix, ba as getProjectionMatrix, bA as getCameraPosition } from './index-DMbDahsc.esm.js'; import { r as registerPickSource, d as disposeGaussianSplattingMesh, a as applyGsFragments, u as uploadPendingSplatOrder, p as postSplatSortIfDirty } from './gltf-feature-gaussian-splatting-BiH_nkp6.esm.js'; const SH_TEXTURE_COUNT = [0, 1, 2, 3, 5]; let _cache = null; function buildShShaderSource(shDegree) { const shVectorCount = (shDegree + 1) * (shDegree + 1) - 1; const shCoefficientCount = shVectorCount * 3; const textureCount = Math.ceil(shCoefficientCount / 16); let textureBindings = ""; for (let i = 0; i < textureCount; i++) { textureBindings += `@group(1) @binding(${6 + i}) var shTexture${i}: texture_2d<u32>; `; } let textureLoads = ""; for (let i = 0; i < textureCount; i++) { textureLoads += ` let sh${i}_u32 = textureLoad(shTexture${i}, splatUVi32, 0); `; } let shUnpack = ` var sh: array<vec3<f32>, ${shVectorCount + 1}>; sh[0] = vec3<f32>(0.0); `; const byteRef = (j) => { const tex = j / 16 | 0; const u32Idx = j % 16 / 4 | 0; const byteIdx = j % 4; const u32Field = ["x", "y", "z", "w"][u32Idx]; const byteField = ["x", "y", "z", "w"][byteIdx]; return `decompose(sh${tex}_u32.${u32Field}).${byteField}`; }; for (let k = 0; k < shVectorCount; k++) { const j = k * 3; shUnpack += ` sh[${k + 1}] = vec3<f32>(${byteRef(j)}, ${byteRef(j + 1)}, ${byteRef(j + 2)}); `; } let shPoly = " result = sh[0];\n"; if (shDegree >= 1) { shPoly += ` result += -SH_C1 * y * sh[1] + SH_C1 * z * sh[2] - SH_C1 * x * sh[3]; `; } if (shDegree >= 2) { shPoly += ` result += SH_C2[0] * xy * sh[4] + SH_C2[1] * yz * sh[5] + SH_C2[2] * (2.0 * zz - xx - yy) * sh[6] + SH_C2[3] * xz * sh[7] + SH_C2[4] * (xx - yy) * sh[8]; `; } if (shDegree >= 3) { shPoly += ` result += SH_C3[0] * y * (3.0 * xx - yy) * sh[9] + SH_C3[1] * xy * z * sh[10] + SH_C3[2] * y * (4.0 * zz - xx - yy) * sh[11] + SH_C3[3] * z * (2.0 * zz - 3.0 * xx - 3.0 * yy) * sh[12] + SH_C3[4] * x * (4.0 * zz - xx - yy) * sh[13] + SH_C3[5] * z * (xx - yy) * sh[14] + SH_C3[6] * x * (xx - 3.0 * yy) * sh[15]; `; } if (shDegree >= 4) { shPoly += ` result += SH_C4[0] * x * y * (xx - yy) * sh[16] + SH_C4[1] * y * z * (3.0 * xx - yy) * sh[17] + SH_C4[2] * x * y * (7.0 * zz - 1.0) * sh[18] + SH_C4[3] * y * z * (7.0 * zz - 3.0) * sh[19] + SH_C4[4] * (zz * (35.0 * zz - 30.0) + 3.0) * sh[20] + SH_C4[5] * x * z * (7.0 * zz - 3.0) * sh[21] + SH_C4[6] * (xx - yy) * (7.0 * zz - 1.0) * sh[22] + SH_C4[7] * x * z * (xx - 3.0 * yy) * sh[23] + SH_C4[8] * (xx * (xx - 3.0 * yy) - yy * (3.0 * xx - yy)) * sh[24]; `; } let constantsBlock = `const SH_C1: f32 = 0.48860251; `; if (shDegree >= 2) { constantsBlock += `const SH_C2: array<f32, 5> = array<f32, 5>(1.092548430, -1.09254843, 0.315391565, -1.09254843, 0.546274215); `; } if (shDegree >= 3) { constantsBlock += `const SH_C3: array<f32, 7> = array<f32, 7>(-0.59004358, 2.890611442, -0.45704579, 0.373176332, -0.45704579, 1.445305721, -0.59004358); `; } if (shDegree >= 4) { constantsBlock += `const SH_C4: array<f32, 9> = array<f32, 9>(2.5033429418, -1.7701307698, 0.9461746958, -0.6690465436, 0.1057855469, -0.6690465436, 0.4730873479, -1.7701307698, 0.6258357354); `; } return `// Gaussian Splatting — vertex + fragment WGSL (SH degree ${shDegree}). // Generated by buildShShaderSource. Mirrors BJS gaussianSplatting.vertex.fx + // gaussianSplatting.fx (SH_DEGREE = ${shDegree}, no compound parts). struct U { world: mat4x4<f32>, view: mat4x4<f32>, projection: mat4x4<f32>, viewport: vec2<f32>, focal: vec2<f32>, dataSize: vec2<f32>, alpha: f32, _pad0: f32, eyePosition: vec3<f32>, _pad1: f32, }; @group(1) @binding(0) var<uniform> u: U; @group(1) @binding(1) var samp: sampler; @group(1) @binding(2) var centersTex: texture_2d<f32>; @group(1) @binding(3) var covATex: texture_2d<f32>; @group(1) @binding(4) var covBTex: texture_2d<f32>; @group(1) @binding(5) var colorsTex: texture_2d<f32>; ${textureBindings} struct VOut { @builtin(position) pos: vec4<f32>, @location(0) vColor: vec4<f32>, @location(1) vPos: vec2<f32>, }; ${constantsBlock} fn dataUv(idx: f32) -> vec2<f32> { let y = floor(idx / u.dataSize.x); let x = idx - y * u.dataSize.x; return vec2<f32>((x + 0.5) / u.dataSize.x, (y + 0.5) / u.dataSize.y); } fn dataUvI(idx: f32) -> vec2<i32> { let y = floor(idx / u.dataSize.x); let x = idx - y * u.dataSize.x; return vec2<i32>(i32(x), i32(y)); } // Unpack a u32 of 4 packed bytes into (b0 b1 b2 b3) * 2/255 - 1. fn decompose(value: u32) -> vec4<f32> { let v = vec4<f32>( f32((value >> 0u) & 255u), f32((value >> 8u) & 255u), f32((value >> 16u) & 255u), f32((value >> 24u) & 255u)); return v * vec4<f32>(2.0 / 255.0) - vec4<f32>(1.0); } fn inverseMat3(m: mat3x3<f32>) -> mat3x3<f32> { let a00 = m[0][0]; let a01 = m[0][1]; let a02 = m[0][2]; let a10 = m[1][0]; let a11 = m[1][1]; let a12 = m[1][2]; let a20 = m[2][0]; let a21 = m[2][1]; let a22 = m[2][2]; let b01 = a22 * a11 - a12 * a21; let b11 = -a22 * a10 + a12 * a20; let b21 = a21 * a10 - a11 * a20; let det = a00 * b01 + a01 * b11 + a02 * b21; return mat3x3<f32>( vec3<f32>(b01 / det, (-a22 * a01 + a02 * a21) / det, (a12 * a01 - a02 * a11) / det), vec3<f32>(b11 / det, (a22 * a00 - a02 * a20) / det, (-a12 * a00 + a02 * a10) / det), vec3<f32>(b21 / det, (-a21 * a00 + a01 * a20) / det, (a11 * a00 - a01 * a10) / det)); } fn computeSH(dir: vec3<f32>, splatUVi32: vec2<i32>) -> vec3<f32> { ${textureLoads}${shUnpack} let x = dir.x; let y = dir.y; let z = dir.z; let xx = x * x; let yy = y * y; let zz = z * z; let xy = x * y; let yz = y * z; let xz = x * z; var result: vec3<f32>; ${shPoly} return result; } @vertex fn vs(@location(0) corner: vec2<f32>, @location(1) splatIndex: f32) -> VOut { var out: VOut; let uv = dataUv(splatIndex); let splatUVi32 = dataUvI(splatIndex); let center = textureSampleLevel(centersTex, samp, uv, 0.0).xyz; let color = textureSampleLevel(colorsTex, samp, uv, 0.0); let covA = textureSampleLevel(covATex, samp, uv, 0.0).xyz; let covB = textureSampleLevel(covBTex, samp, uv, 0.0).xyz; let worldPos = u.world * vec4<f32>(center, 1.0); let modelView = u.view * u.world; let camspace = u.view * worldPos; let pos2d = u.projection * camspace; let bounds = 1.2 * pos2d.w; if (pos2d.z < 0.0 || pos2d.x < -bounds || pos2d.x > bounds || pos2d.y < -bounds || pos2d.y > bounds) { out.pos = vec4<f32>(0.0, 0.0, 2.0, 1.0); out.vColor = vec4<f32>(0.0); out.vPos = vec2<f32>(0.0); return out; } // ── View-dependent SH evaluation ─────────────────────────────────── let worldRot = mat3x3<f32>(u.world[0].xyz, u.world[1].xyz, u.world[2].xyz); let normWorldRot = inverseMat3(worldRot); var dir = normalize(normWorldRot * (worldPos.xyz - u.eyePosition)); // Lite-side Y-flip: compensates for our data-path Y pre-flip vs BJS's // mesh.scaling.y *= -1 (see file header for derivation). dir.y = -dir.y; let shColor = computeSH(dir, splatUVi32); let Vrk = mat3x3<f32>( vec3<f32>(covA.x, covA.y, covA.z), vec3<f32>(covA.y, covB.x, covB.y), vec3<f32>(covA.z, covB.y, covB.z)); let invZ = 1.0 / camspace.z; let invZ2 = invZ * invZ; let J = mat3x3<f32>( vec3<f32>(u.focal.x * invZ, 0.0, -u.focal.x * camspace.x * invZ2), vec3<f32>(0.0, u.focal.y * invZ, -u.focal.y * camspace.y * invZ2), vec3<f32>(0.0, 0.0, 0.0)); let mv3 = mat3x3<f32>(modelView[0].xyz, modelView[1].xyz, modelView[2].xyz); let T = transpose(mv3) * J; var cov2d = transpose(T) * Vrk * T; let kernelSize: f32 = 0.3; cov2d[0][0] += kernelSize; cov2d[1][1] += kernelSize; let mid = (cov2d[0][0] + cov2d[1][1]) * 0.5; let dxy = (cov2d[0][0] - cov2d[1][1]) * 0.5; let radius = length(vec2<f32>(dxy, cov2d[0][1])); let epsilon: f32 = 0.0001; let lambda1 = mid + radius + epsilon; let lambda2 = mid - radius + epsilon; if (lambda2 < 0.0) { out.pos = vec4<f32>(0.0, 0.0, 2.0, 1.0); out.vColor = vec4<f32>(0.0); out.vPos = vec2<f32>(0.0); return out; } let diag = normalize(vec2<f32>(cov2d[0][1], lambda1 - cov2d[0][0])); let majorAxis = min(sqrt(2.0 * lambda1), 1024.0) * diag; let minorAxis = min(sqrt(2.0 * lambda2), 1024.0) * vec2<f32>(diag.y, -diag.x); let vCenter = pos2d.xy; out.pos = vec4<f32>( vCenter + (corner.x * majorAxis + corner.y * minorAxis) * pos2d.w / u.viewport, pos2d.z, pos2d.w); out.vColor = vec4<f32>(color.rgb + shColor, color.a * u.alpha); out.vPos = corner; return out; } /*GS_FRAGMENT_DEFINITIONS*/ @fragment fn fs(in: VOut) -> @location(0) vec4<f32> { /*GS_FRAGMENT_MAIN_BEGIN*/ let A = -dot(in.vPos, in.vPos); if (A < -4.0) { discard; } let B = exp(A) * in.vColor.a; var finalColor = vec4<f32>(in.vColor.rgb, B); /*GS_FRAGMENT_BEFORE_FRAGCOLOR*/ /*GS_FRAGMENT_MAIN_END*/ return finalColor; } `; } function getOrCreateShPipeline(engine, sig, shDegree, fragments) { const device = engine._device; if (!_cache || _cache.device !== device) { _cache = { device, modules: /* @__PURE__ */ new Map(), entries: /* @__PURE__ */ new Map() }; } const fragKey = fragments && fragments.length > 0 ? "|" + fragments.map((f) => f.id).join(",") : ""; let module = _cache.modules.get(shDegree + fragKey); if (!module) { module = device.createShaderModule({ code: fragments && fragments.length > 0 ? applyGsFragments(buildShShaderSource(shDegree), fragments) : buildShShaderSource(shDegree) }); _cache.modules.set(shDegree + fragKey, module); } const key = `${targetSignatureKey(sig)}|sh${shDegree}${fragKey}`; let entry = _cache.entries.get(key); if (entry) { return entry; } const shTextureCount = SH_TEXTURE_COUNT[shDegree]; const layoutEntries = [ { binding: 0, visibility: SS.VERTEX | SS.FRAGMENT, buffer: { type: "uniform" } }, { binding: 1, visibility: SS.VERTEX, sampler: { type: "non-filtering" } }, { binding: 2, visibility: SS.VERTEX, texture: { sampleType: "unfilterable-float" } }, { binding: 3, visibility: SS.VERTEX, texture: { sampleType: "unfilterable-float" } }, { binding: 4, visibility: SS.VERTEX, texture: { sampleType: "unfilterable-float" } }, { binding: 5, visibility: SS.VERTEX, texture: { sampleType: "unfilterable-float" } } ]; for (let i = 0; i < shTextureCount; i++) { layoutEntries.push({ binding: 6 + i, visibility: SS.VERTEX, texture: { sampleType: "uint" } }); } const meshBindGroupLayout = device.createBindGroupLayout({ entries: layoutEntries }); const pipeline = device.createRenderPipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [getSceneBindGroupLayout(engine), meshBindGroupLayout] }), vertex: { module, entryPoint: "vs", buffers: [ { arrayStride: 8, stepMode: "vertex", attributes: [{ shaderLocation: 0, offset: 0, format: "float32x2" }] }, { arrayStride: 4, stepMode: "instance", attributes: [{ shaderLocation: 1, offset: 0, format: "float32" }] } ] }, fragment: { module, entryPoint: "fs", targets: [ { format: sig._colorFormat, blend: { color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha", operation: "add" }, alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" } }, writeMask: CW.ALL } ] }, primitive: { topology: "triangle-list", cullMode: "none" }, depthStencil: { format: sig._depthStencilFormat ?? "depth24plus-stencil8", depthCompare: sig._depthCompare ?? "greater-equal", depthWriteEnabled: false }, multisample: { count: sig._sampleCount } }); entry = { pipeline, meshBindGroupLayout, shTextureCount }; _cache.entries.set(key, entry); return entry; } function buildGaussianSplattingRenderableSH(scene, mesh, fragments) { const engine = scene.surface.engine; const device = engine._device; const UBO_BYTES = 16 * 4 * 3 + 8 * 4 + 4 * 4; const ubo = device.createBuffer({ size: UBO_BYTES, usage: BU.UNIFORM | BU.COPY_DST }); const cpu = new F32(UBO_BYTES / 4); cpu[48 + 4] = mesh.textureWidth; cpu[48 + 5] = mesh.textureHeight; cpu[48 + 6] = 1; cpu[48 + 7] = 0; const bindGroups = /* @__PURE__ */ new Map(); const getBindGroup = (entry) => { let bg = bindGroups.get(entry.pipeline); if (bg) { return bg; } const shViews = mesh._gs._shViews ?? []; const entries = [ { binding: 0, resource: { buffer: ubo } }, { binding: 1, resource: mesh._gs._sampler }, { binding: 2, resource: mesh._gs._centersView }, { binding: 3, resource: mesh._gs._covAView }, { binding: 4, resource: mesh._gs._covBView }, { binding: 5, resource: mesh._gs._colorsView } ]; for (let i = 0; i < entry.shTextureCount; i++) { entries.push({ binding: 6 + i, resource: shViews[i] }); } bg = device.createBindGroup({ layout: entry.meshBindGroupLayout, entries }); bindGroups.set(entry.pipeline, bg); return bg; }; const update = () => { const cam = scene.camera; if (!cam) { return; } uploadPendingSplatOrder(device.queue, mesh); const size = getRenderTargetSize(engine); const aspect = size.width / size.height; const view = getViewMatrix(cam); const proj = getProjectionMatrix(cam, aspect); const world = mesh.worldMatrix; const camPos = getCameraPosition(cam); cpu.set(world, 0); cpu.set(view, 16); cpu.set(proj, 32); cpu[48] = size.width; cpu[48 + 1] = size.height; cpu[48 + 2] = size.width * 0.5 * proj[0]; cpu[48 + 3] = size.height * 0.5 * proj[5]; cpu[56] = camPos.x; cpu[57] = camPos.y; cpu[58] = camPos.z; cpu[59] = 0; device.queue.writeBuffer(ubo, 0, cpu.buffer, 0, UBO_BYTES); postSplatSortIfDirty(mesh, world, view); }; const r = { order: 200, isTransparent: true, bind(eng, sig) { const entry = getOrCreateShPipeline(eng, sig, mesh.shDegree, fragments); const bindGroup = getBindGroup(entry); return { renderable: r, pipeline: entry.pipeline, update, draw(pass) { pass.setBindGroup(1, bindGroup); pass.setVertexBuffer(0, mesh._gs._quadBuffer); pass.setVertexBuffer(1, mesh._gs._splatIndexBuffer); pass.setIndexBuffer(mesh._gs._indexBuffer, "uint16"); pass.drawIndexed(6, mesh.vertexCount); return 1; } }; } }; return r; } function attachGaussianSplattingMeshSH(scene, mesh, shFlat, fragments) { const engine = scene.surface.engine; const device = engine._device; const shDegree = mesh.shDegree; const shVectorCount = (shDegree + 1) * (shDegree + 1) - 1; const shCoefficientCount = shVectorCount * 3; const textureCount = Math.ceil(shCoefficientCount / 16); const width = mesh.textureWidth; const height = mesh.textureHeight; const textures = []; const views = []; const vertexCount = mesh.vertexCount; for (let t = 0; t < textureCount; t++) { const dst = new U8(width * height * 16); const tBase = t * 16; const bytesThisTex = Math.min(16, shCoefficientCount - tBase); for (let i = 0; i < vertexCount; i++) { const srcOff = i * shCoefficientCount + tBase; const dstOff = i * 16; for (let b = 0; b < bytesThisTex; b++) { dst[dstOff + b] = shFlat[srcOff + b]; } } const tex = device.createTexture({ size: [width, height], format: "rgba32uint", usage: TU.TEXTURE_BINDING | TU.COPY_DST }); device.queue.writeTexture({ texture: tex }, dst.buffer, { bytesPerRow: width * 16 }, { width, height }); textures.push(tex); views.push(tex.createView()); } mesh._gs._shTextures = textures; mesh._gs._shViews = views; const ctx = scene; ctx._renderables.push(buildGaussianSplattingRenderableSH(scene, mesh, fragments)); const unregisterPick = registerPickSource(scene, mesh, () => import('./gs-picking-pipeline-CyS6hN5Z.esm.js')); ctx._disposables.push(() => { unregisterPick(); disposeGaussianSplattingMesh(mesh); }); } export { attachGaussianSplattingMeshSH, buildGaussianSplattingRenderableSH }; //# sourceMappingURL=gaussian-splatting-pipeline-sh-B-GBMjvd.esm.js.map