Skip to content

[BREAKING] World-space PCSS penumbra, plus gsplat & cascade fixes#8818

Merged
mvaligursky merged 2 commits into
mainfrom
mv-world-space-pcss
Jun 1, 2026
Merged

[BREAKING] World-space PCSS penumbra, plus gsplat & cascade fixes#8818
mvaligursky merged 2 commits into
mainfrom
mv-world-space-pcss

Conversation

@mvaligursky

@mvaligursky mvaligursky commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Breaking change — light.penumbraSize is now interpreted in world-space units.

Directional PCSS soft shadows previously interpreted penumbraSize in 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. penumbraFalloff keeps its meaning (higher = softens faster with distance, 1 = linear) but its interaction with the new world-space curve may warrant minor adjustment.

Changes:

  • World-space directional PCSS — penumbra is computed from world-space blocker→receiver distance instead of normalized shadow-map depth, making softness consistent regardless of light direction, shadow resolution, or shadow-camera fit. (GLSL + WGSL)
  • Gaussian splat shadows — splats now write correct depth in the shadow pass (previously a constant 0), so they cast proper PCSS contact-hardening shadows. The unified gsplat manager now reports a real world-space AABB for its shared mesh instance, so directional shadow fitting/culling uses the actual splat bounds.
  • Cascaded PCSS stability — the directional shadow camera depth range is derived from a stable cross-cascade union of caster bounds, eliminating sudden softness jumps as casters cross cascade boundaries or the camera moves. Per-cascade ortho radii are passed to the shader so each cascade softens correctly instead of inheriting cascade 0's radius.
  • PCSS-specific shader uniforms are gated behind the PCSS shadow type, so the common PCF/VSM paths carry no extra cost.

API Changes:

  • light.penumbraSize — same signature, behavioral/units change (now world-space). Compiles unchanged; values need re-tuning per the note above.

Examples:

  • New gaussian-splatting/shadow-soft example — gsplat bikes casting soft shadows onto a ground disc.
  • graphics/shadow-cascades — added PCSS Penumbra/Falloff HUD sliders; tripled camera zoom-out range.
  • Re-tuned penumbraSize/penumbraFalloff defaults in shadow-soft, shadow-cascades, shadow-catcher, gaussian-splatting/simple, and gaussian-splatting/spherical-harmonics to the new world-space scale.
Screenshot 2026-06-01 at 14 40 10

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/scene/renderer/forward-renderer.js Outdated
Comment on lines +315 to +322
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);
Comment on lines 105 to 110
#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
Comment on lines 103 to 108
#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
@mvaligursky mvaligursky merged commit 4429ce7 into main Jun 1, 2026
8 checks passed
@mvaligursky mvaligursky deleted the mv-world-space-pcss branch June 1, 2026 14:21
@d0rianb

d0rianb commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Hi, in the shadow example, at a some camera distance, the shadows clip.
Is there a specific parameter controlling this behavior ?
screenshot

@mvaligursky

Copy link
Copy Markdown
Contributor Author

shadowDistance on the light controls this distance.

kpal81xd added a commit that referenced this pull request Jun 2, 2026
* 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
kpal81xd added a commit that referenced this pull request Jun 3, 2026
* 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
@d0rianb

d0rianb commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Hi, on mobile (tested on iphone, Safari, latest iOS), the shadows are not soft, with the default example parameters :
F5230C78-7AB9-4B83-A661-622D7E0B7B21_1_201_a

@mvaligursky

Copy link
Copy Markdown
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants