UNPKG

playcanvas

Version:

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

334 lines (333 loc) 12.7 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 { SEMANTIC_POSITION, CULLFACE_NONE, PIXELFORMAT_RGBA16U } from "../../platform/graphics/constants.js"; import { BLEND_NONE, BLEND_PREMULTIPLIED, BLEND_ADDITIVE, GSPLAT_FORWARD, GSPLAT_SHADOW, 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"; class GSplatQuadRenderer 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 containing splat data. */ constructor(device, node, cameraNode, layer, workBuffer) { super(device, node, cameraNode, layer, workBuffer); /** @type {ShaderMaterial} */ __publicField(this, "_material"); /** @type {MeshInstance} */ __publicField(this, "meshInstance"); /** @type {number} */ __publicField(this, "originalBlendType", BLEND_ADDITIVE); /** @type {Set<string>} */ __publicField(this, "_internalDefines", /* @__PURE__ */ new Set()); /** @type {boolean} */ __publicField(this, "forceCopyMaterial", true); /** @private */ __publicField(this, "_lastFisheyeEnabled", false); /** @private */ __publicField(this, "_lastSourceChunksKey", ""); this._material = new ShaderMaterial({ uniqueName: "UnifiedSplatMaterial", vertexGLSL: '#include "gsplatVS"', fragmentGLSL: '#include "gsplatPS"', vertexWGSL: '#include "gsplatVS"', fragmentWGSL: '#include "gsplatPS"', attributes: { vertex_position: SEMANTIC_POSITION } }); this._material.setDefine("{GSPLAT_INSTANCE_SIZE}", GSplatResourceBase.instanceSize); this.configureMaterial(); this._material.defines.forEach((value, key) => { this._internalDefines.add(key); }); this._internalDefines.add("{GSPLAT_INSTANCE_SIZE}"); this._internalDefines.add("GSPLAT_UNIFIED_ID"); this._internalDefines.add("PICK_CUSTOM_ID"); this._internalDefines.add("GSPLAT_INDIRECT_DRAW"); this._internalDefines.add("GSPLAT_SEPARATE_OPACITY"); this._internalDefines.add("GSPLAT_FISHEYE"); this.meshInstance = this.createMeshInstance(); } /** * Sets the render mode for this renderer, managing layer array membership. * * @param {number} renderMode - Bitmask flags controlling render passes (GSPLAT_FORWARD, GSPLAT_SHADOW, or both). */ setRenderMode(renderMode) { const oldRenderMode = this.renderMode ?? 0; const wasForward = (oldRenderMode & GSPLAT_FORWARD) !== 0; const wasShadow = (oldRenderMode & GSPLAT_SHADOW) !== 0; const isForward = (renderMode & GSPLAT_FORWARD) !== 0; const isShadow = (renderMode & GSPLAT_SHADOW) !== 0; this.meshInstance.castShadow = isShadow; if (wasForward && !isForward) { this.layer.removeMeshInstances([this.meshInstance], true); } if (wasShadow && !isShadow) { this.layer.removeShadowCasters([this.meshInstance]); } if (!wasForward && isForward) { this.layer.addMeshInstances([this.meshInstance], true); } if (!wasShadow && isShadow) { this.layer.addShadowCasters([this.meshInstance]); } super.setRenderMode(renderMode); } destroy() { if (this.renderMode) { if (this.renderMode & GSPLAT_FORWARD) { this.layer.removeMeshInstances([this.meshInstance], true); } if (this.renderMode & GSPLAT_SHADOW) { this.layer.removeShadowCasters([this.meshInstance]); } } this._material.destroy(); this.meshInstance.destroy(); super.destroy(); } get material() { return this._material; } onWorkBufferFormatChanged() { this.configureMaterial(); } configureMaterial() { const { workBuffer } = this; this._injectFormatChunks(); this._material.setDefine("SH_BANDS", "0"); this._material.setDefine("GSPLAT_SEPARATE_OPACITY", ""); const colorStream = workBuffer.format.getStream("dataColor"); if (colorStream && colorStream.format !== PIXELFORMAT_RGBA16U) { this._material.setDefine("GSPLAT_COLOR_FLOAT", ""); } this._updateIdDefines(); this._bindWorkBufferTextures(); const dither = false; this._material.setParameter("numSplats", 0); this.setOrderData(); 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(); } /** * Binds work buffer textures to the material. * * @private */ _bindWorkBufferTextures() { const { workBuffer } = this; for (const stream of workBuffer.format.resourceStreams) { const texture = workBuffer.getTexture(stream.name); if (texture) { this._material.setParameter(stream.name, texture); } } } /** * Injects format shader chunks into the material. * Called during initialization and after copying settings from user material. * * @private */ _injectFormatChunks() { const chunks = this.device.isWebGPU ? this._material.shaderChunks.wgsl : this._material.shaderChunks.glsl; const wbFormat = this.workBuffer.format; chunks.set("gsplatDeclarationsVS", wbFormat.getInputDeclarations()); chunks.set("gsplatReadVS", wbFormat.getReadCode()); } update(count, textureSize) { this.meshInstance.instancingCount = Math.ceil(count / GSplatResourceBase.instanceSize); this._material.setParameter("numSplats", count); this._material.setParameter("splatTextureSize", textureSize); this.meshInstance.visible = count > 0; } /** * Configures the renderer to use GPU-sorted data for rendering. * * @param {number} drawSlot - The indirect draw slot index in the device's buffer. * @param {StorageBuffer} sortedIds - Buffer containing sorted visible splat IDs. * @param {StorageBuffer} numSplatsBuffer - Buffer containing numSplats for vertex shader. * @param {number} textureSize - The work buffer texture size. */ setGpuSortedRendering(drawSlot, sortedIds, numSplatsBuffer, textureSize) { this.meshInstance.setIndirect(null, drawSlot, 1); this._material.setParameter("compactedSplatIds", sortedIds); this._material.setParameter("numSplatsStorage", numSplatsBuffer); if (!this._material.getDefine("GSPLAT_INDIRECT_DRAW")) { this._material.setDefine("GSPLAT_INDIRECT_DRAW", true); this._material.update(); } this._material.setParameter("splatTextureSize", textureSize); this.meshInstance.visible = true; if (this.meshInstance.instancingCount <= 0) { this.meshInstance.instancingCount = 1; } } /** * Switches the renderer to CPU-sorted rendering mode. */ setCpuSortedRendering() { this.meshInstance.setIndirect(null, -1); if (this._material.getDefine("GSPLAT_INDIRECT_DRAW")) { this._material.setDefine("GSPLAT_INDIRECT_DRAW", false); this._material.update(); } this.setOrderData(); this.meshInstance.visible = false; } setOrderData() { if (this.device.isWebGPU) { this._material.setParameter("splatOrder", this.workBuffer.orderBuffer); } else { this._material.setParameter("splatOrder", this.workBuffer.orderTexture); } } frameUpdate(params) { if (params.colorRamp) { this._material.setParameter("colorRampIntensity", params.colorRampIntensity); } const cam = this.cameraNode.camera; this.fisheyeProj.update(params.fisheye, cam.fov, cam.projectionMatrix); const fisheyeEnabled = this.fisheyeProj.enabled; if (fisheyeEnabled !== this._lastFisheyeEnabled) { this._lastFisheyeEnabled = fisheyeEnabled; this._material.setDefine("GSPLAT_FISHEYE", fisheyeEnabled); this._material.update(); } if (fisheyeEnabled) { const fp = this.fisheyeProj; this._material.setParameter("fisheye_k", fp.k); this._material.setParameter("fisheye_inv_k", fp.invK); this._material.setParameter("fisheye_projMat00", fp.projMat00); this._material.setParameter("fisheye_projMat11", fp.projMat11); } const noFog = !params.useFog; if (noFog !== this._lastNoFog) { this._lastNoFog = noFog; this._material.setDefine("GSPLAT_NO_FOG", noFog); this._material.update(); } this._syncWithWorkBufferFormat(); if (this.forceCopyMaterial || params.material.dirty) { this.copyMaterialSettings(params.material); this.forceCopyMaterial = false; } } /** * Updates the ID-related defines based on whether pcId stream exists. * * @private */ _updateIdDefines() { const hasPcId = !!this.workBuffer.format.getStream("pcId"); this._material.setDefine("GSPLAT_UNIFIED_ID", hasPcId); this._material.setDefine("PICK_CUSTOM_ID", hasPcId); } /** * Syncs with work buffer format when extra streams are added. * * @private */ _syncWithWorkBufferFormat() { const wbFormat = this.workBuffer.format; if (this._workBufferFormatVersion !== wbFormat.extraStreamsVersion) { this._workBufferFormatVersion = wbFormat.extraStreamsVersion; this.workBuffer.syncWithFormat(); this._injectFormatChunks(); this._bindWorkBufferTextures(); this._updateIdDefines(); this._material.update(); } } /** * Copies material settings from a source material to the internal material. * Preserves internal defines while copying user defines, parameters, and shader chunks. * * @param {ShaderMaterial} sourceMaterial - The source material to copy settings from. * @private */ copyMaterialSettings(sourceMaterial) { const keysToDelete = []; this._material.defines.forEach((value, key) => { if (!this._internalDefines.has(key) && !sourceMaterial.defines.has(key)) { keysToDelete.push(key); } }); keysToDelete.forEach((key) => this._material.setDefine(key, void 0)); sourceMaterial.defines.forEach((value, key) => { this._material.setDefine(key, value); }); const srcParams = sourceMaterial.parameters; for (const paramName in srcParams) { if (srcParams.hasOwnProperty(paramName)) { this._material.setParameter(paramName, srcParams[paramName].data); } } if (sourceMaterial.hasShaderChunks) { const sourceChunksKey = sourceMaterial.shaderChunks.key; if (sourceChunksKey !== this._lastSourceChunksKey) { this._material.shaderChunks.copy(sourceMaterial.shaderChunks); this._injectFormatChunks(); this._lastSourceChunksKey = sourceChunksKey; } } this._material.update(); } 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; const thisCamera = this.cameraNode.camera; meshInstance.isVisibleFunc = (camera) => { const renderMode = this.renderMode ?? 0; if (thisCamera.camera === camera && renderMode & GSPLAT_FORWARD) { return true; } if (renderMode & GSPLAT_SHADOW) { return camera.node?.name === SHADOWCAMERA_NAME; } return false; }; return meshInstance; } } export { GSplatQuadRenderer };