Skip to content

Commit b6ae981

Browse files
mvaligurskyMartin Valigurskywilleastcott
authored
Few improvements to unified gsplat rendering (#8065)
* Few improvements to unified gsplat rendering * Update src/framework/components/gsplat/component.js Co-authored-by: Will Eastcott <[email protected]> * feedback changes, thanks * additional change --------- Co-authored-by: Martin Valigursky <[email protected]> Co-authored-by: Will Eastcott <[email protected]>
1 parent 9cdf6da commit b6ae981

File tree

10 files changed

+151
-17
lines changed

10 files changed

+151
-17
lines changed

src/framework/components/gsplat/component.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,17 @@ class GSplatComponent extends Component {
221221
/**
222222
* Sets the material used to render the gsplat.
223223
*
224+
* **Note:** This setter is only supported when {@link unified} is `false`. When it's true, multiple
225+
* gsplat components share a single material per camera/layer combination. To access materials in
226+
* unified mode, use {@link GsplatComponentSystem#getGSplatMaterial}.
227+
*
224228
* @param {ShaderMaterial} value - The material instance.
225229
*/
226230
set material(value) {
227-
228-
Debug.assert(!this.unified);
229-
231+
if (this.unified) {
232+
Debug.warn('GSplatComponent#material setter is not supported when unified true. Use app.systems.gsplat.getGSplatMaterial(camera, layer) to access materials.');
233+
return;
234+
}
230235
if (this._instance) {
231236
this._instance.material = value;
232237
} else {
@@ -237,9 +242,17 @@ class GSplatComponent extends Component {
237242
/**
238243
* Gets the material used to render the gsplat.
239244
*
245+
* **Note:** This getter returns `null` when {@link unified} is `true`. In unified mode, materials are
246+
* organized per camera/layer combination rather than per component. To access materials in
247+
* unified mode, use {@link GsplatComponentSystem#getGSplatMaterial}.
248+
*
240249
* @type {ShaderMaterial|null}
241250
*/
242251
get material() {
252+
if (this.unified) {
253+
Debug.warnOnce('GSplatComponent#material getter returns null when unified=true. Use app.systems.gsplat.getGSplatMaterial(camera, layer) instead.');
254+
return null;
255+
}
243256
return this._instance?.material ?? this._materialTmp ?? null;
244257
}
245258

src/framework/components/gsplat/system.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { GSplatAssetLoader } from './gsplat-asset-loader.js';
99

1010
/**
1111
* @import { AppBase } from '../../app-base.js'
12+
* @import { Camera } from '../../../scene/camera.js'
13+
* @import { Layer } from '../../../scene/layer.js'
14+
* @import { ShaderMaterial } from '../../../scene/materials/shader-material.js'
1215
*/
1316

1417
const _schema = [
@@ -32,6 +35,24 @@ const _properties = [
3235
* @category Graphics
3336
*/
3437
class GSplatComponentSystem extends ComponentSystem {
38+
/**
39+
* Fired when a GSplat material is created for a camera and layer combination. In unified
40+
* mode, materials are created during the first frame update when the GSplat is rendered.
41+
* The handler is passed the {@link ShaderMaterial}, the {@link Camera}, and the {@link Layer}.
42+
*
43+
* This event is useful for setting up custom material chunks and parameters before the
44+
* first render.
45+
*
46+
* @event
47+
* @example
48+
* app.systems.gsplat.on('material:created', (material, camera, layer) => {
49+
* console.log(`Material created for camera ${camera.entity.name} on layer ${layer.name}`);
50+
* // Set custom material parameters before first render
51+
* material.setParameter('myParam', value);
52+
* });
53+
*/
54+
static EVENT_MATERIALCREATED = 'material:created';
55+
3556
/**
3657
* Create a new GSplatComponentSystem.
3758
*
@@ -50,7 +71,7 @@ class GSplatComponentSystem extends ComponentSystem {
5071

5172
// loader for splat LOD assets, as asset system is not available on the scene level
5273
const gsplatAssetLoader = new GSplatAssetLoader(app.assets);
53-
app.renderer.gsplatDirector = new GSplatDirector(app.graphicsDevice, app.renderer, app.scene, gsplatAssetLoader);
74+
app.renderer.gsplatDirector = new GSplatDirector(app.graphicsDevice, app.renderer, app.scene, gsplatAssetLoader, this);
5475

5576
this.on('beforeremove', this.onRemove, this);
5677
}
@@ -105,6 +126,34 @@ class GSplatComponentSystem extends ComponentSystem {
105126
onRemove(entity, component) {
106127
component.onRemove();
107128
}
129+
130+
/**
131+
* Gets the GSplat material used by unified GSplat rendering for the given camera and layer.
132+
*
133+
* Returns null if the material hasn't been created yet. In unified mode, materials are created
134+
* during the first frame update when the GSplat is rendered. To be notified immediately when
135+
* materials are created, listen to the 'material:created' event on GSplatComponentSystem:
136+
*
137+
* @example
138+
* app.systems.gsplat.on('material:created', (material, camera, layer) => {
139+
* // Material is now available
140+
* material.setParameter('myParam', value);
141+
* });
142+
*
143+
* @param {Camera} camera - The camera instance.
144+
* @param {Layer} layer - The layer instance.
145+
* @returns {ShaderMaterial|null} The material, or null if not created yet.
146+
*/
147+
getGSplatMaterial(camera, layer) {
148+
const director = this.app.renderer.gsplatDirector;
149+
if (!director) return null;
150+
151+
const cameraData = director.camerasMap.get(camera);
152+
if (!cameraData) return null;
153+
154+
const layerData = cameraData.layersMap.get(layer);
155+
return layerData?.gsplatManager?.material ?? null;
156+
}
108157
}
109158

110159
Component._buildAccessors(GSplatComponent.prototype, _schema);

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { GSplatManager } from './gsplat-manager.js';
99
* @import { GSplatAssetLoaderBase } from './gsplat-asset-loader-base.js'
1010
* @import { Scene } from '../scene.js'
1111
* @import { Renderer } from '../renderer/renderer.js'
12+
* @import { EventHandler } from '../../core/event-handler.js'
1213
*/
1314

1415
/**
@@ -66,6 +67,12 @@ class GSplatCameraData {
6667
if (!layerData) {
6768
layerData = new GSplatLayerData(device, director, layer, cameraNode);
6869
this.layersMap.set(layer, layerData);
70+
71+
// Fire event that material was created
72+
const material = layerData.gsplatManager.material;
73+
if (material && director.eventHandler) {
74+
director.eventHandler.fire('material:created', material, cameraNode.camera, layer);
75+
}
6976
}
7077
return layerData;
7178
}
@@ -99,17 +106,24 @@ class GSplatDirector {
99106
*/
100107
scene;
101108

109+
/**
110+
* @type {EventHandler}
111+
*/
112+
eventHandler;
113+
102114
/**
103115
* @param {GraphicsDevice} device - The graphics device.
104116
* @param {Renderer} renderer - The renderer.
105117
* @param {Scene} scene - The scene.
106118
* @param {GSplatAssetLoaderBase} assetLoader - The asset loader.
119+
* @param {EventHandler} eventHandler - Event handler for firing events.
107120
*/
108-
constructor(device, renderer, scene, assetLoader) {
121+
constructor(device, renderer, scene, assetLoader, eventHandler) {
109122
this.device = device;
110123
this.renderer = renderer;
111124
this.assetLoader = assetLoader;
112125
this.scene = scene;
126+
this.eventHandler = eventHandler;
113127
}
114128

115129
destroy() {

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ class GSplatManager {
122122
*/
123123
octreeInstancesToDestroy = [];
124124

125+
/**
126+
* Flag set when new octree instances are added, to trigger immediate LOD evaluation.
127+
*
128+
* @type {boolean}
129+
*/
130+
hasNewOctreeInstances = false;
131+
125132
constructor(device, director, layer, cameraNode) {
126133
this.device = device;
127134
this.scene = director.scene;
@@ -139,6 +146,10 @@ class GSplatManager {
139146
this.sorter.destroy();
140147
}
141148

149+
get material() {
150+
return this.renderer.material;
151+
}
152+
142153
createSorter() {
143154
// create sorter
144155
const sorter = new GSplatUnifiedSorter();
@@ -163,6 +174,9 @@ class GSplatManager {
163174
// make sure octree instance exists for placement
164175
if (!this.octreeInstances.has(p)) {
165176
this.octreeInstances.set(p, new GSplatOctreeInstance(p.resource.octree, p, this.director.assetLoader));
177+
178+
// mark that we have new instances that need initial LOD evaluation
179+
this.hasNewOctreeInstances = true;
166180
}
167181
tempOctreePlacements.add(p);
168182
} else {
@@ -378,6 +392,10 @@ class GSplatManager {
378392
}
379393
}
380394

395+
// when new octree instances are added, we need to evaluate their LOD immediately
396+
const hasNewInstances = this.hasNewOctreeInstances && this.sorter.jobsInFlight < 3;
397+
if (hasNewInstances) this.hasNewOctreeInstances = false;
398+
381399
let anyInstanceNeedsLodUpdate = false;
382400
let anyOctreeMoved = false;
383401
let cameraMovedOrRotated = false;
@@ -416,8 +434,8 @@ class GSplatManager {
416434
this.renderer.updateOverdrawMode(this.scene.gsplat);
417435
}
418436

419-
// when camera or octree need LOD evaluated, or params are dirty, or resources completed
420-
if (cameraMovedOrRotated || anyOctreeMoved || this.scene.gsplat.dirty || anyInstanceNeedsLodUpdate) {
437+
// when camera or octree need LOD evaluated, or params are dirty, or resources completed, or new instances added
438+
if (cameraMovedOrRotated || anyOctreeMoved || this.scene.gsplat.dirty || anyInstanceNeedsLodUpdate || hasNewInstances) {
421439

422440
// update the previous position where LOD was evaluated for octree instances
423441
for (const [, inst] of this.octreeInstances) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ class GSplatOctreeInstance {
291291

292292
// calculate max LOD once for all nodes
293293
const maxLod = this.octree.lodLevels - 1;
294-
const lodDistances = this.placement.lodDistances || [5, 10, 15, 20, 25];
294+
const lodDistances = this.placement.lodDistances || [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60];
295295

296296
// parameters
297297
const { lodBehindPenalty, lodRangeMin, lodRangeMax, lodUnderfillLimit = 0 } = params;

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

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,25 @@ class GSplatRenderer {
6060
}
6161
});
6262

63+
this.configureMaterial();
64+
65+
this.meshInstance = this.createMeshInstance();
66+
layer.addMeshInstances([this.meshInstance]);
67+
}
68+
69+
destroy() {
70+
this.layer.removeMeshInstances([this.meshInstance]);
71+
this._material.destroy();
72+
this.meshInstance.destroy();
73+
}
74+
75+
get material() {
76+
return this._material;
77+
}
78+
79+
configureMaterial() {
80+
const { device, workBuffer } = this;
81+
6382
// input format
6483
this._material.setDefine('GSPLAT_WORKBUFFER_DATA', true);
6584
this._material.setDefine('STORAGE_ORDER', device.isWebGPU);
@@ -85,15 +104,6 @@ class GSplatRenderer {
85104
this._material.blendType = dither ? BLEND_NONE : BLEND_PREMULTIPLIED;
86105
this._material.depthWrite = !!dither;
87106
this._material.update();
88-
89-
this.meshInstance = this.createMeshInstance();
90-
layer.addMeshInstances([this.meshInstance]);
91-
}
92-
93-
destroy() {
94-
this.layer.removeMeshInstances([this.meshInstance]);
95-
this._material.destroy();
96-
this.meshInstance.destroy();
97107
}
98108

99109
update(count, textureSize) {

src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatCustomize.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ void modifyCovariance(vec3 originalCenter, vec3 modifiedCenter, inout vec3 covA,
1313
// float size = gsplatExtractSize(covA, covB);
1414
// float newSize = clamp(size, 0.01, 0.5);
1515
// gsplatApplyUniformScale(covA, covB, newSize / size);
16+
//
17+
// Example to make splats round/spherical:
18+
// float size = gsplatExtractSize(covA, covB);
19+
// gsplatMakeRound(covA, covB, size * 0.5);
20+
//
21+
// To hide a splat:
22+
// gsplatMakeRound(covA, covB, 0.0);
1623
}
1724
1825
void modifyColor(vec3 center, inout vec4 color) {

src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatHelpers.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,12 @@ void gsplatApplyUniformScale(inout vec3 covA, inout vec3 covB, float scale) {
1111
covA *= s2;
1212
covB *= s2;
1313
}
14+
15+
// Helper function to make splat spherical/round with given size
16+
// Use size = 0.0 to hide the splat
17+
void gsplatMakeRound(inout vec3 covA, inout vec3 covB, float size) {
18+
float s2 = size * size;
19+
covA = vec3(s2, 0.0, 0.0);
20+
covB = vec3(s2, 0.0, s2);
21+
}
1422
`;

src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatCustomize.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ fn modifyCovariance(originalCenter: vec3f, modifiedCenter: vec3f, covA: ptr<func
1313
// let size = gsplatExtractSize(*covA, *covB);
1414
// let newSize = clamp(size, 0.01, 0.5);
1515
// gsplatApplyUniformScale(covA, covB, newSize / size);
16+
//
17+
// Example to make splats round/spherical:
18+
// let size = gsplatExtractSize(*covA, *covB);
19+
// gsplatMakeRound(covA, covB, size * 0.5);
20+
//
21+
// To hide a splat:
22+
// gsplatMakeRound(covA, covB, 0.0);
1623
}
1724
1825
fn modifyColor(center: vec3f, color: ptr<function, vec4f>) {

src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatHelpers.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,12 @@ fn gsplatApplyUniformScale(covA: ptr<function, vec3f>, covB: ptr<function, vec3f
1111
*covA = *covA * s2;
1212
*covB = *covB * s2;
1313
}
14+
15+
// Helper function to make splat spherical/round with given size
16+
// Use size = 0.0 to hide the splat
17+
fn gsplatMakeRound(covA: ptr<function, vec3f>, covB: ptr<function, vec3f>, size: f32) {
18+
let s2 = size * size;
19+
*covA = vec3f(s2, 0.0, 0.0);
20+
*covB = vec3f(s2, 0.0, s2);
21+
}
1422
`;

0 commit comments

Comments
 (0)