three-blocks-benchmark 0.3.3 → 0.4.0-alpha.372.gf7fb70f4af27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # three-blocks-benchmark
2
2
 
3
+ ## 0.4.0-alpha.372.gf7fb70f4af27
4
+
5
+ - Tools: Installing a pro tool works again, and a cached tool refreshes itself when a new build ships.
6
+
7
+ - Packages: Material environments capture reliably and transmission types expose surface distortion.
8
+
9
+ - Starter: New projects install compatible devtools and run status without missing dependencies.
10
+
11
+ - Website: Gaussian benchmark results now fit without horizontal page scrolling.
12
+
13
+ - Website: Pages navigate faster and links stay aligned with available destinations.
14
+
15
+ - Packages: Built from `f7fb70f4af27`.
16
+
17
+
3
18
  ## 0.3.3
4
19
 
5
20
  ### Patch Changes
package/NOTICE CHANGED
@@ -9,3 +9,13 @@ https://polyformproject.org/licenses/noncommercial/1.0.0/
9
9
  LICENSE contains the controlling noncommercial terms. Commercial customers
10
10
  receive separate rights under the agreement they accept:
11
11
  https://threejs-blocks.com/license
12
+
13
+ Third-party code
14
+ ================
15
+
16
+ vendor/three-gs/ contains verbatim copies of three.js addon files from the
17
+ unmerged pull request https://github.com/mrdoob/three.js/pull/33950, used to
18
+ benchmark that implementation. They remain under the MIT License,
19
+ Copyright (c) 2010-2026 three.js authors:
20
+ https://github.com/mrdoob/three.js/blob/dev/LICENSE
21
+ See vendor/three-gs/README.md for the pinned commit and file digests.
package/README.md CHANGED
@@ -1,7 +1,10 @@
1
1
  # three-blocks-benchmark
2
2
 
3
- Reproducible browser benchmarks for Three Blocks, Spark, SuperSplat,
3
+ Reproducible browser benchmarks for Three Blocks, the unmerged three.js
4
+ `GaussianSplatMesh` addon ([PR #33950][pr]), Spark, SuperSplat,
4
5
  GaussianSplats3D, and Babylon.js.
6
+
7
+ [pr]: https://github.com/mrdoob/three.js/pull/33950
5
8
  The default `standard` suite uses fixed held-out cameras from Mip-NeRF 360
6
9
  Bonsai and a pinned NerfBaselines Graphdeco 30K artifact. The synthetic
7
10
  `micro` suite is performance-only.
@@ -29,11 +32,11 @@ Or run it with Bun:
29
32
  bunx three-blocks-benchmark --suite standard --quick --output ./benchmark-results
30
33
  ```
31
34
 
32
- Remove `--quick` for the publication run. Publication evidence requires headed
33
- Chromium on a physical GPU:
35
+ Remove `--quick` for the publication run. Publication evidence requires a
36
+ physical GPU:
34
37
 
35
38
  ```sh
36
- npx three-blocks-benchmark --suite standard --headed --output ./benchmark-results
39
+ npx three-blocks-benchmark --suite standard --output ./benchmark-results
37
40
  ```
38
41
 
39
42
  The standard suite range-downloads and caches only the stored PLY, selected
@@ -56,9 +59,9 @@ a micro scene.
56
59
  `--revision <value>` and `--dirty` add optional repository metadata; neither
57
60
  Git nor a checkout is required.
58
61
 
59
- The CLI uses headed Chromium by default on macOS so WebGPU captures reach the
60
- physical GPU. `--headless` is available where the browser exposes a working
61
- WebGPU adapter in headless mode.
62
+ The CLI runs headless Chromium by default. On macOS it forces the Metal ANGLE
63
+ backend, without which headless Chromium selects SwiftShader and captures blank
64
+ frames. Use `--headed` to watch a run; the recorded metrics are the same.
62
65
 
63
66
  Standard reports contain neutral PSNR and dm_pix-compatible SSIM versus ground
64
67
  truth. Metrics versus the pinned NerfBaselines Graphdeco 30K prediction are
@@ -22,7 +22,7 @@ Options:
22
22
  --quick Use one view/scene and short measurements
23
23
  --omit-captures Write metrics JSON without renderer captures
24
24
  --headed Show Chromium
25
- --headless Hide Chromium
25
+ --headless Hide Chromium (default)
26
26
  --revision <value> Record an optional source revision
27
27
  --dirty Record that the source tree had changes
28
28
  --clean Record that the source tree was clean
@@ -226,6 +226,69 @@ async function createThreeBlocks( bytes ) {
226
226
 
227
227
  }
228
228
 
229
+ async function createThreeGS( bytes ) {
230
+
231
+ const [ THREE, { PLYLoader }, { GaussianSplatMesh }, splatUtils ] = await Promise.all( [
232
+ import( 'three/webgpu' ),
233
+ import( 'three/addons/loaders/PLYLoader.js' ),
234
+ import( '../vendor/three-gs/objects/GaussianSplatMesh.js' ),
235
+ import( '../vendor/three-gs/utils/GaussianSplatUtils.js' ),
236
+ ] );
237
+ const renderer = new THREE.WebGPURenderer( {
238
+ canvas,
239
+ antialias: false,
240
+ alpha: false,
241
+ powerPreference: 'high-performance',
242
+ } );
243
+ // The addon's own example keeps splat colors out of any transfer-function conversion by
244
+ // setting the working color space to sRGB; a LinearSRGB output space is the equivalent
245
+ // no-op here, and matches the display-space compositing the rest of the suite uses.
246
+ renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
247
+ renderer.toneMapping = THREE.NoToneMapping;
248
+ renderer.setPixelRatio( benchmarkViewport.dpr );
249
+ renderer.setSize( benchmarkViewport.width, benchmarkViewport.height, false );
250
+ await renderer.init();
251
+
252
+ const scene3d = new THREE.Scene();
253
+ scene3d.background = new THREE.Color( suiteId === 'standard' ? 0 : BACKGROUND );
254
+ const camera = new THREE.PerspectiveCamera( benchmarkViewport.fov, benchmarkViewport.width / benchmarkViewport.height, 0.01, 100 );
255
+ setThreeCamera( camera, benchmarkPose );
256
+
257
+ // The PR reuses the stock PLYLoader plus a conversion helper rather than shipping its own
258
+ // splat PLY parser, so the benchmark loads it exactly that way.
259
+ const loader = new PLYLoader();
260
+ loader.setCustomPropertyNameMapping( splatUtils.GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING );
261
+ const splatGeometry = splatUtils.createGaussianSplatGeometryFromPLYGeometry( loader.parse( bytes ) );
262
+ const splats = new GaussianSplatMesh( splatGeometry );
263
+ // The dataset PLY is in source (COLMAP-style Y-down) space; apply the suite's shared Rx180
264
+ // model transform, as the other runtimes do.
265
+ splats.quaternion.set( 1, 0, 0, 0 );
266
+ scene3d.add( splats );
267
+
268
+ const render = async pose => {
269
+
270
+ setThreeCamera( camera, pose );
271
+ await renderer.render( scene3d, camera );
272
+ await renderer.backend?.device?.queue?.onSubmittedWorkDone?.();
273
+
274
+ };
275
+ await render( benchmarkPose );
276
+ return {
277
+ backend: 'WebGPU',
278
+ render,
279
+ async diagnostics() {
280
+
281
+ return {
282
+ reportedGpuBytes: null,
283
+ reportedGpuBytesScope: null,
284
+ splatCount: splatGeometry.getAttribute( 'position' ).count,
285
+ };
286
+
287
+ },
288
+ };
289
+
290
+ }
291
+
229
292
  async function createSpark( bytes ) {
230
293
 
231
294
  const [ THREE, { SparkRenderer, SplatMesh } ] = await Promise.all( [
@@ -636,7 +699,7 @@ async function measureBrowserMemory() {
636
699
  async function run() {
637
700
 
638
701
  if ( ! [ 'standard', 'micro' ].includes( suiteId ) ) throw new Error( `Unknown suite: ${suiteId}` );
639
- if ( ! [ 'three-blocks', 'spark', 'supersplat', 'gs3d', 'babylon' ].includes( rendererId ) ) {
702
+ if ( ! [ 'three-blocks', 'three-gs', 'spark', 'supersplat', 'gs3d', 'babylon' ].includes( rendererId ) ) {
640
703
 
641
704
  throw new Error( `Unknown renderer: ${rendererId}` );
642
705
 
@@ -675,6 +738,7 @@ async function run() {
675
738
  }
676
739
  let adapter;
677
740
  if ( rendererId === 'three-blocks' ) adapter = await createThreeBlocks( bytes );
741
+ if ( rendererId === 'three-gs' ) adapter = await createThreeGS( bytes );
678
742
  if ( rendererId === 'spark' ) adapter = await createSpark( bytes );
679
743
  if ( rendererId === 'supersplat' ) adapter = await createSuperSplat( bytes, 'webgpu' );
680
744
  if ( rendererId === 'gs3d' ) adapter = await createGS3D( bytes );
package/lib/run.mjs CHANGED
@@ -47,6 +47,14 @@ const RENDERERS = [
47
47
  backend: 'WebGPU',
48
48
  source: 'https://www.npmjs.com/package/three-blocks',
49
49
  },
50
+ {
51
+ id: 'three-gs',
52
+ label: 'three.js splats',
53
+ version: 'PR #33950',
54
+ backend: 'WebGPU',
55
+ source: 'https://github.com/mrdoob/three.js/pull/33950',
56
+ commit: '5b8fcba16c8e0ab1e3745cd2c8185fe527998a36',
57
+ },
50
58
  {
51
59
  id: 'spark',
52
60
  label: 'Spark',
@@ -275,11 +283,19 @@ async function runPage( browser, baseUrl, { renderer, suite, item, warmup, frame
275
283
  } );
276
284
  search.set( suite === 'standard' ? 'view' : 'scene', item.id );
277
285
  await page.goto( `${baseUrl}/${renderer}/?${search}`, { waitUntil: 'domcontentloaded' } );
278
- await page.waitForFunction(
279
- () => window.__publicGaussianBenchmark && window.__publicGaussianBenchmark.status !== 'running',
280
- null,
281
- { timeout: 300_000 },
282
- );
286
+ try {
287
+
288
+ await page.waitForFunction(
289
+ () => window.__publicGaussianBenchmark && window.__publicGaussianBenchmark.status !== 'running',
290
+ null,
291
+ { timeout: 300_000 },
292
+ );
293
+
294
+ } catch ( error ) {
295
+
296
+ throw new Error( `${renderer}/${item.id}: ${error.message}\n${diagnostics.join( '\n' )}` );
297
+
298
+ }
283
299
  const report = await page.evaluate( () => window.__publicGaussianBenchmark );
284
300
  if ( report.status !== 'ready' ) {
285
301
 
@@ -356,7 +372,7 @@ export async function runBenchmark( options ) {
356
372
 
357
373
  }
358
374
  const quick = options.quick === true;
359
- const headed = options.headed ?? ( process.platform === 'darwin' );
375
+ const headed = options.headed ?? false;
360
376
  const repetitions = quick ? 1 : 3;
361
377
  // 60 settle frames: the frame lane reports each runtime's steady state at rest — long
362
378
  // enough for idle-time optimizations (e.g. Three Blocks' auto tile promotion) to reach
@@ -414,10 +430,17 @@ export async function runBenchmark( options ) {
414
430
  resolve: {
415
431
  alias: [
416
432
  { find: 'supersplat-viewer-runtime', replacement: superSplatRuntime },
433
+ // Both loaders spell their worker as `new URL( './<name>.js', import.meta.url )` so
434
+ // bundlers emit the entry. The published chunks sit at the dist root, where those
435
+ // paths do not exist; point them at the real worker modules.
417
436
  {
418
437
  find: path.join( path.dirname( threeBlocksEntry ), 'SplatVideoInflateWorker.js' ),
419
438
  replacement: threeBlocksInflateWorker,
420
439
  },
440
+ {
441
+ find: path.join( path.dirname( threeBlocksEntry ), 'GaussianSplatsLoadWorker.js' ),
442
+ replacement: threeBlocksWorker,
443
+ },
421
444
  ],
422
445
  dedupe: [ 'three' ],
423
446
  },
@@ -457,15 +480,21 @@ export async function runBenchmark( options ) {
457
480
  server = await serveDirectory( bundleRoot, externalFiles );
458
481
  browser = await chromium.launch( {
459
482
  headless: ! headed,
460
- args: [ '--enable-unsafe-webgpu', '--js-flags=--expose-gc' ],
483
+ args: [
484
+ '--enable-unsafe-webgpu',
485
+ '--js-flags=--expose-gc',
486
+ // Headless Chromium on macOS otherwise picks SwiftShader, which fails the
487
+ // physical-GPU gate and captures blank frames.
488
+ ...( process.platform === 'darwin' ? [ '--use-angle=metal' ] : [] ),
489
+ ],
461
490
  } );
462
491
  const cdp = await browser.newBrowserCDPSession();
463
492
  const systemInfo = await cdp.send( 'SystemInfo.getInfo' );
464
493
  const gpu = gpuSummary( systemInfo );
465
494
  const physicalGpu = isPhysicalGpu( gpu );
466
- if ( ! quick && ( ! headed || ! physicalGpu ) ) {
495
+ if ( ! quick && ! physicalGpu ) {
467
496
 
468
- throw new Error( 'Publication capture requires headed Chromium on a physical GPU; use --quick for local validation.' );
497
+ throw new Error( 'Publication capture requires a physical GPU; use --quick for local validation.' );
469
498
 
470
499
  }
471
500
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "three-blocks-benchmark",
3
- "version": "0.3.3",
4
- "description": "Reproducible Gaussian splat browser benchmarks for Three Blocks, Spark, SuperSplat, GaussianSplats3D, and Babylon.js",
3
+ "version": "0.4.0-alpha.372.gf7fb70f4af27",
4
+ "description": "Reproducible Gaussian splat browser benchmarks for Three Blocks, the three.js GaussianSplatMesh addon, Spark, SuperSplat, GaussianSplats3D, and Babylon.js",
5
5
  "keywords": [
6
6
  "three",
7
7
  "threejs",
@@ -28,6 +28,7 @@
28
28
  "bin",
29
29
  "browser",
30
30
  "lib",
31
+ "vendor",
31
32
  "README.md",
32
33
  "CHANGELOG.md",
33
34
  "LICENSE",
@@ -51,7 +52,11 @@
51
52
  "playwright": "1.61.1",
52
53
  "pngjs": "7.0.0",
53
54
  "three": "0.185.1",
54
- "three-blocks": "0.9.0",
55
+ "three-blocks": "0.9.1-alpha.372.gf7fb70f4af27",
55
56
  "vite": "8.1.3"
57
+ },
58
+ "threeBlocks": {
59
+ "channel": "alpha",
60
+ "site": "https://preprod.threejs-blocks.com"
56
61
  }
57
62
  }
@@ -0,0 +1,29 @@
1
+ # three.js Gaussian splat addon (unmerged)
2
+
3
+ Verbatim copies of the addon files from mrdoob/three.js PR #33950
4
+ ("Gaussian Splat renderer / loader using TSL for WebGPU/WebGL + glTF import",
5
+ by Ben Houston), pinned to commit `5b8fcba16c8e0ab1e3745cd2c8185fe527998a36`.
6
+ The PR is not merged and ships in no npm release, so the benchmark vendors the
7
+ files instead of resolving a package version.
8
+
9
+ | File | SHA-256 |
10
+ | --- | --- |
11
+ | `objects/GaussianSplatMesh.js` | `fb04d66d3c5d10068c0aa88e2eb2002aac096def8234216b28133b1de3ea6921` |
12
+ | `gpgpu/CountingSort.js` | `e9e90b6940a4da923534c3f249e8e7bd2c7603f8b432c54c62ecdc6db4be47e3` |
13
+ | `utils/GaussianSplatUtils.js` | `1094941578d4ad2a56c79444d4607a679764e6fc07e83b662292e37a15a8e27e` |
14
+
15
+ Refresh with:
16
+
17
+ ```sh
18
+ sha=5b8fcba16c8e0ab1e3745cd2c8185fe527998a36
19
+ for f in objects/GaussianSplatMesh.js gpgpu/CountingSort.js utils/GaussianSplatUtils.js; do
20
+ curl -sSfL "https://raw.githubusercontent.com/mrdoob/three.js/$sha/examples/jsm/$f" -o "$f"
21
+ done
22
+ ```
23
+
24
+ The directory layout mirrors `examples/jsm/` so the files stay byte-identical to
25
+ upstream (`GaussianSplatMesh.js` imports `../gpgpu/CountingSort.js`). PLY input
26
+ uses the `PLYLoader` from the installed `three` release, as the PR intends.
27
+
28
+ Licensed under the MIT License, Copyright © 2010-2026 three.js authors:
29
+ <https://github.com/mrdoob/three.js/blob/dev/LICENSE>
@@ -0,0 +1,271 @@
1
+ import { StorageBufferAttribute, DynamicDrawUsage } from 'three/webgpu';
2
+ import { Fn, Loop, atomicAdd, atomicLoad, atomicStore, instanceIndex, storage, uint } from 'three/tsl';
3
+
4
+ /**
5
+ * A reusable GPU counting sort.
6
+ *
7
+ * This computes a stable-ish permutation of the integers `[0, count)` that orders them by an
8
+ * arbitrary, user supplied `uint` key ("bin") in the range `[0, binCount)`. It is a good fit for
9
+ * approximate ordering of large element counts (hundreds of thousands to millions) where an exact
10
+ * comparison sort such as a bitonic sort (see {@link BitonicSort}) would be too slow: a counting
11
+ * sort only requires a fixed number of passes (reset, histogram, prefix sum, scatter) regardless of
12
+ * `count`, at the cost of only being accurate to the resolution of `binCount` - elements that land
13
+ * in the same bin end up in an unspecified relative order.
14
+ *
15
+ * This class does not compute the sort key itself. Instead, a TSL function is supplied via
16
+ * {@link CountingSort#setBinNode} that maps the current `instanceIndex` to a bin, and an equivalent
17
+ * plain JavaScript function can be supplied to {@link CountingSort#computeCPU} for platforms without
18
+ * compute shader support (e.g. the WebGL backend of {@link WebGPURenderer}).
19
+ *
20
+ * ```js
21
+ * const sort = new CountingSort( count, { binCount: 4096 } );
22
+ * sort.setBinNode( () => {
23
+ *
24
+ * // return a `Node<uint>` bin index for `instanceIndex`, e.g. derived from a depth value.
25
+ *
26
+ * } );
27
+ *
28
+ * sort.compute( renderer );
29
+ *
30
+ * // `sort.orderRead` now holds a storage buffer of `count` indices, ordered by bin.
31
+ * ```
32
+ *
33
+ * @three_import import { CountingSort } from 'three/addons/gpgpu/CountingSort.js';
34
+ */
35
+ class CountingSort {
36
+
37
+ /**
38
+ * Constructs a new counting sort.
39
+ *
40
+ * @param {number} count - The number of elements to sort.
41
+ * @param {Object} [options={}] - Options that modify the counting sort.
42
+ * @param {number} [options.binCount=4096] - The number of bins/buckets the sort key is quantized into. Larger values improve sort accuracy at the cost of a longer (but still single-pass) prefix sum.
43
+ * @param {number} [options.workgroupSize=256] - The workgroup size of the compute shaders executed during the sort.
44
+ */
45
+ constructor( count, { binCount = 4096, workgroupSize = 256 } = {} ) {
46
+
47
+ /**
48
+ * The number of elements to sort.
49
+ *
50
+ * @type {number}
51
+ */
52
+ this.count = count;
53
+
54
+ /**
55
+ * The number of bins/buckets the sort key is quantized into.
56
+ *
57
+ * @type {number}
58
+ */
59
+ this.binCount = binCount;
60
+
61
+ /**
62
+ * The workgroup size of the compute shaders executed during the sort.
63
+ *
64
+ * @type {number}
65
+ */
66
+ this.workgroupSize = workgroupSize;
67
+
68
+ const orderData = new Uint32Array( count );
69
+ for ( let i = 0; i < count; i ++ ) orderData[ i ] = i;
70
+
71
+ /**
72
+ * The buffer attribute holding the sorted order (a permutation of `[0, count)`). This is
73
+ * also the attribute that is kept up to date by {@link CountingSort#computeCPU}.
74
+ *
75
+ * @type {StorageBufferAttribute}
76
+ */
77
+ this.orderAttribute = new StorageBufferAttribute( orderData, 1, Uint32Array );
78
+
79
+ const binAttribute = new StorageBufferAttribute( new Uint32Array( count ), 1, Uint32Array );
80
+ const histogramAttribute = new StorageBufferAttribute( new Uint32Array( binCount ), 1, Uint32Array );
81
+ const offsetAttribute = new StorageBufferAttribute( new Uint32Array( binCount ), 1, Uint32Array );
82
+
83
+ /**
84
+ * A read-only storage node for the sorted order buffer.
85
+ *
86
+ * @type {StorageBufferNode}
87
+ */
88
+ this.orderRead = storage( this.orderAttribute, 'uint', count ).toReadOnly();
89
+
90
+ /**
91
+ * A writable storage node for the sorted order buffer.
92
+ *
93
+ * @type {StorageBufferNode}
94
+ */
95
+ this.orderWrite = storage( this.orderAttribute, 'uint', count );
96
+
97
+ /**
98
+ * A read-only storage node holding each element's bin, computed during the histogram pass.
99
+ *
100
+ * @type {StorageBufferNode}
101
+ */
102
+ this.binRead = storage( binAttribute, 'uint', count ).toReadOnly();
103
+
104
+ /**
105
+ * A writable storage node holding each element's bin.
106
+ *
107
+ * @type {StorageBufferNode}
108
+ */
109
+ this.binWrite = storage( binAttribute, 'uint', count );
110
+
111
+ /**
112
+ * An atomic storage node used to accumulate the per-bin histogram.
113
+ *
114
+ * @type {StorageBufferNode}
115
+ */
116
+ this.histogramAtomic = storage( histogramAttribute, 'uint', binCount ).toAtomic();
117
+
118
+ /**
119
+ * An atomic storage node used both for the exclusive prefix sum of the histogram and, during
120
+ * the scatter pass, as a per-bin write cursor.
121
+ *
122
+ * @type {StorageBufferNode}
123
+ */
124
+ this.offsetAtomic = storage( offsetAttribute, 'uint', binCount ).toAtomic();
125
+
126
+ this._webGLBuffersEnabled = false;
127
+
128
+ this._cpuBins = new Uint32Array( count );
129
+ this._cpuCounts = new Uint32Array( binCount );
130
+ this._cpuOffsets = new Uint32Array( binCount );
131
+
132
+ this._resetNode = null;
133
+ this._histogramNode = null;
134
+ this._prefixNode = null;
135
+ this._scatterNode = null;
136
+
137
+ }
138
+
139
+ /**
140
+ * Sets the TSL function used to compute the bin of the element currently referenced by
141
+ * `instanceIndex`, and (re)builds the compute nodes used by {@link CountingSort#compute}.
142
+ *
143
+ * @param {Function} binNode - A parameterless function returning a `Node<uint>` in `[0, binCount)`.
144
+ */
145
+ setBinNode( binNode ) {
146
+
147
+ const { binCount, workgroupSize, count } = this;
148
+
149
+ this._resetNode = Fn( () => {
150
+
151
+ atomicStore( this.histogramAtomic.element( instanceIndex ), uint( 0 ) );
152
+ atomicStore( this.offsetAtomic.element( instanceIndex ), uint( 0 ) );
153
+
154
+ } )().compute( binCount, [ workgroupSize ] ).setName( 'CountingSortReset' );
155
+
156
+ this._histogramNode = Fn( () => {
157
+
158
+ const bin = binNode().toVar( 'bin' );
159
+
160
+ this.binWrite.element( instanceIndex ).assign( bin );
161
+ atomicAdd( this.histogramAtomic.element( bin ), uint( 1 ) );
162
+
163
+ } )().compute( count, [ workgroupSize ] ).setName( 'CountingSortHistogram' );
164
+
165
+ this._prefixNode = Fn( () => {
166
+
167
+ const sum = uint( 0 ).toVar( 'sum' );
168
+
169
+ Loop( { start: 0, end: binCount, type: 'uint', name: 'bin', condition: '<' }, ( { bin } ) => {
170
+
171
+ const binCountValue = atomicLoad( this.histogramAtomic.element( bin ) ).toVar( 'count' );
172
+ atomicStore( this.offsetAtomic.element( bin ), sum );
173
+ sum.addAssign( binCountValue );
174
+
175
+ } );
176
+
177
+ } )().compute( 1 ).setName( 'CountingSortPrefix' );
178
+
179
+ this._scatterNode = Fn( () => {
180
+
181
+ const bin = this.binRead.element( instanceIndex ).toVar( 'bin' );
182
+ const targetIndex = atomicAdd( this.offsetAtomic.element( bin ), uint( 1 ) ).toVar( 'targetIndex' );
183
+
184
+ this.orderWrite.element( targetIndex ).assign( instanceIndex );
185
+
186
+ } )().compute( count, [ workgroupSize ] ).setName( 'CountingSortScatter' );
187
+
188
+ }
189
+
190
+ /**
191
+ * Executes a complete counting sort on the GPU, updating {@link CountingSort#orderRead}.
192
+ *
193
+ * @param {Renderer} renderer - The current scene's renderer.
194
+ */
195
+ compute( renderer ) {
196
+
197
+ renderer.compute( this._resetNode );
198
+ renderer.compute( this._histogramNode );
199
+ renderer.compute( this._prefixNode );
200
+ renderer.compute( this._scatterNode );
201
+
202
+ }
203
+
204
+ /**
205
+ * Executes a complete counting sort on the CPU, updating {@link CountingSort#orderAttribute}.
206
+ * Intended as a fallback for backends without compute shader support.
207
+ *
208
+ * @param {Function} binFn - A function taking an element index and returning its bin (a plain number in `[0, binCount)`).
209
+ */
210
+ computeCPU( binFn ) {
211
+
212
+ const { count, binCount } = this;
213
+ const order = this.orderAttribute.array;
214
+ const bins = this._cpuBins;
215
+ const counts = this._cpuCounts;
216
+ const offsets = this._cpuOffsets;
217
+
218
+ counts.fill( 0 );
219
+
220
+ for ( let i = 0; i < count; i ++ ) {
221
+
222
+ const bin = binFn( i );
223
+
224
+ bins[ i ] = bin;
225
+ counts[ bin ] ++;
226
+
227
+ }
228
+
229
+ let sum = 0;
230
+
231
+ for ( let i = 0; i < binCount; i ++ ) {
232
+
233
+ offsets[ i ] = sum;
234
+ sum += counts[ i ];
235
+
236
+ }
237
+
238
+ for ( let i = 0; i < count; i ++ ) {
239
+
240
+ order[ offsets[ bins[ i ] ] ++ ] = i;
241
+
242
+ }
243
+
244
+ this.orderAttribute.needsUpdate = true;
245
+
246
+ if ( this.orderAttribute.pbo !== undefined ) {
247
+
248
+ this.orderAttribute.pbo.needsUpdate = true;
249
+
250
+ }
251
+
252
+ }
253
+
254
+ /**
255
+ * Enables the WebGL-specific storage buffer path (PBO + dynamic draw usage) for the order buffer.
256
+ * Only needed when {@link CountingSort#computeCPU} is used with the WebGL backend of {@link WebGPURenderer}.
257
+ */
258
+ enableWebGLBuffers() {
259
+
260
+ if ( this._webGLBuffersEnabled === true ) return;
261
+
262
+ this.orderAttribute.setUsage( DynamicDrawUsage );
263
+ this.orderRead.setPBO( true );
264
+
265
+ this._webGLBuffersEnabled = true;
266
+
267
+ }
268
+
269
+ }
270
+
271
+ export { CountingSort };
@@ -0,0 +1,474 @@
1
+ import {
2
+ BufferAttribute,
3
+ InstancedBufferGeometry,
4
+ Matrix4,
5
+ Mesh,
6
+ NodeMaterial,
7
+ StorageBufferAttribute,
8
+ Vector2,
9
+ Vector3
10
+ } from 'three/webgpu';
11
+
12
+ import {
13
+ Discard,
14
+ Fn,
15
+ If,
16
+ atan,
17
+ cameraProjectionMatrix,
18
+ cos,
19
+ dot,
20
+ exp,
21
+ float,
22
+ highpModelViewMatrix,
23
+ instanceIndex,
24
+ max,
25
+ min,
26
+ positionGeometry,
27
+ screenSize,
28
+ sin,
29
+ sqrt,
30
+ storage,
31
+ uint,
32
+ uniform,
33
+ varyingProperty,
34
+ vec2,
35
+ vec3,
36
+ vec4
37
+ } from 'three/tsl';
38
+
39
+ import { CountingSort } from '../gpgpu/CountingSort.js';
40
+
41
+ const BIN_COUNT = 4096;
42
+ const WORKGROUP_SIZE = 256;
43
+ const SORT_DIRECTION_THRESHOLD = 0.9995;
44
+ const SORT_POSITION_THRESHOLD = 0.0025;
45
+ const KERNEL_2D_SIZE = 0.3;
46
+ const MAX_SCREEN_SPACE_SPLAT_SIZE = 1024;
47
+ const CLIP_XY = 1.4;
48
+
49
+ const _worldCenter = /*@__PURE__*/ new Vector3();
50
+ const _viewCenter = /*@__PURE__*/ new Vector3();
51
+ const _worldScale = /*@__PURE__*/ new Vector3();
52
+ const _cameraPosition = /*@__PURE__*/ new Vector3();
53
+ const _cameraDirection = /*@__PURE__*/ new Vector3();
54
+ const _sortDepthRange = /*@__PURE__*/ new Vector2();
55
+
56
+ /**
57
+ * A minimal renderer for 3D Gaussian splat geometry.
58
+ *
59
+ * Note that this class can only be used with {@link WebGPURenderer}. The
60
+ * `forceWebGL` fallback of {@link WebGPURenderer} is supported, but
61
+ * {@link WebGLRenderer} is not. Import maps or package exports must resolve
62
+ * both `three/webgpu` and `three/tsl`.
63
+ *
64
+ * ```js
65
+ * const splats = new GaussianSplatMesh( geometry );
66
+ * scene.add( splats );
67
+ * ```
68
+ *
69
+ * @augments Mesh
70
+ * @three_import import { GaussianSplatMesh } from 'three/addons/objects/GaussianSplatMesh.js';
71
+ */
72
+ class GaussianSplatMesh extends Mesh {
73
+
74
+ /**
75
+ * Constructs a new Gaussian splat mesh.
76
+ *
77
+ * @param {BufferGeometry} splatGeometry - The splat geometry to render.
78
+ * @param {Object} [options] - Options.
79
+ * @param {boolean} [options.autoSort=true] - Whether to sort automatically in `onBeforeRender`.
80
+ */
81
+ constructor( splatGeometry, { autoSort = true } = {} ) {
82
+
83
+ const positionAttribute = splatGeometry.getAttribute( 'position' );
84
+ const covarianceAttribute = splatGeometry.getAttribute( 'covariance' );
85
+ const colorAttribute = splatGeometry.getAttribute( 'color' );
86
+ const count = positionAttribute.count;
87
+
88
+ if ( splatGeometry.boundingBox === null ) splatGeometry.computeBoundingBox();
89
+ if ( splatGeometry.boundingSphere === null ) splatGeometry.computeBoundingSphere();
90
+
91
+ const geometry = createGeometry( count );
92
+ const buffers = createStorageBuffers( count, positionAttribute.array, covarianceAttribute.array, colorAttribute.array );
93
+ const sort = new CountingSort( count, { binCount: BIN_COUNT, workgroupSize: WORKGROUP_SIZE } );
94
+ const material = createMaterial( buffers, sort );
95
+
96
+ super( geometry, material );
97
+
98
+ /**
99
+ * This flag can be used for type testing.
100
+ *
101
+ * @type {boolean}
102
+ * @readonly
103
+ * @default true
104
+ */
105
+ this.isGaussianSplatMesh = true;
106
+
107
+ this.type = 'GaussianSplatMesh';
108
+
109
+ /**
110
+ * The source splat geometry.
111
+ *
112
+ * @type {BufferGeometry}
113
+ */
114
+ this.splatGeometry = splatGeometry;
115
+
116
+ /**
117
+ * Whether to sort automatically in `onBeforeRender`.
118
+ *
119
+ * @type {boolean}
120
+ */
121
+ this.autoSort = autoSort;
122
+
123
+ this.frustumCulled = false;
124
+
125
+ this._buffers = buffers;
126
+ this._sort = sort;
127
+ this._sortMatrix = uniform( new Matrix4() );
128
+ this._sortDepthRange = uniform( new Vector2( 0, 1 ) );
129
+ this._sortInitialized = false;
130
+ this._lastSortPosition = new Vector3( Infinity, Infinity, Infinity );
131
+ this._lastSortDirection = new Vector3( 0, 0, - 1 );
132
+ this._positionAttribute = positionAttribute;
133
+
134
+ const centerRead = buffers.centerRead;
135
+ const sortMatrix = this._sortMatrix;
136
+ const sortDepthRange = this._sortDepthRange;
137
+
138
+ sort.setBinNode( () => {
139
+
140
+ const center = centerRead.element( instanceIndex ).xyz.toVar( 'center' );
141
+ const viewCenter = sortMatrix.mul( vec4( center, 1 ) ).xyz.toVar( 'viewCenter' );
142
+ const depth = viewCenter.z.negate().toVar( 'depth' );
143
+ const range = max( sortDepthRange.y.sub( sortDepthRange.x ), 0.0001 ).toVar( 'range' );
144
+ const normalized = depth.sub( sortDepthRange.x ).div( range ).clamp( 0, 1 ).toVar( 'normalized' );
145
+ const depthBin = uint( normalized.mul( BIN_COUNT - 1 ) ).toVar( 'depthBin' );
146
+
147
+ return uint( BIN_COUNT - 1 ).sub( depthBin );
148
+
149
+ } );
150
+
151
+ this.onBeforeRender = ( renderer, scene, camera ) => {
152
+
153
+ if ( this.autoSort === true ) {
154
+
155
+ this.updateSort( renderer, camera );
156
+
157
+ }
158
+
159
+ };
160
+
161
+ }
162
+
163
+ /**
164
+ * Updates the draw order if the camera has moved enough to need a new sort.
165
+ *
166
+ * @param {Renderer} renderer - The renderer.
167
+ * @param {Camera} camera - The camera used for rendering.
168
+ * @return {boolean} Whether a sort was dispatched this call.
169
+ */
170
+ updateSort( renderer, camera ) {
171
+
172
+ if ( this._sortInitialized === false || this._needsSort( camera ) === true ) {
173
+
174
+ this._updateSortUniforms( camera );
175
+
176
+ if ( renderer.backend && renderer.backend.isWebGLBackend === true ) {
177
+
178
+ enableWebGLBuffers( this._buffers );
179
+ this._sort.enableWebGLBuffers();
180
+ this._sortCPU();
181
+
182
+ } else {
183
+
184
+ this._sort.compute( renderer );
185
+
186
+ }
187
+
188
+ this._sortInitialized = true;
189
+
190
+ return true;
191
+
192
+ }
193
+
194
+ return false;
195
+
196
+ }
197
+
198
+ _needsSort( camera ) {
199
+
200
+ _cameraPosition.setFromMatrixPosition( camera.matrixWorld );
201
+
202
+ const e = camera.matrixWorld.elements;
203
+ _cameraDirection.set( - e[ 8 ], - e[ 9 ], - e[ 10 ] ).normalize();
204
+
205
+ const positionChanged = _cameraPosition.distanceToSquared( this._lastSortPosition ) > SORT_POSITION_THRESHOLD * SORT_POSITION_THRESHOLD;
206
+ const directionChanged = _cameraDirection.dot( this._lastSortDirection ) < SORT_DIRECTION_THRESHOLD;
207
+
208
+ if ( positionChanged === true || directionChanged === true ) {
209
+
210
+ this._lastSortPosition.copy( _cameraPosition );
211
+ this._lastSortDirection.copy( _cameraDirection );
212
+ return true;
213
+
214
+ }
215
+
216
+ return false;
217
+
218
+ }
219
+
220
+ _updateSortUniforms( camera ) {
221
+
222
+ this.updateWorldMatrix( true, false );
223
+
224
+ this._sortMatrix.value.multiplyMatrices( camera.matrixWorldInverse, this.matrixWorld );
225
+
226
+ _worldCenter.copy( this.splatGeometry.boundingSphere.center ).applyMatrix4( this.matrixWorld );
227
+ _viewCenter.copy( _worldCenter ).applyMatrix4( camera.matrixWorldInverse );
228
+ this.getWorldScale( _worldScale );
229
+
230
+ const radius = this.splatGeometry.boundingSphere.radius * Math.max( _worldScale.x, _worldScale.y, _worldScale.z );
231
+ const depth = - _viewCenter.z;
232
+ const nearDepth = Math.max( camera.near, depth - radius );
233
+ const farDepth = Math.max( nearDepth + 0.0001, depth + radius );
234
+
235
+ _sortDepthRange.set( nearDepth, farDepth );
236
+ this._sortDepthRange.value.copy( _sortDepthRange );
237
+
238
+ }
239
+
240
+ _sortCPU() {
241
+
242
+ const centers = this._positionAttribute.array;
243
+ const matrix = this._sortMatrix.value.elements;
244
+ const nearDepth = this._sortDepthRange.value.x;
245
+ const range = Math.max( this._sortDepthRange.value.y - nearDepth, 0.0001 );
246
+ const scale = ( BIN_COUNT - 1 ) / range;
247
+
248
+ this._sort.computeCPU( ( i ) => {
249
+
250
+ const i3 = i * 3;
251
+ const depth = - ( matrix[ 2 ] * centers[ i3 ] + matrix[ 6 ] * centers[ i3 + 1 ] + matrix[ 10 ] * centers[ i3 + 2 ] + matrix[ 14 ] );
252
+ const depthBin = Math.min( BIN_COUNT - 1, Math.max( 0, Math.floor( ( depth - nearDepth ) * scale ) ) );
253
+
254
+ return BIN_COUNT - 1 - depthBin;
255
+
256
+ } );
257
+
258
+ }
259
+
260
+ }
261
+
262
+ function createGeometry( count ) {
263
+
264
+ const geometry = new InstancedBufferGeometry();
265
+ geometry.setAttribute( 'position', new BufferAttribute( new Float32Array( [
266
+ - 2, - 2, 0,
267
+ 2, - 2, 0,
268
+ 2, 2, 0,
269
+ - 2, 2, 0
270
+ ] ), 3 ) );
271
+ geometry.setIndex( [ 0, 1, 2, 0, 2, 3 ] );
272
+ geometry.instanceCount = count;
273
+
274
+ return geometry;
275
+
276
+ }
277
+
278
+ function createStorageBuffers( count, centers, covariances, colors ) {
279
+
280
+ const centerData = new Float32Array( count * 4 );
281
+ const covarianceAData = new Float32Array( count * 4 );
282
+ const covarianceBData = new Float32Array( count * 4 );
283
+ const colorData = new Float32Array( count * 4 );
284
+
285
+ for ( let i = 0; i < count; i ++ ) {
286
+
287
+ const i3 = i * 3;
288
+ const i4 = i * 4;
289
+ const i6 = i * 6;
290
+
291
+ centerData[ i4 ] = centers[ i3 ];
292
+ centerData[ i4 + 1 ] = centers[ i3 + 1 ];
293
+ centerData[ i4 + 2 ] = centers[ i3 + 2 ];
294
+
295
+ covarianceAData[ i4 ] = covariances[ i6 ];
296
+ covarianceAData[ i4 + 1 ] = covariances[ i6 + 1 ];
297
+ covarianceAData[ i4 + 2 ] = covariances[ i6 + 2 ];
298
+ covarianceAData[ i4 + 3 ] = covariances[ i6 + 3 ];
299
+
300
+ covarianceBData[ i4 ] = covariances[ i6 + 4 ];
301
+ covarianceBData[ i4 + 1 ] = covariances[ i6 + 5 ];
302
+
303
+ colorData[ i4 ] = colors[ i4 ] / 255;
304
+ colorData[ i4 + 1 ] = colors[ i4 + 1 ] / 255;
305
+ colorData[ i4 + 2 ] = colors[ i4 + 2 ] / 255;
306
+ colorData[ i4 + 3 ] = colors[ i4 + 3 ] / 255;
307
+
308
+ }
309
+
310
+ const centerAttribute = new StorageBufferAttribute( centerData, 4 );
311
+ const covarianceAAttribute = new StorageBufferAttribute( covarianceAData, 4 );
312
+ const covarianceBAttribute = new StorageBufferAttribute( covarianceBData, 4 );
313
+ const colorAttribute = new StorageBufferAttribute( colorData, 4 );
314
+
315
+ return {
316
+ count,
317
+ webGLBuffersEnabled: false,
318
+ centerRead: storage( centerAttribute, 'vec4', count ).toReadOnly(),
319
+ covarianceARead: storage( covarianceAAttribute, 'vec4', count ).toReadOnly(),
320
+ covarianceBRead: storage( covarianceBAttribute, 'vec4', count ).toReadOnly(),
321
+ colorRead: storage( colorAttribute, 'vec4', count ).toReadOnly()
322
+ };
323
+
324
+ }
325
+
326
+ function enableWebGLBuffers( buffers ) {
327
+
328
+ if ( buffers.webGLBuffersEnabled === true ) return;
329
+
330
+ buffers.centerRead.setPBO( true );
331
+ buffers.covarianceARead.setPBO( true );
332
+ buffers.covarianceBRead.setPBO( true );
333
+ buffers.colorRead.setPBO( true );
334
+ buffers.webGLBuffersEnabled = true;
335
+
336
+ }
337
+
338
+ function createMaterial( buffers, sort ) {
339
+
340
+ const splatUv = varyingProperty( 'vec2', 'vSplatUv' );
341
+ const splatColor = varyingProperty( 'vec4', 'vSplatColor' );
342
+
343
+ const vertexNode = Fn( () => {
344
+
345
+ const splatIndex = sort.orderRead.element( instanceIndex ).toVar( 'splatIndex' );
346
+ const center = buffers.centerRead.element( splatIndex ).xyz.toVar( 'center' );
347
+ const covA = buffers.covarianceARead.element( splatIndex ).toVar( 'covA' );
348
+ const covB = buffers.covarianceBRead.element( splatIndex ).toVar( 'covB' );
349
+ const color = buffers.colorRead.element( splatIndex ).toVar( 'splatColor' );
350
+
351
+ splatUv.assign( positionGeometry.xy );
352
+
353
+ const viewCenter4 = highpModelViewMatrix.mul( vec4( center, 1 ) ).toVar( 'viewCenter4' );
354
+ const viewCenter = viewCenter4.xyz.toVar( 'viewCenter' );
355
+ const centerClip = cameraProjectionMatrix.mul( viewCenter4 ).toVar( 'centerClip' );
356
+
357
+ const m = highpModelViewMatrix;
358
+ const r0 = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x ).toVar( 'r0' );
359
+ const r1 = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y ).toVar( 'r1' );
360
+ const r2 = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z ).toVar( 'r2' );
361
+
362
+ const cov0 = vec3( covA.x, covA.y, covA.z ).toVar( 'cov0' );
363
+ const cov1 = vec3( covA.y, covA.w, covB.x ).toVar( 'cov1' );
364
+ const cov2 = vec3( covA.z, covB.x, covB.y ).toVar( 'cov2' );
365
+
366
+ const vc0 = vec3( dot( r0, cov0 ), dot( r0, cov1 ), dot( r0, cov2 ) ).toVar( 'vc0' );
367
+ const vc1 = vec3( dot( r1, cov0 ), dot( r1, cov1 ), dot( r1, cov2 ) ).toVar( 'vc1' );
368
+ const vc2 = vec3( dot( r2, cov0 ), dot( r2, cov1 ), dot( r2, cov2 ) ).toVar( 'vc2' );
369
+
370
+ const c00 = dot( vc0, r0 ).toVar( 'c00' );
371
+ const c01 = dot( vc0, r1 ).toVar( 'c01' );
372
+ const c02 = dot( vc0, r2 ).toVar( 'c02' );
373
+ const c11 = dot( vc1, r1 ).toVar( 'c11' );
374
+ const c12 = dot( vc1, r2 ).toVar( 'c12' );
375
+ const c22 = dot( vc2, r2 ).toVar( 'c22' );
376
+
377
+ const z = min( viewCenter.z, - 0.01 ).toVar( 'z' );
378
+ const invZ = float( 1 ).div( z ).toVar( 'invZ' );
379
+ const invZ2 = invZ.mul( invZ ).toVar( 'invZ2' );
380
+ const focal = screenSize.mul( 0.5 ).mul( vec2( cameraProjectionMatrix[ 0 ].x, cameraProjectionMatrix[ 1 ].y ) ).toVar( 'focal' );
381
+
382
+ const j00 = focal.x.negate().mul( invZ ).toVar( 'j00' );
383
+ const j11 = focal.y.negate().mul( invZ ).toVar( 'j11' );
384
+ const j02 = focal.x.mul( viewCenter.x ).mul( invZ2 ).toVar( 'j02' );
385
+ const j12 = focal.y.mul( viewCenter.y ).mul( invZ2 ).toVar( 'j12' );
386
+
387
+ const aBase = j00.mul( j00 ).mul( c00 )
388
+ .add( j00.mul( j02 ).mul( c02 ).mul( 2 ) )
389
+ .add( j02.mul( j02 ).mul( c22 ) )
390
+ .toVar( 'cov2dABase' );
391
+ const b = j00.mul( j11 ).mul( c01 )
392
+ .add( j00.mul( j12 ).mul( c02 ) )
393
+ .add( j02.mul( j11 ).mul( c12 ) )
394
+ .add( j02.mul( j12 ).mul( c22 ) )
395
+ .toVar( 'cov2dB' );
396
+ const cBase = j11.mul( j11 ).mul( c11 )
397
+ .add( j11.mul( j12 ).mul( c12 ).mul( 2 ) )
398
+ .add( j12.mul( j12 ).mul( c22 ) )
399
+ .toVar( 'cov2dCBase' );
400
+ const a = aBase.add( KERNEL_2D_SIZE ).toVar( 'cov2dA' );
401
+ const c = cBase.add( KERNEL_2D_SIZE ).toVar( 'cov2dC' );
402
+ const detBase = aBase.mul( cBase ).sub( b.mul( b ) ).toVar( 'detBase' );
403
+ const det = a.mul( c ).sub( b.mul( b ) ).toVar( 'det' );
404
+ const alphaScale = sqrt( max( detBase.div( max( det, 0.000001 ) ), 0 ) ).toVar( 'alphaScale' );
405
+
406
+ splatColor.assign( vec4( color.rgb, color.a.mul( alphaScale ) ) );
407
+
408
+ const halfTrace = a.add( c ).mul( 0.5 ).toVar( 'halfTrace' );
409
+ const radius = sqrt( max( a.sub( c ).mul( 0.5 ).pow2().add( b.mul( b ) ), 0.0000001 ) ).toVar( 'radius' );
410
+ const lambda1 = max( halfTrace.add( radius ), 0.0000001 ).toVar( 'lambda1' );
411
+ const lambda2 = max( halfTrace.sub( radius ), 0.0000001 ).toVar( 'lambda2' );
412
+ const axis1 = vec2( 1, 0 ).toVar( 'axis1' );
413
+
414
+ If( radius.greaterThan( 0.00001 ), () => {
415
+
416
+ const angle = atan( b.mul( 2 ), a.sub( c ) ).mul( 0.5 ).toVar( 'angle' );
417
+ axis1.assign( vec2( cos( angle ), sin( angle ) ) );
418
+
419
+ } );
420
+
421
+ const axis2 = vec2( axis1.y.negate(), axis1.x ).toVar( 'axis2' );
422
+
423
+ const scale1 = min( sqrt( lambda1 ), MAX_SCREEN_SPACE_SPLAT_SIZE ).toVar( 'scale1' );
424
+ const scale2 = min( sqrt( lambda2 ), MAX_SCREEN_SPACE_SPLAT_SIZE ).toVar( 'scale2' );
425
+ const offsetPixels = axis1.mul( positionGeometry.x ).mul( scale1 ).add( axis2.mul( positionGeometry.y ).mul( scale2 ) ).toVar( 'offsetPixels' );
426
+ const offsetNdc = offsetPixels.mul( 2 ).div( screenSize ).toVar( 'offsetNdc' );
427
+ const clip = centerClip.add( vec4( offsetNdc.mul( centerClip.w ), 0, 0 ) ).toVar( 'clip' );
428
+
429
+ const clipLimit = centerClip.w.mul( CLIP_XY ).toVar( 'clipLimit' );
430
+
431
+ If( viewCenter.z.greaterThanEqual( - 0.01 )
432
+ .or( centerClip.z.lessThan( centerClip.w.negate() ) )
433
+ .or( centerClip.z.greaterThan( centerClip.w ) )
434
+ .or( centerClip.x.lessThan( clipLimit.negate() ) )
435
+ .or( centerClip.x.greaterThan( clipLimit ) )
436
+ .or( centerClip.y.lessThan( clipLimit.negate() ) )
437
+ .or( centerClip.y.greaterThan( clipLimit ) ), () => {
438
+
439
+ clip.assign( vec4( 2, 2, 2, 1 ) );
440
+
441
+ } );
442
+
443
+ return clip;
444
+
445
+ } )();
446
+
447
+ const fragmentNode = Fn( () => {
448
+
449
+ const r2 = dot( splatUv, splatUv ).toVar( 'r2' );
450
+
451
+ If( r2.greaterThan( 4 ), () => {
452
+
453
+ Discard();
454
+
455
+ } );
456
+
457
+ return vec4( splatColor.rgb, exp( r2.mul( - 0.5 ) ).mul( splatColor.a ) );
458
+
459
+ } )();
460
+
461
+ const material = new NodeMaterial();
462
+ material.vertexNode = vertexNode;
463
+ material.colorNode = fragmentNode;
464
+ material.transparent = true;
465
+ material.depthWrite = false;
466
+ material.depthTest = true;
467
+ material.forceSinglePass = true;
468
+ material.fog = false;
469
+
470
+ return material;
471
+
472
+ }
473
+
474
+ export { GaussianSplatMesh };
@@ -0,0 +1,218 @@
1
+ import {
2
+ BufferAttribute,
3
+ BufferGeometry
4
+ } from 'three';
5
+
6
+ const SH_C0 = 0.2820947917738781;
7
+ const GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING = {
8
+ scale: [ 'scale_0', 'scale_1', 'scale_2' ],
9
+ rotation: [ 'rot_0', 'rot_1', 'rot_2', 'rot_3' ],
10
+ f_dc: [ 'f_dc_0', 'f_dc_1', 'f_dc_2' ],
11
+ opacity: [ 'opacity' ]
12
+ };
13
+
14
+ function clampByte( value ) {
15
+
16
+ return Math.min( 255, Math.max( 0, Math.round( value ) ) );
17
+
18
+ }
19
+
20
+ function sigmoid( value ) {
21
+
22
+ return 1 / ( 1 + Math.exp( - value ) );
23
+
24
+ }
25
+
26
+ function writeColorBytes( target, offset, r, g, b, a ) {
27
+
28
+ target[ offset ] = clampByte( r );
29
+ target[ offset + 1 ] = clampByte( g );
30
+ target[ offset + 2 ] = clampByte( b );
31
+ target[ offset + 3 ] = clampByte( a );
32
+
33
+ }
34
+
35
+ function sh0ToLinear( coefficient ) {
36
+
37
+ return coefficient * SH_C0 + 0.5;
38
+
39
+ }
40
+
41
+ function linearToSH0( color ) {
42
+
43
+ return ( color - 0.5 ) / SH_C0;
44
+
45
+ }
46
+
47
+ function writeColorBytesFromSH0( target, offset, r, g, b, a ) {
48
+
49
+ writeColorBytes(
50
+ target,
51
+ offset,
52
+ sh0ToLinear( r ) * 255,
53
+ sh0ToLinear( g ) * 255,
54
+ sh0ToLinear( b ) * 255,
55
+ a * 255
56
+ );
57
+
58
+ }
59
+
60
+ function writeCovariance( target, offset, sx, sy, sz, qx, qy, qz, qw ) {
61
+
62
+ const length = Math.hypot( qx, qy, qz, qw );
63
+
64
+ if ( length === 0 ) {
65
+
66
+ qx = 0;
67
+ qy = 0;
68
+ qz = 0;
69
+ qw = 1;
70
+
71
+ } else {
72
+
73
+ const invLength = 1 / length;
74
+ qx *= invLength;
75
+ qy *= invLength;
76
+ qz *= invLength;
77
+ qw *= invLength;
78
+
79
+ }
80
+
81
+ const x2 = qx + qx;
82
+ const y2 = qy + qy;
83
+ const z2 = qz + qz;
84
+ const xx = qx * x2;
85
+ const xy = qx * y2;
86
+ const xz = qx * z2;
87
+ const yy = qy * y2;
88
+ const yz = qy * z2;
89
+ const zz = qz * z2;
90
+ const wx = qw * x2;
91
+ const wy = qw * y2;
92
+ const wz = qw * z2;
93
+
94
+ const r00 = 1 - ( yy + zz );
95
+ const r01 = xy - wz;
96
+ const r02 = xz + wy;
97
+ const r10 = xy + wz;
98
+ const r11 = 1 - ( xx + zz );
99
+ const r12 = yz - wx;
100
+ const r20 = xz - wy;
101
+ const r21 = yz + wx;
102
+ const r22 = 1 - ( xx + yy );
103
+
104
+ const sxx = sx * sx;
105
+ const syy = sy * sy;
106
+ const szz = sz * sz;
107
+
108
+ target[ offset ] = r00 * r00 * sxx + r01 * r01 * syy + r02 * r02 * szz;
109
+ target[ offset + 1 ] = r00 * r10 * sxx + r01 * r11 * syy + r02 * r12 * szz;
110
+ target[ offset + 2 ] = r00 * r20 * sxx + r01 * r21 * syy + r02 * r22 * szz;
111
+ target[ offset + 3 ] = r10 * r10 * sxx + r11 * r11 * syy + r12 * r12 * szz;
112
+ target[ offset + 4 ] = r10 * r20 * sxx + r11 * r21 * syy + r12 * r22 * szz;
113
+ target[ offset + 5 ] = r20 * r20 * sxx + r21 * r21 * syy + r22 * r22 * szz;
114
+
115
+ }
116
+
117
+ function createGaussianSplatGeometry( centers, covariances, colors ) {
118
+
119
+ const geometry = new BufferGeometry();
120
+ geometry.setAttribute( 'position', new BufferAttribute( centers, 3 ) );
121
+ geometry.setAttribute( 'covariance', new BufferAttribute( covariances, 6 ) );
122
+ geometry.setAttribute( 'color', new BufferAttribute( colors, 4, true ) );
123
+ geometry.computeBoundingBox();
124
+ geometry.computeBoundingSphere();
125
+
126
+ return geometry;
127
+
128
+ }
129
+
130
+ function createGaussianSplatGeometryFromPLYGeometry( geometry, {
131
+ scaleAttribute = 'scale',
132
+ rotationAttribute = 'rotation',
133
+ sh0Attribute = 'f_dc',
134
+ opacityAttribute = 'opacity'
135
+ } = {} ) {
136
+
137
+ if ( geometry === undefined || geometry.isBufferGeometry !== true ) {
138
+
139
+ throw new Error( 'THREE.createGaussianSplatGeometryFromPLYGeometry: PLY geometry must be a BufferGeometry.' );
140
+
141
+ }
142
+
143
+ const position = geometry.getAttribute( 'position' );
144
+ const scale = geometry.getAttribute( scaleAttribute );
145
+ const rotation = geometry.getAttribute( rotationAttribute );
146
+ const sh0 = geometry.getAttribute( sh0Attribute );
147
+ const opacity = geometry.getAttribute( opacityAttribute );
148
+
149
+ if ( position === undefined || scale === undefined || rotation === undefined || sh0 === undefined || opacity === undefined ) {
150
+
151
+ throw new Error( 'THREE.createGaussianSplatGeometryFromPLYGeometry: PLY geometry requires position, scale, rotation, f_dc and opacity attributes.' );
152
+
153
+ }
154
+
155
+ const count = position.count;
156
+
157
+ if ( position.itemSize !== 3 || scale.itemSize !== 3 || rotation.itemSize !== 4 || sh0.itemSize !== 3 || opacity.itemSize !== 1 ) {
158
+
159
+ throw new Error( 'THREE.createGaussianSplatGeometryFromPLYGeometry: Invalid Gaussian splat PLY attribute itemSize.' );
160
+
161
+ }
162
+
163
+ if ( scale.count !== count || rotation.count !== count || sh0.count !== count || opacity.count !== count ) {
164
+
165
+ throw new Error( 'THREE.createGaussianSplatGeometryFromPLYGeometry: Gaussian splat PLY attribute counts must match position.' );
166
+
167
+ }
168
+
169
+ const centers = new Float32Array( count * 3 );
170
+ const covariances = new Float32Array( count * 6 );
171
+ const colors = new Uint8Array( count * 4 );
172
+
173
+ for ( let i = 0; i < count; i ++ ) {
174
+
175
+ const i3 = i * 3;
176
+ centers[ i3 ] = position.getX( i );
177
+ centers[ i3 + 1 ] = position.getY( i );
178
+ centers[ i3 + 2 ] = position.getZ( i );
179
+
180
+ const sx = Math.exp( scale.getX( i ) );
181
+ const sy = Math.exp( scale.getY( i ) );
182
+ const sz = Math.exp( scale.getZ( i ) );
183
+
184
+ // GraphDECO/INRIA PLY stores quaternions as rot_0=w, rot_1=x, rot_2=y, rot_3=z.
185
+ const qw = rotation.getX( i );
186
+ const qx = rotation.getY( i );
187
+ const qy = rotation.getZ( i );
188
+ const qz = rotation.getW( i );
189
+
190
+ writeCovariance( covariances, i * 6, sx, sy, sz, qx, qy, qz, qw );
191
+ writeColorBytesFromSH0(
192
+ colors,
193
+ i * 4,
194
+ sh0.getX( i ),
195
+ sh0.getY( i ),
196
+ sh0.getZ( i ),
197
+ sigmoid( opacity.getX( i ) )
198
+ );
199
+
200
+ }
201
+
202
+ return createGaussianSplatGeometry( centers, covariances, colors );
203
+
204
+ }
205
+
206
+ export {
207
+ GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING,
208
+ SH_C0,
209
+ clampByte,
210
+ createGaussianSplatGeometry,
211
+ createGaussianSplatGeometryFromPLYGeometry,
212
+ linearToSH0,
213
+ sh0ToLinear,
214
+ sigmoid,
215
+ writeColorBytes,
216
+ writeColorBytesFromSH0,
217
+ writeCovariance
218
+ };