[BREAKING] World-space PCSS penumbra, plus gsplat & cascade fixes#8818
Conversation
BREAKING: light.penumbraSize is now interpreted in world-space units instead of shadow-map texel space. The useful numeric range shifts from ~1-100 to ~0.01-0.2; applications setting penumbraSize/penumbraFalloff will need to re-tune them. - World-space directional PCSS penumbra (GLSL + WGSL) - Gaussian splats write correct shadow-pass depth; unified gsplat manager reports a real world-space AABB for its shared mesh instance - Cascaded PCSS uses a stable cross-cascade union AABB for the depth range and per-cascade ortho radii, removing softness jumps - PCSS-only shader uniforms gated behind the PCSS shadow type so PCF/VSM paths carry no extra cost - New gaussian-splatting/shadow-soft example; shadow-cascades PCSS controls; re-tuned PCSS defaults across affected examples
There was a problem hiding this comment.
Pull request overview
This PR updates the engine’s directional PCSS implementation so light.penumbraSize is interpreted in world-space (breaking behavioral change), and fixes several shadowing issues affecting cascades and Gaussian Splats.
Changes:
- Switch directional PCSS penumbra computation from shadow-map/UV space to world-space (GLSL + WGSL), including cascade-stable depth-range handling and per-cascade ortho radius plumbing.
- Fix Gaussian Splats shadow pass depth output and keep the unified GSplat mesh-instance AABB synchronized with placement bounds for correct culling/fitting.
- Update examples/HUD controls and defaults to match the new world-space penumbra scale.
Reviewed changes
Copilot reviewed 21 out of 23 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/scene/shader-lib/wgsl/chunks/lit/frag/lighting/shadowSoft.js | Implements world-space directional PCSS and cascade-stable sampling behavior (WGSL). |
| src/scene/shader-lib/wgsl/chunks/lit/frag/lighting/lightFunctionShadow.js | Passes per-cascade ortho radius into PCSS path (WGSL). |
| src/scene/shader-lib/wgsl/chunks/lit/frag/lighting/lightDeclaration.js | Declares new PCSS uniform for per-cascade radii (WGSL). |
| src/scene/shader-lib/wgsl/chunks/gsplat/frag/gsplat.js | Writes correct depth during GSplat shadow pass (WGSL). |
| src/scene/shader-lib/glsl/chunks/lit/frag/lighting/shadowSoft.js | Implements world-space directional PCSS (GLSL). |
| src/scene/shader-lib/glsl/chunks/lit/frag/lighting/lightFunctionShadow.js | Passes per-cascade ortho radius into PCSS path (GLSL). |
| src/scene/shader-lib/glsl/chunks/lit/frag/lighting/lightDeclaration.js | Declares new PCSS uniform for per-cascade radii (GLSL). |
| src/scene/shader-lib/glsl/chunks/gsplat/frag/gsplat.js | Writes correct depth during GSplat shadow pass (GLSL). |
| src/scene/renderer/shadow-renderer-directional.js | Builds a cross-cascade union caster AABB for stable PCSS depth range; stores per-cascade radii. |
| src/scene/renderer/forward-renderer.js | Gates PCSS-only uniform uploads; uploads per-cascade ortho radii for directional PCSS. |
| src/scene/light.js | Adds _isPcss flag derived from shadow type info for cheaper runtime gating. |
| src/scene/gsplat-unified/gsplat-manager.js | Updates shared GSplat meshInstance custom AABB from placement world AABBs each frame. |
| examples/src/examples/graphics/shadow-soft.example.mjs | Retunes penumbra defaults for world-space PCSS. |
| examples/src/examples/graphics/shadow-soft.controls.mjs | Updates UI slider range/precision for world-space penumbra. |
| examples/src/examples/graphics/shadow-catcher.example.mjs | Retunes penumbra defaults for world-space PCSS. |
| examples/src/examples/graphics/shadow-cascades.example.mjs | Adds PCSS sliders/defaults and increases camera zoom range. |
| examples/src/examples/graphics/shadow-cascades.controls.mjs | Adds HUD controls for PCSS penumbra/falloff. |
| examples/src/examples/gaussian-splatting/spherical-harmonics.example.mjs | Retunes penumbra defaults for world-space PCSS. |
| examples/src/examples/gaussian-splatting/simple.example.mjs | Retunes penumbra defaults for world-space PCSS. |
| examples/src/examples/gaussian-splatting/shadow-soft.example.mjs | New example demonstrating GSplats casting PCSS soft shadows. |
| examples/src/examples/gaussian-splatting/shadow-soft.controls.mjs | New HUD for GSplat soft shadow parameters + renderer selection. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const radii = this.shadowCascadeRadii; | ||
| for (let c = 0; c < 4; c++) { | ||
| const r = c < directional.numCascades ? directional.getRenderData(camera, c).projectionCompensation : 0; | ||
| // fall back to cascade 0's radius for unused / not-yet-culled cascades to | ||
| // avoid a zero ortho radius (which would divide-by-zero in the shader) | ||
| radii[c] = r > 0 ? r : lightRenderData.projectionCompensation; | ||
| } | ||
| this.shadowCascadeRadiiId[cnt].setValue(radii); |
| #if LIGHT{i}SHADOWTYPE == PCSS_32F | ||
|
|
||
| #if LIGHT{i}SHAPE != PUNCTUAL | ||
| let shadowSearchArea = vec2f(length(uniform.light{i}_halfWidth), length(uniform.light{i}_halfHeight)) * uniform.light{i}_shadowSearchArea; | ||
| return getShadowPCSS(light{i}_shadowMap, light{i}_shadowMapSampler, shadowCoord, uniform.light{i}_shadowParams, uniform.light{i}_cameraParams, shadowSearchArea, lightDirW_in); | ||
| #else |
| #if LIGHT{i}SHADOWTYPE == PCSS_32F | ||
|
|
||
| #if LIGHT{i}SHAPE != PUNCTUAL | ||
| vec2 shadowSearchArea = vec2(length(light{i}_halfWidth), length(light{i}_halfHeight)) * light{i}_shadowSearchArea; | ||
| return getShadowPCSS(SHADOWMAP_PASS(light{i}_shadowMap), shadowCoord, light{i}_shadowParams, light{i}_cameraParams, shadowSearchArea, lightDirW); | ||
| #else |
|
Hi, in the shadow example, at a some camera distance, the shadows clip. |
|
shadowDistance on the light controls this distance. |
* refactor(examples): author controls as JSX components
Replace the argument-injected `controls.mjs` factory (deps passed in as
`{ observer, React, ReactPCUI, jsx, fragment }`) with developer-friendly
`*.controls.jsx` files that import React/PCUI/playcanvas directly and use real
JSX. Controls are now a named `export function Controls({ observer })` component.
Controls are transpiled in the browser via a lazily-loaded `@babel/standalone`
(CommonJS output + a `require` shim in Example.mjs), so editing controls in the
code panel and reloading now updates the UI. The server-side controls transpile
and all legacy `controls.mjs` handling are removed.
Includes build/dev/prod, eslint (jsx), tsconfig (jsx), monaco language and
prettier updates, plus README + template scaffolding for the new format.
* fix(examples): port #8818 PCSS penumbra controls into migrated JSX
#8818 landed concurrently and modified controls that this branch migrated to
JSX. Re-apply those changes in the new format so nothing regresses:
- graphics/shadow-soft: Penumbra slider 1/100/0 -> 0/0.2/3
- graphics/shadow-cascades: add PCSS Penumbra + PCSS Falloff sliders
- gaussian-splatting/shadow-soft: migrate the newly-added controls to JSX and
drop the orphan .controls.mjs
* refactor(examples): author controls as JSX components
Replace the argument-injected `controls.mjs` factory (deps passed in as
`{ observer, React, ReactPCUI, jsx, fragment }`) with developer-friendly
`*.controls.jsx` files that import React/PCUI/playcanvas directly and use real
JSX. Controls are now a named `export function Controls({ observer })` component.
Controls are transpiled in the browser via a lazily-loaded `@babel/standalone`
(CommonJS output + a `require` shim in Example.mjs), so editing controls in the
code panel and reloading now updates the UI. The server-side controls transpile
and all legacy `controls.mjs` handling are removed.
Includes build/dev/prod, eslint (jsx), tsconfig (jsx), monaco language and
prettier updates, plus README + template scaffolding for the new format.
* fix(examples): port #8818 PCSS penumbra controls into migrated JSX
JSX. Re-apply those changes in the new format so nothing regresses:
- graphics/shadow-soft: Penumbra slider 1/100/0 -> 0/0.2/3
- graphics/shadow-cascades: add PCSS Penumbra + PCSS Falloff sliders
- gaussian-splatting/shadow-soft: migrate the newly-added controls to JSX and
drop the orphan .controls.mjs
|
It's only soft on devices that support float-32 filterable textures, and unfortunately many iPhones do not have support for this, and so it falls back to hard shadows. |


Breaking change —
light.penumbraSizeis now interpreted in world-space units.Directional PCSS soft shadows previously interpreted
penumbraSizein shadow-map texel/UV space, so the resulting softness depended on the shadow resolution and on how the shadow camera happened to be fitted to the scene. It is now interpreted in world units, matching the documented meaning ("the area size of the light"): the penumbra width scales with the real blocker→receiver distance in the scene and is independent of shadow resolution and light direction.As a result the useful numeric range has changed dramatically — values that previously read as roughly 1–100 now correspond to roughly 0.01–0.2. Applications that set
penumbraSize(and, to a lesser degree,penumbraFalloff) will need to re-tune them; old values produce extremely over-soft, washed-out shadows. As a guideline, ~0.05 is a sensible starting point for a typical scene, ~0.01–0.02 approximates a sharp sun, and ~0.5+ a broad area light.penumbraFalloffkeeps its meaning (higher = softens faster with distance, 1 = linear) but its interaction with the new world-space curve may warrant minor adjustment.Changes:
API Changes:
light.penumbraSize— same signature, behavioral/units change (now world-space). Compiles unchanged; values need re-tuning per the note above.Examples:
gaussian-splatting/shadow-softexample — gsplat bikes casting soft shadows onto a ground disc.graphics/shadow-cascades— added PCSS Penumbra/Falloff HUD sliders; tripled camera zoom-out range.penumbraSize/penumbraFalloffdefaults inshadow-soft,shadow-cascades,shadow-catcher,gaussian-splatting/simple, andgaussian-splatting/spherical-harmonicsto the new world-space scale.