playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
202 lines (164 loc) • 7.22 kB
JavaScript
var gsplatHybrid_default = (
/* wgsl */
`
attribute vertex_position: vec3f;
uniform viewport_size: vec4f;
// -inverse(matrix_projection)[row 2]; lets the VS reconstruct linear view
// depth from clipPos via dot(clipToViewZ, clip). Set per-camera by the
// renderer (see GSplatHybridRenderer).
uniform clipToViewZ: vec4f;
// Stereo: the cache stores per-eye NDC.xy plus a shared w; the active eye is selected by the
// per-view uniform view_index, and clip.z is reconstructed from w via matrix_projection.
// Both come from the per-view (BINDGROUP_VIEW) uniform buffer, set per eye by the renderer.
uniform view_index: u32;
uniform matrix_projection: mat4x4f;
uniform alphaClip: f32;
uniform alphaClipForward: f32;
// Globally sorted indices into projCache (output of the radix sort).
var<storage, read> sortedIndices: array<u32>;
// Pre-projected splat cache (8 u32 slots per splat).
var<storage, read> projCache: array<u32>;
// Visible splat count written by compute-gsplat-projector-write-indirect-args.js.
var<storage, read> numSplatsStorage: array<u32>;
varying gaussianUV: half2;
varying gaussianColor: half4;
varying id: f32;
varying vLinearDepth: f32;
varying @interpolate(flat) vPickId: u32;
uniform colorRampIntensity: f32;
var colorRamp: texture_2d<f32>;
var colorRampSampler: sampler;
const discardVec: vec4f = vec4f(0.0, 0.0, 2.0, 1.0);
@vertex
fn vertexMain(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
// Same instance/quad linearisation as gsplatSourceVS:
// order = instanceIdx * GSPLAT_INSTANCE_SIZE + perInstanceQuadIdx.
let order = pcInstanceIndex * {GSPLAT_INSTANCE_SIZE}u + u32(vertex_position.z);
let numSplats = numSplatsStorage[0];
if (order >= numSplats) {
output.position = discardVec;
return output;
}
let cacheIdx = sortedIndices[order];
let base = cacheIdx * {CACHE_STRIDE}u;
// Stereo layout: pick this eye's NDC.xy ([0,1] eye 0 / [2,3] eye 1), shared w/v1/v2/color.
let off = uniform.view_index * 2u;
let ndc = vec2f(
bitcast<f32>(projCache[base + off + 0u]),
bitcast<f32>(projCache[base + off + 1u])
);
let w = bitcast<f32>(projCache[base + 4u]);
// Reconstruct clip.z from the shared w (perspective only). With P column-major:
// clip.z = (P[2][2] / P[2][3]) * w + P[3][2] (P[2][3] is the w-row z term)
// The z-row entries are identical for both eyes (same near/far), so this is eye-consistent.
let pz = (uniform.matrix_projection[2][2] / uniform.matrix_projection[2][3]) * w + uniform.matrix_projection[3][2];
let proj = vec4f(ndc * w, clamp(pz, 0.0, abs(w)), w);
let v1 = unpack2x16float(projCache[base + 5u]);
let v2 = unpack2x16float(projCache[base + 6u]);
let rgba = unpack4x8unorm(projCache[base + 7u]);
let alpha = half(rgba.a);
var clr: half4 = half4(half(rgba.r), half(rgba.g), half(rgba.b), alpha);
let proj = vec4f(
bitcast<f32>(projCache[base + 0u]),
bitcast<f32>(projCache[base + 1u]),
bitcast<f32>(projCache[base + 2u]),
bitcast<f32>(projCache[base + 3u])
);
let v1 = unpack2x16float(projCache[base + 4u]);
let v2 = unpack2x16float(projCache[base + 5u]);
// Color / opacity / pickId: slot 6 is either packed (rg) or a raw u32 pcId.
let ba = unpack2x16float(projCache[base + 7u]);
let alpha = half(ba.y);
let pickId = projCache[base + 6u];
// In the pick path slot 6 is repurposed as the picking ID; alpha is the only
// colour we care about in the FS gate. Slot 7's r channel is unused.
var clr: half4 = half4(half(0.0), half(0.0), half(0.0), alpha);
let rg = unpack2x16float(projCache[base + 6u]);
var clr: half4 = half4(half(rg.x), half(rg.y), half(ba.x), alpha);
// clipCorner: shrink the quad to exclude near-zero alpha regions (matches gsplatCommonVS per-pass threshold).
let cornerUV = vec2f(vertex_position.xy);
let alphaClipValue = half(uniform.alphaClip);
let alphaClipValue = half(uniform.alphaClipForward);
let clip = min(half(1.0), sqrt(max(half(0.0), log(alpha / alphaClipValue))) * half(0.5));
let cornerClipped = cornerUV * f32(clip);
// Convert pixel-space offset into clip-space. proj.w is the real clipPos.w,
// so c == clip.w / viewport matches gsplatCorner.js line 97 for both
// perspective (w == -view.z) and orthographic (w == 1) projections.
let c = vec2f(proj.w) * uniform.viewport_size.zw;
let pixelOffset = cornerClipped.x * v1 + cornerClipped.y * v2;
let clipOffset = pixelOffset * c;
output.position = proj + vec4f(clipOffset, 0.0, 0.0);
output.gaussianUV = half2(cornerClipped);
// read user varying values from the projection cache and pass them to the outputs
// Reconstruct linear view depth from clip via the per-camera clipToViewZ
// uniform = -inverse(matrix_projection)[row 2]. For perspective this
// collapses to clip.w; for ortho it produces the correct -view.z. Used by
// fog/overdraw/prepass below.
// XR is perspective-only, so linear view depth is exactly the shared w (= -view.z).
let viewDepth = proj.w;
let viewDepth = dot(uniform.clipToViewZ, proj);
// Overdraw mode renders a flat colour ramp; depth-shade input not needed.
let t: f32 = clamp(viewDepth / 20.0, 0.0, 1.0);
let rampColor: vec3f = textureSampleLevel(colorRamp, colorRampSampler, vec2f(t, 0.5), 0.0).rgb;
let outAlpha = alpha * half(1.0 / 32.0) * half(uniform.colorRampIntensity);
output.gaussianColor = half4(half3(rampColor), outAlpha);
output.gaussianColor = half4(
half3(prepareOutputFromGamma(max(vec3f(clr.xyz), vec3f(0.0)), viewDepth)),
alpha
);
// Best-effort id from the cache index \u2014 used only for blue-noise dither.
output.id = f32(cacheIdx);
output.vLinearDepth = viewDepth;
output.vPickId = pickId;
return output;
}
`
);
export {
gsplatHybrid_default as default
};