Skip to content

Commit e4d7397

Browse files
committed
Add CPU based sorting and basic BatchedSplat implementation
1 parent df1f998 commit e4d7397

File tree

13 files changed

+547
-31
lines changed

13 files changed

+547
-31
lines changed

examples/procedural-splats/index.html

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
</script>
2727
<script type="module">
2828
import * as THREE from "three";
29-
import { Splat, PackedSplats } from "@sparkjsdev/spark";
29+
import { BatchedSplat, Splat, PackedSplats } from "@sparkjsdev/spark";
3030
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
3131
import { SparkControls } from "/examples/js/controls.js";
3232

@@ -70,7 +70,6 @@
7070
}
7171
});
7272
pyramid.position.set(0, -0.5, -2);
73-
scene.add(pyramid);
7473

7574
const stars = Splat.constructFixed(100000, (splatEncoder, numSplats) => {
7675
const center = new THREE.Vector3();
@@ -88,8 +87,6 @@
8887
}
8988
});
9089
stars.position.set(0, -0.5, -2);
91-
stars.renderOrder = -1;
92-
scene.add(stars);
9390

9491
// Generate splats from rendered text
9592
const text = Splat.fromText("I GSplats!", {
@@ -99,7 +96,6 @@
9996
});
10097
text.scale.setScalar(0.5 / 80);
10198
text.position.set(0.15, -1, -2);
102-
scene.add(text);
10399

104100
const text2 = Splat.fromText("WASD + mouse to move", {
105101
font: "Verdana",
@@ -108,14 +104,20 @@
108104
});
109105
text2.scale.setScalar(0.2 / 40);
110106
text2.position.set(0, 1.0, -2);
111-
scene.add(text2);
112107

113108
// Generate splats from image pixels with alpha channel
114109
const imageURL = await getAssetFileURL("butterfly.png");
115110
const image = await Splat.fromImageUrl(imageURL);
116111
image.scale.setScalar(0.7 / 400);
117112
image.position.set(-0.5, -1, -2);
118-
scene.add(image);
113+
114+
const batchedSplat = new BatchedSplat(5);
115+
batchedSplat.addSplat(pyramid);
116+
batchedSplat.addSplat(stars);
117+
batchedSplat.addSplat(text);
118+
batchedSplat.addSplat(text2);
119+
batchedSplat.addSplat(image);
120+
scene.add(batchedSplat);
119121

120122
const controls = new SparkControls({ canvas: renderer.domElement });
121123

@@ -139,6 +141,15 @@
139141

140142
// Animate opacity of top text
141143
text2.opacity = Math.abs(Math.sin(time / 1000) * 0.3);
144+
145+
pyramid.updateMatrixWorld();
146+
stars.updateMatrixWorld();
147+
image.updateMatrixWorld();
148+
text2.updateMatrixWorld();
149+
batchedSplat.setMatrixAt(0, pyramid.matrixWorld);
150+
batchedSplat.setMatrixAt(1, stars.matrixWorld);
151+
batchedSplat.setMatrixAt(3, text2.matrixWorld);
152+
batchedSplat.setMatrixAt(4, image.matrixWorld);
142153
});
143154
</script>
144155
</body>

src/BatchedSplat.ts

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import * as THREE from "three";
2+
import {
3+
type IterableSplatData,
4+
type SortContext,
5+
Splat,
6+
type SplatData,
7+
} from "./Splat";
8+
import { CpuSplatSorter, type SplatOrdering } from "./SplatSorter";
9+
import { isIterableSplatData } from "./SplatUtils";
10+
import type { TransformRange } from "./defines";
11+
import { DefaultSplatEncoding } from "./encoding/encoder";
12+
13+
/**
14+
* Specialized Splat class for combining multiple splats in one draw call.
15+
* All splats are sorted allowing for overlapping splats, while each instance
16+
* retains its own transform matrix.
17+
*/
18+
export class BatchedSplat extends Splat {
19+
readonly maxInstanceCount: number;
20+
private readonly batchedSplatData: BatchedSplatData;
21+
22+
private matricesArray: Float32Array;
23+
private matricesTexture: THREE.DataTexture;
24+
25+
constructor(maxInstanceCount: number) {
26+
const batchingTextureUniform: THREE.IUniform = {
27+
value: null as THREE.Texture | null,
28+
};
29+
const batchedSplatData = new BatchedSplatData(batchingTextureUniform);
30+
super(batchedSplatData, { sorter: new CpuSplatSorter() });
31+
this.batchedSplatData = batchedSplatData;
32+
33+
this.maxInstanceCount = maxInstanceCount;
34+
let size = Math.sqrt(this.maxInstanceCount * 4); // 4 pixels needed for 1 matrix
35+
size = Math.ceil(size / 4) * 4;
36+
size = Math.max(size, 4);
37+
38+
this.matricesArray = new Float32Array(size * size * 4); // 4 floats per RGBA pixel
39+
this.matricesTexture = new THREE.DataTexture(
40+
this.matricesArray,
41+
size,
42+
size,
43+
THREE.RGBAFormat,
44+
THREE.FloatType,
45+
);
46+
batchingTextureUniform.value = this.matricesTexture;
47+
48+
// Disable frustum culling as the transform of BatchedSplat is ignored
49+
// in favour of the individual instance transform matrices.
50+
this.frustumCulled = false;
51+
}
52+
53+
addSplat(splat: Splat) {
54+
const splatData = splat.splatData;
55+
if (!isIterableSplatData(splatData)) {
56+
throw new Error(
57+
"Splat can't be added to BatchedSplat as its splat data is not iterable",
58+
);
59+
}
60+
61+
this.addSplatData(splatData);
62+
const index = this.batchedSplatData.instanceCount - 1;
63+
splat.updateMatrixWorld();
64+
this.setMatrixAt(index, splat.matrixWorld);
65+
}
66+
67+
addSplatData(splatData: IterableSplatData) {
68+
this.batchedSplatData.addSplatData(splatData);
69+
this.batchedSplatData.setupMaterial(this.material);
70+
this.needsUpdate = true;
71+
}
72+
73+
removeSplatData(splatData: IterableSplatData) {
74+
this.batchedSplatData.removeSplatData(splatData);
75+
this.batchedSplatData.setupMaterial(this.material);
76+
this.needsUpdate = true;
77+
}
78+
79+
setMatrixAt(instanceId: number, matrix: THREE.Matrix4) {
80+
matrix.toArray(this.matricesArray, instanceId * 16);
81+
this.matricesTexture.needsUpdate = true;
82+
this.needsUpdate = true;
83+
return this;
84+
}
85+
86+
getTransformRanges(): Array<TransformRange> {
87+
const result: Array<TransformRange> = [];
88+
89+
let start = 0;
90+
for (let i = 0; i < this.batchedSplatData.instanceCount; i++) {
91+
const numSplats = this.batchedSplatData.sources[i].numSplats;
92+
result.push({
93+
start,
94+
end: start + numSplats,
95+
matrix: [...this.matricesArray.slice(i * 16, (i + 1) * 16)],
96+
});
97+
start += numSplats;
98+
}
99+
100+
return result;
101+
}
102+
103+
protected onSortComplete(context: SortContext, result: SplatOrdering) {
104+
// Include object index into ordering array
105+
for (let i = 0; i < result.activeSplats; i++) {
106+
const splatIndex = result.ordering[i];
107+
const objectIndex = this.batchedSplatData.getInstanceIndexFor(splatIndex);
108+
result.ordering[i] = splatIndex | (objectIndex << 26);
109+
}
110+
super.onSortComplete(context, result);
111+
}
112+
113+
dispose(): void {
114+
super.dispose();
115+
this.batchedSplatData.dispose();
116+
}
117+
}
118+
119+
/**
120+
* SplatData implementation that allows combining multiple individual
121+
* splat data sources into one for batched draw calls.
122+
*/
123+
class BatchedSplatData implements SplatData {
124+
private splatData: SplatData;
125+
sources: Array<IterableSplatData> = [];
126+
private batchingTextureUniform: THREE.IUniform<THREE.Texture>;
127+
128+
constructor(batchingTextureUniform: THREE.IUniform<THREE.Texture>) {
129+
this.splatData = this.recreate();
130+
this.batchingTextureUniform = batchingTextureUniform;
131+
}
132+
133+
private recreate(): SplatData {
134+
const numSh = this.sources[0]?.numSh ?? 0;
135+
const numSplats = this.sources.reduce(
136+
(sum, source) => sum + source.numSplats,
137+
0,
138+
);
139+
140+
const splatEncoder = DefaultSplatEncoding.createSplatEncoder();
141+
splatEncoder.allocate(numSplats, numSh);
142+
143+
let splatIndex = 0;
144+
for (const source of this.sources) {
145+
source.iterateSplats(
146+
(
147+
_,
148+
x,
149+
y,
150+
z,
151+
scaleX,
152+
scaleY,
153+
scaleZ,
154+
quatX,
155+
quatY,
156+
quatZ,
157+
quatW,
158+
opacity,
159+
r,
160+
g,
161+
b,
162+
sh,
163+
) => {
164+
splatEncoder.setSplat(
165+
splatIndex,
166+
x,
167+
y,
168+
z,
169+
scaleX,
170+
scaleY,
171+
scaleZ,
172+
quatX,
173+
quatY,
174+
quatZ,
175+
quatW,
176+
opacity,
177+
r,
178+
g,
179+
b,
180+
);
181+
if (sh) {
182+
splatEncoder.setSplatSh(splatIndex, sh);
183+
}
184+
splatIndex++;
185+
},
186+
);
187+
}
188+
189+
this.splatData?.dispose();
190+
this.splatData = splatEncoder.close();
191+
192+
return this.splatData;
193+
}
194+
195+
get instanceCount() {
196+
return this.sources.length;
197+
}
198+
199+
getInstanceIndexFor(splatIndex: number): number {
200+
let instanceIndex = 0;
201+
let instanceEnd = this.sources[instanceIndex].numSplats;
202+
while (splatIndex > instanceEnd) {
203+
instanceIndex++;
204+
instanceEnd += this.sources[instanceIndex].numSplats;
205+
}
206+
return instanceIndex;
207+
}
208+
209+
addSplatData(source: IterableSplatData) {
210+
this.sources.push(source);
211+
this.recreate();
212+
}
213+
214+
removeSplatData(source: IterableSplatData) {
215+
if (this.sources.indexOf(source)) {
216+
this.sources.splice(this.sources.indexOf(source), 1);
217+
this.recreate();
218+
}
219+
}
220+
221+
get maxSplats() {
222+
return this.splatData.maxSplats;
223+
}
224+
225+
get numSplats() {
226+
return this.splatData.numSplats;
227+
}
228+
229+
get numSh() {
230+
return this.splatData.numSh;
231+
}
232+
233+
setupMaterial(material: THREE.ShaderMaterial) {
234+
this.splatData.setupMaterial(material);
235+
if (!("batchingTexture" in material.uniforms)) {
236+
material.uniforms.batchingTexture = this.batchingTextureUniform;
237+
}
238+
material.defines.USE_BATCHING = true;
239+
}
240+
241+
iterateCenters(
242+
callback: (index: number, x: number, y: number, z: number) => void,
243+
) {
244+
this.splatData.iterateCenters(callback);
245+
}
246+
247+
dispose(): void {
248+
// Only dispose the combined splat. The other splat sources aren't owned by this instance.
249+
this.splatData.dispose();
250+
}
251+
}

0 commit comments

Comments
 (0)