Skip to content

Commit ff6c26e

Browse files
mvaligurskyMartin Valigursky
andauthored
Public API to control unified gsplat behavior (#7959)
* Public API to control unified gsplat behavior * lint --------- Co-authored-by: Martin Valigursky <[email protected]>
1 parent 4052cef commit ff6c26e

File tree

14 files changed

+267
-59
lines changed

14 files changed

+267
-59
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* @param {import('../../app/components/Example.mjs').ControlOptions} options - The options.
3+
* @returns {JSX.Element} The returned JSX Element.
4+
*/
5+
export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
6+
const { BindingTwoWay, LabelGroup, BooleanInput, SliderInput } = ReactPCUI;
7+
if (!observer.get('lod')) {
8+
observer.set('lod', { distance: 5 });
9+
}
10+
return fragment(
11+
jsx(
12+
LabelGroup,
13+
{ text: 'Debug AABBs' },
14+
jsx(BooleanInput, {
15+
type: 'toggle',
16+
binding: new BindingTwoWay(),
17+
link: { observer, path: 'debugAabbs' },
18+
value: observer.get('debugAabbs')
19+
})
20+
),
21+
jsx(
22+
LabelGroup,
23+
{ text: 'Debug LOD' },
24+
jsx(BooleanInput, {
25+
type: 'toggle',
26+
binding: new BindingTwoWay(),
27+
link: { observer, path: 'debugLod' },
28+
value: observer.get('debugLod')
29+
})
30+
),
31+
jsx(
32+
LabelGroup,
33+
{ text: 'Distance' },
34+
jsx(SliderInput, {
35+
precision: 1,
36+
min: 3,
37+
max: 20,
38+
step: 0.1,
39+
binding: new BindingTwoWay(),
40+
link: { observer, path: 'lod.distance' }
41+
})
42+
)
43+
);
44+
};

examples/src/examples/gaussian-splatting/lod.example.mjs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// @config HIDDEN
2+
import { data } from 'examples/observer';
23
import { deviceType, rootPath, fileImport } from 'examples/utils';
34
import * as pc from 'playcanvas';
45

@@ -46,8 +47,8 @@ app.on('destroy', () => {
4647
window.removeEventListener('resize', resize);
4748
});
4849

49-
pc.Tracing.set(pc.TRACEID_SHADER_ALLOC, true);
50-
pc.Tracing.set(pc.TRACEID_OCTREE_RESOURCES, true);
50+
// pc.Tracing.set(pc.TRACEID_SHADER_ALLOC, true);
51+
// pc.Tracing.set(pc.TRACEID_OCTREE_RESOURCES, true);
5152

5253
const assets = {
5354
// church: new pc.Asset('gsplat', 'gsplat', { url: `${rootPath}/static/assets/splats/morocco.ply` }),
@@ -62,6 +63,21 @@ const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets
6263
assetListLoader.load(() => {
6364
app.start();
6465

66+
// initialize UI settings
67+
data.set('debugAabbs', false);
68+
data.set('debugLod', false);
69+
data.set('lod', { distance: 5 });
70+
app.scene.gsplat.debugAabbs = !!data.get('debugAabbs');
71+
app.scene.gsplat.colorizeLod = !!data.get('debugLod');
72+
73+
// handle UI changes
74+
data.on('debugAabbs:set', () => {
75+
app.scene.gsplat.debugAabbs = !!data.get('debugAabbs');
76+
});
77+
data.on('debugLod:set', () => {
78+
app.scene.gsplat.colorizeLod = !!data.get('debugLod');
79+
});
80+
6581
// create a splat entity and place it in the world
6682
const skull = new pc.Entity('skull');
6783
skull.addComponent('gsplat', {
@@ -126,6 +142,15 @@ assetListLoader.load(() => {
126142
enablePan: false
127143
});
128144

145+
// bind LOD distance slider to component lodDistances for church and logo
146+
const updateLodDistances = () => {
147+
const base = Number(data.get('lod.distance')) || 5;
148+
const distances = [base, base * 2, base * 3, base * 4, base * 5];
149+
logo.gsplat.lodDistances = distances;
150+
church.gsplat.lodDistances = distances;
151+
};
152+
updateLodDistances();
153+
data.on('lod.distance:set', updateLodDistances);
129154

130155
let timeToChange = 3;
131156
let time = 0;

src/framework/components/gsplat/component.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ class GSplatComponent extends Component {
7272
/** @private */
7373
_highQualitySH = true;
7474

75+
/**
76+
* LOD distance thresholds, stored as a copy.
77+
*
78+
* @type {number[]|null}
79+
* @private
80+
*/
81+
_lodDistances = [5, 10, 15, 20, 25];
82+
7583
/**
7684
* @type {BoundingBox|null}
7785
* @private
@@ -313,6 +321,28 @@ class GSplatComponent extends Component {
313321
return this._castShadows;
314322
}
315323

324+
/**
325+
* Sets LOD distance thresholds used by octree-based gsplat rendering. The provided array
326+
* is copied.
327+
*
328+
* @type {number[]|null}
329+
*/
330+
set lodDistances(value) {
331+
this._lodDistances = Array.isArray(value) ? value.slice() : null;
332+
if (this._placement) {
333+
this._placement.lodDistances = this._lodDistances;
334+
}
335+
}
336+
337+
/**
338+
* Gets a copy of LOD distance thresholds previously set, or null when not set.
339+
*
340+
* @type {number[]|null}
341+
*/
342+
get lodDistances() {
343+
return this._lodDistances ? this._lodDistances.slice() : null;
344+
}
345+
316346
/**
317347
* Sets whether to use the unified gsplat rendering. Can be changed only when the component is
318348
* not enabled. Default is false.
@@ -590,6 +620,7 @@ class GSplatComponent extends Component {
590620

591621
if (asset) {
592622
this._placement = new GSplatPlacement(asset.resource, this.entity);
623+
this._placement.lodDistances = this._lodDistances;
593624
}
594625

595626
} else {

src/scene/gsplat-unified/gsplat-director.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ class GSplatDirector {
191191
// update gsplat managers
192192
// const cameraData = this.camerasMap.get(camera);
193193
cameraData?.layersMap.forEach((layerData) => {
194-
layerData.gsplatManager.update(this.scene);
194+
layerData.gsplatManager.update();
195195
});
196196
}
197197

src/scene/gsplat-unified/gsplat-manager.js

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,9 @@ import { Color } from '../../core/math/color.js';
1515
/**
1616
* @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js'
1717
* @import { GSplatPlacement } from './gsplat-placement.js'
18+
* @import { Scene } from '../scene.js'
1819
*/
1920

20-
// render aabb's for debugging
21-
const _debugAabbs = false;
22-
2321
const cameraPosition = new Vec3();
2422
const cameraDirection = new Vec3();
2523
const translation = new Vec3();
@@ -29,6 +27,23 @@ const tempOctreePlacements = new Set();
2927
const _updatedSplats = [];
3028
const tempOctreesTicked = new Set();
3129

30+
const _lodColorsRaw = [
31+
[1, 0, 0], // red
32+
[0, 1, 0], // green
33+
[0, 0, 1], // blue
34+
[1, 1, 0], // yellow
35+
[1, 0, 1] // magenta
36+
];
37+
38+
// Color instances used by debug wireframe rendering
39+
const _lodColors = [
40+
new Color(1, 0, 0),
41+
new Color(0, 1, 0),
42+
new Color(0, 0, 1),
43+
new Color(1, 1, 0),
44+
new Color(1, 0, 1)
45+
];
46+
3247
/**
3348
* GSplatManager manages the rendering of splats using a work buffer, where all active splats are
3449
* stored and rendered from.
@@ -77,6 +92,9 @@ class GSplatManager {
7792
/** @type {GraphNode} */
7893
cameraNode;
7994

95+
/** @type {Scene} */
96+
scene;
97+
8098
/**
8199
* Layer placements, only non-octree placements are included.
82100
*
@@ -100,6 +118,7 @@ class GSplatManager {
100118

101119
constructor(device, director, layer, cameraNode) {
102120
this.device = device;
121+
this.scene = director.scene;
103122
this.director = director;
104123
this.cameraNode = cameraNode;
105124
this.workBuffer = new GSplatWorkBuffer(device);
@@ -204,7 +223,9 @@ class GSplatManager {
204223
// add octree splats
205224
for (const [, inst] of this.octreeInstances) {
206225
inst.activePlacements.forEach((p) => {
207-
splats.push(new GSplatInfo(this.device, p.resource, p));
226+
if (p.resource) {
227+
splats.push(new GSplatInfo(this.device, p.resource, p));
228+
}
208229
});
209230
}
210231

@@ -276,8 +297,9 @@ class GSplatManager {
276297
this.renderer.setMaxNumSplats(textureSize * textureSize);
277298
}
278299

279-
// render all splats to work buffer
280-
this.workBuffer.render(worldState.splats, this.cameraNode);
300+
// render all splats to work buffer with LOD color palette
301+
const colorize = this.scene.gsplat.colorizeLod;
302+
this.workBuffer.render(worldState.splats, this.cameraNode, colorize ? _lodColorsRaw : undefined);
281303

282304
// apply pending file-release requests
283305
if (worldState.pendingReleases && worldState.pendingReleases.length) {
@@ -298,22 +320,28 @@ class GSplatManager {
298320
}
299321
}
300322

301-
update(scene) {
323+
update() {
302324

303325
// check if any octree instances have moved enough to require LOD update
304326
let anyOctreeMoved = false;
327+
const threshold = this.scene.gsplat.lodUpdateThreshold;
305328
for (const [, inst] of this.octreeInstances) {
306-
anyOctreeMoved ||= inst.testMoved();
329+
anyOctreeMoved ||= inst.testMoved(threshold);
307330
}
308331

309332
// check if camera has moved enough to require LOD update
310333
const currentCameraPos = this.cameraNode.getPosition();
311334
const distance = this.lastCameraPos.distance(currentCameraPos);
312-
const cameraMoved = distance > 1.0;
335+
const cameraMoved = distance > threshold;
313336

314337
// when camera of octree need LOD evaluated
315338
if (cameraMoved || anyOctreeMoved) {
316339

340+
// update the previous position where LOD was evaluated
341+
for (const [, inst] of this.octreeInstances) {
342+
inst.updateMoved();
343+
}
344+
317345
this.lastCameraPos.copy(currentCameraPos);
318346

319347
// update LOD for all octree instances
@@ -338,11 +366,12 @@ class GSplatManager {
338366

339367
// debug render world space bounds for all splats
340368
Debug.call(() => {
341-
if (_debugAabbs) {
369+
if (this.scene.gsplat.debugAabbs) {
342370
const tempAabb = new BoundingBox();
371+
const scene = this.scene;
343372
lastState.splats.forEach((splat) => {
344373
tempAabb.setFromTransformedAabb(splat.aabb, splat.node.getWorldTransform());
345-
scene.immediate.drawWireAlignedBox(tempAabb.getMin(), tempAabb.getMax(), Color.WHITE, true, scene.defaultDrawLayer);
374+
scene.immediate.drawWireAlignedBox(tempAabb.getMin(), tempAabb.getMax(), _lodColors[splat.lodIndex], true, scene.defaultDrawLayer);
346375
});
347376
}
348377
});
@@ -364,7 +393,8 @@ class GSplatManager {
364393

365394
// Batch render all updated splats in a single render pass
366395
if (_updatedSplats.length > 0) {
367-
this.workBuffer.render(_updatedSplats, this.cameraNode);
396+
const colorize = this.scene.gsplat.colorizeLod;
397+
this.workBuffer.render(_updatedSplats, this.cameraNode, colorize ? _lodColorsRaw : undefined);
368398
_updatedSplats.length = 0;
369399
}
370400
}

src/scene/gsplat-unified/gsplat-octree-instance.js

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,15 @@ class GSplatOctreeInstance {
124124
* @param {Vec3} localCameraPosition - The camera position in local space.
125125
* @param {number} nodeIndex - The node index.
126126
* @param {number} maxLod - The maximum LOD index (lodLevels - 1).
127+
* @param {number[]} lodDistances - Array of distance thresholds per LOD.
127128
* @returns {number} The LOD index for this node, or -1 if node should not be rendered.
128129
*/
129-
calculateNodeLod(localCameraPosition, nodeIndex, maxLod) {
130+
calculateNodeLod(localCameraPosition, nodeIndex, maxLod, lodDistances) {
130131
const node = this.octree.nodes[nodeIndex];
131132

132-
// Calculate distance in local space (no transforms needed)
133+
// Calculate distance in local space
133134
const distance = localCameraPosition.distance(node.bounds.center);
134135

135-
// Distance thresholds for LOD selection
136-
const lodDistances = [2, 4, 6, 8, 10];
137-
138136
// Find appropriate LOD based on distance and available LOD levels
139137
for (let lod = 0; lod < maxLod; lod++) {
140138
if (distance < lodDistances[lod]) {
@@ -163,14 +161,15 @@ class GSplatOctreeInstance {
163161

164162
// calculate max LOD once for all nodes
165163
const maxLod = this.octree.lodLevels - 1;
164+
const lodDistances = this.placement.lodDistances || [5, 10, 15, 20, 25];
166165

167166
// process all nodes
168167
const nodes = this.octree.nodes;
169168
for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++) {
170169
const node = nodes[nodeIndex];
171170

172171
// LOD for the node
173-
const newLodIndex = this.calculateNodeLod(localCameraPosition, nodeIndex, maxLod);
172+
const newLodIndex = this.calculateNodeLod(localCameraPosition, nodeIndex, maxLod, lodDistances);
174173
const currentLodIndex = this.nodeLods[nodeIndex];
175174

176175
// if LOD changed
@@ -327,20 +326,27 @@ class GSplatOctreeInstance {
327326
}
328327

329328
/**
330-
* Tests if the octree instance has moved by more than 1 unit.
329+
* Tests if the octree instance has moved by more than the provided LOD update distance.
331330
*
332-
* @returns {boolean} True if the octree instance has moved by more than 1 unit, false otherwise.
331+
* @param {number} threshold - Distance threshold to trigger an update.
332+
* @returns {boolean} True if the octree instance has moved by more than the threshold, false otherwise.
333333
*/
334-
testMoved() {
334+
testMoved(threshold) {
335335
const position = this.placement.node.getPosition();
336336
const length = position.distance(this.previousPosition);
337-
if (length > 1) {
338-
this.previousPosition.copy(position);
337+
if (length > threshold) {
339338
return true;
340339
}
341340
return false;
342341
}
343342

343+
/**
344+
* Updates the previous position of the octree instance.
345+
*/
346+
updateMoved() {
347+
this.previousPosition.copy(this.placement.node.getPosition());
348+
}
349+
344350
/**
345351
* Updates the octree instance each frame.
346352
*
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Parameters for GSplat unified system.
3+
*
4+
* @category Graphics
5+
*/
6+
class GSplatParams {
7+
/**
8+
* Enables debug rendering of AABBs for GSplat objects. Defaults to false.
9+
*
10+
* @type {boolean}
11+
*/
12+
debugAabbs = false;
13+
14+
/**
15+
* Enables colorization by selected LOD level when rendering GSplat objects. Defaults to false.
16+
*
17+
* @type {boolean}
18+
*/
19+
colorizeLod = false;
20+
21+
/**
22+
* Distance threshold in world units to trigger LOD updates for camera and gsplat instances.
23+
* Defaults to 1.
24+
*
25+
* @type {number}
26+
*/
27+
lodUpdateThreshold = 1;
28+
}
29+
30+
export { GSplatParams };

0 commit comments

Comments
 (0)