playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
287 lines (246 loc) • 10.1 kB
JavaScript
const computeGsplatCommonSource = (
/* wgsl */
`
#include "halfTypesCS"
const TILE_SIZE: u32 = 16u;
fn quatToMat3(r: half4) -> half3x3 {
let r2: half4 = r + r;
let x: half = r2.x * r.w;
let y: half4 = r2.y * r;
let z: half4 = r2.z * r;
let w: half = r2.w * r.w;
return half3x3(
half(1.0) - z.z - w, y.z + x, y.w - z.x,
y.z - x, half(1.0) - y.y - w, z.w + y.x,
y.w + z.x, z.w - y.x, half(1.0) - y.y - z.z
);
}
struct SplatCov2D {
screen: vec2f,
a: f32,
b: f32,
c: f32,
viewDepth: f32,
valid: bool,
#if GSPLAT_AA
aaFactor: f32,
#endif
#ifdef GSPLAT_XR
// Eye-1 NDC, computed for the union visibility test and reused by the cache write.
ndc1: vec2f,
#endif
}
fn computeSplatCov(
worldCenter: vec3f,
rotation: half4,
scale: half3,
viewMatrix: mat4x4f,
viewProj: mat4x4f,
focal: f32,
viewportWidth: f32,
viewportHeight: f32,
nearClip: f32,
farClip: f32,
opacity: f32,
minPixelSize: f32,
isOrtho: u32,
alphaClip: f32,
minContribution: f32,
foveationStrength: f32,
foveationCenter: f32,
#ifdef GSPLAT_FISHEYE
fisheye_k: f32,
fisheye_inv_k: f32,
fisheye_projMat00: f32,
fisheye_projMat11: f32,
#endif
#ifdef GSPLAT_XR
viewProj1: mat4x4f,
#endif
) -> SplatCov2D {
var result: SplatCov2D;
result.valid = false;
let viewCenter = (viewMatrix * vec4f(worldCenter, 1.0)).xyz;
#ifdef GSPLAT_FISHEYE
// Generalized fisheye: g(\u03B8) = k\xB7tan(\u03B8/k)
let fv = viewCenter;
let r_xy = length(fv.xy);
let neg_z = -fv.z;
let theta = atan2(r_xy, neg_z);
// Cull near the singularity at \u03B8 = k\xB7\u03C0/2, and at camera origin
let maxTheta = min(fisheye_k * 1.5707963, 3.13);
if (theta > maxTheta - 0.01 || dot(fv, fv) < 0.0001) {
return result;
}
let tk = theta * fisheye_inv_k;
let sin_tk = sin(tk);
let cos_tk = cos(tk);
let g_theta = fisheye_k * sin_tk / cos_tk;
let fisheye_s = select(select(0.0, 1.0 / neg_z, neg_z > 0.0), g_theta / r_xy, r_xy > 1e-4);
let fndc = vec2f(fisheye_projMat00 * fisheye_s * fv.x, fisheye_projMat11 * fisheye_s * fv.y);
let screen = vec2f(
(fndc.x * 0.5 + 0.5) * viewportWidth,
(fndc.y * 0.5 + 0.5) * viewportHeight
);
#else
if (viewCenter.z > 0.0) {
return result;
}
let clip = viewProj * vec4f(worldCenter, 1.0);
let ndc = clip.xy / clip.w;
let screen = vec2f(
(ndc.x * 0.5 + 0.5) * viewportWidth,
(ndc.y * 0.5 + 0.5) * viewportHeight
);
#endif
#ifdef GSPLAT_XR
// Eye-1 projection: used for union visibility (a splat is kept when visible in either
// eye) and stored in the result so the cache write does not re-project. The behind-camera
// test above covers both eyes (parallel-axis stereo shares view z).
let clip1 = viewProj1 * vec4f(worldCenter, 1.0);
let ndc1 = clip1.xy / clip1.w;
let screen1 = vec2f(
(ndc1.x * 0.5 + 0.5) * viewportWidth,
(ndc1.y * 0.5 + 0.5) * viewportHeight
);
#endif
let rot: half3x3 = quatToMat3(rotation);
let s: vec3f = vec3f(scale);
let M: mat3x3f = transpose(mat3x3f(
s.x * vec3f(rot[0]),
s.y * vec3f(rot[1]),
s.z * vec3f(rot[2])
));
let w0 = vec3f(viewMatrix[0].x, viewMatrix[1].x, viewMatrix[2].x);
let w1 = vec3f(viewMatrix[0].y, viewMatrix[1].y, viewMatrix[2].y);
let w2 = vec3f(viewMatrix[0].z, viewMatrix[1].z, viewMatrix[2].z);
#ifdef GSPLAT_FISHEYE
// Fisheye Jacobian for g(\u03B8) = k\xB7tan(\u03B8/k)
let fisheyeFocal = viewportWidth * fisheye_projMat00;
let g_prime = 1.0 / (cos_tk * cos_tk);
let d2 = dot(fv, fv);
let r_sq = max(r_xy * r_xy, 1e-8);
let K_coeff = select(0.0, (g_prime * neg_z / d2 - fisheye_s) / r_sq, r_xy > 1e-4);
let Jxx = fisheyeFocal * (fisheye_s + K_coeff * fv.x * fv.x);
let Jxy = fisheyeFocal * K_coeff * fv.x * fv.y;
let Jyy = fisheyeFocal * (fisheye_s + K_coeff * fv.y * fv.y);
let Jzx = fisheyeFocal * g_prime * fv.x / d2;
let Jzy = fisheyeFocal * g_prime * fv.y / d2;
let tt0 = Jxx * w0 + Jxy * w1 + Jzx * w2;
let tt1 = Jxy * w0 + Jyy * w1 + Jzy * w2;
#else
let ortho = isOrtho == 1u;
let v = select(viewCenter.xyz, vec3f(0.0, 0.0, 1.0), ortho);
let vz = select(min(v.z, -0.001), v.z, ortho);
let J1 = focal / vz;
let J2 = -J1 / vz * v.xy;
// Compute TT columns directly without materializing full J and W matrices.
// Original code:
// let J = mat3x3f(vec3f(J1, 0.0, J2.x), vec3f(0.0, J1, J2.y), vec3f(0.0, 0.0, 0.0));
// let W = transpose(mat3x3f(viewMatrix[0].xyz, viewMatrix[1].xyz, viewMatrix[2].xyz));
// let TT = W * J;
let tt0 = J1 * w0 + J2.x * w2;
let tt1 = J1 * w1 + J2.y * w2;
#endif
// Fused covariance: cov = TT^T * Vrk * TT = TT^T * (M^T * M) * TT = (M * TT)^T * (M * TT).
// Compute B = M * TT then cov = B^T * B, avoiding the intermediate Vrk (mat3x3f) matrix.
let b0 = M * tt0;
let b1 = M * tt1;
let aRaw = dot(b0, b0);
let b = dot(b0, b1);
let cRaw = dot(b1, b1);
let a = aRaw + 0.3;
let c = cRaw + 0.3;
let det = a * c - b * b;
if (det <= 0.0) {
return result;
}
#if GSPLAT_AA
// AA compensation: ratio of pre-blur to post-blur determinant. Matches the
// quad renderer's GSPLAT_AA branch in gsplatCorner.js initCornerCov.
let detOrig = aRaw * cRaw - b * b;
result.aaFactor = sqrt(max(detOrig / det, 0.0));
#endif
// Rejects splats whose total visual contribution (opacity * projected area) is
// negligible. Near the camera, projected areas are large so contributions naturally
// exceed the threshold; at distance, areas shrink and low-impact splats are culled.
//
// Foveation: the threshold is raised radially from the screen centre. Within
// foveationCenter (NDC radius) there is no effect; the boost ramps with a smoothstep
// to full foveationStrength at the screen edge (r = 1) and beyond (corners). With
// strength 0 this is an exact no-op.
let fovNdc = screen * vec2f(2.0 / viewportWidth, 2.0 / viewportHeight) - vec2f(1.0);
#ifdef GSPLAT_XR
// Stereo: use the smaller of the two eyes' radii, so a splat near the centre of either
// eye is protected (the foveated boost only applies where it is peripheral in both).
let fovR = min(length(fovNdc), length(ndc1));
#else
let fovR = length(fovNdc);
#endif
// manual smoothstep with a guard so foveationCenter near 1.0 cannot divide by zero
let fovT = saturate((fovR - foveationCenter) / max(1.0 - foveationCenter, 1e-4));
let effMinContribution = minContribution + foveationStrength * fovT * fovT * (3.0 - 2.0 * fovT);
let totalContribution = opacity * 6.283185 * sqrt(det);
if (totalContribution < effMinContribution) {
return result;
}
// Opacity-aware radius tightening based on FlashGS
// https://github.com/InternLandMark/FlashGS
// The fixed factor 8.0 corresponds to power = -4.0 (exp(-4) \u2248 0.018).
// For low-opacity splats, pixels become invisible (alpha < alphaClip) at a closer
// distance. We solve for the power where opacity * exp(power) = alphaClip,
// giving radiusFactor = min(8.0, 2.0 * ln(opacity / alphaClip)). This shrinks
// the effective radius for low-opacity splats, reducing tile assignments.
let radiusFactor = computeRadiusFactor(half(opacity), alphaClip);
let vmin = min(1024.0, min(viewportWidth, viewportHeight));
let maxRadius = vmin;
let radiusXUncapped = sqrt(2.0 * a);
let radiusYUncapped = sqrt(2.0 * c);
let radiusX = min(radiusXUncapped, maxRadius);
let radiusY = min(radiusYUncapped, maxRadius);
if (max(radiusX, radiusY) < minPixelSize) {
return result;
}
// Frustum cull: reject splats entirely off-screen
#ifdef GSPLAT_XR
// Stereo union: reject only when off-screen in BOTH eyes, otherwise splats visible only
// near one eye's edge (e.g. the right edge of the right eye) would be missing.
if ((screen.x + radiusX < 0.0 || screen.x - radiusX > viewportWidth ||
screen.y + radiusY < 0.0 || screen.y - radiusY > viewportHeight) &&
(screen1.x + radiusX < 0.0 || screen1.x - radiusX > viewportWidth ||
screen1.y + radiusY < 0.0 || screen1.y - radiusY > viewportHeight)) {
return result;
}
#else
if (screen.x + radiusX < 0.0 || screen.x - radiusX > viewportWidth ||
screen.y + radiusY < 0.0 || screen.y - radiusY > viewportHeight) {
return result;
}
#endif
// When the projected extent exceeds the radius cap, rescale the covariance
// so the Gaussian reaches its cutoff at the capped boundary. Without this,
// the Gaussian is still opaque at the boundary, creating hard rectangular
// edges. This matches the quad renderer's implicit UV renormalization.
let capScale = max(1.0, max(radiusXUncapped, radiusYUncapped) / maxRadius);
let invCapScale2 = 1.0 / (capScale * capScale);
result.screen = screen;
let scaledCov = vec3f(a, b, c) * invCapScale2;
result.a = scaledCov.x;
result.b = scaledCov.y;
result.c = scaledCov.z;
#ifdef GSPLAT_FISHEYE
result.viewDepth = sqrt(d2);
#else
result.viewDepth = -viewCenter.z;
#endif
#ifdef GSPLAT_XR
result.ndc1 = ndc1;
#endif
result.valid = true;
return result;
}
`
);
export {
computeGsplatCommonSource
};