playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
282 lines (281 loc) • 10.8 kB
JavaScript
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 { Debug, DebugHelper } from "../../core/debug.js";
import {
ADDRESS_CLAMP_TO_EDGE,
PIXELFORMAT_R32U,
PIXELFORMAT_RGBA16U,
BUFFERUSAGE_COPY_DST,
SEMANTIC_POSITION,
getGlslShaderType
} 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 glslGsplatCopyInstancedQuadVS from "../shader-lib/glsl/chunks/gsplat/vert/gsplatCopyInstancedQuad.js";
import wgslGsplatCopyInstancedQuadVS from "../shader-lib/wgsl/chunks/gsplat/vert/gsplatCopyInstancedQuad.js";
import { GSplatFrustumCuller } from "./gsplat-frustum-culler.js";
import { GSplatWorkBufferRenderPass } from "./gsplat-work-buffer-render-pass.js";
import { GSplatStreams } from "../gsplat/gsplat-streams.js";
let id = 0;
class WorkBufferRenderInfo {
/**
* @param {GraphicsDevice} device - The graphics device.
* @param {string} key - Cache key for this render info.
* @param {ShaderMaterial} material - The material to use.
* @param {boolean} colorOnly - Whether to render only color (not full MRT).
* @param {GSplatFormat} format - The work buffer format descriptor.
*/
constructor(device, key, material, colorOnly, format) {
/** @type {ShaderMaterial} */
__publicField(this, "material");
/** @type {QuadRender} */
__publicField(this, "quadRender");
this.material = material;
const clonedDefines = new Map(material.defines);
const colorStream = format.getStream("dataColor");
if (colorStream.format === PIXELFORMAT_RGBA16U) {
clonedDefines.set("GSPLAT_COLOR_UINT", "");
}
if (colorOnly) {
clonedDefines.set("GSPLAT_COLOR_ONLY", "");
}
if (format.getStream("pcId")) {
clonedDefines.set("GSPLAT_ID", "");
}
const fragmentIncludes = material.hasShaderChunks ? device.isWebGPU ? material.shaderChunks.wgsl : material.shaderChunks.glsl : void 0;
const outputStreams = colorOnly ? [colorStream] : [...format.streams, ...format.extraStreams];
const fragmentOutputTypes = [];
for (const stream of outputStreams) {
const info = getGlslShaderType(stream.format);
fragmentOutputTypes.push(info.returnType);
}
const useInstanced = clonedDefines.has("GSPLAT_LOD");
const shaderOptions = {
uniqueName: `SplatCopyToWorkBuffer:${key}`,
attributes: { vertex_position: SEMANTIC_POSITION },
vertexDefines: clonedDefines,
fragmentDefines: clonedDefines,
fragmentGLSL: glslGsplatCopyToWorkBufferPS,
fragmentWGSL: wgslGsplatCopyToWorkBufferPS,
fragmentIncludes,
fragmentOutputTypes
};
if (useInstanced) {
shaderOptions.vertexGLSL = glslGsplatCopyInstancedQuadVS;
shaderOptions.vertexWGSL = wgslGsplatCopyInstancedQuadVS;
} else {
shaderOptions.vertexChunk = "fullscreenQuadVS";
}
const shader = ShaderUtils.createShader(device, shaderOptions);
this.quadRender = new QuadRender(shader);
}
destroy() {
this.material?.destroy();
this.quadRender?.destroy();
}
}
class GSplatWorkBuffer {
/**
* @param {GraphicsDevice} device - The graphics device.
* @param {GSplatFormat} format - The work buffer format descriptor.
*/
constructor(device, format) {
/** @type {GraphicsDevice} */
__publicField(this, "device");
/** @type {GSplatFormat} */
__publicField(this, "format");
/** @type {number} */
__publicField(this, "id", id++);
/**
* Manages textures for format streams.
*
* @type {GSplatStreams}
*/
__publicField(this, "streams");
/**
* Main MRT render target for all work buffer streams.
*
* @type {RenderTarget}
*/
__publicField(this, "renderTarget");
/**
* Color-only render target for updating just the dataColor stream.
*
* @type {RenderTarget}
*/
__publicField(this, "colorRenderTarget");
/** @type {Texture|undefined} */
__publicField(this, "orderTexture");
/** @type {StorageBuffer|undefined} */
__publicField(this, "orderBuffer");
/** @type {UploadStream} */
__publicField(this, "uploadStream");
/** @type {GSplatWorkBufferRenderPass} */
__publicField(this, "renderPass");
/** @type {GSplatWorkBufferRenderPass} */
__publicField(this, "colorRenderPass");
/**
* GPU frustum culler for octree node visibility.
*
* @type {GSplatFrustumCuller}
*/
__publicField(this, "frustumCuller");
this.device = device;
this.format = format;
this.frustumCuller = new GSplatFrustumCuller(device);
this.streams = new GSplatStreams(device);
this.streams.init(format, 1);
this._createRenderTargets();
this.uploadStream = new UploadStream(device, !device.isWebGPU);
if (device.isWebGPU) {
this.orderBuffer = new StorageBuffer(device, 4, BUFFERUSAGE_COPY_DST);
DebugHelper.setName(this.orderBuffer, "GsplatWorkBuffer.order");
} else {
this.orderTexture = new Texture(device, {
name: "SplatGlobalOrder",
width: 1,
height: 1,
format: PIXELFORMAT_R32U,
mipmaps: false,
addressU: ADDRESS_CLAMP_TO_EDGE,
addressV: ADDRESS_CLAMP_TO_EDGE
});
}
this.renderPass = new GSplatWorkBufferRenderPass(device, this);
this.renderPass.init(this.renderTarget);
this.colorRenderPass = new GSplatWorkBufferRenderPass(device, this, true);
this.colorRenderPass.init(this.colorRenderTarget);
}
/**
* Creates or recreates render targets from current textures.
*
* @private
*/
_createRenderTargets() {
Debug.assert(
this.format.instanceStreams.length === 0,
"Work buffer format does not support instance-level streams (GSPLAT_STREAM_INSTANCE)"
);
this.renderTarget?.destroy();
this.colorRenderTarget?.destroy();
const colorBuffers = this.streams.getTexturesInOrder();
this.renderTarget = new RenderTarget({
name: `GsplatWorkBuffer-MRT-${this.id}`,
colorBuffers,
depth: false,
flipY: true
});
const colorTexture = this.streams.getTexture("dataColor");
this.colorRenderTarget = new RenderTarget({
name: `GsplatWorkBuffer-Color-${this.id}`,
colorBuffer: colorTexture,
depth: false,
flipY: true
});
this.renderPass?.init(this.renderTarget);
this.colorRenderPass?.init(this.colorRenderTarget);
}
/**
* Syncs textures and render targets with the format when extra streams are added.
* Call this before rendering to ensure all streams have textures.
*/
syncWithFormat() {
const prevVersion = this.streams._formatVersion;
this.streams.syncWithFormat(this.format);
if (prevVersion !== this.streams._formatVersion) {
this._createRenderTargets();
}
}
/**
* Gets a texture by name.
*
* @param {string} name - The texture name.
* @returns {Texture|undefined} The texture, or undefined if not found.
*/
getTexture(name) {
return this.streams.getTexture(name);
}
destroy() {
this.renderPass?.destroy();
this.colorRenderPass?.destroy();
this.streams.destroy();
this.orderTexture?.destroy();
this.orderBuffer?.destroy();
this.renderTarget?.destroy();
this.colorRenderTarget?.destroy();
this.uploadStream.destroy();
this.frustumCuller.destroy();
}
get textureSize() {
return this.streams.textureDimensions.x;
}
setOrderData(data) {
const size = this.textureSize;
if (this.device.isWebGPU) {
Debug.assert(data.length <= size * size);
this.uploadStream.upload(data, this.orderBuffer, 0, data.length);
} else {
Debug.assert(data.length === size * size);
this.uploadStream.upload(data, this.orderTexture, 0, data.length);
}
}
/**
* @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.streams.resize(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);
DebugHelper.setName(this.orderBuffer, "GsplatWorkBuffer.order");
}
} 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.
* @param {Set<number>|null} [changedAllocIds] - When provided, only render sub-draws for intervals
* whose allocIds are in this set (per-node partial update).
*/
render(splats, cameraNode, colorsByLod, changedAllocIds = null) {
if (this.renderPass.update(splats, cameraNode, colorsByLod, changedAllocIds)) {
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.
* @param {Set<number>|null} [changedAllocIds] - Set of changed allocIds for partial render.
*/
renderColor(splats, cameraNode, colorsByLod, changedAllocIds = null) {
if (this.colorRenderPass.update(splats, cameraNode, colorsByLod, changedAllocIds)) {
this.colorRenderPass.render();
}
}
}
export {
GSplatWorkBuffer,
WorkBufferRenderInfo
};