Skip to content

Commit 38c05b9

Browse files
mvaligurskyMartin Valigursky
andauthored
Add frame:ready event to GSplatComponentSystem for video capture workflows (#8128)
Co-authored-by: Martin Valigursky <[email protected]>
1 parent 7ee986f commit 38c05b9

File tree

6 files changed

+74
-1
lines changed

6 files changed

+74
-1
lines changed

src/framework/components/gsplat/gsplat-asset-loader.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ class GSplatAssetLoader extends GSplatAssetLoaderBase {
8181
this._registry = registry;
8282
}
8383

84+
get isLoading() {
85+
return this._currentlyLoading.size > 0 || this._loadQueue.length > 0;
86+
}
87+
8488
/**
8589
* Initiates loading of a gsplat asset. This is a fire-and-forget operation that starts
8690
* the loading process. Use getResource() later to check if the asset has finished loading.

src/framework/components/gsplat/system.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ class GSplatComponentSystem extends ComponentSystem {
4242
/**
4343
* Fired when a GSplat material is created for a camera and layer combination. In unified
4444
* mode, materials are created during the first frame update when the GSplat is rendered.
45-
* The handler is passed the {@link ShaderMaterial}, the {@link Camera}, and the {@link Layer}.
45+
* The handler is passed the {@link ShaderMaterial}, the {@link CameraComponent}, and
46+
* the {@link Layer}.
4647
*
4748
* This event is useful for setting up custom material chunks and parameters before the
4849
* first render.
@@ -57,6 +58,30 @@ class GSplatComponentSystem extends ComponentSystem {
5758
*/
5859
static EVENT_MATERIALCREATED = 'material:created';
5960

61+
/**
62+
* Fired every frame for each camera and layer combination rendering GSplats in unified mode.
63+
* The handler is passed the {@link CameraComponent}, the {@link Layer}, a boolean indicating
64+
* if the current frame has up-to-date sorting, and a boolean indicating if resources are loading.
65+
*
66+
* The `ready` parameter indicates whether the current frame reflects all recent changes (camera
67+
* movement, splat transforms, lod updates, etc.) with the latest sorting applied. The `loading`
68+
* parameter indicates if octree LOD resources are still being loaded.
69+
*
70+
* This event is useful for video capture or other workflows that need to wait for frames
71+
* to be fully ready. Only capture frames and move camera to next position when both
72+
* `ready === true` and `loading === false`.
73+
*
74+
* @event
75+
* @example
76+
* app.systems.gsplat.on('frame:ready', (camera, layer, ready, loading) => {
77+
* if (ready && !loading) {
78+
* console.log(`Frame ready to capture for camera ${camera.entity.name}`);
79+
* // Capture frame here
80+
* }
81+
* });
82+
*/
83+
static EVENT_FRAMEREADY = 'frame:ready';
84+
6085
/**
6186
* Create a new GSplatComponentSystem.
6287
*

src/scene/gsplat-unified/gsplat-asset-loader-base.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ class GSplatAssetLoaderBase {
4343
getResource(url) {
4444
Debug.error('GSplatAssetLoaderBase#getResource: Not implemented');
4545
}
46+
47+
/**
48+
* Returns true if any resources are currently loading or queued to load.
49+
*
50+
* @type {boolean}
51+
* @abstract
52+
*/
53+
get isLoading() {
54+
Debug.error('GSplatAssetLoaderBase#isLoading: Not implemented');
55+
return false;
56+
}
4657
}
4758

4859
export { GSplatAssetLoaderBase };

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,23 @@ class GSplatManager {
500500
return _cameraDeltas;
501501
}
502502

503+
/**
504+
* Fires the frame:ready event with current sorting and loading state.
505+
*/
506+
fireFrameReadyEvent() {
507+
const ready = this.sortedVersion === this.lastWorldStateVersion;
508+
509+
// Check loader queue and octree instances' pending loads
510+
let loading = this.director.assetLoader.isLoading;
511+
if (!loading) {
512+
for (const [, inst] of this.octreeInstances) {
513+
loading ||= inst.hasPendingLoads;
514+
}
515+
}
516+
517+
this.director.eventHandler.fire('frame:ready', this.cameraNode.camera, this.renderer.layer, ready, loading);
518+
}
519+
503520
update() {
504521

505522
// apply any pending sorted results
@@ -689,6 +706,9 @@ class GSplatManager {
689706
tempOctreesTicked.clear();
690707
}
691708

709+
// fire frame:ready event
710+
this.fireFrameReadyEvent();
711+
692712
// return the number of visible splats for stats
693713
const { textureSize } = this.workBuffer;
694714
return textureSize * textureSize;

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ class GSplatOctreeInstance {
112112
*/
113113
pendingVisibleAdds = new Map();
114114

115+
/**
116+
* Returns true if this instance has resources pending load or prefetch.
117+
*
118+
* @type {boolean}
119+
*/
120+
get hasPendingLoads() {
121+
return this.pending.size > 0 || this.prefetchPending.size > 0;
122+
}
123+
115124
/**
116125
* Environment placement.
117126
* @type {GSplatPlacement|null}

src/scene/gsplat-unified/gsplat-work-buffer.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ class GSplatWorkBuffer {
123123
// Detect compatible HDR format for color texture, fallback to RGBA16U if RGBA16F not supported
124124
this.colorTextureFormat = device.getRenderableHdrFormat([PIXELFORMAT_RGBA16F]) || PIXELFORMAT_RGBA16U;
125125

126+
// Work buffer textures format:
127+
// - colorTexture (RGBA16F/RGBA16U): RGBA color with alpha
128+
// - splatTexture0 (RGBA32U): modelCenter.xyz (3×32-bit floats as uint) + 2×16-bit covariance halfs (covA.z, covB.z)
129+
// - splatTexture1 (RG32U): 4×16-bit covariance halfs packed as (covA.xy, covB.xy)
126130
this.colorTexture = this.createTexture('splatColor', this.colorTextureFormat, 1, 1);
127131
this.splatTexture0 = this.createTexture('splatTexture0', PIXELFORMAT_RGBA32U, 1, 1);
128132
this.splatTexture1 = this.createTexture('splatTexture1', PIXELFORMAT_RG32U, 1, 1);

0 commit comments

Comments
 (0)