UNPKG

playcanvas

Version:

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

230 lines (227 loc) 11.4 kB
import { Debug } from '../../core/debug.js'; import { Vec2 } from '../../core/math/vec2.js'; import { Mat4 } from '../../core/math/mat4.js'; import { Vec3 } from '../../core/math/vec3.js'; import { Quat } from '../../core/math/quat.js'; import { RenderPass } from '../../platform/graphics/render-pass.js'; import { DebugGraphics } from '../../platform/graphics/debug-graphics.js'; import { PIXELFORMAT_RGBA32U } from '../../platform/graphics/constants.js'; import { Texture } from '../../platform/graphics/texture.js'; import { TextureUtils } from '../../platform/graphics/texture-utils.js'; /** * @import { GSplatInfo } from './gsplat-info.js' * @import { GraphNode } from '../graph-node.js' * @import { RenderTarget } from '../../platform/graphics/render-target.js' * @import { GSplatWorkBuffer } from './gsplat-work-buffer.js' */ const _viewMat = new Mat4(); const _modelScale = new Vec3(); const _modelRotation = new Quat(); const _tmpSize = new Vec2(); const _whiteColor = [ 1, 1, 1 ]; /** * A render pass used to render multiple gsplats to a work buffer render target. * * @ignore */ class GSplatWorkBufferRenderPass extends RenderPass { destroy() { this.splats.length = 0; this._subDrawTexture.destroy(); super.destroy(); } /** * Initialize the render pass with the specified render target. * * @param {RenderTarget} renderTarget - The target to render to. */ init(renderTarget) { super.init(renderTarget); this.colorOps.clear = false; this.depthStencilOps.clearDepth = false; } /** * Update the render pass with splats to render and camera. * * @param {GSplatInfo[]} splats - Array of GSplatInfo objects to render. * @param {GraphNode} cameraNode - The camera node for rendering. * @param {number[][]|undefined} colorsByLod - Optional array of RGB colors per LOD index. * @param {Set<number>|null} [changedAllocIds] - Set of changed allocIds for partial render. * @returns {boolean} True if there are splats to render, false otherwise. */ update(splats, cameraNode, colorsByLod, changedAllocIds = null) { this.splats.length = 0; this._partialData.length = 0; this.colorsByLod = colorsByLod; const textureWidth = this.workBuffer.textureSize; if (changedAllocIds) { // Ensure shared sub-draw texture has enough capacity (grow-only) const requiredCapacity = changedAllocIds.size * 3; if (this._subDrawTexture.width * this._subDrawTexture.height < requiredCapacity) { TextureUtils.calcTextureSize(requiredCapacity, _tmpSize); this._subDrawTexture.resize(_tmpSize.x, _tmpSize.y); } const texData = /** @type {Uint32Array} */ this._subDrawTexture.lock(); let writeOffset = 0; for(let i = 0; i < splats.length; i++){ const splatInfo = splats[i]; if (splatInfo.activeSplats <= 0) continue; const intervals = splatInfo.intervals; const numIntervals = intervals.length / 2; if (numIntervals === 0) { // Non-octree: render using splat's own sub-draws if changed if (changedAllocIds.has(splatInfo.allocId)) { this.splats.push(splatInfo); this._partialData.push(0, 0); } } else { // Octree: write sub-draws for changed intervals into shared texture const baseOffset = writeOffset; const allocIds = splatInfo.intervalAllocIds; for(let j = 0; j < numIntervals; j++){ if (changedAllocIds.has(allocIds[j])) { writeOffset = splatInfo.appendSubDraws(texData, writeOffset, intervals[j * 2], intervals[j * 2 + 1] - intervals[j * 2], splatInfo.intervalOffsets[j], textureWidth); } } const count = writeOffset - baseOffset; if (count > 0) { this.splats.push(splatInfo); this._partialData.push(baseOffset, count); } } } this._subDrawTexture.unlock(); } else { // Full rebuild: all active splats, no partial data for(let i = 0; i < splats.length; i++){ const splatInfo = splats[i]; if (splatInfo.activeSplats > 0) { this.splats.push(splatInfo); this._partialData.push(0, 0); } } } // Lazily create per-splat sub-draw textures only for splats that will use them // (those not using the shared partial texture, i.e. _partialData count === 0). for(let i = 0; i < this.splats.length; i++){ if (this._partialData[i * 2 + 1] === 0) { this.splats[i].ensureSubDrawTexture(textureWidth); } } this.cameraNode = cameraNode; return this.splats.length > 0; } execute() { const { device, splats, cameraNode, _partialData } = this; DebugGraphics.pushGpuMarker(device, 'GSplatWorkBuffer'); // Set up render state device.setDrawStates(); // view matrix const viewInvMat = cameraNode.getWorldTransform(); const viewMat = _viewMat.copy(viewInvMat).invert(); device.scope.resolve('matrix_view').setValue(viewMat.data); // render each splat info for(let i = 0; i < splats.length; i++){ const count = _partialData[i * 2 + 1]; if (count > 0) { // Partial render using shared sub-draw texture with base offset this.renderSplat(splats[i], this._subDrawTexture, count, _partialData[i * 2]); } else { this.renderSplat(splats[i]); } } DebugGraphics.popGpuMarker(device); } /** * Render a single splat info object. Optionally renders only a subset of sub-draws * using an override texture and count (for partial work buffer updates). * * @param {GSplatInfo} splatInfo - The splat info to render. * @param {Texture} [overrideSubDrawTexture] - Override sub-draw texture for partial renders. * @param {number} [overrideSubDrawCount] - Override sub-draw count for partial renders. * @param {number} [subDrawBase] - Base offset into the sub-draw texture. */ renderSplat(splatInfo, overrideSubDrawTexture, overrideSubDrawCount, subDrawBase = 0) { const { device, resource } = splatInfo; const scope = device.scope; Debug.assert(resource); const subDrawTexture = overrideSubDrawTexture ?? splatInfo.subDrawTexture; const subDrawCount = overrideSubDrawCount ?? splatInfo.subDrawCount; // Get work buffer modifier (live from placement, not a snapshot copy) const workBufferModifier = splatInfo.getWorkBufferModifier?.() ?? null; // Get format info directly from resource (always current, not snapshotted) const formatHash = resource.format.hash; const formatDeclarations = resource.format.getInputDeclarations(); // quad renderer and material are cached in the resource const workBufferRenderInfo = resource.getWorkBufferRenderInfo(this.colorOnly, workBufferModifier, formatHash, formatDeclarations, this.workBuffer.format); // Assign material properties to scope workBufferRenderInfo.material.setParameters(device); // Colorize by LOD using provided colors; use index 0 as fallback for non-LOD splats const color = this.colorsByLod?.[splatInfo.lodIndex] ?? this.colorsByLod?.[0] ?? _whiteColor; scope.resolve('uColorMultiply').setValue(color); // Decompose model matrix into scale and rotation const worldTransform = splatInfo.node.getWorldTransform(); worldTransform.getScale(_modelScale); _modelRotation.setFromMat4(worldTransform); // Ensure w positive for sqrt reconstruction if (_modelRotation.w < 0) { _modelRotation.mulScalar(-1); } // set as uniforms this._modelScaleData[0] = _modelScale.x; this._modelScaleData[1] = _modelScale.y; this._modelScaleData[2] = _modelScale.z; this._modelRotationData[0] = _modelRotation.x; this._modelRotationData[1] = _modelRotation.y; this._modelRotationData[2] = _modelRotation.z; this._modelRotationData[3] = _modelRotation.w; scope.resolve('matrix_model').setValue(worldTransform.data); scope.resolve('model_scale').setValue(this._modelScaleData); scope.resolve('model_rotation').setValue(this._modelRotationData); // Set placement ID for picking (unconditionally - cheap even if shader doesn't use it) scope.resolve('uId').setValue(splatInfo.placementId); // Apply per-instance shader parameters if (splatInfo.parameters) { for (const param of splatInfo.parameters.values()){ param.scopeId.setValue(param.data); } } // Bind instance textures if available (fetched live from placement) const instanceStreams = splatInfo.getInstanceStreams?.(); if (instanceStreams) { // Sync to ensure textures exist for any newly added streams instanceStreams.syncWithFormat(splatInfo.resource.format); for (const [name, texture] of instanceStreams.textures){ scope.resolve(name).setValue(texture); } } // Instanced draw: one quad per sub-draw row-segment scope.resolve('uSubDrawData').setValue(subDrawTexture); scope.resolve('uSubDrawBase').setValue(subDrawBase); const ts = this.workBuffer.textureSize; this._textureSize[0] = ts; this._textureSize[1] = ts; scope.resolve('uTextureSize').setValue(this._textureSize); workBufferRenderInfo.quadRender.render(undefined, undefined, subDrawCount); } constructor(device, workBuffer, colorOnly = false){ super(device), /** * Array of GSplatInfo objects to render in this pass. * * @type {GSplatInfo[]} */ this.splats = [], /** @type {number[][]|undefined} */ this.colorsByLod = undefined, /** * The camera node used for rendering. * * @type {GraphNode} */ this.cameraNode = /** @type {any} */ null, /** @type {Float32Array} */ this._modelScaleData = new Float32Array(3), /** @type {Float32Array} */ this._modelRotationData = new Float32Array(4), /** @type {Int32Array} */ this._textureSize = new Int32Array(2), /** * Flat array of interleaved [baseOffset, count] pairs, parallel to this.splats. * For splat at index i: _partialData[i*2] = base offset into _subDrawTexture, * _partialData[i*2+1] = sub-draw count (0 means use splat's own sub-draws). * * @type {number[]} */ this._partialData = []; this.workBuffer = workBuffer; this.colorOnly = colorOnly; this._subDrawTexture = Texture.createDataTexture2D(device, 'GsplatSubDrawData', 1, 1, PIXELFORMAT_RGBA32U); } } export { GSplatWorkBufferRenderPass };