Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions examples/jsm/objects/GaussianSplat.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import {
Box3,
BufferAttribute,
InstancedBufferGeometry,
Matrix3,
Matrix4,
Mesh,
NodeMaterial,
Ray,
Sphere,
StorageBufferAttribute,
Vector2,
Expand Down Expand Up @@ -52,6 +54,8 @@ 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;

Expand All @@ -62,6 +66,13 @@ 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();

/**
Expand Down Expand Up @@ -335,6 +346,51 @@ class GaussianSplat extends Mesh {

}

/**
* 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.
Expand Down Expand Up @@ -430,6 +486,130 @@ class GaussianSplat extends Mesh {

}

// 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();
Expand Down
6 changes: 6 additions & 0 deletions examples/tags.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
"webgl_interactive_raycasting_points": [ "raycast" ],
"webgl_interactive_voxelpainter": [ "raycast" ],
"webgl_layers": [ "groups" ],
"webgl_lightprobes": [ "global illumination", "indirect diffuse" ],
"webgl_lightprobes_complex": [ "global illumination", "indirect diffuse" ],
"webgl_lightprobes_sponza": [ "global illumination", "indirect diffuse" ],
"webgl_lights_hemisphere": [ "directional" ],
"webgl_lights_pointlights": [ "multiple" ],
"webgl_lines_fat": [ "gpu", "stats", "panel" ],
Expand Down Expand Up @@ -144,6 +147,9 @@
"webgpu_compute_texture_pingpong": [ "gpgpu" ],
"webgpu_compute_water": [ "gpgpu", "struct" ],
"webgpu_depth_texture": [ "renderTarget" ],
"webgpu_lightprobes": [ "global illumination", "indirect diffuse" ],
"webgpu_lightprobes_complex": [ "global illumination", "indirect diffuse" ],
"webgpu_lightprobes_sponza": [ "global illumination", "indirect diffuse" ],
"webgpu_loader_gltf_dispersion": [ "transmission" ],
"webgpu_materials_lightmap": [ "shadow" ],
"webgpu_materials_sss": [ "subsurface scattering", "derivatives", "translucency" ],
Expand Down
76 changes: 72 additions & 4 deletions examples/webgpu_gaussian_splat.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,18 @@
import { GaussianSplat } from 'three/addons/objects/GaussianSplat.js';
import { Inspector } from 'three/addons/inspector/Inspector.js';

let camera, scene, renderer, controls, splats, splatRoot;
let camera, scene, renderer, controls, splats, splatRoot, hitMarker;
THREE.ColorManagement.workingColorSpace = THREE.SRGBColorSpace;

const zeroVector = new THREE.Vector3();
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();

// for raycast testing
const CLICK_DRAG_THRESHOLD = 4;
const HIT_MARKER_SIZE_RATIO = 1 / 50;
let pointerDownX = 0;
let pointerDownY = 0;

const sources = {
millipede: {
Expand Down Expand Up @@ -135,9 +143,55 @@

} );

hitMarker = new THREE.Mesh(
new THREE.SphereGeometry( 1, 16, 12 ),
new THREE.MeshBasicMaterial( { color: 0xff0000, depthTest: false } )
);
hitMarker.visible = false;
scene.add( hitMarker );

await loadSplatSource( getSourceKeyByName( params.source ) );

window.addEventListener( 'resize', onWindowResize );
renderer.domElement.addEventListener( 'pointerdown', onPointerDown );
renderer.domElement.addEventListener( 'click', onClick );

}

function onPointerDown( event ) {

pointerDownX = event.clientX;
pointerDownY = event.clientY;

}

function onClick( event ) {

// check drag distance
const dx = event.clientX - pointerDownX;
const dy = event.clientY - pointerDownY;
if ( Math.sqrt( dx * dx + dy * dy ) > CLICK_DRAG_THRESHOLD ) {

return;

}

pointer.x = ( event.clientX / window.innerWidth ) * 2 - 1;
pointer.y = - ( event.clientY / window.innerHeight ) * 2 + 1;

raycaster.setFromCamera( pointer, camera );

const hit = raycaster.intersectObject( splatRoot, true )[ 0 ];
if ( hit ) {

hitMarker.position.copy( hit.point );
hitMarker.visible = true;

} else {

hitMarker.visible = false;

}

}

Expand All @@ -156,11 +210,27 @@
scene.add( splatRoot );

fitCameraToSplats( splats, source );
updateHitMarkerSize( splats );

updateLicenseInfo( source );

}

function getWorldBoundingSphere( mesh ) {

mesh.updateWorldMatrix( true, false );

return mesh.splatGeometry.boundingSphere.clone().applyMatrix4( mesh.matrixWorld );

}

function updateHitMarkerSize( mesh ) {

const sphere = getWorldBoundingSphere( mesh );
hitMarker.scale.setScalar( Math.max( sphere.radius * HIT_MARKER_SIZE_RATIO, 0.0001 ) );

}

function updateLicenseInfo( source ) {

const license = document.getElementById( 'license' );
Expand Down Expand Up @@ -199,9 +269,7 @@

function fitCameraToSplats( mesh, source ) {

mesh.updateWorldMatrix( true, false );

const sphere = mesh.splatGeometry.boundingSphere.clone().applyMatrix4( mesh.matrixWorld );
const sphere = getWorldBoundingSphere( mesh );
const radius = Math.max( sphere.radius, 0.01 );
const distance = radius / Math.sin( THREE.MathUtils.degToRad( camera.fov ) * 0.5 );

Expand Down
9 changes: 0 additions & 9 deletions test/unit/src/animation/AnimationUtils.tests.js

This file was deleted.

9 changes: 0 additions & 9 deletions test/unit/src/animation/PropertyMixer.tests.js

This file was deleted.

9 changes: 0 additions & 9 deletions test/unit/src/audio/AudioAnalyser.tests.js

This file was deleted.

11 changes: 0 additions & 11 deletions test/unit/src/extras/Earcut.tests.js

This file was deleted.

Loading