UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

175 lines (172 loc) 7.98 kB
import { SEMANTIC_ATTR13, SEMANTIC_POSITION, PIXELFORMAT_RGBA16U, CULLFACE_NONE } from '../../platform/graphics/constants.js'; import { BLEND_ADDITIVE, BLEND_PREMULTIPLIED } 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 { math } from '../../core/math/math.js'; /** * @import { VertexBuffer } from '../../platform/graphics/vertex-buffer.js' * @import { Layer } from '../layer.js' * @import { GraphNode } from '../graph-node.js' */ /** * Class that renders the splats from the work buffer. * * @ignore */ class GSplatRenderer { constructor(device, node, cameraNode, layer, workBuffer){ /** @type {VertexBuffer|null} */ this.instanceIndices = null; /** @type {number} */ this.instanceIndicesCount = 0; this.viewportParams = [ 0, 0 ]; /** @type {number} */ this.originalBlendType = BLEND_ADDITIVE; this.device = device; this.node = node; this.cameraNode = cameraNode; this.layer = layer; this.workBuffer = workBuffer; // construct the material which renders the splats from the work buffer this._material = new ShaderMaterial({ uniqueName: 'UnifiedSplatMaterial', vertexGLSL: '#include "gsplatVS"', fragmentGLSL: '#include "gsplatPS"', vertexWGSL: '#include "gsplatVS"', fragmentWGSL: '#include "gsplatPS"', attributes: { vertex_position: SEMANTIC_POSITION, vertex_id_attrib: SEMANTIC_ATTR13 } }); this.configureMaterial(); this.meshInstance = this.createMeshInstance(); layer.addMeshInstances([ this.meshInstance ]); } destroy() { this.layer.removeMeshInstances([ this.meshInstance ]); this._material.destroy(); this.meshInstance.destroy(); } get material() { return this._material; } configureMaterial() { const { device, workBuffer } = this; // input format this._material.setDefine('GSPLAT_WORKBUFFER_DATA', true); this._material.setDefine('STORAGE_ORDER', device.isWebGPU); // Check if using RGBA16U format (fallback for when RGBA16F not supported) const isColorUint = workBuffer.colorTextureFormat === PIXELFORMAT_RGBA16U; this._material.setDefine('GSPLAT_COLOR_UINT', isColorUint); // input textures (work buffer textures) this._material.setParameter('splatColor', workBuffer.colorTexture); this._material.setParameter('splatTexture0', workBuffer.splatTexture0); this._material.setParameter('splatTexture1', workBuffer.splatTexture1); this._material.setDefine('SH_BANDS', '0'); this._material.setParameter('numSplats', 0); // Set order data - texture for WebGL only at init time, it does not need to be updated if (workBuffer.orderTexture) { this._material.setParameter('splatOrder', workBuffer.orderTexture); } this._material.setParameter('alphaClip', 0.3); this._material.setDefine(`DITHER_${'NONE'}`, ''); this._material.cull = CULLFACE_NONE; this._material.blendType = BLEND_PREMULTIPLIED; this._material.depthWrite = false; this._material.update(); } update(count, textureSize) { // limit splat render count to exclude those behind the camera this.meshInstance.instancingCount = Math.ceil(count / GSplatResourceBase.instanceSize); // update splat count on the material this._material.setParameter('numSplats', count); this._material.setParameter('splatTextureSize', textureSize); // disable rendering if no splats to render this.meshInstance.visible = count > 0; } frameUpdate(params) { // Set the appropriate order data resource based on device type if (this.device.isWebGPU) { this._material.setParameter('splatOrder', this.workBuffer.orderBuffer); } else { this._material.setParameter('splatOrder', this.workBuffer.orderTexture); } // Update colorRampIntensity parameter every frame when overdraw is enabled if (params.colorRamp) { this._material.setParameter('colorRampIntensity', params.colorRampIntensity); } } 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) { // TODO: when overdraw mode is enabled, we could disable sorting of splats, // as additive blend mode does not require them to be sorted // Store the current blend type before switching to additive this.originalBlendType = this._material.blendType; this._material.blendType = BLEND_ADDITIVE; } else { this._material.blendType = this.originalBlendType; } this._material.update(); } } setMaxNumSplats(numSplats) { // round up to the nearest multiple of instanceSize (same as createInstanceIndices does internally) const roundedNumSplats = math.roundUp(numSplats, GSplatResourceBase.instanceSize); if (this.instanceIndicesCount < roundedNumSplats) { this.instanceIndicesCount = roundedNumSplats; // destroy old instance indices this.instanceIndices?.destroy(); // create new instance indices this.instanceIndices = GSplatResourceBase.createInstanceIndices(this.device, numSplats); this.meshInstance.setInstancing(this.instanceIndices, true); // update texture size uniform this._material.setParameter('splatTextureSize', this.workBuffer.textureSize); } } createMeshInstance() { const mesh = GSplatResourceBase.createMesh(this.device); const textureSize = this.workBuffer.textureSize; const instanceIndices = GSplatResourceBase.createInstanceIndices(this.device, textureSize * textureSize); const meshInstance = new MeshInstance(mesh, this._material); meshInstance.node = this.node; meshInstance.setInstancing(instanceIndices, true); // only start rendering the splat after we've received the splat order data meshInstance.instancingCount = 0; // custom culling to only disable rendering for matching camera // TODO: consider using aabb as well to avoid rendering off-screen splats const thisCamera = this.cameraNode.camera; meshInstance.isVisibleFunc = (camera)=>{ const vis = thisCamera.camera === camera; return vis; }; return meshInstance; } updateViewport(cameraNode) { const camera = cameraNode.camera; const cameraRect = camera.rect; const renderTarget = camera?.renderTarget; const { width, height } = renderTarget ?? this.device; const viewport = this.viewportParams; viewport[0] = width * cameraRect.z; viewport[1] = height * cameraRect.w; // adjust viewport for stereoscopic VR sessions const xr = camera?.camera?.xr; if (xr?.active && xr.views.list.length === 2) { viewport[0] *= 0.5; } this._material.setParameter('viewport', viewport); } } export { GSplatRenderer };