UNPKG

three

Version:

JavaScript 3D library

1,040 lines (739 loc) 32.1 kB
import { Box3, BufferAttribute, InstancedBufferGeometry, Matrix3, Matrix4, Mesh, NodeMaterial, Ray, Sphere, StorageBufferAttribute, Vector2, Vector3 } from 'three/webgpu'; import { Discard, Fn, If, atan, cameraProjectionMatrix, cameraViewport, cos, dot, exp, float, highpModelViewMatrix, instanceIndex, max, min, normalize, positionGeometry, sin, sqrt, storage, uint, uniform, unpackUnorm4x8, varyingProperty, vec2, vec3, vec4 } from 'three/tsl'; import { CountingSort } from '../gpgpu/CountingSort.js'; import { SH_BAND_COMPONENTS, SH_BAND_WORDS, getSphericalHarmonicsDegree } from '../utils/GaussianSplatUtils.js'; const BIN_COUNT = 4096; const WORKGROUP_SIZE = 256; const SORT_DIRECTION_THRESHOLD = 0.9995; const KERNEL_2D_SIZE = 0.3; const SPLAT_KERNEL_CUTOFF = 2; const COVARIANCE_FLATNESS = 1e-4; const MIN_RAYCAST_OPACITY = 0.2; const MAX_SCREEN_SPACE_SPLAT_SIZE = 1024; const CLIP_XY = 1.4; const _worldCenter = /*@__PURE__*/ new Vector3(); const _viewCenter = /*@__PURE__*/ new Vector3(); const _worldScale = /*@__PURE__*/ new Vector3(); const _sortDirection = /*@__PURE__*/ new Vector3(); const _sortDepthRange = /*@__PURE__*/ new Vector2(); const _worldMatrixInverse = /*@__PURE__*/ new Matrix4(); const _modelViewMatrix = /*@__PURE__*/ new Matrix4(); const _inverseMatrix = /*@__PURE__*/ new Matrix4(); const _ray = /*@__PURE__*/ new Ray(); const _sphere = /*@__PURE__*/ new Sphere(); const _covarianceMatrix = /*@__PURE__*/ new Matrix3(); const _originOffset = /*@__PURE__*/ new Vector3(); const _mDirection = /*@__PURE__*/ new Vector3(); const _mOriginOffset = /*@__PURE__*/ new Vector3(); const _vector = /*@__PURE__*/ new Vector3(); /** * A minimal renderer for 3D Gaussian splat geometry. * * Note that this class can only be used with {@link WebGPURenderer}. The * `forceWebGL` fallback of {@link WebGPURenderer} is supported, but * {@link WebGLRenderer} is not. Import maps or package exports must resolve * both `three/webgpu` and `three/tsl`. * * ```js * const splats = new GaussianSplat( geometry ); * scene.add( splats ); * ``` * * @augments Mesh * @three_import import { GaussianSplat } from 'three/addons/objects/GaussianSplat.js'; */ class GaussianSplat extends Mesh { /** * Constructs a new Gaussian splat mesh. * * @param {BufferGeometry} splatGeometry - The splat geometry to render. Higher-order spherical harmonics attributes must use packed `Uint32Array` words from {@link createGaussianSplatGeometry} (`SH_BAND_WORDS[ degree ]` words per splat, four clamped-byte coefficients per word). * @param {Object} [options] - Options. * @param {boolean} [options.autoSort=true] - Whether to sort automatically in `onBeforeRender`. */ constructor( splatGeometry, { autoSort = true } = {} ) { const positionAttribute = splatGeometry.getAttribute( 'position' ); const covarianceAttribute = splatGeometry.getAttribute( 'covariance' ); const colorAttribute = splatGeometry.getAttribute( 'color' ); const sphericalHarmonicsDegree = getSphericalHarmonicsDegree( splatGeometry ); const count = positionAttribute.count; if ( splatGeometry.boundingBox === null ) splatGeometry.computeBoundingBox(); if ( splatGeometry.boundingSphere === null ) splatGeometry.computeBoundingSphere(); const geometry = createGeometry( count ); const buffers = createStorageBuffers( count, positionAttribute.array, covarianceAttribute.array, colorAttribute.array, { degree: sphericalHarmonicsDegree, sh1: sphericalHarmonicsDegree >= 1 ? splatGeometry.getAttribute( 'sphericalHarmonics1' ).array : undefined, sh2: sphericalHarmonicsDegree >= 2 ? splatGeometry.getAttribute( 'sphericalHarmonics2' ).array : undefined, sh3: sphericalHarmonicsDegree >= 3 ? splatGeometry.getAttribute( 'sphericalHarmonics3' ).array : undefined } ); const localCameraPosition = uniform( new Vector3() ); const sphericalHarmonicsComputeNode = createSphericalHarmonicsComputeNode( buffers, localCameraPosition ); const sort = new CountingSort( count, { binCount: BIN_COUNT, workgroupSize: WORKGROUP_SIZE } ); const materialNodes = createMaterialNodes( buffers, sort, localCameraPosition ); const material = createMaterial( materialNodes.vertexNode, materialNodes.fragmentNode ); super( geometry, material ); /** * This flag can be used for type testing. * * @type {boolean} * @readonly * @default true */ this.isGaussianSplat = true; this.type = 'GaussianSplat'; /** * The source splat geometry. * * @type {BufferGeometry} */ this.splatGeometry = splatGeometry; /** * The bounding box of the splats. Can be computed via {@link GaussianSplat#computeBoundingBox}. * * @type {?Box3} * @default null */ this.boundingBox = null; /** * The bounding sphere of the splats. Can be computed via {@link GaussianSplat#computeBoundingSphere}. * * @type {?Sphere} * @default null */ this.boundingSphere = null; /** * Whether to sort automatically in `onBeforeRender`. * * @type {boolean} */ this.autoSort = autoSort; this._buffers = buffers; this._sort = sort; this._sortMatrix = uniform( new Matrix4() ); this._sortDepthRange = uniform( new Vector2( 0, 1 ) ); this._sortInitialized = false; this._lastSortDirection = new Vector3(); this._localCameraPosition = localCameraPosition; this._sphericalHarmonicsComputeNode = sphericalHarmonicsComputeNode; this._sphericalHarmonicsInitialized = false; this._lastSphericalHarmonicsCameraMatrix = new Matrix4(); this._lastSphericalHarmonicsWorldMatrix = new Matrix4(); this._sphericalHarmonicsVertexNode = materialNodes.sphericalHarmonicsVertexNode; this._precomputedSphericalHarmonicsVertexNode = materialNodes.vertexNode; this._positionAttribute = positionAttribute; const centerRead = buffers.centerRead; const sortMatrix = this._sortMatrix; const sortDepthRange = this._sortDepthRange; sort.setBinNode( () => { const center = centerRead.element( instanceIndex ).xyz.toVar( 'center' ); const viewCenter = sortMatrix.mul( vec4( center, 1 ) ).xyz.toVar( 'viewCenter' ); const depth = viewCenter.z.negate().toVar( 'depth' ); const range = max( sortDepthRange.y.sub( sortDepthRange.x ), 0.0001 ).toVar( 'range' ); const normalized = depth.sub( sortDepthRange.x ).div( range ).clamp( 0, 1 ).toVar( 'normalized' ); const depthBin = uint( normalized.mul( BIN_COUNT - 1 ) ).toVar( 'depthBin' ); return uint( BIN_COUNT - 1 ).sub( depthBin ); } ); this.onBeforeRender = ( renderer, scene, camera ) => { const vertexNode = renderer.backend && renderer.backend.isWebGLBackend === true ? this._sphericalHarmonicsVertexNode : this._precomputedSphericalHarmonicsVertexNode; if ( vertexNode !== null && material.vertexNode !== vertexNode ) { material.vertexNode = vertexNode; material.needsUpdate = true; } this.updateSphericalHarmonics( renderer, camera ); if ( this.autoSort === true ) { this.updateSort( renderer, camera ); } }; } /** * Updates the view-dependent spherical harmonics colors if the camera or * mesh transform has changed. * * @param {Renderer} renderer - The renderer. * @param {Camera} camera - The camera used for rendering. * @return {boolean} Whether a compute pass was dispatched this call. */ updateSphericalHarmonics( renderer, camera ) { if ( this._sphericalHarmonicsComputeNode === null ) return false; const isWebGLBackend = renderer.backend && renderer.backend.isWebGLBackend === true; if ( this._sphericalHarmonicsInitialized === true && camera.matrixWorld.equals( this._lastSphericalHarmonicsCameraMatrix ) && this.matrixWorld.equals( this._lastSphericalHarmonicsWorldMatrix ) && ( isWebGLBackend === true || this._buffers.sphericalHarmonicsContributionRead !== undefined ) ) { return false; } if ( isWebGLBackend === true ) { enableWebGLBuffers( this._buffers ); } _worldMatrixInverse.copy( this.matrixWorld ).invert(); this._localCameraPosition.value.setFromMatrixPosition( camera.matrixWorld ).applyMatrix4( _worldMatrixInverse ); this._lastSphericalHarmonicsCameraMatrix.copy( camera.matrixWorld ); this._lastSphericalHarmonicsWorldMatrix.copy( this.matrixWorld ); this._sphericalHarmonicsInitialized = true; if ( isWebGLBackend === true ) return false; ensureSphericalHarmonicsContributionBuffer( this._buffers ); renderer.compute( this._sphericalHarmonicsComputeNode ); return true; } /** * Computes the bounding box of the splats, updating {@link GaussianSplat#boundingBox}. * * Each splat is expanded by its own extent rather than treated as a point, so the bounds cover * what is drawn. */ computeBoundingBox() { if ( this.boundingBox === null ) this.boundingBox = new Box3(); this.boundingBox.makeEmpty(); const positionAttribute = this.splatGeometry.getAttribute( 'position' ); const covarianceAttribute = this.splatGeometry.getAttribute( 'covariance' ); const count = positionAttribute.count; for ( let i = 0; i < count; i ++ ) { const x = positionAttribute.getX( i ); const y = positionAttribute.getY( i ); const z = positionAttribute.getZ( i ); const c00 = covarianceAttribute.getComponent( i, 0 ); const c11 = covarianceAttribute.getComponent( i, 3 ); const c22 = covarianceAttribute.getComponent( i, 5 ); // the radius of the drawn largest extent const radius = SPLAT_KERNEL_CUTOFF * Math.sqrt( Math.max( c00, c11, c22 ) ); this.boundingBox.expandByPoint( _vector.set( x - radius, y - radius, z - radius ) ); this.boundingBox.expandByPoint( _vector.set( x + radius, y + radius, z + radius ) ); } } /** * Computes the bounding sphere of the splats, updating {@link GaussianSplat#boundingSphere}. * * Each splat is expanded by its own extent rather than treated as a point, so the bounds cover * what is drawn. */ computeBoundingSphere() { if ( this.boundingSphere === null ) this.boundingSphere = new Sphere(); this.computeBoundingBox(); this.boundingBox.getBoundingSphere( this.boundingSphere ); const positionAttribute = this.splatGeometry.getAttribute( 'position' ); const covarianceAttribute = this.splatGeometry.getAttribute( 'covariance' ); const count = positionAttribute.count; const center = this.boundingSphere.center; let maxRadius = 0; for ( let i = 0; i < count; i ++ ) { const x = positionAttribute.getX( i ); const y = positionAttribute.getY( i ); const z = positionAttribute.getZ( i ); const c00 = covarianceAttribute.getComponent( i, 0 ); const c11 = covarianceAttribute.getComponent( i, 3 ); const c22 = covarianceAttribute.getComponent( i, 5 ); // the radius of the drawn largest extent const radius = SPLAT_KERNEL_CUTOFF * Math.sqrt( Math.max( c00, c11, c22 ) ); maxRadius = Math.max( maxRadius, center.distanceTo( _vector.set( x, y, z ) ) + radius ); } this.boundingSphere.radius = maxRadius; } /** * Computes intersection points between a casted ray and the splats. * * @param {Raycaster} raycaster - The raycaster. * @param {Array<Object>} intersects - The target array that holds the intersection points. */ raycast( raycaster, intersects ) { const matrixWorld = this.matrixWorld; // Checking boundingSphere distance to ray if ( this.boundingSphere === null ) this.computeBoundingSphere(); _sphere.copy( this.boundingSphere ); _sphere.applyMatrix4( matrixWorld ); if ( raycaster.ray.intersectsSphere( _sphere ) === false ) return; // _inverseMatrix.copy( matrixWorld ).invert(); _ray.copy( raycaster.ray ).applyMatrix4( _inverseMatrix ); // test with bounding box in local space if ( this.boundingBox !== null ) { if ( _ray.intersectsBox( this.boundingBox ) === false ) return; } const positionAttribute = this.splatGeometry.getAttribute( 'position' ); const covarianceAttribute = this.splatGeometry.getAttribute( 'covariance' ); const colorAttribute = this.splatGeometry.getAttribute( 'color' ); const count = positionAttribute.count; for ( let i = 0; i < count; i ++ ) { computeRayIntersection( positionAttribute, covarianceAttribute, colorAttribute, i, matrixWorld, raycaster, intersects, this ); } } /** * Updates the draw order if the camera or mesh orientation has changed enough * to need a new sort. * * @param {Renderer} renderer - The renderer. * @param {Camera} camera - The camera used for rendering. * @return {boolean} Whether a sort was dispatched this call. */ updateSort( renderer, camera ) { this.updateWorldMatrix( true, false ); const needsSort = this._needsSort( camera ); if ( this._sortInitialized === false || needsSort === true ) { this._updateSortUniforms( camera ); if ( renderer.backend && renderer.backend.isWebGLBackend === true ) { enableWebGLBuffers( this._buffers ); this._sort.enableWebGLBuffers(); this._sortCPU(); } else { this._sort.compute( renderer ); } this._sortInitialized = true; this._lastSortDirection.copy( _sortDirection ); return true; } return false; } _needsSort( camera ) { _modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, this.matrixWorld ); const e = _modelViewMatrix.elements; _sortDirection.set( e[ 2 ], e[ 6 ], e[ 10 ] ).normalize(); return _sortDirection.dot( this._lastSortDirection ) < SORT_DIRECTION_THRESHOLD; } _updateSortUniforms( camera ) { this._sortMatrix.value.multiplyMatrices( camera.matrixWorldInverse, this.matrixWorld ); if ( this.boundingSphere === null ) this.computeBoundingSphere(); _worldCenter.copy( this.boundingSphere.center ).applyMatrix4( this.matrixWorld ); _viewCenter.copy( _worldCenter ).applyMatrix4( camera.matrixWorldInverse ); _worldScale.setFromMatrixScale( this.matrixWorld ); const radius = this.boundingSphere.radius * Math.max( _worldScale.x, _worldScale.y, _worldScale.z ); const depth = - _viewCenter.z; const nearDepth = Math.max( camera.near, depth - radius ); const farDepth = Math.max( nearDepth + 0.0001, depth + radius ); _sortDepthRange.set( nearDepth, farDepth ); this._sortDepthRange.value.copy( _sortDepthRange ); } _sortCPU() { const centers = this._positionAttribute.array; const matrix = this._sortMatrix.value.elements; const nearDepth = this._sortDepthRange.value.x; const range = Math.max( this._sortDepthRange.value.y - nearDepth, 0.0001 ); const scale = ( BIN_COUNT - 1 ) / range; this._sort.computeCPU( ( i ) => { const i3 = i * 3; const depth = - ( matrix[ 2 ] * centers[ i3 ] + matrix[ 6 ] * centers[ i3 + 1 ] + matrix[ 10 ] * centers[ i3 + 2 ] + matrix[ 14 ] ); const depthBin = Math.min( BIN_COUNT - 1, Math.max( 0, Math.floor( ( depth - nearDepth ) * scale ) ) ); return BIN_COUNT - 1 - depthBin; } ); } } // Intersects the ray with the ellipsoid the splat's covariance describes, which reduces to a // quadratic in t whose smaller root is the near surface. function computeRayIntersection( positionAttribute, covarianceAttribute, colorAttribute, index, matrixWorld, raycaster, intersects, object ) { // skip faint splats - cheapest possible rejection, a single attribute read if ( colorAttribute.getW( index ) < MIN_RAYCAST_OPACITY ) { return; } // the diagonal of the covariance bounds the splat's drawn extent (same radius used by // computeBoundingBox/computeBoundingSphere); reject rays that don't pass near the splat // at all before doing any of the more expensive matrix work below const c00 = covarianceAttribute.getComponent( index, 0 ); const c11 = covarianceAttribute.getComponent( index, 3 ); const c22 = covarianceAttribute.getComponent( index, 5 ); const maxVariance = Math.max( c00, c11, c22 ); if ( maxVariance <= 0 ) { return; } const center = _vector.fromBufferAttribute( positionAttribute, index ); const boundingRadius = SPLAT_KERNEL_CUTOFF * Math.sqrt( maxVariance ); if ( _ray.distanceSqToPoint( center ) > boundingRadius * boundingRadius ) { return; } // the attribute holds the upper triangle of the symmetric covariance const c01 = covarianceAttribute.getComponent( index, 1 ); const c02 = covarianceAttribute.getComponent( index, 2 ); const c12 = covarianceAttribute.getComponent( index, 4 ); // splats are often flat enough to make the covariance singular, so the thinnest axis is floored // relative to the widest to keep the quadratic solvable const minVariance = maxVariance * COVARIANCE_FLATNESS; _covarianceMatrix.set( c00 + minVariance, c01, c02, c01, c11 + minVariance, c12, c02, c12, c22 + minVariance ); const determinant = _covarianceMatrix.determinant(); if ( determinant <= 0 ) { return; } // inverse( covariance ), applied below to the ray direction and to the origin offset _covarianceMatrix.invert(); _mDirection.copy( _ray.direction ).applyMatrix3( _covarianceMatrix ); // squared length of the ray direction in the ellipsoid's metric; must be positive for a valid covariance const a = _ray.direction.dot( _mDirection ); if ( a <= 0 ) { return; } _originOffset.copy( _ray.origin ).sub( center ); _mOriginOffset.copy( _originOffset ).applyMatrix3( _covarianceMatrix ); const b = 2 * _originOffset.dot( _mDirection ); const c = _originOffset.dot( _mOriginOffset ) - SPLAT_KERNEL_CUTOFF * SPLAT_KERNEL_CUTOFF; const discriminant = b * b - 4 * a * c; if ( discriminant < 0 ) { return; } const sqrtDiscriminant = Math.sqrt( discriminant ); let t = ( - b - sqrtDiscriminant ) / ( 2 * a ); // the near surface is behind the origin when the ray starts inside the splat if ( t < 0 ) { t = ( - b + sqrtDiscriminant ) / ( 2 * a ); } if ( t < 0 ) { return; } const intersectPoint = new Vector3(); _ray.at( t, intersectPoint ).applyMatrix4( matrixWorld ); const distance = raycaster.ray.origin.distanceTo( intersectPoint ); if ( distance < raycaster.near || distance > raycaster.far ) { return; } intersects.push( { distance: distance, point: intersectPoint, index: index, face: null, faceIndex: null, barycoord: null, object: object } ); } function createGeometry( count ) { const geometry = new InstancedBufferGeometry(); geometry.setAttribute( 'position', new BufferAttribute( new Float32Array( [ - 2, - 2, 0, 2, - 2, 0, 2, 2, 0, - 2, 2, 0 ] ), 3 ) ); geometry.setIndex( [ 0, 1, 2, 0, 2, 3 ] ); geometry.instanceCount = count; return geometry; } function createStorageBuffers( count, centers, covariances, colors, sphericalHarmonics ) { const centerData = new Float32Array( count * 4 ); const covarianceAData = new Float32Array( count * 4 ); const covarianceBData = new Float32Array( count * 4 ); const colorData = new Uint32Array( count ); const sphericalHarmonicsDegree = sphericalHarmonics.degree; for ( let i = 0; i < count; i ++ ) { const i3 = i * 3; const i4 = i * 4; const i6 = i * 6; centerData[ i4 ] = centers[ i3 ]; centerData[ i4 + 1 ] = centers[ i3 + 1 ]; centerData[ i4 + 2 ] = centers[ i3 + 2 ]; covarianceAData[ i4 ] = covariances[ i6 ]; covarianceAData[ i4 + 1 ] = covariances[ i6 + 1 ]; covarianceAData[ i4 + 2 ] = covariances[ i6 + 2 ]; covarianceAData[ i4 + 3 ] = covariances[ i6 + 3 ]; covarianceBData[ i4 ] = covariances[ i6 + 4 ]; covarianceBData[ i4 + 1 ] = covariances[ i6 + 5 ]; colorData[ i ] = ( colors[ i4 ] | colors[ i4 + 1 ] << 8 | colors[ i4 + 2 ] << 16 | colors[ i4 + 3 ] << 24 ) >>> 0; } const centerAttribute = new StorageBufferAttribute( centerData, 4 ); const covarianceAAttribute = new StorageBufferAttribute( covarianceAData, 4 ); const covarianceBAttribute = new StorageBufferAttribute( covarianceBData, 4 ); const colorAttribute = new StorageBufferAttribute( colorData, 1 ); const buffers = { count, sphericalHarmonicsDegree, webGLBuffersEnabled: false, centerRead: storage( centerAttribute, 'vec4', count ).toReadOnly(), covarianceARead: storage( covarianceAAttribute, 'vec4', count ).toReadOnly(), covarianceBRead: storage( covarianceBAttribute, 'vec4', count ).toReadOnly(), colorRead: storage( colorAttribute, 'uint', count ).toReadOnly() }; for ( let degree = 1; degree <= sphericalHarmonicsDegree; degree ++ ) { const words = SH_BAND_WORDS[ degree ]; const attribute = new StorageBufferAttribute( sphericalHarmonics[ `sh${ degree }` ], 1 ); buffers[ `sphericalHarmonics${ degree }Attribute` ] = attribute; buffers[ `sphericalHarmonics${ degree }Read` ] = storage( attribute, 'uint', count * words ).toReadOnly(); buffers[ `sphericalHarmonics${ degree }Words` ] = words; } return buffers; } function ensureSphericalHarmonicsContributionBuffer( buffers ) { if ( buffers.sphericalHarmonicsContributionRead !== undefined ) return; // WebGPU stores one precomputed SH contribution per splat. The WebGL // fallback evaluates SH in the vertex shader because its transform-feedback // compute path cannot perform the packed buffer's indexed reads, so allocate // this additional buffer lazily only when the WebGPU pre-pass runs. const attribute = new StorageBufferAttribute( new Float32Array( buffers.count * 4 ), 4 ); buffers.sphericalHarmonicsContributionRead = storage( attribute, 'vec4', buffers.count ).toReadOnly(); buffers.sphericalHarmonicsContributionWrite = storage( attribute, 'vec4', buffers.count ); } function enableWebGLBuffers( buffers ) { if ( buffers.webGLBuffersEnabled === true ) return; buffers.centerRead.setPBO( true ); buffers.covarianceARead.setPBO( true ); buffers.covarianceBRead.setPBO( true ); buffers.colorRead.setPBO( true ); for ( let degree = 1; degree <= buffers.sphericalHarmonicsDegree; degree ++ ) { buffers[ `sphericalHarmonics${ degree }Read` ].setPBO( true ); } buffers.webGLBuffersEnabled = true; } function unpackSphericalHarmonicsCoefficients( buffer, splatIndex, words, componentCount ) { const coefficients = []; let remaining = componentCount; for ( let word = 0; word < words && remaining > 0; word ++ ) { const packed = buffer.element( splatIndex.mul( words ).add( word ) ).toVar(); const bytesInWord = Math.min( 4, remaining ); for ( let byteIndex = 0; byteIndex < bytesInWord; byteIndex ++ ) { const byte = packed.shiftRight( byteIndex * 8 ).bitAnd( 0xff ); coefficients.push( float( byte ).sub( 128 ).div( 128 ) ); } remaining -= bytesInWord; } return coefficients; } function assembleSphericalHarmonicsVectors( coefficients, name ) { const vectors = []; const vectorCount = coefficients.length / 3; for ( let i = 0; i < vectorCount; i ++ ) { const offset = i * 3; vectors.push( vec3( coefficients[ offset ], coefficients[ offset + 1 ], coefficients[ offset + 2 ] ).toVar( `${ name }${ i }` ) ); } return vectors; } function accumulateSphericalHarmonics( vectors, weights ) { let result = vectors[ 0 ].mul( weights[ 0 ] ); for ( let i = 1; i < vectors.length; i ++ ) { result = result.add( vectors[ i ].mul( weights[ i ] ) ); } return result; } function applySphericalHarmonicsBand( buffer, splatIndex, words, componentCount, name, weights ) { const coefficients = unpackSphericalHarmonicsCoefficients( buffer, splatIndex, words, componentCount ); const vectors = assembleSphericalHarmonicsVectors( coefficients, name ); return accumulateSphericalHarmonics( vectors, weights ); } function applySphericalHarmonics( rgb, center, localCameraPosition, splatIndex, buffers ) { const viewDirection = normalize( center.sub( localCameraPosition ) ).toVar( 'sphericalHarmonicsViewDirection' ); const x = viewDirection.x; const y = viewDirection.y; const z = viewDirection.z; rgb.addAssign( applySphericalHarmonicsBand( buffers.sphericalHarmonics1Read, splatIndex, buffers.sphericalHarmonics1Words, SH_BAND_COMPONENTS[ 1 ], 'sh1_', [ y.mul( - 0.4886025 ), z.mul( 0.4886025 ), x.mul( - 0.4886025 ) ] ) ); if ( buffers.sphericalHarmonicsDegree >= 2 ) { const xx = x.mul( x ).toVar( 'shXX' ); const yy = y.mul( y ).toVar( 'shYY' ); const zz = z.mul( z ).toVar( 'shZZ' ); rgb.addAssign( applySphericalHarmonicsBand( buffers.sphericalHarmonics2Read, splatIndex, buffers.sphericalHarmonics2Words, SH_BAND_COMPONENTS[ 2 ], 'sh2_', [ x.mul( y ).mul( 1.0925484 ), y.mul( z ).mul( - 1.0925484 ), zz.mul( 2 ).sub( xx ).sub( yy ).mul( 0.3153915 ), x.mul( z ).mul( - 1.0925484 ), xx.sub( yy ).mul( 0.5462742 ) ] ) ); if ( buffers.sphericalHarmonicsDegree >= 3 ) { const xy = x.mul( y ).toVar( 'shXY' ); rgb.addAssign( applySphericalHarmonicsBand( buffers.sphericalHarmonics3Read, splatIndex, buffers.sphericalHarmonics3Words, SH_BAND_COMPONENTS[ 3 ], 'sh3_', [ y.mul( xx.mul( 3 ).sub( yy ) ).mul( - 0.5900436 ), xy.mul( z ).mul( 2.8906114 ), y.mul( zz.mul( 4 ).sub( xx ).sub( yy ) ).mul( - 0.4570458 ), z.mul( zz.mul( 2 ).sub( xx.mul( 3 ) ).sub( yy.mul( 3 ) ) ).mul( 0.3731763 ), x.mul( zz.mul( 4 ).sub( xx ).sub( yy ) ).mul( - 0.4570458 ), z.mul( xx.sub( yy ) ).mul( 1.4453057 ), x.mul( xx.sub( yy.mul( 3 ) ) ).mul( - 0.5900436 ) ] ) ); } } } function createSphericalHarmonicsComputeNode( buffers, localCameraPosition ) { if ( buffers.sphericalHarmonicsDegree === 0 ) return null; return Fn( () => { const splatIndex = instanceIndex; const center = buffers.centerRead.element( splatIndex ).xyz.toVar( 'center' ); const rgb = vec3( 0 ).toVar( 'sphericalHarmonicsContribution' ); applySphericalHarmonics( rgb, center, localCameraPosition, splatIndex, buffers ); buffers.sphericalHarmonicsContributionWrite.element( splatIndex ).assign( vec4( rgb, 0 ) ); } )().compute( buffers.count, [ WORKGROUP_SIZE ] ).setName( 'GaussianSplatSphericalHarmonics' ); } function createMaterialNodes( buffers, sort, localCameraPosition ) { const splatUv = varyingProperty( 'vec2', 'vSplatUv' ); const splatColor = varyingProperty( 'vec4', 'vSplatColor' ); const createVertexNode = ( usePrecomputedSphericalHarmonics ) => Fn( () => { const splatIndex = sort.orderRead.element( instanceIndex ).toVar( 'splatIndex' ); const center = buffers.centerRead.element( splatIndex ).xyz.toVar( 'center' ); const covA = buffers.covarianceARead.element( splatIndex ).toVar( 'covA' ); const covB = buffers.covarianceBRead.element( splatIndex ).toVar( 'covB' ); const color = unpackUnorm4x8( buffers.colorRead.element( splatIndex ) ).toVar( 'splatColor' ); const rgb = color.rgb.toVar( 'splatRgb' ); if ( buffers.sphericalHarmonicsDegree > 0 ) { if ( usePrecomputedSphericalHarmonics === true ) { rgb.addAssign( buffers.sphericalHarmonicsContributionRead.element( splatIndex ).rgb ); } else { applySphericalHarmonics( rgb, center, localCameraPosition, splatIndex, buffers ); } } splatUv.assign( positionGeometry.xy ); const viewCenter4 = highpModelViewMatrix.mul( vec4( center, 1 ) ).toVar( 'viewCenter4' ); const viewCenter = viewCenter4.xyz.toVar( 'viewCenter' ); const centerClip = cameraProjectionMatrix.mul( viewCenter4 ).toVar( 'centerClip' ); const m = highpModelViewMatrix; const r0 = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x ).toVar( 'r0' ); const r1 = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y ).toVar( 'r1' ); const r2 = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z ).toVar( 'r2' ); const cov0 = vec3( covA.x, covA.y, covA.z ).toVar( 'cov0' ); const cov1 = vec3( covA.y, covA.w, covB.x ).toVar( 'cov1' ); const cov2 = vec3( covA.z, covB.x, covB.y ).toVar( 'cov2' ); const vc0 = vec3( dot( r0, cov0 ), dot( r0, cov1 ), dot( r0, cov2 ) ).toVar( 'vc0' ); const vc1 = vec3( dot( r1, cov0 ), dot( r1, cov1 ), dot( r1, cov2 ) ).toVar( 'vc1' ); const vc2 = vec3( dot( r2, cov0 ), dot( r2, cov1 ), dot( r2, cov2 ) ).toVar( 'vc2' ); const c00 = dot( vc0, r0 ).toVar( 'c00' ); const c01 = dot( vc0, r1 ).toVar( 'c01' ); const c02 = dot( vc0, r2 ).toVar( 'c02' ); const c11 = dot( vc1, r1 ).toVar( 'c11' ); const c12 = dot( vc1, r2 ).toVar( 'c12' ); const c22 = dot( vc2, r2 ).toVar( 'c22' ); const z = min( viewCenter.z, - 0.01 ).toVar( 'z' ); const invZ = float( 1 ).div( z ).toVar( 'invZ' ); const invZ2 = invZ.mul( invZ ).toVar( 'invZ2' ); const focal = cameraViewport.zw.mul( 0.5 ).mul( vec2( cameraProjectionMatrix[ 0 ].x, cameraProjectionMatrix[ 1 ].y ) ).toVar( 'focal' ); const j00 = focal.x.negate().mul( invZ ).toVar( 'j00' ); const j11 = focal.y.negate().mul( invZ ).toVar( 'j11' ); const j02 = focal.x.mul( viewCenter.x ).mul( invZ2 ).toVar( 'j02' ); const j12 = focal.y.mul( viewCenter.y ).mul( invZ2 ).toVar( 'j12' ); const aBase = j00.mul( j00 ).mul( c00 ) .add( j00.mul( j02 ).mul( c02 ).mul( 2 ) ) .add( j02.mul( j02 ).mul( c22 ) ) .toVar( 'cov2dABase' ); const b = j00.mul( j11 ).mul( c01 ) .add( j00.mul( j12 ).mul( c02 ) ) .add( j02.mul( j11 ).mul( c12 ) ) .add( j02.mul( j12 ).mul( c22 ) ) .toVar( 'cov2dB' ); const cBase = j11.mul( j11 ).mul( c11 ) .add( j11.mul( j12 ).mul( c12 ).mul( 2 ) ) .add( j12.mul( j12 ).mul( c22 ) ) .toVar( 'cov2dCBase' ); const a = aBase.add( KERNEL_2D_SIZE ).toVar( 'cov2dA' ); const c = cBase.add( KERNEL_2D_SIZE ).toVar( 'cov2dC' ); const detBase = aBase.mul( cBase ).sub( b.mul( b ) ).toVar( 'detBase' ); const det = a.mul( c ).sub( b.mul( b ) ).toVar( 'det' ); const alphaScale = sqrt( max( detBase.div( max( det, 0.000001 ) ), 0 ) ).toVar( 'alphaScale' ); splatColor.assign( vec4( rgb.clamp( 0, 1 ), color.a.mul( alphaScale ) ) ); const halfTrace = a.add( c ).mul( 0.5 ).toVar( 'halfTrace' ); const radius = sqrt( max( a.sub( c ).mul( 0.5 ).pow2().add( b.mul( b ) ), 0.0000001 ) ).toVar( 'radius' ); const lambda1 = max( halfTrace.add( radius ), 0.0000001 ).toVar( 'lambda1' ); const lambda2 = max( halfTrace.sub( radius ), 0.0000001 ).toVar( 'lambda2' ); const axis1 = vec2( 1, 0 ).toVar( 'axis1' ); If( radius.greaterThan( 0.00001 ), () => { const angle = atan( b.mul( 2 ), a.sub( c ) ).mul( 0.5 ).toVar( 'angle' ); axis1.assign( vec2( cos( angle ), sin( angle ) ) ); } ); const axis2 = vec2( axis1.y.negate(), axis1.x ).toVar( 'axis2' ); const scale1 = min( sqrt( lambda1 ), MAX_SCREEN_SPACE_SPLAT_SIZE ).toVar( 'scale1' ); const scale2 = min( sqrt( lambda2 ), MAX_SCREEN_SPACE_SPLAT_SIZE ).toVar( 'scale2' ); const offsetPixels = axis1.mul( positionGeometry.x ).mul( scale1 ).add( axis2.mul( positionGeometry.y ).mul( scale2 ) ).toVar( 'offsetPixels' ); const offsetNdc = offsetPixels.mul( 2 ).div( cameraViewport.zw ).toVar( 'offsetNdc' ); const clip = centerClip.add( vec4( offsetNdc.mul( centerClip.w ), 0, 0 ) ).toVar( 'clip' ); const clipLimit = centerClip.w.mul( CLIP_XY ).toVar( 'clipLimit' ); If( viewCenter.z.greaterThanEqual( - 0.01 ) .or( centerClip.z.lessThan( centerClip.w.negate() ) ) .or( centerClip.z.greaterThan( centerClip.w ) ) .or( centerClip.x.lessThan( clipLimit.negate() ) ) .or( centerClip.x.greaterThan( clipLimit ) ) .or( centerClip.y.lessThan( clipLimit.negate() ) ) .or( centerClip.y.greaterThan( clipLimit ) ), () => { clip.assign( vec4( 2, 2, 2, 1 ) ); } ); return clip; } )(); const vertexNode = createVertexNode( true ); const sphericalHarmonicsVertexNode = buffers.sphericalHarmonicsDegree > 0 ? createVertexNode( false ) : null; const fragmentNode = Fn( () => { const r2 = dot( splatUv, splatUv ).toVar( 'r2' ); If( r2.greaterThan( 4 ), () => { Discard(); } ); return vec4( splatColor.rgb, exp( r2.mul( - 0.5 ) ).mul( splatColor.a ) ); } )(); return { vertexNode, sphericalHarmonicsVertexNode, fragmentNode }; } function createMaterial( vertexNode, fragmentNode ) { const material = new NodeMaterial(); material.vertexNode = vertexNode; material.colorNode = fragmentNode; material.transparent = true; material.depthWrite = false; material.depthTest = true; material.forceSinglePass = true; material.fog = false; return material; } export { GaussianSplat };