@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
JavaScript
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 += ` 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,
};
var<uniform> u: U;
var samp: sampler;
var centersTex: texture_2d<f32>;
var covATex: texture_2d<f32>;
var covBTex: texture_2d<f32>;
var colorsTex: texture_2d<f32>;
${textureBindings}
struct VOut {
pos: vec4<f32>,
vColor: vec4<f32>,
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;
}
fn vs( corner: vec2<f32>, 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*/
fn fs(in: VOut) -> 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