playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
296 lines (295 loc) • 14.2 kB
TypeScript
/**
* Per-light shadow draw entry. One cheap material + mesh instance over the shared quad mesh, plus a
* visible-index buffer and an atomic visible-count buffer. The mesh instance is registered as a
* shadow caster and is visible only for its light's shadow camera.
*/
export type ShadowLightEntry = object;
/**
* Per-light shadow draw entry. One cheap material + mesh instance over the shared quad mesh, plus a
* visible-index buffer and an atomic visible-count buffer. The mesh instance is registered as a
* shadow caster and is visible only for its light's shadow camera.
*
* @typedef {object} ShadowLightEntry
* @ignore
* @property {Light} light - The directional light this entry casts for.
* @property {ShaderMaterial} material - The per-light shadow draw material.
* @property {MeshInstance} meshInstance - The cast mesh instance (registered as a shadow caster).
* @property {StorageBuffer|null} indexBuffer - Dense visible work-buffer index list (grows with splat count).
* @property {number} allocatedIndexCount - Capacity of `indexBuffer` in splats.
* @property {StorageBuffer} countBuffer - Single-element atomic visible counter (also bound as `numSplatsStorage`).
* @property {Compute|null} cullCompute - Per-entry cull compute (own uniform buffer/bind group),
* lazily (re)created against the current cull shader; see {@link ShadowLightEntry.cullComputeGen}.
* @property {number} cullComputeGen - Cull-shader generation `cullCompute` was built for; when it no
* longer matches the renderer's {@link GSplatShadowRenderer#_cullShaderGen} the compute is rebuilt.
* @property {Compute} argsCompute - Per-entry indirect-args compute (own uniform buffer/bind group).
*/
/**
* Casts gsplat directional shadows on behalf of the GPU-sort ({@link GSplatHybridRenderer}) path,
* which cannot self-cast. It shares the manager's {@link GSplatWorld} (work buffer + camera-
* independent cull bounds + world states) and never allocates a world of its own.
*
* For each non-cascaded directional light affecting the manager's layer it maintains a cheap draw
* entry (a per-light material + mesh instance over one shared quad mesh, plus a visible-index and
* count buffer). A projection-free compute cull against the light's frustum produces the visible
* index list, and a quad-style per-vertex-projected indirect draw writes the shadow map via the
* standard caster pipeline. No sort and no projection cache are needed.
*
* Lifecycle is split across the frame:
* - {@link syncLights} runs pre-cull (from {@link GSplatManager#update}) to reconcile the per-light
* pool and register/unregister shadow casters, so `cullComposition` sees them.
* - {@link cull} runs post-cull (from {@link GSplatManager#updateShadows} via the director) once
* each light's shadow-camera frustum has been fitted, to dispatch the culls and bind results.
*
* @ignore
*/
export class GSplatShadowRenderer {
/**
* @param {GraphicsDevice} device - The graphics device.
* @param {GraphNode} node - The graph node the cast mesh instances are parented to.
* @param {GraphNode} cameraNode - The main camera node this manager renders for; used to
* resolve each light's shadow camera via `light.getRenderData(sceneCamera, 0)`.
* @param {Layer} layer - The layer to register shadow casters on (and read directional lights from).
* @param {GSplatWorld} world - The shared world (work buffer, cull bounds, world states).
* @param {import('./gsplat-hybrid-renderer-scratch.js').GSplatHybridRendererScratch|null} [scratch] -
* Manager-owned shared scratch; forwarded to the pass-1 compaction so its candidate index list is
* shared with the forward hybrid renderer (they use it at disjoint points in the frame).
*/
constructor(device: GraphicsDevice, node: GraphNode, cameraNode: GraphNode, layer: Layer, world: GSplatWorld, scratch?: import("./gsplat-hybrid-renderer-scratch.js").GSplatHybridRendererScratch | null);
/** @type {GraphicsDevice} */
device: GraphicsDevice;
/** @type {GraphNode} */
node: GraphNode;
/** @type {GraphNode} */
cameraNode: GraphNode;
/** @type {Layer} */
layer: Layer;
/** @type {GSplatWorld} */
world: GSplatWorld;
/**
* Per-light draw entries, keyed by light.
*
* @type {Map<Light, ShadowLightEntry>}
*/
entries: Map<Light, ShadowLightEntry>;
/**
* Reused scratch set of the qualifying directional shadow lights, rebuilt each {@link syncLights}
* to diff against {@link entries}.
*
* @type {Set<Light>}
* @private
*/
private _desiredLights;
/**
* Pass 1 (coarse): interval compaction run with each light's frustum, producing a dense candidate
* list (`compactedSplatIds`) + candidate count (`countBuffer[numIntervals]`). Reused across all
* lights — one shared scratch, since lights are culled sequentially (light A's pass 2 consumes the
* candidate list before light B's pass 1 overwrites it). The expensive per-splat fine cull (pass
* 2) then runs flat over the candidate list, so occupancy is independent of interval count.
*
* @type {GSplatIntervalCompaction|null}
*/
_compaction: GSplatIntervalCompaction | null;
/** @type {Vec2} */
_cullDispatchSize: Vec2;
/**
* Reused light frustum planes (6 × vec4(normal, distance)) for the cull uniform, refilled per
* light entry from its shadow camera.
*
* @type {Float32Array}
*/
_frustumPlanes: Float32Array;
/**
* Change-detection key for the scene material's shader chunks; when it changes, the user
* `gsplatModifyVS` chunk is re-applied to the per-light shadow materials.
*
* @type {string}
* @private
*/
private _userChunksKey;
/**
* The scene material's user `gsplatModifyVS` WGSL chunk source (or null for the default no-op).
* The shadow path is WebGPU-only, so only the WGSL variant is tracked.
*
* @type {string|null}
* @private
*/
private _userModifyWgsl;
/** @type {Shader|null} */
_cullShader: Shader | null;
/** @type {BindGroupFormat|null} */
_cullBindGroupFormat: BindGroupFormat | null;
/**
* Work-buffer format version the cull shader was last built for. The cull shader reads the work
* buffer (texture bindings + read code derived from the format), so a format change rebuilds it.
*
* @type {number}
* @private
*/
private _cullFormatVersion;
/**
* The scene material's shader-chunks key the cull shader was last built for. A change means the
* user `gsplatModifyVS` chunk changed, so the cull shader is rebuilt to match the shadow draw.
*
* @type {string|null}
* @private
*/
private _cullBuiltChunksKey;
/**
* Monotonic cull-shader generation, bumped on every (re)build. Per-light cull Computes reference
* the shared shader, so they are recreated when this changes (see {@link _cullEntry}).
*
* @type {number}
* @private
*/
private _cullShaderGen;
/** @type {Shader|null} */
_argsShader: Shader | null;
/** @type {BindGroupFormat|null} */
_argsBindGroupFormat: BindGroupFormat | null;
destroy(): void;
/**
* (Re)builds the shared cull shader when the work-buffer format or the user `gsplatModifyVS`
* chunk changes, bumping {@link _cullShaderGen} so per-light Computes are recreated. Must run
* after {@link _syncUserModify} (which refreshes the tracked modify chunk) and once the work
* buffer is ready.
*
* @private
*/
private _ensureCullShader;
/**
* Builds the pass-2 fine-cull shader + bind group format against the current work-buffer format
* and the tracked user modify chunk. The fixed bindings (0..4) are followed by the work-buffer
* format texture bindings; the shader reads each candidate splat (center/opacity/rotation/scale),
* applies the render-stage modifier, and runs the opacity/size/frustum fine tests.
*
* @private
*/
private _buildCullShader;
/** @private */
private _createArgsShader;
/**
* Rebinds to a new work buffer after a format/resize swap. The per-light materials read the
* work-buffer textures, so they must be re-pointed when the manager recreates it.
*
* @param {GSplatWorkBuffer} workBuffer - The new work buffer.
*/
setDataSource(workBuffer: GSplatWorkBuffer): void;
/**
* Sets the world-space AABB on every cast mesh instance. The directional shadow cull derives
* each cascade's depth range from the casters' AABBs (which world-space PCSS penumbra scaling
* depends on), and the shared quad mesh has no meaningful spatial bounds of its own — so without
* this the depth range is wrong and soft shadows are mis-scaled. Must run pre-cull (the manager
* calls it before cullComposition fits the shadow cameras). `setCustomAabb` copies, so passing a
* shared box instance to every entry is safe.
*
* @param {import('../../core/shape/bounding-box.js').BoundingBox|null} aabb - World-space splat AABB.
*/
setCastersAabb(aabb: import("../../core/shape/bounding-box.js").BoundingBox | null): void;
/**
* Pre-cull pass: reconcile the per-light caster pool against the layer's directional shadow
* lights (enabled, shadow-casting, non-cascaded). Adds entries for new lights and tears down
* entries for lights that were disabled, removed, stopped casting, or became cascaded — freeing
* their GPU resources and unregistering their caster. Cascaded directional lights are skipped
* (warned once); they would need a per-cascade cull.
*/
syncLights(): void;
/**
* Post-cull pass: for each light entry run the two-pass cull (coarse candidate compaction with
* the light frustum, then a flat per-splat fine cull) and the indirect-args write, then bind the
* results to the entry's mesh instance. Runs after `cullComposition` and before the frame graph
* renders the shadow maps.
*
* @param {GSplatParams} gsplatParams - Scene gsplat params (alphaClip etc.).
*/
cull(gsplatParams: GSplatParams): void;
/**
* Applies the scene material's user `gsplatModifyVS` chunk to every per-light shadow material
* (recompiling only when the chunk changes) and forwards the scene material's parameters (e.g.
* `uTime`) to them each frame. This keeps cast shadows in sync with any forward-pass vertex
* animation, since the shadow draw uses the same quad VS + modify hooks.
*
* @param {GSplatParams} gsplatParams - Scene gsplat params (carries the template material).
* @private
*/
private _syncUserModify;
/**
* Sets (or clears) the tracked user `gsplatModifyVS` chunk on one entry's material and rebuilds
* its shader. Called when the chunk changes and when a new entry is created.
*
* @param {ShadowLightEntry} entry - The light entry.
* @private
*/
private _applyUserModify;
/**
* Dispatches the cull + indirect-args for one light entry and binds the results.
*
* @param {ShadowLightEntry} entry - The light entry.
* @param {number} numIntervals - Total interval count.
* @param {number} totalActiveSplats - Max output index count.
* @param {number} textureSize - Work buffer texture size.
* @param {GSplatParams} gsplatParams - Scene gsplat params.
* @private
*/
private _cullEntry;
/**
* Fills {@link _frustumPlanes} from a frustum: 6 planes packed as vec4(normal.xyz, distance).
*
* @param {import('../../core/shape/frustum.js').Frustum} frustum - The light's shadow-camera frustum.
* @private
*/
private _fillFrustumPlanes;
/**
* Creates a per-light shadow draw entry (material + caster mesh instance + count buffer).
*
* @param {Light} light - The directional light.
* @returns {ShadowLightEntry} The created entry.
* @private
*/
private _createEntry;
/**
* Tears down a light entry: unregisters the caster and frees its GPU resources.
*
* @param {ShadowLightEntry} entry - The entry to destroy.
* @private
*/
private _destroyEntry;
/**
* Creates the quad-style shadow draw material. Uses the same gsplat vertex/fragment chunks as
* the forward quad renderer (direct per-vertex projection from the bound view/projection — the
* shadow camera's, supplied by the engine's shadow pass), trimmed to depth + alpha-clip (the
* engine injects `SHADOW_PASS` when compiling the shadow variant). Indirect-draw mode reads the
* visible index list and GPU count.
*
* @returns {ShaderMaterial} The configured material.
* @private
*/
private _createMaterial;
/**
* Injects the work-buffer format shader chunks and binds its textures + format-dependent defines
* to a material. Mirrors the relevant parts of {@link GSplatQuadRenderer}.
*
* @param {ShaderMaterial} material - The material to configure.
* @private
*/
private _configureMaterialWorkBuffer;
/**
* Creates the cast mesh instance for a light, visible only for that light's shadow camera.
*
* @param {Light} light - The directional light.
* @param {ShaderMaterial} material - The entry's material.
* @returns {MeshInstance} The mesh instance.
* @private
*/
private _createMeshInstance;
}
import type { GraphicsDevice } from '../../platform/graphics/graphics-device.js';
import type { GraphNode } from '../graph-node.js';
import type { Layer } from '../layer.js';
import type { GSplatWorld } from './gsplat-world.js';
import type { Light } from '../light.js';
import { GSplatIntervalCompaction } from './gsplat-interval-compaction.js';
import { Vec2 } from '../../core/math/vec2.js';
import { Shader } from '../../platform/graphics/shader.js';
import { BindGroupFormat } from '../../platform/graphics/bind-group-format.js';
import type { GSplatWorkBuffer } from './gsplat-work-buffer.js';
import type { GSplatParams } from './gsplat-params.js';