UNPKG

playcanvas

Version:

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

634 lines (633 loc) 27.8 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Debug, DebugHelper } from "../../core/debug.js"; import { Vec2 } from "../../core/math/vec2.js"; import { Compute } from "../../platform/graphics/compute.js"; import { Shader } from "../../platform/graphics/shader.js"; import { StorageBuffer } from "../../platform/graphics/storage-buffer.js"; import { BindGroupFormat, BindStorageBufferFormat, BindUniformBufferFormat } from "../../platform/graphics/bind-group-format.js"; import { UniformBufferFormat, UniformFormat } from "../../platform/graphics/uniform-buffer-format.js"; import { BUFFERUSAGE_COPY_DST, CULLFACE_NONE, PIXELFORMAT_RGBA16U, SEMANTIC_POSITION, SHADERLANGUAGE_WGSL, SHADERSTAGE_COMPUTE, UNIFORMTYPE_FLOAT, UNIFORMTYPE_UINT, UNIFORMTYPE_VEC4 } from "../../platform/graphics/constants.js"; import { BLEND_PREMULTIPLIED, LIGHTTYPE_DIRECTIONAL } from "../constants.js"; import { ShaderMaterial } from "../materials/shader-material.js"; import { MeshInstance } from "../mesh-instance.js"; import { GSplatResourceBase } from "../gsplat/gsplat-resource-base.js"; import { computeGsplatShadowCullSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-shadow-cull.js"; import { computeGsplatShadowIndirectArgsSource } from "../shader-lib/wgsl/chunks/gsplat/compute-gsplat-shadow-indirect-args.js"; import computeSplatSource from "../shader-lib/wgsl/chunks/gsplat/vert/gsplatComputeSplat.js"; import gsplatModifyDefaultSource from "../shader-lib/wgsl/chunks/gsplat/vert/gsplatModify.js"; import gsplatHelpersSource from "../shader-lib/wgsl/chunks/gsplat/vert/gsplatHelpers.js"; import { GSplatIntervalCompaction } from "./gsplat-interval-compaction.js"; const WORKGROUP_SIZE = 256; const INDEX_COUNT = 6 * GSplatResourceBase.instanceSize; 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, node, cameraNode, layer, world, scratch = null) { /** @type {GraphicsDevice} */ __publicField(this, "device"); /** @type {GraphNode} */ __publicField(this, "node"); /** @type {GraphNode} */ __publicField(this, "cameraNode"); /** @type {Layer} */ __publicField(this, "layer"); /** @type {GSplatWorld} */ __publicField(this, "world"); /** * Per-light draw entries, keyed by light. * * @type {Map<Light, ShadowLightEntry>} */ __publicField(this, "entries", /* @__PURE__ */ new Map()); /** * Reused scratch set of the qualifying directional shadow lights, rebuilt each {@link syncLights} * to diff against {@link entries}. * * @type {Set<Light>} * @private */ __publicField(this, "_desiredLights", /* @__PURE__ */ new Set()); /** * 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} */ __publicField(this, "_compaction", null); /** @type {Vec2} */ __publicField(this, "_cullDispatchSize", new Vec2(1, 1)); /** * Reused light frustum planes (6 × vec4(normal, distance)) for the cull uniform, refilled per * light entry from its shadow camera. * * @type {Float32Array} */ __publicField(this, "_frustumPlanes", new Float32Array(24)); /** * 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 */ __publicField(this, "_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 */ __publicField(this, "_userModifyWgsl", null); // The cull/args shaders are shared, but each light entry gets its OWN Compute instances // (created in _createEntry). A Compute owns a persistent uniform buffer, so a single shared // Compute dispatched once per light per frame would have all dispatches read the last-written // uniforms (frustum planes / draw slot) — making all but one light's shadow draw empty. /** @type {Shader|null} */ __publicField(this, "_cullShader", null); /** @type {BindGroupFormat|null} */ __publicField(this, "_cullBindGroupFormat", 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 */ __publicField(this, "_cullFormatVersion", -1); /** * 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 */ __publicField(this, "_cullBuiltChunksKey", null); /** * 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 */ __publicField(this, "_cullShaderGen", 0); /** @type {Shader|null} */ __publicField(this, "_argsShader", null); /** @type {BindGroupFormat|null} */ __publicField(this, "_argsBindGroupFormat", null); this.device = device; this.node = node; this.cameraNode = cameraNode; this.layer = layer; this.world = world; this._compaction = new GSplatIntervalCompaction(device, scratch); this._createArgsShader(); } destroy() { this.entries.forEach((entry) => this._destroyEntry(entry)); this.entries.clear(); this._compaction?.destroy(); this._compaction = null; this._cullShader?.destroy(); this._cullBindGroupFormat?.destroy(); this._argsShader?.destroy(); this._argsBindGroupFormat?.destroy(); this._cullShader = null; this._argsShader = null; } /** * (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 */ _ensureCullShader() { const wbFormat = this.world.workBuffer.format; const version = wbFormat.extraStreamsVersion; if (!this._cullShader || version !== this._cullFormatVersion || this._userChunksKey !== this._cullBuiltChunksKey) { this._cullFormatVersion = version; this._cullBuiltChunksKey = this._userChunksKey; this._buildCullShader(); } } /** * 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 */ _buildCullShader() { const device = this.device; const wbFormat = this.world.workBuffer.format; const fixedBindings = [ new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE), // pass-1 outputs (read): the shared candidate list + its count at [numIntervals] new BindStorageBufferFormat("compactedSplatIds", SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat("candidateCountBuffer", SHADERSTAGE_COMPUTE, true), // per-light outputs (read/write): the final visible list + atomic count new BindStorageBufferFormat("outputIndices", SHADERSTAGE_COMPUTE, false), new BindStorageBufferFormat("globalCount", SHADERSTAGE_COMPUTE, false) ]; this._cullBindGroupFormat?.destroy(); this._cullBindGroupFormat = new BindGroupFormat(device, [ ...fixedBindings, ...wbFormat.getComputeBindFormats() ]); const uniformBufferFormat = new UniformBufferFormat(device, [ new UniformFormat("frustumPlanes", UNIFORMTYPE_VEC4, 6), new UniformFormat("numIntervals", UNIFORMTYPE_UINT), new UniformFormat("splatTextureSize", UNIFORMTYPE_UINT), new UniformFormat("alphaClip", UNIFORMTYPE_FLOAT), new UniformFormat("worldSizeThreshold", UNIFORMTYPE_FLOAT) ]); const cincludes = /* @__PURE__ */ new Map(); cincludes.set("gsplatComputeSplatCS", computeSplatSource); cincludes.set("gsplatFormatDeclCS", wbFormat.getComputeInputDeclarations(fixedBindings.length)); cincludes.set("gsplatFormatReadCS", wbFormat.getReadCode()); cincludes.set("gsplatHelpersVS", gsplatHelpersSource); cincludes.set("gsplatModifyVS", this._userModifyWgsl ?? gsplatModifyDefaultSource); const cdefines = /* @__PURE__ */ new Map([["{WORKGROUP_SIZE}", WORKGROUP_SIZE.toString()]]); const colorStream = wbFormat.getStream("dataColor"); if (colorStream && colorStream.format !== PIXELFORMAT_RGBA16U) { cdefines.set("GSPLAT_COLOR_FLOAT", ""); } this._cullShader?.destroy(); this._cullShader = new Shader(device, { name: "GSplatShadowCull", shaderLanguage: SHADERLANGUAGE_WGSL, cshader: computeGsplatShadowCullSource, cincludes, cdefines, computeBindGroupFormat: this._cullBindGroupFormat, computeUniformBufferFormats: { uniforms: uniformBufferFormat } }); this._cullShaderGen++; } /** @private */ _createArgsShader() { const device = this.device; this._argsBindGroupFormat = new BindGroupFormat(device, [ new BindStorageBufferFormat("countBuffer", SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat("indirectDrawArgs", SHADERSTAGE_COMPUTE, false), new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE) ]); const uniformBufferFormat = new UniformBufferFormat(device, [ new UniformFormat("drawSlot", UNIFORMTYPE_UINT), new UniformFormat("indexCount", UNIFORMTYPE_UINT), new UniformFormat("pad0", UNIFORMTYPE_UINT), new UniformFormat("pad1", UNIFORMTYPE_UINT) ]); this._argsShader = new Shader(device, { name: "GSplatShadowIndirectArgs", shaderLanguage: SHADERLANGUAGE_WGSL, cshader: computeGsplatShadowIndirectArgsSource, cdefines: /* @__PURE__ */ new Map([["{INSTANCE_SIZE}", GSplatResourceBase.instanceSize.toString()]]), computeBindGroupFormat: this._argsBindGroupFormat, computeUniformBufferFormats: { uniforms: uniformBufferFormat } }); } /** * 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) { this._compaction?.invalidateUpload(); this._cullFormatVersion = -1; this.entries.forEach((entry) => { this._configureMaterialWorkBuffer(entry.material); entry.material.update(); }); } /** * 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) { if (!aabb) return; this.entries.forEach((entry) => { entry.meshInstance.setCustomAabb(aabb); }); } /** * 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() { const lights = this.layer.splitLights[LIGHTTYPE_DIRECTIONAL]; const desired = this._desiredLights; desired.clear(); for (let i = 0; i < lights.length; i++) { const light = lights[i]; if (!light.enabled || !light.castShadows) continue; if (light.numCascades !== 1) { Debug.warnOnce("GSplatShadowRenderer: cascaded directional shadows are not supported for gsplats; the light will not cast a gsplat shadow."); continue; } desired.add(light); } this.entries.forEach((entry, light) => { if (!desired.has(light)) { this._destroyEntry(entry); this.entries.delete(light); } }); desired.forEach((light) => { if (!this.entries.has(light)) { this.entries.set(light, this._createEntry(light)); } }); } /** * 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) { const worldState = this.world.getState(this.world.currentVersion); const ready = worldState && worldState.sortedBefore && worldState.totalActiveSplats > 0; if (!ready) { this.entries.forEach((entry) => { entry.meshInstance.visible = false; }); return; } this._compaction.uploadIntervals(worldState); this.world.workBuffer.frustumCuller.updateTransformsData(worldState.boundsGroups); this._syncUserModify(gsplatParams); this._ensureCullShader(); const numIntervals = worldState.totalIntervals; const totalActiveSplats = worldState.totalActiveSplats; const textureSize = this.world.workBuffer.textureSize; this.entries.forEach((entry) => { this._cullEntry(entry, numIntervals, totalActiveSplats, textureSize, gsplatParams); }); } /** * 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 */ _syncUserModify(gsplatParams) { const userMat = gsplatParams.material; if (!userMat) return; const chunksKey = userMat.shaderChunks?.key ?? ""; if (chunksKey !== this._userChunksKey) { this._userChunksKey = chunksKey; this._userModifyWgsl = userMat.getShaderChunks?.("wgsl")?.get("gsplatModifyVS") ?? null; this.entries.forEach((entry) => this._applyUserModify(entry)); } const params = userMat.parameters; this.entries.forEach((entry) => { for (const name in params) { if (params.hasOwnProperty(name)) { entry.material.setParameter(name, params[name].data); } } }); } /** * 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 */ _applyUserModify(entry) { const wgsl = entry.material.shaderChunks.wgsl; if (this._userModifyWgsl) { wgsl.set("gsplatModifyVS", this._userModifyWgsl); } else { wgsl.delete("gsplatModifyVS"); } entry.material.update(); } /** * 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 */ _cullEntry(entry, numIntervals, totalActiveSplats, textureSize, gsplatParams) { const device = this.device; const sceneCamera = this.cameraNode.camera?.camera; const shadowCamera = sceneCamera && entry.light.getRenderData(sceneCamera, 0).shadowCamera; const frustum = shadowCamera && shadowCamera.frustum; const frustumCuller = this.world.workBuffer.frustumCuller; if (!frustum || !frustumCuller?.boundsBuffer || !frustumCuller?.transformsBuffer) { entry.meshInstance.visible = false; return; } this._fillFrustumPlanes(frustum); const orthoHeight = shadowCamera.orthoHeight; const shadowRes = entry.light._shadowResolution; const minPixelSize = gsplatParams.minPixelSize; const focal = orthoHeight > 0 && shadowRes > 0 ? shadowRes / orthoHeight : 0; const t2 = minPixelSize * minPixelSize * 0.5 - 0.3; const worldSizeThreshold = focal > 0 && t2 > 0 ? Math.sqrt(t2) / focal : 0; if (totalActiveSplats > entry.allocatedIndexCount) { entry.indexBuffer?.destroy(); entry.allocatedIndexCount = totalActiveSplats; entry.indexBuffer = new StorageBuffer(device, totalActiveSplats * 4); DebugHelper.setName(entry.indexBuffer, "GSplatShadow.indices"); } const compaction = this._compaction; compaction.dispatchCompact({ boundsBuffer: frustumCuller.boundsBuffer, transformsBuffer: frustumCuller.transformsBuffer, frustumPlanes: this._frustumPlanes }, numIntervals, totalActiveSplats, false); entry.countBuffer.clear(); if (!entry.cullCompute || entry.cullComputeGen !== this._cullShaderGen) { entry.cullCompute?.destroy(); entry.cullCompute = new Compute(device, this._cullShader, "GSplatShadowCull"); entry.cullComputeGen = this._cullShaderGen; } const cull = entry.cullCompute; const userMat = gsplatParams.material; if (userMat) { const srcParams = userMat.parameters; for (const name in srcParams) { if (srcParams.hasOwnProperty(name)) { cull.setParameter(name, srcParams[name].data); } } } cull.setParameter("compactedSplatIds", compaction.compactedSplatIds); cull.setParameter("candidateCountBuffer", compaction.countBuffer); cull.setParameter("outputIndices", entry.indexBuffer); cull.setParameter("globalCount", entry.countBuffer); cull.setParameter("frustumPlanes[0]", this._frustumPlanes); cull.setParameter("numIntervals", numIntervals); cull.setParameter("splatTextureSize", textureSize); cull.setParameter("alphaClip", gsplatParams.alphaClip); cull.setParameter("worldSizeThreshold", worldSizeThreshold); const workBuffer = this.world.workBuffer; for (const stream of workBuffer.format.resourceStreams) { const texture = workBuffer.getTexture(stream.name); if (texture) { cull.setParameter(stream.name, texture); } } const workgroupCount = Math.ceil(totalActiveSplats / WORKGROUP_SIZE); Compute.calcDispatchSize(workgroupCount, this._cullDispatchSize, device.limits.maxComputeWorkgroupsPerDimension || 65535); cull.setupDispatch(this._cullDispatchSize.x, this._cullDispatchSize.y, 1); device.computeDispatch([cull], "GSplatShadowCull"); const drawSlot = device.getIndirectDrawSlot(1); const args = entry.argsCompute; args.setParameter("countBuffer", entry.countBuffer); args.setParameter("indirectDrawArgs", device.indirectDrawBuffer); args.setParameter("drawSlot", drawSlot); args.setParameter("indexCount", INDEX_COUNT); args.setParameter("pad0", 0); args.setParameter("pad1", 0); args.setupDispatch(1); device.computeDispatch([args], "GSplatShadowIndirectArgs"); const material = entry.material; entry.meshInstance.setIndirect(null, drawSlot, 1); material.setParameter("compactedSplatIds", entry.indexBuffer); material.setParameter("numSplatsStorage", entry.countBuffer); material.setParameter("splatTextureSize", textureSize); material.setParameter("alphaClip", gsplatParams.alphaClip); entry.meshInstance.visible = true; if (entry.meshInstance.instancingCount <= 0) { entry.meshInstance.instancingCount = 1; } } /** * 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 */ _fillFrustumPlanes(frustum) { const p = this._frustumPlanes; for (let i = 0; i < 6; i++) { const plane = frustum.planes[i]; p[i * 4 + 0] = plane.normal.x; p[i * 4 + 1] = plane.normal.y; p[i * 4 + 2] = plane.normal.z; p[i * 4 + 3] = plane.distance; } } /** * 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 */ _createEntry(light) { const device = this.device; const material = this._createMaterial(); const meshInstance = this._createMeshInstance(light, material); meshInstance.castShadow = true; this.layer.addShadowCasters([meshInstance]); const countBuffer = new StorageBuffer(device, 4, BUFFERUSAGE_COPY_DST); DebugHelper.setName(countBuffer, "GSplatShadow.count"); const argsCompute = new Compute(device, this._argsShader, "GSplatShadowIndirectArgs"); const entry = { light, material, meshInstance, indexBuffer: null, allocatedIndexCount: 0, countBuffer, cullCompute: null, cullComputeGen: -1, argsCompute }; this._applyUserModify(entry); return entry; } /** * Tears down a light entry: unregisters the caster and frees its GPU resources. * * @param {ShadowLightEntry} entry - The entry to destroy. * @private */ _destroyEntry(entry) { this.layer.removeShadowCasters([entry.meshInstance]); entry.meshInstance.destroy(); entry.material.destroy(); entry.indexBuffer?.destroy(); entry.countBuffer.destroy(); entry.cullCompute?.destroy(); entry.argsCompute.destroy(); } /** * 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 */ _createMaterial() { const material = new ShaderMaterial({ uniqueName: "GSplatShadowMaterial", vertexGLSL: '#include "gsplatVS"', fragmentGLSL: '#include "gsplatPS"', vertexWGSL: '#include "gsplatVS"', fragmentWGSL: '#include "gsplatPS"', attributes: { vertex_position: SEMANTIC_POSITION } }); material.setDefine("{GSPLAT_INSTANCE_SIZE}", GSplatResourceBase.instanceSize); material.setDefine("SH_BANDS", "0"); material.setDefine("GSPLAT_SEPARATE_OPACITY", ""); material.setDefine("DITHER_NONE", ""); material.setDefine("GSPLAT_INDIRECT_DRAW", true); this._configureMaterialWorkBuffer(material); material.cull = CULLFACE_NONE; material.blendType = BLEND_PREMULTIPLIED; material.depthWrite = false; material.update(); return material; } /** * 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 */ _configureMaterialWorkBuffer(material) { const workBuffer = this.world.workBuffer; const wbFormat = workBuffer.format; const chunks = this.device.isWebGPU ? material.shaderChunks.wgsl : material.shaderChunks.glsl; chunks.set("gsplatDeclarationsVS", wbFormat.getInputDeclarations()); chunks.set("gsplatReadVS", wbFormat.getReadCode()); const colorStream = wbFormat.getStream("dataColor"); if (colorStream && colorStream.format !== PIXELFORMAT_RGBA16U) { material.setDefine("GSPLAT_COLOR_FLOAT", ""); } const hasPcId = !!wbFormat.getStream("pcId"); material.setDefine("GSPLAT_UNIFIED_ID", hasPcId); material.setDefine("PICK_CUSTOM_ID", hasPcId); for (const stream of wbFormat.resourceStreams) { const texture = workBuffer.getTexture(stream.name); if (texture) { material.setParameter(stream.name, texture); } } } /** * 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 */ _createMeshInstance(light, material) { const mesh = GSplatResourceBase.createMesh(this.device); const meshInstance = new MeshInstance(mesh, material); meshInstance.node = this.node; meshInstance.setInstancing(true, true); meshInstance.instancingCount = 0; meshInstance.pick = false; const cameraNode = this.cameraNode; meshInstance.isVisibleFunc = (camera) => { const sceneCamera = cameraNode.camera?.camera; if (!sceneCamera) return false; return camera === light.getRenderData(sceneCamera, 0).shadowCamera; }; return meshInstance; } } export { GSplatShadowRenderer };