UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

296 lines (295 loc) 14.2 kB
/** * 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';