UNPKG

playcanvas

Version:

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

195 lines (192 loc) 8.6 kB
import { Frustum } from '../../core/shape/frustum.js'; import { Mat4 } from '../../core/math/mat4.js'; import { BUFFERUSAGE_COPY_DST } from '../../platform/graphics/constants.js'; import { StorageBuffer } from '../../platform/graphics/storage-buffer.js'; /** * @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js' * @import { GSplatInfo } from "./gsplat-info.js" */ const _viewProjMat = new Mat4(); const _frustum = new Frustum(); // 8 u32/f32 elements per BoundsEntry (matches WGSL struct layout): // [centerX, centerY, centerZ, radius, transformIndex, pad, pad, pad] const BOUNDS_ENTRY_FLOATS = 8; /** * Frustum culling data for GSplat octree nodes. Manages bounding-sphere and * transform storage buffers and computes frustum planes from camera matrices. * The actual culling test is performed inline by the interval compaction compute shader. * * @ignore */ class GSplatFrustumCuller { destroy() { this.boundsBuffer?.destroy(); this.transformsBuffer?.destroy(); } /** * Updates the bounds buffer with local-space bounding spheres and transform * indices from pre-built bounds groups. * * @param {Array<{splat: GSplatInfo, boundsBaseIndex: number, numBoundsEntries: number}>} boundsGroups - Pre-built bounds groups. */ updateBoundsData(boundsGroups) { let totalEntries = 0; for(let i = 0; i < boundsGroups.length; i++){ totalEntries += boundsGroups[i].numBoundsEntries; } this.totalBoundsEntries = totalEntries; if (totalEntries === 0) return; if (totalEntries > this._allocatedBoundsEntries) { this.boundsBuffer?.destroy(); this._allocatedBoundsEntries = totalEntries; this.boundsBuffer = new StorageBuffer(this.device, totalEntries * BOUNDS_ENTRY_FLOATS * 4, BUFFERUSAGE_COPY_DST); const ab = new ArrayBuffer(totalEntries * BOUNDS_ENTRY_FLOATS * 4); this._boundsFloatView = new Float32Array(ab); this._boundsUintView = new Uint32Array(ab); this._tmpSpheres = new Float32Array(totalEntries * 4); } const floatView = this._boundsFloatView; const uintView = this._boundsUintView; const tmpSpheres = this._tmpSpheres; for(let i = 0; i < boundsGroups.length; i++){ const group = boundsGroups[i]; const base = group.boundsBaseIndex; const count = group.numBoundsEntries; group.splat.writeBoundsSpheres(tmpSpheres, base * 4); for(let j = 0; j < count; j++){ const src = (base + j) * 4; const dst = (base + j) * BOUNDS_ENTRY_FLOATS; floatView[dst + 0] = tmpSpheres[src + 0]; floatView[dst + 1] = tmpSpheres[src + 1]; floatView[dst + 2] = tmpSpheres[src + 2]; floatView[dst + 3] = tmpSpheres[src + 3]; uintView[dst + 4] = i; // [dst+5..dst+7] are zero-initialized by ArrayBuffer } } this.boundsBuffer.write(0, floatView); } /** * Updates the transforms buffer with one world matrix per bounds group. * Each matrix is stored as 3 vec4f (rows of a 4x3 affine matrix). * * @param {Array<{splat: GSplatInfo, boundsBaseIndex: number, numBoundsEntries: number}>} boundsGroups - Pre-built bounds groups. */ updateTransformsData(boundsGroups) { const numMatrices = boundsGroups.length; if (numMatrices === 0) return; if (numMatrices > this._allocatedTransformCount) { this.transformsBuffer?.destroy(); this._allocatedTransformCount = numMatrices; // 3 vec4f per matrix = 12 floats = 48 bytes this.transformsBuffer = new StorageBuffer(this.device, numMatrices * 12 * 4, BUFFERUSAGE_COPY_DST); this._transformsData = new Float32Array(numMatrices * 12); } const data = this._transformsData; // Write world matrices as 3 rows of a 4x3 matrix (row-major, 12 floats per matrix). // Mat4.data is column-major: [col0(4), col1(4), col2(4), col3(4)]. // We store 3 rows, each as (Rx, Ry, Rz, T): // row0 = data[0], data[4], data[8], data[12] // row1 = data[1], data[5], data[9], data[13] // row2 = data[2], data[6], data[10], data[14] let offset = 0; for(let i = 0; i < boundsGroups.length; i++){ const m = boundsGroups[i].splat.node.getWorldTransform().data; // row 0 data[offset++] = m[0]; data[offset++] = m[4]; data[offset++] = m[8]; data[offset++] = m[12]; // row 1 data[offset++] = m[1]; data[offset++] = m[5]; data[offset++] = m[9]; data[offset++] = m[13]; // row 2 data[offset++] = m[2]; data[offset++] = m[6]; data[offset++] = m[10]; data[offset++] = m[14]; } this.transformsBuffer.write(0, data); } /** * Computes frustum planes from camera matrices and stores them in * {@link frustumPlanes} for use by the interval cull compute shader. * * @param {Mat4} projectionMatrix - The camera projection matrix. * @param {Mat4} viewMatrix - The camera view matrix. */ computeFrustumPlanes(projectionMatrix, viewMatrix) { _viewProjMat.mul2(projectionMatrix, viewMatrix); _frustum.setFromMat4(_viewProjMat); const planes = this.frustumPlanes; for(let p = 0; p < 6; p++){ const plane = _frustum.planes[p]; planes[p * 4 + 0] = plane.normal.x; planes[p * 4 + 1] = plane.normal.y; planes[p * 4 + 2] = plane.normal.z; planes[p * 4 + 3] = plane.distance; } } /** * Sets fisheye cone culling data for the interval cull shader. * * @param {import('../../core/math/vec3.js').Vec3} cameraPos - Camera world position. * @param {import('../../core/math/vec3.js').Vec3} cameraForward - Camera forward direction (normalized). * @param {number} maxTheta - Maximum visible angle from forward direction in radians. */ setFisheyeData(cameraPos, cameraForward, maxTheta) { this.fisheyeCameraPos[0] = cameraPos.x; this.fisheyeCameraPos[1] = cameraPos.y; this.fisheyeCameraPos[2] = cameraPos.z; this.fisheyeCameraForward[0] = cameraForward.x; this.fisheyeCameraForward[1] = cameraForward.y; this.fisheyeCameraForward[2] = cameraForward.z; this.fisheyeMaxTheta = maxTheta; } /** * @param {GraphicsDevice} device - The graphics device. */ constructor(device){ /** * Storage buffer holding interleaved BoundsEntry structs (center.xyz, radius, * transformIndex, pad x3). 32 bytes per entry. * * @type {StorageBuffer|null} */ this.boundsBuffer = null; /** * Total number of bounds entries across all GSplatInfos. * * @type {number} */ this.totalBoundsEntries = 0; /** @type {number} */ this._allocatedBoundsEntries = 0; /** @type {Float32Array|null} */ this._boundsFloatView = null; /** @type {Uint32Array|null} */ this._boundsUintView = null; /** @type {Float32Array|null} */ this._tmpSpheres = null; /** * Storage buffer holding world matrices as vec4f triplets (3 vec4f per matrix, * rows of a 4x3 affine matrix). 48 bytes per matrix. * * @type {StorageBuffer|null} */ this.transformsBuffer = null; /** @type {number} */ this._allocatedTransformCount = 0; /** @type {Float32Array|null} */ this._transformsData = null; /** * Packed frustum planes (6 planes x 4 floats: nx, ny, nz, distance). * Updated by {@link computeFrustumPlanes} and consumed by the interval cull shader. * * @type {Float32Array} */ this.frustumPlanes = new Float32Array(24); /** * Camera world position for fisheye cone culling (xyz). * * @type {Float32Array} */ this.fisheyeCameraPos = new Float32Array(3); /** * Camera forward direction (normalized) for fisheye cone culling (xyz). * * @type {Float32Array} */ this.fisheyeCameraForward = new Float32Array(3); /** * Maximum visible angle from forward direction for fisheye cone culling. * * @type {number} */ this.fisheyeMaxTheta = Math.PI; this.device = device; } } export { GSplatFrustumCuller };