UNPKG

playcanvas

Version:

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

214 lines (213 loc) 8.81 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 { Debug, DebugHelper } from "../../core/debug.js"; import { Mat4 } from "../../core/math/mat4.js"; import { Vec3 } from "../../core/math/vec3.js"; import { BUFFERUSAGE_COPY_DST, CULLFACE_NONE, SEMANTIC_POSITION, PIXELFORMAT_R32U } from "../../platform/graphics/constants.js"; import { StorageBuffer } from "../../platform/graphics/storage-buffer.js"; import { MeshInstance } from "../mesh-instance.js"; import { GSplatResolveSH } from "./gsplat-resolve-sh.js"; import { GSplatSorter } from "./gsplat-sorter.js"; import { GSplatSogData } from "./gsplat-sog-data.js"; import { GSplatResourceBase } from "./gsplat-resource-base.js"; import { ShaderMaterial } from "../materials/shader-material.js"; import { BLEND_NONE, BLEND_PREMULTIPLIED } from "../constants.js"; const mat = new Mat4(); const cameraPosition = new Vec3(); const cameraDirection = new Vec3(); class GSplatInstance { /** * @param {GSplatResourceBase} resource - The splat instance. * @param {object} [options] - Options for the instance. * @param {ShaderMaterial|null} [options.material] - The material instance. * @param {boolean} [options.highQualitySH] - Whether to use the high quality or the approximate spherical harmonic calculation. Only applies to SOG data. * @param {import('../scene.js').Scene} [options.scene] - The scene to fire sort timing events on. */ constructor(resource, options = {}) { /** @type {GSplatResourceBase} */ __publicField(this, "resource"); /** @type {Texture|undefined} */ __publicField(this, "orderTexture"); /** @type {StorageBuffer|undefined} */ __publicField(this, "orderBuffer"); /** @type {ShaderMaterial} */ __publicField(this, "_material"); /** @type {MeshInstance} */ __publicField(this, "meshInstance"); __publicField(this, "options", {}); /** @type {GSplatSorter|null} */ __publicField(this, "sorter", null); __publicField(this, "lastCameraPosition", new Vec3()); __publicField(this, "lastCameraDirection", new Vec3()); /** @type {GSplatResolveSH|null} */ __publicField(this, "resolveSH", null); /** * List of cameras this instance is visible for. Updated every frame by the renderer. * * @type {Camera[]} * @ignore */ __publicField(this, "cameras", []); this.resource = resource; const device = resource.device; const dims = resource.streams.textureDimensions; Debug.assert(dims.x > 0 && dims.y > 0, "Resource must have valid texture dimensions before creating instance"); const numSplats = dims.x * dims.y; if (device.isWebGPU) { this.orderBuffer = new StorageBuffer(device, numSplats * 4, BUFFERUSAGE_COPY_DST); DebugHelper.setName(this.orderBuffer, "GsplatInstance.order"); } else { this.orderTexture = resource.streams.createTexture( "splatOrder", PIXELFORMAT_R32U, dims ); } if (options.material) { this._material = options.material; this._material.setDefine("{GSPLAT_INSTANCE_SIZE}", String(GSplatResourceBase.instanceSize)); this.setMaterialOrderData(this._material); this._material.setParameter("alphaClipForward", 1 / 255); } else { this._material = new ShaderMaterial({ uniqueName: "SplatMaterial", vertexGLSL: '#include "gsplatVS"', fragmentGLSL: '#include "gsplatPS"', vertexWGSL: '#include "gsplatVS"', fragmentWGSL: '#include "gsplatPS"', attributes: { vertex_position: SEMANTIC_POSITION } }); this.configureMaterial(this._material); this._material.update(); } resource.ensureMesh(); this.meshInstance = new MeshInstance( /** @type {Mesh} */ resource.mesh, this._material ); this.meshInstance.setInstancing(true, true); this.meshInstance.gsplatInstance = this; this.meshInstance.instancingCount = 0; if (resource.hasCenters) { const centers = resource.centers.slice(); const chunks = resource.chunks?.slice(); const orderTarget = this.orderBuffer ?? this.orderTexture; this.sorter = new GSplatSorter(device, options.scene); this.sorter.init(orderTarget, numSplats, centers, chunks); } else { Debug.warnOnce(`Skipping gsplat resource id ${resource.id} on the non-unified rendering path \u2014 no centers buffer. Scene#gsplatCentersEnabled needs to be true.`); } this.setHighQualitySH(options.highQualitySH ?? false); } destroy() { this.resource?.releaseMesh(); this.orderTexture?.destroy(); this.orderBuffer?.destroy(); this.resolveSH?.destroy(); this.material?.destroy(); this.meshInstance?.destroy(); this.sorter?.destroy(); } /** * Set order data parameters on the material. * * @param {ShaderMaterial} material - The material to configure. */ setMaterialOrderData(material) { if (this.orderBuffer) { material.setParameter("splatOrder", this.orderBuffer); } else { material.setParameter("splatOrder", this.orderTexture); material.setParameter("splatTextureSize", this.orderTexture.width); } } /** * @param {ShaderMaterial} value - The material instance. */ set material(value) { if (this._material !== value) { this._material = value; this._material.setDefine("{GSPLAT_INSTANCE_SIZE}", String(GSplatResourceBase.instanceSize)); this.setMaterialOrderData(this._material); this._material.setParameter("alphaClipForward", 1 / 255); if (this.meshInstance) { this.meshInstance.material = value; } } } get material() { return this._material; } /** * Configure the material with gsplat instance and resource properties. * * @param {ShaderMaterial} material - The material to configure. * @param {object} [options] - Object for passing optional arguments. * @param {boolean} [options.dither] - Specify true to configure the material for dithered rendering (stochastic alpha). */ configureMaterial(material, options = {}) { this.resource.configureMaterial(material, null, this.resource.format.getInputDeclarations()); material.setDefine("{GSPLAT_INSTANCE_SIZE}", GSplatResourceBase.instanceSize); material.setParameter("numSplats", 0); this.setMaterialOrderData(material); material.setParameter("alphaClip", 0.3); material.setParameter("alphaClipForward", 1 / 255); material.setParameter("minPixelSize", 2); material.setDefine(`DITHER_${options.dither ? "BLUENOISE" : "NONE"}`, ""); material.cull = CULLFACE_NONE; material.blendType = options.dither ? BLEND_NONE : BLEND_PREMULTIPLIED; material.depthWrite = !!options.dither; } /** * Sorts the GS vertices based on the given camera. * @param {GraphNode} cameraNode - The camera node used for sorting. */ sort(cameraNode) { if (this.sorter) { const cameraMat = cameraNode.getWorldTransform(); cameraMat.getTranslation(cameraPosition); cameraMat.getZ(cameraDirection); const modelMat = this.meshInstance.node.getWorldTransform(); const invModelMat = mat.invert(modelMat); invModelMat.transformPoint(cameraPosition, cameraPosition); invModelMat.transformVector(cameraDirection, cameraDirection); if (!cameraPosition.equalsApprox(this.lastCameraPosition) || !cameraDirection.equalsApprox(this.lastCameraDirection)) { this.lastCameraPosition.copy(cameraPosition); this.lastCameraDirection.copy(cameraDirection); this.sorter.setCamera(cameraPosition, cameraDirection); } } } update() { const count = this.sorter?.applyPendingSorted() ?? -1; if (count >= 0) { this.meshInstance.instancingCount = Math.ceil(count / GSplatResourceBase.instanceSize); this.material.setParameter("numSplats", count); } if (this.cameras.length > 0) { const camera = this.cameras[0]; this.sort(camera._node); this.resolveSH?.render(camera._node, this.meshInstance.node.getWorldTransform()); this.cameras.length = 0; } } setHighQualitySH(value) { const { resource } = this; const { gsplatData } = resource; if (gsplatData instanceof GSplatSogData && gsplatData.shBands > 0 && value === !!this.resolveSH) { if (this.resolveSH) { this.resolveSH.destroy(); this.resolveSH = null; } else { this.resolveSH = new GSplatResolveSH(resource.device, this); } } } } export { GSplatInstance };