playcanvas / playcanvas/engine

ShaderMaterial: support omni light and spot VSM/PCSS shadow casting

Open
#9,118 0 comments 0 reactions 1 assignee View on GitHub

@mvaligursky is already working on this.

Since Jul 24, 2026.

area: graphics
Dominant language
JavaScript
Stars
16.8k
Forks
2k
Avg merge
4h 32m
Merged PRs (30d)
222

Description

Follow-up to #6835 and #9116.

The shadowCasterPS chunk added in #9116 lets a custom shader (ShaderMaterial) cast shadows via #include "shadowCasterPS" and getShadowOutput(). The initial implementation covers:

  • Directional lights — all shadow types (PCF, VSM, PCSS)
  • Spot lights — PCF only

Not yet supported:

  • Omni lights — no shadow types
  • Spot lights — VSM and PCSS
Why these were skipped

These cases don't use rasterized perspective depth. They need distance-based depth, min(distance(view_position, vPositionW) / light_radius, 0.99999), which requires:

  • a world-space position varying supplied by the user's vertex shader (there is no fixed naming convention to rely on for custom shaders),
  • the view_position and light_radius uniforms,
  • for the non-VSM/PCSS path, writing the computed depth to gl_FragDepth.

The gl_FragDepth requirement is the main API design problem: in WGSL, output.fragDepth lives on the main function's FragmentOutput struct and cannot be written from a chunk helper function, so the clean no-argument getShadowOutput() API used for the perspective-depth cases does not extend to it symmetrically. Spot PCSS additionally needs linearized depth.

Possible approaches
  • A statement-style include used inside main() (like litUserMainEndPS is used in litShadowMain), which writes gl_FragColor/gl_FragDepth (GLSL) or output.color/output.fragDepth (WGSL), relying on the engine convention that the WGSL output variable is named output.
  • Or an explicit getShadowDepth(worldPos) helper that returns the depth, which the user assigns to output.fragDepth / gl_FragDepth themselves, gated behind a chunk-provided define.

Either way the user's vertex shader needs to provide the world position.

See the existing lit implementation for reference: src/scene/shader-lib/glsl/chunks/lit/frag/pass-shadow/litShadowMain.js (and the WGSL equivalent).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.