@@ -155,8 +155,6 @@ class Viewer {
155155
156156 annotations : Annotations ;
157157
158- forceRenderNextFrame = false ;
159-
160158 voxelOverlay : VoxelDebugOverlay | null = null ;
161159
162160 meshOverlay : MeshDebugOverlay | null = null ;
@@ -200,8 +198,13 @@ class Viewer {
200198 }
201199 } ;
202200
203- // disable auto render, we'll render only when camera changes
204- app . autoRender = false ;
201+ // Render every frame while the scene loads, then switch to on-demand rendering once the
202+ // first complete frame is ready (autoRender is disabled in the frame:ready handler below).
203+ // Loading progress and the ready transition are both reported via frame:ready, which only
204+ // fires when a frame is rendered — so we must render continuously through loading. After
205+ // that, rendering is driven on demand by frame:request and camera-change detection. This
206+ // mirrors the engine's simple-on-demand gsplat example.
207+ app . autoRender = true ;
205208
206209 // configure the camera
207210 this . configureCamera ( settings ) ;
@@ -252,15 +255,6 @@ class Viewer {
252255 }
253256 }
254257
255- // suppress rendering till we're ready
256- if ( ! state . readyToRender ) {
257- app . renderNextFrame = false ;
258- }
259-
260- if ( this . forceRenderNextFrame ) {
261- app . renderNextFrame = true ;
262- }
263-
264258 if ( app . renderNextFrame ) {
265259 prevWorld . copy ( world ) ;
266260 prevProj . copy ( proj ) ;
@@ -287,7 +281,7 @@ class Viewer {
287281 const near = Math . max ( dist - boundRadius , far / ( 1024 * 16 ) ) ;
288282
289283 cameraEntity . camera . farClip = far ;
290- cameraEntity . camera . nearClip = near ;
284+ cameraEntity . camera . nearClip = Math . min ( 1.0 , near ) ;
291285 } ;
292286
293287 // handle application update
@@ -445,35 +439,22 @@ class Viewer {
445439
446440 const eventHandler = app . systems . gsplat ;
447441
448- // idle timer: force continuous rendering until 4s of inactivity
449- let idleTime = 0 ;
450- this . forceRenderNextFrame = true ;
451-
452- app . on ( 'update' , ( dt : number ) => {
453- idleTime += dt ;
454- this . forceRenderNextFrame = idleTime < 4 ;
455- } ) ;
456-
457- events . on ( 'inputEvent' , ( type : string ) => {
458- if ( type !== 'interact' ) {
459- idleTime = 0 ;
460- }
461- } ) ;
462-
463- eventHandler . on ( 'frame:ready' , ( _camera : CameraComponent , _layer : Layer , ready : boolean , loading : number ) => {
464- if ( loading > 0 || ! ready ) {
465- idleTime = 0 ;
466- }
442+ // Once on demand (autoRender is disabled at ready, below), render when gsplat streaming
443+ // produces new data a render would show (new LOD/world-state, pending sort). This also
444+ // keeps work-buffer texture lifetime coupled to the submit under app.autoRender = false.
445+ eventHandler . on ( 'frame:request' , ( ) => {
446+ app . renderNextFrame = true ;
467447 } ) ;
468448
469449 let current = 0 ;
470450 let watermark = 1 ;
471451 const readyHandler = ( camera : CameraComponent , layer : Layer , ready : boolean , loading : number ) => {
472452 if ( ready && loading === 0 ) {
473- // scene is done loading
453+ // scene is done with initial/reveal loading
474454 eventHandler . off ( 'frame:ready' , readyHandler ) ;
475455
476- state . readyToRender = true ;
456+ // switch to on-demand rendering (frame:request + camera-change detection)
457+ app . autoRender = false ;
477458
478459 // handle quality mode changes
479460 events . on ( 'performanceMode:changed' , applyPerfSettings ) ;
0 commit comments