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 +15 -0
- package/NOTICE +10 -0
- package/README.md +10 -7
- package/bin/three-blocks-benchmark.mjs +1 -1
- package/browser/harness.js +65 -1
- package/lib/run.mjs +38 -9
- package/package.json +8 -3
- package/vendor/three-gs/README.md +29 -0
- package/vendor/three-gs/gpgpu/CountingSort.js +271 -0
- package/vendor/three-gs/objects/GaussianSplatMesh.js +474 -0
- package/vendor/three-gs/utils/GaussianSplatUtils.js +218 -0
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,
|
|
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
|
|
33
|
-
|
|
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 --
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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
|
package/browser/harness.js
CHANGED
|
@@ -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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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 ??
|
|
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: [
|
|
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 &&
|
|
495
|
+
if ( ! quick && ! physicalGpu ) {
|
|
467
496
|
|
|
468
|
-
throw new Error( 'Publication capture requires
|
|
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.
|
|
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.
|
|
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
|
+
};
|