UNPKG

playcanvas

Version:

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

334 lines (333 loc) 13.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 { Mat4 } from "../../core/math/mat4.js"; import { SEMANTIC_POSITION, CULLFACE_NONE } from "../../platform/graphics/constants.js"; import { BLEND_NONE, BLEND_PREMULTIPLIED, BLEND_ADDITIVE, GSPLAT_FORWARD, SHADOWCAMERA_NAME } from "../constants.js"; import { ShaderMaterial } from "../materials/shader-material.js"; import { GSplatResourceBase } from "../gsplat/gsplat-resource-base.js"; import { MeshInstance } from "../mesh-instance.js"; import { GSplatRenderer } from "./gsplat-renderer.js"; import { CACHE_STRIDE } from "./gsplat-projector-constants.js"; import { Camera } from "../camera.js"; const _invProjMat = new Mat4(); const _shaderProjMat = new Mat4(); class GSplatHybridRenderer extends GSplatRenderer { /** * @param {GraphicsDevice} device - The graphics device. * @param {GraphNode} node - The graph node. * @param {GraphNode} cameraNode - The camera node. * @param {Layer} layer - The layer to add mesh instances to. * @param {GSplatWorkBuffer} workBuffer - The work buffer (kept for parent compatibility; * the hybrid renderer does not bind work-buffer textures itself). */ constructor(device, node, cameraNode, layer, workBuffer) { super(device, node, cameraNode, layer, workBuffer); /** @type {ShaderMaterial} */ __publicField(this, "_material"); /** @type {MeshInstance} */ __publicField(this, "meshInstance"); /** @type {ShaderMaterial|null} */ __publicField(this, "_pickMaterial", null); /** @type {MeshInstance|null} */ __publicField(this, "_pickMeshInstance", null); /** * Per-camera `clipToViewZ` value for the forward material. Persistent: the GPU * upload happens at draw time, so the buffer must outlive `setHybridSortedRendering`. * * @type {Float32Array} */ __publicField(this, "_clipToViewZ", new Float32Array(4)); /** * Per-camera `clipToViewZ` value for the pick material. Allocated on first * `prepareForPicking` call and reused thereafter. * * @type {Float32Array|null} */ __publicField(this, "_clipToViewZPick", null); /** @type {number} */ __publicField(this, "originalBlendType", BLEND_ADDITIVE); /** @type {Set<string>} */ __publicField(this, "_internalDefines", /* @__PURE__ */ new Set()); /** @type {boolean} */ __publicField(this, "forceCopyMaterial", true); this._material = new ShaderMaterial({ uniqueName: "UnifiedSplatHybridMaterial", vertexWGSL: '#include "gsplatHybridVS"', fragmentWGSL: '#include "gsplatPS"', attributes: { vertex_position: SEMANTIC_POSITION } }); this._material.setDefine("{GSPLAT_INSTANCE_SIZE}", GSplatResourceBase.instanceSize); this._material.setDefine("{CACHE_STRIDE}", CACHE_STRIDE); this.configureMaterial(); this._material.defines.forEach((value, key) => { this._internalDefines.add(key); }); this._internalDefines.add("{GSPLAT_INSTANCE_SIZE}"); this._internalDefines.add("{CACHE_STRIDE}"); this._internalDefines.add("GSPLAT_UNIFIED_ID"); this._internalDefines.add("PICK_CUSTOM_ID"); this._internalDefines.add("GSPLAT_OVERDRAW"); this._internalDefines.add("GSPLAT_NO_FOG"); this.meshInstance = this.createMeshInstance(); } /** * Sets the render mode. The hybrid path does not add shadow casters; shadow cameras * need their own projection cache and remain unsupported here. * * @param {number} renderMode - Bitmask flags controlling render passes. */ setRenderMode(renderMode) { const oldRenderMode = this.renderMode ?? 0; const wasForward = (oldRenderMode & GSPLAT_FORWARD) !== 0; const isForward = (renderMode & GSPLAT_FORWARD) !== 0; if (wasForward && !isForward) { this.layer.removeMeshInstances([this.meshInstance], true); } if (!wasForward && isForward) { this.layer.addMeshInstances([this.meshInstance], true); } super.setRenderMode(renderMode); } destroy() { if (this.renderMode && this.renderMode & GSPLAT_FORWARD) { this.layer.removeMeshInstances([this.meshInstance], true); } this._material.destroy(); this._pickMaterial?.destroy(); this.meshInstance.destroy(); this._pickMeshInstance?.destroy(); super.destroy(); } get material() { return this._material; } onWorkBufferFormatChanged() { this.configureMaterial(); } configureMaterial() { this._material.setDefine("SH_BANDS", "0"); this._material.setDefine("GSPLAT_INDIRECT_DRAW", true); this._updateIdDefines(this._material); const dither = false; this._material.setDefine(`DITHER_${dither ? "BLUENOISE" : "NONE"}`, ""); this._material.cull = CULLFACE_NONE; this._material.blendType = dither ? BLEND_NONE : BLEND_PREMULTIPLIED; this._material.depthWrite = !!dither; this._material.update(); } update(count, textureSize) { if (this.meshInstance.instancingCount <= 0) { this.meshInstance.instancingCount = 1; } this.meshInstance.visible = count > 0; } /** * Configures the renderer to draw from the projector's caches. * * @param {number} drawSlot - The indirect draw slot index. * @param {StorageBuffer} sortedIndices - Globally-sorted indices into projCache. * @param {StorageBuffer} projCache - Per-splat projection cache produced by the projector. * @param {StorageBuffer} numSplatsBuffer - GPU-written visible-splat count. */ setHybridSortedRendering(drawSlot, sortedIndices, projCache, numSplatsBuffer) { this.meshInstance.setIndirect(null, drawSlot, 1); this._material.setParameter("sortedIndices", sortedIndices); this._material.setParameter("projCache", projCache); this._material.setParameter("numSplatsStorage", numSplatsBuffer); this._computeClipToViewZ(this.cameraNode, this._clipToViewZ); this._material.setParameter("clipToViewZ", this._clipToViewZ); this.meshInstance.visible = true; if (this.meshInstance.instancingCount <= 0) { this.meshInstance.instancingCount = 1; } } /** * Configures and returns a transient pick mesh instance for the picker render pass. * * @param {number} drawSlot - The indirect draw slot index. * @param {StorageBuffer} sortedIndices - Globally-sorted indices into projCache. * @param {StorageBuffer} projCache - Per-splat projection cache produced by the projector. * @param {StorageBuffer} numSplatsBuffer - GPU-written visible-splat count. * @param {number} alphaClip - Fragment alpha threshold for picking. * @param {number} alphaClipForward - Forward alpha floor (must match {@link GSplatRenderer#frameUpdate}). * @param {GraphNode} cameraNode - The picker camera node, used to derive the * `clipToViewZ` reconstruction uniform. * @returns {MeshInstance} The pick mesh instance. */ prepareForPicking(drawSlot, sortedIndices, projCache, numSplatsBuffer, alphaClip, alphaClipForward, cameraNode) { if (!this._pickMaterial) { this._pickMaterial = new ShaderMaterial({ uniqueName: "UnifiedSplatHybridPickMaterial", vertexWGSL: '#include "gsplatHybridVS"', fragmentWGSL: '#include "gsplatPS"', attributes: { vertex_position: SEMANTIC_POSITION } }); this._pickMaterial.setDefine("{GSPLAT_INSTANCE_SIZE}", GSplatResourceBase.instanceSize); this._pickMaterial.setDefine("{CACHE_STRIDE}", CACHE_STRIDE); this._pickMaterial.setDefine("SH_BANDS", "0"); this._pickMaterial.setDefine("GSPLAT_INDIRECT_DRAW", true); this._pickMaterial.setDefine("DITHER_NONE", ""); this._updateIdDefines(this._pickMaterial); this._pickMaterial.cull = CULLFACE_NONE; this._pickMaterial.blendType = BLEND_NONE; this._pickMaterial.depthWrite = false; this._pickMaterial.update(); const mesh = GSplatResourceBase.createMesh(this.device); this._pickMeshInstance = new MeshInstance(mesh, this._pickMaterial); this._pickMeshInstance.node = this.node; this._pickMeshInstance.setInstancing(true, true); this._pickMeshInstance.instancingCount = 1; } else { if (this._updateIdDefines(this._pickMaterial)) { this._pickMaterial.update(); } } const pickMaterial = ( /** @type {ShaderMaterial} */ this._pickMaterial ); const pickMeshInstance = ( /** @type {MeshInstance} */ this._pickMeshInstance ); pickMeshInstance.setIndirect(null, drawSlot, 1); pickMaterial.setParameter("sortedIndices", sortedIndices); pickMaterial.setParameter("projCache", projCache); pickMaterial.setParameter("numSplatsStorage", numSplatsBuffer); pickMaterial.setParameter("alphaClip", alphaClip); pickMaterial.setParameter("alphaClipForward", alphaClipForward); this._clipToViewZPick ?? (this._clipToViewZPick = new Float32Array(4)); this._computeClipToViewZ(cameraNode, this._clipToViewZPick); pickMaterial.setParameter("clipToViewZ", this._clipToViewZPick); return pickMeshInstance; } /** * Computes the per-camera `clipToViewZ` value into `dst`. The hybrid VS dot-products * this with the cached `clipPos` to recover linear view depth, used by fog / overdraw * / prepass. * * - Perspective + orthographic: `dst = -inverse(matrix_projection)[row 2]`. The * projector stores `clipPos.w` in slot [3] and the dot-product yields `-view.z`. * - Fisheye: `dst = (0, 0, far - near, near)`. The projector stores depthNdc in * slot [2] and `1.0` in slot [3], so the dot-product reduces to * `depthNdc * (far - near) + near`, which equals linear `-view.z`. * * The destination buffer must be retained by the caller (typically a per-material * instance field) because the GPU upload happens at draw time. * * @param {GraphNode} cameraNode - Camera node to derive the uniform from. * @param {Float32Array} dst - 4-element destination, written in place. * @private */ _computeClipToViewZ(cameraNode, dst) { const camComp = cameraNode.camera; const cam = camComp.camera; if (this.fisheyeProj.enabled) { const near = cam.nearClip; const far = cam.farClip; dst[0] = 0; dst[1] = 0; dst[2] = far - near; dst[3] = near; return; } const flipY = !!camComp.renderTarget?.flipY; _invProjMat.copy(Camera.applyShaderProjectionTransform(cam.projectionMatrix, _shaderProjMat, flipY, this.device.isWebGPU)).invert(); const d = _invProjMat.data; dst[0] = -d[2]; dst[1] = -d[6]; dst[2] = -d[10]; dst[3] = -d[14]; } /** * The hybrid path is GPU-driven only; CPU-sort is handled by the quad renderer * (the manager swaps renderers when the active mode changes). */ setCpuSortedRendering() { this.meshInstance.setIndirect(null, -1); this.meshInstance.visible = false; } setOrderData() { } frameUpdate(params) { this._material.setParameter("alphaClip", params.alphaClip); this._material.setParameter("alphaClipForward", params.alphaClipForward); this._pickMaterial?.setParameter("alphaClip", params.alphaClip); this._pickMaterial?.setParameter("alphaClipForward", params.alphaClipForward); if (params.colorRamp) { this._material.setParameter("colorRampIntensity", params.colorRampIntensity); } const noFog = !params.useFog; if (noFog !== this._lastNoFog) { this._lastNoFog = noFog; this._material.setDefine("GSPLAT_NO_FOG", noFog); this._material.update(); } } /** * Updates pick ID defines to match the work-buffer format. * * @param {ShaderMaterial} material - Material to update. * @returns {boolean} True if the material defines changed. * @private */ _updateIdDefines(material) { const hasPcId = !!this.workBuffer.format.getStream("pcId"); const changed = material.getDefine("GSPLAT_UNIFIED_ID") !== hasPcId || material.getDefine("PICK_CUSTOM_ID") !== hasPcId; material.setDefine("GSPLAT_UNIFIED_ID", hasPcId); material.setDefine("PICK_CUSTOM_ID", hasPcId); return changed; } updateOverdrawMode(params) { const overdrawEnabled = !!params.colorRamp; const wasOverdrawEnabled = this._material.getDefine("GSPLAT_OVERDRAW"); if (overdrawEnabled) { this._material.setParameter("colorRamp", params.colorRamp); this._material.setParameter("colorRampIntensity", params.colorRampIntensity); } if (overdrawEnabled !== wasOverdrawEnabled) { this._material.setDefine("GSPLAT_OVERDRAW", overdrawEnabled); if (overdrawEnabled) { this.originalBlendType = this._material.blendType; this._material.blendType = BLEND_ADDITIVE; } else { this._material.blendType = this.originalBlendType; } this._material.update(); } } createMeshInstance() { const mesh = GSplatResourceBase.createMesh(this.device); const meshInstance = new MeshInstance(mesh, this._material); meshInstance.node = this.node; meshInstance.setInstancing(true, true); meshInstance.instancingCount = 0; meshInstance.pick = false; const thisCamera = this.cameraNode.camera; meshInstance.isVisibleFunc = (camera) => { const renderMode = this.renderMode ?? 0; if (thisCamera.camera === camera && renderMode & GSPLAT_FORWARD) { return true; } if (camera.node?.name === SHADOWCAMERA_NAME) { return false; } return false; }; return meshInstance; } } export { GSplatHybridRenderer };