Skip to content

Commit 3d90cff

Browse files
mvaligurskyMartin Valigursky
andauthored
Moved the splatBudget API to gsplat component (#8231)
* Moved the splatBudget API to gsplat component * added streaming script for the editor, and added splatBudget settings * lint --------- Co-authored-by: Martin Valigursky <[email protected]>
1 parent bfcf530 commit 3d90cff

File tree

6 files changed

+403
-35
lines changed

6 files changed

+403
-35
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ assetListLoader.load(() => {
185185
'4M': 4000000,
186186
'6M': 6000000
187187
};
188-
app.scene.gsplat.splatBudget = budgetMap[preset] || 0;
188+
gs.splatBudget = budgetMap[preset] || 0;
189189
};
190190

191191
applySplatBudget();
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
import { Script, Asset, Entity, platform } from 'playcanvas';
2+
3+
class StreamedGsplat extends Script {
4+
static scriptName = 'streamedGsplat';
5+
6+
/**
7+
* @attribute
8+
* @type {string}
9+
*/
10+
splatUrl = '';
11+
12+
/**
13+
* @attribute
14+
* @type {string}
15+
*/
16+
environmentUrl = '';
17+
18+
/**
19+
* @attribute
20+
* @type {number[]}
21+
*/
22+
ultraLodDistances = [5, 20, 35, 50, 65, 90, 150];
23+
24+
/**
25+
* @attribute
26+
* @type {number[]}
27+
*/
28+
highLodDistances = [5, 20, 35, 50, 65, 90, 150];
29+
30+
/**
31+
* @attribute
32+
* @type {number[]}
33+
*/
34+
mediumLodDistances = [5, 7, 12, 25, 75, 120, 200];
35+
36+
/**
37+
* @attribute
38+
* @type {number[]}
39+
*/
40+
lowLodDistances = [5, 7, 12, 25, 75, 120, 200];
41+
42+
/**
43+
* @attribute
44+
* @type {number[]}
45+
*/
46+
ultraLodRange = [0, 5];
47+
48+
/**
49+
* @attribute
50+
* @type {number[]}
51+
*/
52+
highLodRange = [1, 5];
53+
54+
/**
55+
* @attribute
56+
* @type {number[]}
57+
*/
58+
mediumLodRange = [2, 5];
59+
60+
/**
61+
* @attribute
62+
* @type {number[]}
63+
*/
64+
lowLodRange = [3, 5];
65+
66+
/**
67+
* @attribute
68+
* @type {number}
69+
*/
70+
ultraSplatBudget = 6000000;
71+
72+
/**
73+
* @attribute
74+
* @type {number}
75+
*/
76+
highSplatBudget = 4000000;
77+
78+
/**
79+
* @attribute
80+
* @type {number}
81+
*/
82+
mediumSplatBudget = 2000000;
83+
84+
/**
85+
* @attribute
86+
* @type {number}
87+
*/
88+
lowSplatBudget = 1000000;
89+
90+
/** @type {Asset[]} */
91+
_assets = [];
92+
93+
/** @type {Entity[]} */
94+
_children = [];
95+
96+
_highRes = false;
97+
98+
_colorize = false;
99+
100+
initialize() {
101+
const app = this.app;
102+
103+
this._currentPreset = platform.mobile ? 'low' : 'medium';
104+
105+
// global settings
106+
app.scene.gsplat.radialSorting = true;
107+
app.scene.gsplat.lodUpdateAngle = 90;
108+
app.scene.gsplat.lodBehindPenalty = 5;
109+
app.scene.gsplat.lodUpdateDistance = 1;
110+
app.scene.gsplat.lodUnderfillLimit = 10;
111+
112+
// Listen for UI events
113+
app.on('preset:ultra', () => this._setPreset('ultra'), this);
114+
app.on('preset:high', () => this._setPreset('high'), this);
115+
app.on('preset:medium', () => this._setPreset('medium'), this);
116+
app.on('preset:low', () => this._setPreset('low'), this);
117+
app.on('colorize:toggle', this._toggleColorize, this);
118+
119+
// Apply initial resolution
120+
this._applyResolution();
121+
122+
// Load main splat - attach to entity directly
123+
if (!this.splatUrl) {
124+
console.warn('[StreamedGsplat] No splatUrl provided.');
125+
} else {
126+
const mainAsset = new Asset('MainGsplat_asset', 'gsplat', { url: this.splatUrl });
127+
app.assets.add(mainAsset);
128+
app.assets.load(mainAsset);
129+
this._assets.push(mainAsset);
130+
131+
mainAsset.ready((a) => {
132+
// Temporarily disable entity to allow unified property to be set
133+
const wasEnabled = this.entity.enabled;
134+
this.entity.enabled = false;
135+
136+
// Add component directly to this entity
137+
this.entity.addComponent('gsplat', {
138+
unified: true,
139+
lodDistances: this._getCurrentLodDistances(),
140+
asset: a
141+
});
142+
143+
// Restore entity enabled state
144+
this.entity.enabled = wasEnabled;
145+
146+
// Apply initial preset
147+
this._applyPreset();
148+
});
149+
}
150+
151+
// Load environment splat - attach to child entity
152+
if (!this.environmentUrl) {
153+
console.warn('[StreamedGsplat] No environmentUrl provided (skipping env child).');
154+
} else {
155+
const envAsset = new Asset('EnvironmentGsplat_asset', 'gsplat', { url: this.environmentUrl });
156+
app.assets.add(envAsset);
157+
app.assets.load(envAsset);
158+
this._assets.push(envAsset);
159+
160+
envAsset.ready((a) => {
161+
// Create child entity disabled to allow unified property to be set
162+
const child = new Entity('EnvironmentGsplat');
163+
child.enabled = false;
164+
165+
// Attach to the scene graph
166+
this.entity.addChild(child);
167+
this._children.push(child);
168+
169+
// Add the component while entity is disabled
170+
child.addComponent('gsplat', {
171+
unified: true,
172+
lodDistances: this._getCurrentLodDistances(),
173+
asset: a
174+
});
175+
176+
// Enable the child entity
177+
child.enabled = true;
178+
});
179+
}
180+
181+
this.once('destroy', () => {
182+
this.onDestroy();
183+
});
184+
}
185+
186+
_getCurrentLodDistances() {
187+
let distances;
188+
switch (this._currentPreset) {
189+
case 'ultra':
190+
distances = this.ultraLodDistances;
191+
break;
192+
case 'high':
193+
distances = this.highLodDistances;
194+
break;
195+
case 'medium':
196+
distances = this.mediumLodDistances;
197+
break;
198+
case 'low':
199+
distances = this.lowLodDistances;
200+
break;
201+
default:
202+
distances = [5, 20, 35, 50, 65, 90, 150];
203+
}
204+
return distances && distances.length > 0 ? distances : [5, 20, 35, 50, 65, 90, 150];
205+
}
206+
207+
_getCurrentLodRange() {
208+
let range;
209+
switch (this._currentPreset) {
210+
case 'ultra':
211+
range = this.ultraLodRange;
212+
break;
213+
case 'high':
214+
range = this.highLodRange;
215+
break;
216+
case 'medium':
217+
range = this.mediumLodRange;
218+
break;
219+
case 'low':
220+
range = this.lowLodRange;
221+
break;
222+
default:
223+
range = [0, 5];
224+
}
225+
return range && range.length >= 2 ? range : [0, 5];
226+
}
227+
228+
_getCurrentSplatBudget() {
229+
let budget;
230+
switch (this._currentPreset) {
231+
case 'ultra':
232+
budget = this.ultraSplatBudget;
233+
break;
234+
case 'high':
235+
budget = this.highSplatBudget;
236+
break;
237+
case 'medium':
238+
budget = this.mediumSplatBudget;
239+
break;
240+
case 'low':
241+
budget = this.lowSplatBudget;
242+
break;
243+
default:
244+
budget = 0;
245+
}
246+
return budget || 0;
247+
}
248+
249+
_applyPreset() {
250+
const range = this._getCurrentLodRange();
251+
if (!range) return;
252+
253+
const app = this.app;
254+
app.scene.gsplat.lodRangeMin = range[0];
255+
app.scene.gsplat.lodRangeMax = range[1];
256+
257+
const lodDistances = this._getCurrentLodDistances();
258+
const splatBudget = this._getCurrentSplatBudget();
259+
260+
// Apply to main streaming asset only (environment doesn't support these settings)
261+
if (this.entity.gsplat) {
262+
this.entity.gsplat.lodDistances = lodDistances;
263+
this.entity.gsplat.splatBudget = splatBudget;
264+
}
265+
}
266+
267+
_setPreset(presetName) {
268+
this._currentPreset = presetName;
269+
this._applyPreset();
270+
271+
// Notify UI of preset change
272+
this.app.fire('ui:setPreset', presetName);
273+
}
274+
275+
_applyResolution() {
276+
const device = this.app.graphicsDevice;
277+
const dpr = window.devicePixelRatio || 1;
278+
device.maxPixelRatio = this._highRes ? Math.min(dpr, 2) : (dpr >= 2 ? dpr * 0.5 : dpr);
279+
this.app.resizeCanvas();
280+
}
281+
282+
_toggleColorize() {
283+
this._colorize = !this._colorize;
284+
this.app.scene.gsplat.colorizeLod = this._colorize;
285+
286+
const statusEl = document.getElementById('colorize-status');
287+
if (statusEl) {
288+
statusEl.textContent = this._colorize ? 'On' : 'Off';
289+
}
290+
}
291+
292+
update() {
293+
const rendered = this.app.stats.frame.gsplats || 0;
294+
this.app.fire('ui:updateStats', rendered);
295+
}
296+
297+
onDestroy() {
298+
// Clean up event listeners
299+
this.app.off('preset:ultra');
300+
this.app.off('preset:high');
301+
this.app.off('preset:medium');
302+
this.app.off('preset:low');
303+
this.app.off('colorize:toggle');
304+
305+
// unload/remove assets
306+
for (let i = 0; i < this._assets.length; i++) {
307+
const a = this._assets[i];
308+
if (a) {
309+
a.unload();
310+
this.app.assets.remove(a);
311+
}
312+
}
313+
this._assets.length = 0;
314+
315+
// remove gsplat component from entity if present
316+
if (this.entity.gsplat) {
317+
this.entity.removeComponent('gsplat');
318+
}
319+
320+
// destroy created children
321+
for (let j = 0; j < this._children.length; j++) {
322+
const c = this._children[j];
323+
if (c && c.destroy) c.destroy();
324+
}
325+
this._children.length = 0;
326+
}
327+
}
328+
329+
export { StreamedGsplat };

0 commit comments

Comments
 (0)