Skip to content

Commit a568475

Browse files
committed
Introduce SplatUtils.mergeSplats and solve envMap global sorting
1 parent d43e2fb commit a568475

File tree

7 files changed

+337
-45
lines changed

7 files changed

+337
-45
lines changed

examples/envmap/index.html

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
</script>
4141
<script type="module">
4242
import * as THREE from "three";
43-
import { Splat, SplatLoader } from "@sparkjsdev/spark";
43+
import { Splat, SplatLoader, SplatUtils } from "@sparkjsdev/spark";
4444
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
4545
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
4646

@@ -61,18 +61,22 @@
6161

6262
const splatLoader = new SplatLoader();
6363
const splatURL = await getAssetFileURL("fireplace.spz");
64+
6465
const background = await splatLoader.loadAsync(splatURL);
6566
background.quaternion.set(1, 0, 0, 0);
6667
background.position.set(0.5, 0, -1);
6768
background.scale.setScalar(0.5);
68-
scene.add(background);
6969

7070
const background2 = background.clone();
7171
background2.quaternion.set(1, 0, 0, 0);
7272
background2.rotation.y = Math.PI;
7373
background2.position.set(-0.5, 0, 0.0);
7474
background2.scale.setScalar(0.5);
75-
scene.add(background2); // FIXME: Combine/merge splats before rendering
75+
76+
const combinedBackground = SplatUtils.mergeSplats([ background, background2 ]);
77+
scene.add(combinedBackground);
78+
combinedBackground.sorter.sortRadial = true;
79+
combinedBackground.sorter.sort360 = true;
7680

7781
// Add rubber duck
7882
const gltfLoader = new GLTFLoader();
@@ -82,15 +86,9 @@
8286
duck.position.set(0, 0.45, -0.4);
8387
scene.add(duck);
8488

85-
background2.sorter.sortRadial = true;
86-
background2.sorter.sort360 = true;
87-
Promise.all([
88-
background.sortFor(renderer, camera),
89-
background2.sortFor(renderer, camera),
90-
]).then(() => {
89+
combinedBackground.sortFor(renderer, camera).then(() => {
9190
// Disable sorting while rendering the env map
92-
background.autoSort = false;
93-
background2.autoSort = false;
91+
combinedBackground.autoSort = false;
9492

9593
const sceneRT = new THREE.PMREMGenerator(renderer).fromScene(scene);
9694
for (let obj of duck.children) {
@@ -100,8 +98,7 @@
10098
obj.material.roughness = 0.02;
10199
}
102100

103-
background.autoSort = true;
104-
background2.autoSort = true;
101+
combinedBackground.autoSort = true;
105102
});
106103

107104
let lastTime;

src/Splat.ts

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,42 @@ export interface SplatData {
4848
) => void;
4949
}
5050

51+
export type SplatCallback = (
52+
i: number,
53+
x: number,
54+
y: number,
55+
z: number,
56+
scaleX: number,
57+
scaleY: number,
58+
scaleZ: number,
59+
quatX: number,
60+
quatY: number,
61+
quatZ: number,
62+
quatW: number,
63+
opacity: number,
64+
r: number,
65+
g: number,
66+
b: number,
67+
sh?: ArrayLike<number>,
68+
) => void;
69+
70+
/**
71+
* Extended SplatData interface that allows the splat properties
72+
* to be decoded and read back.
73+
*/
74+
export interface IterableSplatData extends SplatData {
75+
iterateSplats: (callback: SplatCallback) => void;
76+
}
77+
78+
export type SortContext = {
79+
lastOriginToCamera: THREE.Matrix4;
80+
sortJob: Promise<SplatOrdering> | null;
81+
ordering: Uint32Array;
82+
pendingOrdering: Uint32Array;
83+
activeSplats: number;
84+
orderingId: number;
85+
};
86+
5187
/**
5288
* Object representing a collection of Gaussian Splats in a scene.
5389
*/
@@ -76,19 +112,9 @@ export class Splat extends THREE.Mesh<SplatGeometry, THREE.ShaderMaterial> {
76112
sorter: SplatSorter = new ReadbackSplatSorter();
77113
/**
78114
* Mapping from camera to sort context.
79-
* This allows multiple ca
115+
* This allows multiple viewpoints from different cameras.
80116
*/
81-
private sortContext: WeakMap<
82-
THREE.Camera,
83-
{
84-
lastOriginToCamera: THREE.Matrix4;
85-
sortJob: Promise<SplatOrdering> | null;
86-
ordering: Uint32Array;
87-
pendingOrdering: Uint32Array;
88-
activeSplats: number;
89-
orderingId: number;
90-
}
91-
> = new WeakMap();
117+
private sortContext: WeakMap<THREE.Camera, SortContext> = new WeakMap();
92118
/**
93119
* Id of the current ordering used by the SplatGeometry.
94120
*/
@@ -360,15 +386,7 @@ export class Splat extends THREE.Mesh<SplatGeometry, THREE.ShaderMaterial> {
360386
renderer,
361387
context.pendingOrdering,
362388
);
363-
context.sortJob.then((result) => {
364-
context.sortJob = null;
365-
// Swap ordering arrays
366-
context.pendingOrdering = context.ordering;
367-
context.ordering = result.ordering;
368-
369-
context.activeSplats = result.activeSplats;
370-
context.orderingId = globalOrderingId++;
371-
});
389+
context.sortJob.then((result) => this.onSortComplete(context, result));
372390
} else if (needsSort) {
373391
console.log("Suppressing sort as one is in progress...");
374392
}
@@ -383,6 +401,16 @@ export class Splat extends THREE.Mesh<SplatGeometry, THREE.ShaderMaterial> {
383401
}
384402
}
385403

404+
protected onSortComplete(context: SortContext, result: SplatOrdering) {
405+
context.sortJob = null;
406+
// Swap ordering arrays
407+
context.pendingOrdering = context.ordering;
408+
context.ordering = result.ordering;
409+
410+
context.activeSplats = result.activeSplats;
411+
context.orderingId = globalOrderingId++;
412+
}
413+
386414
setShaderHooks(hooks: ShaderHooks | null): ShaderHooks | null {
387415
const previousShaderHooks = this.shaderHooks;
388416
this.shaderHooks = hooks;

src/SplatUtils.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import * as THREE from "three";
2+
import { type IterableSplatData, Splat } from "./Splat";
3+
import { DefaultSplatEncoding, type SplatEncoder } from "./encoding/encoder";
4+
5+
const tempCenter = new THREE.Vector3();
6+
const tempScales = new THREE.Vector3();
7+
const tempQuat = new THREE.Quaternion();
8+
9+
/**
10+
* Combines multiple Splat objects into a single Splat object. The individual
11+
* world transforms are applied to the individual splats. Each splat must
12+
* have the same number of spherical harmonics and the underlying SplatData
13+
* must be iterable.
14+
* @param splats The splats to combine
15+
* @param options Additional options
16+
* @returns The combined splat
17+
*/
18+
export function mergeSplats<T>(
19+
splats: Array<Splat>,
20+
options?: {
21+
splatEncoder?: SplatEncoder<T> | (() => SplatEncoder<T>);
22+
},
23+
): Splat | null {
24+
const numSh = splats[0].splatData.numSh;
25+
const splatEncoderFactory =
26+
options?.splatEncoder ?? DefaultSplatEncoding.createSplatEncoder;
27+
const splatEncoder =
28+
typeof splatEncoderFactory === "function"
29+
? splatEncoderFactory()
30+
: splatEncoderFactory;
31+
32+
// Sum the total amount of combined splats.
33+
const numSplats = splats.reduce(
34+
(acc, splat) => acc + splat.splatData.numSplats,
35+
0,
36+
);
37+
splatEncoder.allocate(numSplats, numSh);
38+
39+
let newSplatIndex = 0;
40+
for (let i = 0; i < splats.length; ++i) {
41+
const splatData = splats[i].splatData;
42+
if (splatData.numSh !== numSh) {
43+
console.error(
44+
`SplatUtils: .mergeSplats() failed with splat at index ${i}. All splats must have the same amount of spherical harmonics.`,
45+
);
46+
return null;
47+
}
48+
49+
if (!("iterateSplats" in splatData)) {
50+
console.error(
51+
`SplatUtils: .mergeSplats() failed with splat at index ${i}. All splats must have iterable splat data.`,
52+
);
53+
return null;
54+
}
55+
56+
// Ensure matrix world is up to date
57+
splats[i].updateMatrixWorld();
58+
const splatScale = splats[i].getWorldScale(new THREE.Vector3());
59+
const splatRotation = splats[i].getWorldQuaternion(new THREE.Quaternion());
60+
61+
const iterableSplatData = splatData as IterableSplatData;
62+
iterableSplatData.iterateSplats(
63+
(
64+
_,
65+
x,
66+
y,
67+
z,
68+
scaleX,
69+
scaleY,
70+
scaleZ,
71+
quatX,
72+
quatY,
73+
quatZ,
74+
quatW,
75+
opacity,
76+
r,
77+
g,
78+
b,
79+
sh,
80+
) => {
81+
// Apply splat transform
82+
tempCenter.set(x, y, z).applyMatrix4(splats[i].matrixWorld);
83+
tempScales.set(scaleX, scaleY, scaleZ).multiplyScalar(splatScale.x); // Assume uniform scaling
84+
tempQuat.set(quatX, quatY, quatZ, quatW).premultiply(splatRotation);
85+
86+
splatEncoder.setSplat(
87+
newSplatIndex++,
88+
tempCenter.x,
89+
tempCenter.y,
90+
tempCenter.z,
91+
tempScales.x,
92+
tempScales.y,
93+
tempScales.z,
94+
tempQuat.x,
95+
tempQuat.y,
96+
tempQuat.z,
97+
tempQuat.w,
98+
opacity,
99+
r,
100+
g,
101+
b,
102+
);
103+
if (sh) {
104+
splatEncoder.setSplatSh(newSplatIndex, sh);
105+
}
106+
},
107+
);
108+
}
109+
110+
return new Splat(splatEncoder.close());
111+
}

src/encoding/ExtendedSplats.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as THREE from "three";
2-
import type { SplatData } from "../Splat";
2+
import type { IterableSplatData, SplatCallback, SplatData } from "../Splat";
33
import {
44
LN_SCALE_MAX,
55
LN_SCALE_MIN,
@@ -8,13 +8,14 @@ import {
88
} from "../defines";
99
import {
1010
computeMaxSplats,
11+
decodeQuatOctXy88R8,
1112
encodeQuatOctXy88R8,
1213
floatBitsToUint,
1314
floatToUint8,
1415
getTextureSize,
1516
uintBitsToFloat,
1617
} from "../utils";
17-
import type { ResizableSplatEncoder, SplatEncoder } from "./encoder";
18+
import type { ResizableSplatEncoder } from "./encoder";
1819

1920
export type ExtendedSplatsOptions = {
2021
// Reserve space for at least this many splats when constructing the collection
@@ -27,7 +28,7 @@ export type ExtendedSplatsOptions = {
2728
numSh?: number;
2829
};
2930

30-
export class ExtendedSplats implements SplatData {
31+
export class ExtendedSplats implements IterableSplatData {
3132
maxSplats = 0;
3233
numSplats = 0;
3334
numSh = 0;
@@ -150,6 +151,63 @@ export class ExtendedSplats implements SplatData {
150151
}
151152
}
152153

154+
iterateSplats(callback: SplatCallback) {
155+
const shCoeffients = SH_DEGREE_TO_NUM_COEFF[this.numSh];
156+
const sh = this.numSh > 0 ? new Float32Array(shCoeffients) : undefined;
157+
158+
for (let i = 0; i < this.numSplats; i++) {
159+
const i4 = i * 4;
160+
const word0 = this.packedArray1[i4 + 0];
161+
const word1 = this.packedArray1[i4 + 1];
162+
const word2 = this.packedArray1[i4 + 2];
163+
const word3 = this.packedArray1[i4 + 3];
164+
const word4 = this.packedArray2[i4 + 0];
165+
const word5 = this.packedArray2[i4 + 1];
166+
const word6 = this.packedArray2[i4 + 2];
167+
const word7 = this.packedArray2[i4 + 3];
168+
169+
const r = (word5 & 0xff) / 255;
170+
const g = ((word5 >>> 8) & 0xff) / 255;
171+
const b = ((word5 >>> 16) & 0xff) / 255;
172+
const a = ((word5 >>> 24) & 0xff) / 255;
173+
174+
const lnScaleScale = (LN_SCALE_MAX - LN_SCALE_MIN) / 1023.0;
175+
const uScalesX = word3 & 0x3ff;
176+
const scaleX = Math.exp(LN_SCALE_MIN + uScalesX * lnScaleScale);
177+
const uScalesY = (word3 >>> 10) & 0x3ff;
178+
const scaleY = Math.exp(LN_SCALE_MIN + uScalesY * lnScaleScale);
179+
const uScalesZ = (word3 >>> 20) & 0x3ff;
180+
const scaleZ = Math.exp(LN_SCALE_MIN + uScalesZ * lnScaleScale);
181+
182+
decodeQuatOctXy88R8(word4, tempQuaternion);
183+
184+
if (sh && this.packedShArray) {
185+
for (let j = 0; j < shCoeffients; j++) {
186+
sh[j] = (this.packedShArray[i * shCoeffients + j] - 127) / 127;
187+
}
188+
}
189+
190+
callback(
191+
i,
192+
uintBitsToFloat(word0),
193+
uintBitsToFloat(word1),
194+
uintBitsToFloat(word2),
195+
scaleX,
196+
scaleY,
197+
scaleZ,
198+
tempQuaternion.x,
199+
tempQuaternion.y,
200+
tempQuaternion.z,
201+
tempQuaternion.w,
202+
a,
203+
r,
204+
g,
205+
b,
206+
sh,
207+
);
208+
}
209+
}
210+
153211
static encodingName = "extended";
154212

155213
static createSplatEncoder(): ResizableSplatEncoder<EncodedExtendedSplats> {

0 commit comments

Comments
 (0)