@@ -32,9 +32,37 @@ export type EngineProps = React.PropsWithChildren<{
3232 * This prop is only for testing purpose and should not be passed to this component.
3333 */
3434 _nullEngine ?: NullEngine ;
35+ /**
36+ * Overrides Reactylon's default render-loop scheduling.
37+ *
38+ * By default, Reactylon starts a continuous Babylon.js render loop via `engine.runRenderLoop(...)`
39+ * once all scenes are marked as ready.
40+ *
41+ * When `manualRenderLoop` is provided, Reactylon will **not** call `engine.runRenderLoop`.
42+ * Instead, it will invoke this callback exactly once at the moment the render loop would normally start.
43+ *
44+ * This is primarily intended for **tests** (deterministic stepping) or advanced integrations that
45+ * require full control over scheduling.
46+ *
47+ * @param renderFrame A function that renders a single frame using Reactylon's internal rules
48+ * (e.g., multi-scene view selection). Call it manually to step rendering deterministically.
49+ * @param engine The Babylon.js engine instance. Optional; provided for advanced use-cases
50+ * (e.g., starting/stopping a continuous loop manually).
51+ *
52+ */
53+ manualRenderLoop ?: ( renderFrame : ( ) => void , engine : WebGLEngine | WebGPUEngine ) => void ;
3554} > ;
3655
37- export const Engine = ( { engineOptions, loadingScreenOptions, canvasId = 'reactylon-canvas' , _nullEngine, isMultipleCanvas, forceWebGL, ...rest } : EngineProps ) => {
56+ export const Engine = ( {
57+ engineOptions,
58+ loadingScreenOptions,
59+ canvasId = 'reactylon-canvas' ,
60+ _nullEngine,
61+ isMultipleCanvas,
62+ forceWebGL,
63+ manualRenderLoop,
64+ ...rest
65+ } : EngineProps ) => {
3866 const [ context , setContext ] = useState < EngineContext | null > ( null ) ;
3967 const engineRef = useRef < {
4068 engine : WebGLEngine | WebGPUEngine ;
@@ -44,6 +72,10 @@ export const Engine = ({ engineOptions, loadingScreenOptions, canvasId = 'reacty
4472 const canvasRef = useRef < HTMLCanvasElement > ( null ) ;
4573
4674 const children = Children . toArray ( rest . children ) as Array < React . ReactElement > ;
75+ const initialScenesRef = useRef ( children . length ) ;
76+ const readyScenesRef = useRef ( 0 ) ;
77+ const isRenderLoopStarted = useRef ( false ) ;
78+
4779 const isMultipleScene = children . length > 1 ;
4880
4981 useEffect ( ( ) => {
@@ -75,20 +107,36 @@ export const Engine = ({ engineOptions, loadingScreenOptions, canvasId = 'reacty
75107 const { component, animationStyle } = loadingScreenOptions ;
76108 engine . loadingScreen = new CustomLoadingScreen ( canvas as HTMLCanvasElement , component , animationStyle ) as unknown as ILoadingScreen ;
77109 }
78- engine . runRenderLoop ( ( ) => {
110+ const renderFrame = ( ) => {
79111 const camera = engine ! . activeView ?. camera ;
80112 engine ! . scenes . forEach ( scene => {
81- if ( ! scene . activeCamera ) {
82- // meantime you are setting a camera
83- Logger . warn ( 'Engine - runRenderLoop - Waiting for active camera...' ) ;
84- }
85- if ( scene . cameras ?. length > 0 ) {
113+ if ( scene . activeCamera ) {
86114 if ( ! isMultipleScene || scene . activeCamera === camera ) {
87115 scene . render ( ) ;
88116 }
89117 }
90118 } ) ;
91- } ) ;
119+ } ;
120+
121+ const startRenderLoop = ( ) => {
122+ if ( isRenderLoopStarted . current ) return ;
123+ isRenderLoopStarted . current = true ;
124+
125+ if ( manualRenderLoop ) {
126+ manualRenderLoop ( renderFrame , engine ) ;
127+ return ;
128+ }
129+ engine . runRenderLoop ( renderFrame ) ;
130+ } ;
131+
132+ const markSceneAsReady = ( ) => {
133+ readyScenesRef . current += 1 ;
134+ // start render loop only when all scenes are marked as ready
135+ if ( readyScenesRef . current >= initialScenesRef . current ) {
136+ startRenderLoop ( ) ;
137+ }
138+ } ;
139+
92140 engineRef . current = {
93141 engine,
94142 onResizeWindow : ( ) => engine . resize ( ) ,
@@ -99,6 +147,7 @@ export const Engine = ({ engineOptions, loadingScreenOptions, canvasId = 'reacty
99147 engine,
100148 isMultipleCanvas : ! ! isMultipleCanvas ,
101149 isMultipleScene,
150+ markSceneAsReady,
102151 disposeEngine : ( ) => {
103152 window . removeEventListener ( 'resize' , engineRef . current ! . onResizeWindow ) ;
104153 engineRef . current ! . engine . dispose ( ) ;
0 commit comments