playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
127 lines (107 loc) • 4.64 kB
JavaScript
const computeGsplatShadowCullSource = (
/* wgsl */
`
// Conservative gaussian extent (in std-devs) for the per-splat frustum radius, so a splat whose
// tail still reaches into the light frustum is not culled at the boundary.
const SPLAT_FRUSTUM_SIGMA: f32 = 3.0;
struct CullUniforms {
frustumPlanes: array<vec4f, 6>,
numIntervals: u32,
splatTextureSize: u32,
alphaClip: f32,
worldSizeThreshold: f32
};
var<uniform> uniforms: CullUniforms;
// Pass-1 outputs (read): the shared candidate list + the candidate count at [numIntervals].
var<storage, read> compactedSplatIds: array<u32>;
var<storage, read> candidateCountBuffer: array<u32>;
// Per-light outputs: dense visible work-buffer indices (unordered) + a single atomic visible count.
var<storage, read_write> outputIndices: array<u32>;
var<storage, read_write> globalCount: array<atomic<u32>>;
// Work-buffer format texture bindings (binding 5+) and the splat read/modify helpers. setSplat
// (gsplatComputeSplatCS) reads uniforms.splatTextureSize, so the uniform struct is declared above.
#include "gsplatComputeSplatCS"
#include "gsplatFormatDeclCS"
#include "gsplatFormatReadCS"
#include "gsplatHelpersVS"
#include "gsplatModifyVS"
// Fine per-splat cull: read + apply the render-stage modifier + opacity/size/frustum tests. Returns
// true when the splat should be drawn into the shadow map.
fn fineCull(splatId: u32) -> bool {
setSplat(splatId);
// world-space center + render-stage position modifier (matches the quad VS / forward projector)
let originalCenter = getCenter();
var center = originalCenter;
modifySplatCenter(¢er);
// opacity cull (matches the forward alpha cull + the shadow PS alphaClip)
let opacity = getOpacity();
if (opacity <= uniforms.alphaClip) {
return false;
}
// render-stage rotation/scale modifier; getRotation() is (w,x,y,z), the hook contract is (x,y,z,w)
var rotation: vec4f = getRotation().yzwx;
var scale: vec3f = getScale();
modifySplatRotationScale(originalCenter, center, &rotation, &scale);
// size cull: the orthographic shadow projection is linear, so a splat whose max world-space
// extent maps below the precomputed per-light threshold is sub-pixel in the shadow map. The max
// scale axis is an orientation-independent upper bound on the projected radius (conservative \u2014
// never culls a splat the forward path would keep). A threshold of 0 disables the test.
let maxScale = max(scale.x, max(scale.y, scale.z));
if (maxScale < uniforms.worldSizeThreshold) {
return false;
}
// per-splat frustum cull: the modified center (\xB1 a conservative gaussian radius) against the
// light's 6 frustum planes. Trims splats in boundary nodes; the ortho near/far planes also cull
// by depth.
let splatRadius = maxScale * SPLAT_FRUSTUM_SIGMA;
for (var p = 0; p < 6; p++) {
let plane = uniforms.frustumPlanes[p];
if (dot(plane.xyz, center) + plane.w <= -splatRadius) {
return false;
}
}
return true;
}
// One global atomicAdd per workgroup; intra-workgroup slots via a workgroup atomic (no subgroups).
var<workgroup> wgCount: atomic<u32>;
var<workgroup> wgBase: u32;
fn main(
gid: vec3u,
numWorkgroups: vec3u,
localIdx: u32
) {
if (localIdx == 0u) {
atomicStore(&wgCount, 0u);
}
workgroupBarrier();
// flat thread index from the (possibly Y-tiled) 2D dispatch grid
let threadIdx = gid.y * (numWorkgroups.x * {WORKGROUP_SIZE}u) + gid.x;
let candidateCount = candidateCountBuffer[uniforms.numIntervals];
var valid = false;
var splatId = 0u;
if (threadIdx < candidateCount) {
splatId = compactedSplatIds[threadIdx];
valid = fineCull(splatId);
}
var localSlot = 0u;
if (valid) {
localSlot = atomicAdd(&wgCount, 1u);
}
workgroupBarrier();
// workgroup leader reserves a contiguous global block for this workgroup's survivors
if (localIdx == 0u) {
wgBase = atomicAdd(&globalCount[0], atomicLoad(&wgCount));
}
workgroupBarrier();
if (valid) {
outputIndices[wgBase + localSlot] = splatId;
}
}
`
);
var compute_gsplat_shadow_cull_default = computeGsplatShadowCullSource;
export {
computeGsplatShadowCullSource,
compute_gsplat_shadow_cull_default as default
};