playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
634 lines (633 loc) • 27.8 kB
JavaScript
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
};