playcanvas
Version:
PlayCanvas WebGL game engine
196 lines (193 loc) • 8.46 kB
JavaScript
import { Debug } from '../../core/debug.js';
import { SEMANTIC_POSITION, PIXELFORMAT_RGBA16U, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32U, PIXELFORMAT_RG32U, BUFFERUSAGE_COPY_DST, PIXELFORMAT_R32U, ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST } from '../../platform/graphics/constants.js';
import { RenderTarget } from '../../platform/graphics/render-target.js';
import { StorageBuffer } from '../../platform/graphics/storage-buffer.js';
import { Texture } from '../../platform/graphics/texture.js';
import { UploadStream } from '../../platform/graphics/upload-stream.js';
import { QuadRender } from '../graphics/quad-render.js';
import { ShaderUtils } from '../shader-lib/shader-utils.js';
import glslGsplatCopyToWorkBufferPS from '../shader-lib/glsl/chunks/gsplat/frag/gsplatCopyToWorkbuffer.js';
import wgslGsplatCopyToWorkBufferPS from '../shader-lib/wgsl/chunks/gsplat/frag/gsplatCopyToWorkbuffer.js';
import { GSplatWorkBufferRenderPass } from './gsplat-work-buffer-render-pass.js';
let id = 0;
/**
* @import { GSplatInfo } from "./gsplat-info.js"
* @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js'
* @import { GraphNode } from '../graph-node.js';
* @import { ShaderMaterial } from '../materials/shader-material.js'
*/ /**
* A helper class to cache quad renders for work buffer rendering.
*
* @ignore
*/ class WorkBufferRenderInfo {
constructor(device, key, material, colorTextureFormat, colorOnly){
this.device = device;
this.material = material;
const clonedDefines = new Map(material.defines);
// when using fallback RGBA16U format
const isColorUint = colorTextureFormat === PIXELFORMAT_RGBA16U;
const colorOutputType = isColorUint ? 'uvec4' : 'vec4';
if (isColorUint) {
clonedDefines.set('GSPLAT_COLOR_UINT', '');
}
// when rendering only color (not full MRT)
if (colorOnly) {
clonedDefines.set('GSPLAT_COLOR_ONLY', '');
}
const shader = ShaderUtils.createShader(this.device, {
uniqueName: `SplatCopyToWorkBuffer:${key}`,
attributes: {
vertex_position: SEMANTIC_POSITION
},
vertexDefines: clonedDefines,
fragmentDefines: clonedDefines,
vertexChunk: 'fullscreenQuadVS',
fragmentGLSL: glslGsplatCopyToWorkBufferPS,
fragmentWGSL: wgslGsplatCopyToWorkBufferPS,
fragmentOutputTypes: colorOnly ? [
colorOutputType
] : [
colorOutputType,
'uvec4',
'uvec2'
]
});
this.quadRender = new QuadRender(shader);
}
destroy() {
this.material?.destroy();
this.quadRender?.destroy();
}
}
/**
* @ignore
*/ class GSplatWorkBuffer {
constructor(device){
/** @type {number} */ this.id = id++;
/** @type {number} */ this._textureSize = 1;
this.device = device;
// Detect compatible HDR format for color texture, fallback to RGBA16U if RGBA16F not supported
this.colorTextureFormat = device.getRenderableHdrFormat([
PIXELFORMAT_RGBA16F
]) || PIXELFORMAT_RGBA16U;
// Work buffer textures format:
// - colorTexture (RGBA16F/RGBA16U): RGBA color with alpha
// - splatTexture0 (RGBA32U): modelCenter.xyz (3×32-bit floats as uint) + 2×16-bit covariance halfs (covA.z, covB.z)
// - splatTexture1 (RG32U): 4×16-bit covariance halfs packed as (covA.xy, covB.xy)
this.colorTexture = this.createTexture('splatColor', this.colorTextureFormat, 1, 1);
this.splatTexture0 = this.createTexture('splatTexture0', PIXELFORMAT_RGBA32U, 1, 1);
this.splatTexture1 = this.createTexture('splatTexture1', PIXELFORMAT_RG32U, 1, 1);
this.renderTarget = new RenderTarget({
name: `GsplatWorkBuffer-MRT-${this.id}`,
colorBuffers: [
this.colorTexture,
this.splatTexture0,
this.splatTexture1
],
depth: false,
flipY: true
});
this.colorRenderTarget = new RenderTarget({
name: `GsplatWorkBuffer-Color-${this.id}`,
colorBuffer: this.colorTexture,
depth: false,
flipY: true
});
// Create upload stream for non-blocking uploads
this.uploadStream = new UploadStream(device);
// Use storage buffer on WebGPU, texture on WebGL
if (device.isWebGPU) {
this.orderBuffer = new StorageBuffer(device, 4, BUFFERUSAGE_COPY_DST);
} else {
this.orderTexture = this.createTexture('SplatGlobalOrder', PIXELFORMAT_R32U, 1, 1);
}
// Create the optimized render pass for batched splat rendering
this.renderPass = new GSplatWorkBufferRenderPass(device, this);
this.renderPass.init(this.renderTarget);
// Create the color-only render pass for updating just the color texture
this.colorRenderPass = new GSplatWorkBufferRenderPass(device, this, true);
this.colorRenderPass.init(this.colorRenderTarget);
}
destroy() {
this.renderPass?.destroy();
this.colorRenderPass?.destroy();
this.colorTexture?.destroy();
this.splatTexture0?.destroy();
this.splatTexture1?.destroy();
this.orderTexture?.destroy();
this.orderBuffer?.destroy();
this.renderTarget?.destroy();
this.colorRenderTarget?.destroy();
this.uploadStream.destroy();
}
get textureSize() {
return this._textureSize;
}
setOrderData(data) {
if (this.device.isWebGPU) {
Debug.assert(data.length <= this._textureSize * this._textureSize);
this.uploadStream.upload(data, this.orderBuffer, 0, data.length);
} else {
Debug.assert(data.length === this._textureSize * this._textureSize);
this.uploadStream.upload(data, this.orderTexture, 0, data.length);
}
}
createTexture(name, format, w, h) {
return new Texture(this.device, {
name: name,
width: w,
height: h,
format: format,
cubemap: false,
mipmaps: false,
minFilter: FILTER_NEAREST,
magFilter: FILTER_NEAREST,
addressU: ADDRESS_CLAMP_TO_EDGE,
addressV: ADDRESS_CLAMP_TO_EDGE
});
}
/**
* @param {number} textureSize - The texture size to resize to.
*/ resize(textureSize) {
Debug.assert(textureSize);
this.renderTarget.resize(textureSize, textureSize);
this.colorRenderTarget.resize(textureSize, textureSize);
this._textureSize = textureSize;
if (this.device.isWebGPU) {
const newByteSize = textureSize * textureSize * 4;
if (this.orderBuffer.byteSize < newByteSize) {
this.orderBuffer.destroy();
this.orderBuffer = new StorageBuffer(this.device, newByteSize, BUFFERUSAGE_COPY_DST);
}
} else {
this.orderTexture.resize(textureSize, textureSize);
}
}
/**
* Render given splats to the work buffer.
*
* @param {GSplatInfo[]} splats - The splats to render.
* @param {GraphNode} cameraNode - The camera node.
* @param {number[][]|undefined} colorsByLod - Array of RGB colors per LOD. Index by lodIndex; if a
* shorter array is provided, index 0 will be reused as fallback.
*/ render(splats, cameraNode, colorsByLod) {
// render splats using render pass
if (this.renderPass.update(splats, cameraNode, colorsByLod)) {
this.renderPass.render();
}
}
/**
* Render only the color data to the work buffer (not geometry/covariance).
*
* @param {GSplatInfo[]} splats - The splats to render.
* @param {GraphNode} cameraNode - The camera node.
* @param {number[][]|undefined} colorsByLod - Array of RGB colors per LOD. Index by lodIndex; if a
* shorter array is provided, index 0 will be reused as fallback.
*/ renderColor(splats, cameraNode, colorsByLod) {
// render only color using color-only render pass
if (this.colorRenderPass.update(splats, cameraNode, colorsByLod)) {
this.colorRenderPass.render();
}
}
}
export { GSplatWorkBuffer, WorkBufferRenderInfo };