Bladeren bron

refactor!: 979 move renderer logic from usetrescontextprovider to userenderer 2 (#993)

* added typecheck script

* added typecheck to ci

* ts error fixes

* fixed import

* moved render state stuff to useRenderer

* chore!: stripped unnecessarily returned elements from useRenderer

BREAKING CHANGE:

- useRenderer now returns invalidate and advance
- useTresContextProvider no longer contains render state

* chore: removed internal renderer ref

* refactor!: the renderer instance is now returned from useRenderer, made renderer being wrapped inside the context

BREAKING CHANGE:

- invalidate, advance, canBeInvalidated and the renderer instance are now accessed through the context via `renderer`
- the renderer instance in the context is now readonly

* refactor: removed one emit dependency

* tofo cleanup

* merge fix

* refactor: updated other parts to match structural changes

* worked around loop errors

* fixes concerning changes in playground

* updated english docs

* fix: wrong render mode in docs

* removed obsolete todo

* fix: added null check

* fix: removed deprecated toValue import

* chore: restored renderer tyope in LoopCallbackWithCtx

* refactor: renamed useRenderer

* fix: test

* fix: lint fix

---------

Co-authored-by: Alvaro Saburido <alvaro.saburido@gmail.com>
Tino Koch 3 weken geleden
bovenliggende
commit
36bcb1c0c2

+ 1 - 1
docs/.vitepress/theme/components/BlenderCube.vue

@@ -9,7 +9,7 @@ model.position.set(0, 1, 0)
 
 const state = useTresContext()
 
-state.invalidate()
+state.renderer.invalidate()
 </script>
 
 <template>

+ 1 - 1
docs/advanced/attach.md

@@ -170,7 +170,7 @@ useLoop().render(() => {
 <template>
   <TresEffectComposer
     ref="composer"
-    :args="[renderer]"
+    :args="[renderer.instance]"
     :set-size="[sizes.width.value, sizes.height.value]"
   >
     <TresRenderPass

+ 7 - 7
docs/advanced/performance.md

@@ -57,7 +57,7 @@ setTimeout(() => {
 
 #### Manual Invalidation
 
-Since it is not really possible to observe all the possible changes in your application, you can also manually invalidate the frame by calling the `invalidate()` method from the [`useTresContext` composable](../api/composables.md#usetrescontext):
+Since it is not really possible to observe all the possible changes in your application, you can also manually invalidate the frame by calling the `invalidate()` method from `renderer` provided by the [`useTresContext` composable](../api/composables.md#usetrescontext):
 
 ::: code-group
 
@@ -69,7 +69,7 @@ import Scene from './Scene.vue'
 
 <template>
   <TresCanvas
-    render-mode="manual"
+    render-mode="on-demand"
   >
     <Scene />
   </TresCanvas>
@@ -82,13 +82,13 @@ import { useTres } from '@tresjs/core'
 import { shallowRef, watch } from 'vue'
 
 const boxRef = shallowRef(null)
-const { invalidate } = useTres()
+const { renderer } = useTres()
 
 watch(boxRef, () => {
   if (boxRef.value?.position) {
     boxRef.value.position.x = 1
   }
-  invalidate()
+  renderer.invalidate()
 })
 </script>
 
@@ -116,15 +116,15 @@ If you want to have full control of when the scene is rendered, you can set the
 </TresCanvas>
 ```
 
-In this mode, Tres will not render the scene automatically. You will need to call the `advance()` method from the [`useTresContext` composable](../api/composables.md#usetrescontext) to render the scene:
+In this mode, Tres will not render the scene automatically. You will need to call the `advance()` method from `renderer` provided by the [`useTresContext` composable](../api/composables.md#usetrescontext) to render the scene:
 
 ```vue
 <script setup>
 import { useTres } from '@tresjs/core'
 
-const { advance } = useTres()
+const { renderer } = useTres()
 
-advance()
+renderer.advance()
 </script>
 ```
 

+ 8 - 10
docs/api/composables.md

@@ -47,19 +47,17 @@ const context = useTresContext()
 ### Properties of context
 | Property | Description |
 | --- | --- |
-| **camera** | The currently active camera |
-| **cameras** | The cameras that exist in the scene |
-| **controls** | The controls of your scene |
+| **camera** | the currently active camera |
+| **cameras** | the cameras that exist in the scene |
+| **controls** | the controls of your scene |
 | **deregisterCamera** | A method to deregister a camera. This is only required if you manually create a camera. Cameras in the template are deregistered automatically. |
-| **extend** | Extends the component catalogue. See [extending](/advanced/extending) |
+| **extend** | Extends the component catalogue. See [extending](/advanced/extending). |
 | **raycaster** | the global raycaster used for pointer events |
-| **registerCamera** | a method to register a camera. This is only required if you manually create a camera. Cameras in the template are registered automatically. |
-| **renderer** | the [WebGLRenderer](https://threejs.org/docs/#api/en/renderers/WebGLRenderer) of your scene |
-| **scene** | the [scene](https://threejs.org/docs/?q=sce#api/en/scenes/Scene). |
+| **registerCamera** | A method to register a camera. This is only required if you manually create a camera. Cameras in the template are registered automatically. |
+| **renderer** | Contains the [WebGLRenderer](https://threejs.org/docs/#api/en/renderers/WebGLRenderer) instance of your scene, a method the invalidate the render loop (only required if you set the `render-mode` prop to `on-demand`), a computed that indicates whether invalidating is possible and a method to advance the render loop (only required if you set the `render-mode` prop to `manual`). |
+| **scene** | the [scene](https://threejs.org/docs/?q=sce#api/en/scenes/Scene) |
 | **setCameraActive** | a method to set a camera active |
 | **sizes** | contains width, height and aspect ratio of your canvas |
-| **invalidate** | a method to invalidate the render loop. This is only required if you set the `render-mode` prop to `on-demand`. |
-| **advance** | a method to advance the render loop. This is only required if you set the `render-mode` prop to `manual`. |
 | **loop** | the renderer loop |
 
 ### useLoop <Badge text="v4.0.0" />
@@ -123,7 +121,7 @@ You can take over the render call by using the `render` method.
 const { render } = useLoop()
 
 render(({ renderer, scene, camera }) => {
-  renderer.render(scene, camera)
+  renderer.instance.value.render(scene, camera)
 })
 ```
 

+ 1 - 1
docs/cookbook/orbit-controls.md

@@ -80,7 +80,7 @@ const { camera, renderer } = useTresContext()
 <template>
   <TresOrbitControls
     v-if="renderer"
-    :args="[camera, renderer?.domElement]"
+    :args="[camera, renderer.instance.domElement]"
   />
 </template>
 ```

+ 1 - 1
playground/vue/src/pages/advanced/devicePixelRatio/index.vue

@@ -10,7 +10,7 @@ const currDprRef = shallowRef(-1)
 const dpr = shallowRef<number | [number, number]>([minDpr, maxDpr])
 
 const onReady = ({ renderer }) => {
-  rendererRef.value = renderer.value
+  rendererRef.value = renderer.instance.value
 }
 
 const isRendererDprClamped = (renderer: WebGLRenderer) => {

+ 3 - 3
playground/vue/src/pages/advanced/manual/experience.vue

@@ -3,10 +3,10 @@ import { OrbitControls } from '@tresjs/cientos'
 import { useTres } from '@tresjs/core'
 import BlenderCube from '../../../components/BlenderCube.vue'
 
-const { advance } = useTres()
+const { renderer } = useTres()
 
 onMounted(() => {
-  advance()
+  renderer.advance()
 })
 </script>
 
@@ -18,7 +18,7 @@ onMounted(() => {
   <BlenderCube />
 
   <TresGridHelper />
-  <OrbitControls @change="advance" />
+  <OrbitControls @change="renderer.advance" />
   <TresAmbientLight :intensity="1" />
   <TresDirectionalLight
     :position="[0, 8, 4]"

+ 3 - 3
playground/vue/src/pages/advanced/on-demand/experience.vue

@@ -4,17 +4,17 @@ import { useTres } from '@tresjs/core'
 import { ref, watch } from 'vue'
 import BlenderCube from '../../../components/BlenderCube.vue'
 
-const { invalidate } = useTres()
+const { renderer } = useTres()
 
 const blenderCubeRef = ref()
 
 watch(blenderCubeRef, (prev, next) => {
   if (!next) { return }
-  invalidate()
+  renderer.invalidate()
 })
 
 function onControlChange() {
-  invalidate()
+  renderer.invalidate()
 }
 </script>
 

+ 3 - 1
playground/vue/src/pages/advanced/suspense/AsyncComponent.vue

@@ -3,7 +3,9 @@ import { useGLTF } from '@tresjs/cientos'
 import { useTresContext } from '@tresjs/core'
 
 const { scene } = await useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb', { draco: true })
-useTresContext().invalidate()
+const { renderer } = useTresContext()
+
+renderer.invalidate()
 </script>
 
 <template>

+ 1 - 1
playground/vue/src/pages/advanced/suspense/index.vue

@@ -13,7 +13,7 @@ const { show } = useControls({
     label: 'Render dispose',
     type: 'button',
     onClick() {
-      ctx?.value?.dispose()
+      ctx?.value?.renderer?.instance?.value?.dispose()
     },
   },
 })

+ 1 - 1
playground/vue/src/pages/advanced/takeOverRender/TakeOverRenderExperience.vue

@@ -7,7 +7,7 @@ import { useControls } from '@tresjs/leches'
 const { render, pauseRender, resumeRender } = useLoop()
 
 const { off } = render(({ renderer, scene, camera }) => {
-  renderer.render(scene, camera)
+  renderer.instance.value.render(scene, camera)
 })
 
 const { isRenderPaused, unregisterRender } = useControls({

+ 8 - 4
src/components/TresCanvas.vue

@@ -158,9 +158,9 @@ const mountCustomRenderer = (context: TresContext, empty = false) => {
 const dispose = (context: TresContext, force = false) => {
   disposeObject3D(context.scene.value as unknown as TresObject)
   if (force) {
-    context.renderer.value.dispose()
-    context.renderer.value.renderLists.dispose()
-    context.renderer.value.forceContextLoss()
+    context.renderer.instance.value.dispose()
+    context.renderer.instance.value.renderLists.dispose()
+    context.renderer.instance.value.forceContextLoss()
   }
   (scene.value as TresScene).__tres = {
     root: context,
@@ -192,7 +192,7 @@ onMounted(() => {
     emit,
   })
 
-  const { registerCamera, camera, cameras, deregisterCamera } = context.value
+  const { registerCamera, camera, cameras, deregisterCamera, renderer } = context.value
 
   mountCustomRenderer(context.value)
 
@@ -234,6 +234,10 @@ onMounted(() => {
     addDefaultCamera()
   }
 
+  renderer.onRender.on((renderer) => {
+    emit('render', renderer)
+  })
+
   // HMR support
   if (import.meta.hot && context.value) { import.meta.hot.on('vite:afterUpdate', () => handleHMR(context.value as TresContext)) }
 })

+ 1 - 1
src/composables/index.ts

@@ -6,7 +6,7 @@ export * from './useGraph'
 export * from './useLoader'
 export * from './useLoop'
 export * from './useRaycaster'
-export * from './useRenderer/'
+export * from './useRenderer/useRendererManager'
 export * from './useRenderLoop'
 export * from './useTexture'
 export * from './useTresContextProvider'

+ 1 - 5
src/composables/useLoop/index.ts

@@ -9,19 +9,15 @@ export function useLoop() {
     loop,
     raycaster,
     controls,
-    invalidate,
-    advance,
   } = useTresContext()
 
   // Pass context to loop
   loop.setContext({
     camera,
     scene,
-    renderer,
+    renderer: renderer.instance,
     raycaster,
     controls,
-    invalidate,
-    advance,
   })
 
   function onBeforeRender(cb: LoopCallbackFn, index = 0) {

+ 1 - 1
src/composables/useRaycaster/index.ts

@@ -13,7 +13,7 @@ export const useRaycaster = (
   ctx: TresContext,
 ) => {
   // having a separate computed makes useElementBounding work
-  const canvas = computed(() => ctx.renderer.value.domElement as HTMLCanvasElement)
+  const canvas = computed(() => ctx.renderer.instance.value.domElement as HTMLCanvasElement)
   const intersects: ShallowRef<Intersection[]> = shallowRef([])
   const { x, y } = usePointer({ target: canvas })
   let delta = 0

+ 84 - 29
src/composables/useRenderer/index.ts → src/composables/useRenderer/useRendererManager.ts

@@ -1,17 +1,17 @@
 import type { ColorSpace, Scene, ShadowMapType, ToneMapping, WebGLRendererParameters } from 'three'
-import type { EmitEventFn, TresColor } from '../../types'
+import type { TresColor } from '../../types'
 
 import type { TresContext } from '../useTresContextProvider'
 
 import type { RendererPresetsType } from './const'
 import {
+  createEventHook,
   type MaybeRefOrGetter,
-  toValue,
   unrefElement,
   useDevicePixelRatio,
 } from '@vueuse/core'
 import { ACESFilmicToneMapping, Color, WebGLRenderer } from 'three'
-import { computed, type MaybeRef, onUnmounted, shallowRef, watch, watchEffect } from 'vue'
+import { computed, type MaybeRef, onUnmounted, ref, shallowRef, toValue, watch, watchEffect } from 'vue'
 
 // Solution taken from Thretle that actually support different versions https://github.com/threlte/threlte/blob/5fa541179460f0dadc7dc17ae5e6854d1689379e/packages/core/src/lib/lib/useRenderer.ts
 import { revision } from '../../core/revision'
@@ -25,6 +25,13 @@ type TransformToMaybeRefOrGetter<T> = {
   [K in keyof T]: MaybeRefOrGetter<T[K]> | MaybeRefOrGetter<T[K]>;
 }
 
+/**
+ * If set to 'on-demand', the scene will only be rendered when the current frame is invalidated
+ * If set to 'manual', the scene will only be rendered when advance() is called
+ * If set to 'always', the scene will be rendered every frame
+ */
+export type RenderMode = 'always' | 'on-demand' | 'manual'
+
 export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRendererParameters> {
   /**
    * Enable shadows in the Renderer
@@ -91,7 +98,7 @@ export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRen
   clearColor?: MaybeRefOrGetter<TresColor>
   windowSize?: MaybeRefOrGetter<boolean | string>
   preset?: MaybeRefOrGetter<RendererPresetsType>
-  renderMode?: MaybeRefOrGetter<'always' | 'on-demand' | 'manual'>
+  renderMode?: MaybeRef<RenderMode>
   /**
    * A `number` sets the renderer's device pixel ratio.
    * `[number, number]` clamp's the renderer's device pixel ratio.
@@ -99,18 +106,18 @@ export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRen
   dpr?: MaybeRefOrGetter<number | [number, number]>
 }
 
-export function useRenderer(
+export function useRendererManager(
   {
+    scene,
     canvas,
     options,
-    contextParts: { sizes, render, invalidate, advance },
+    contextParts: { sizes, loop, camera },
   }:
   {
-    canvas: MaybeRef<HTMLCanvasElement>
     scene: Scene
+    canvas: MaybeRef<HTMLCanvasElement>
     options: UseRendererOptions
-    emit: EmitEventFn
-    contextParts: Pick<TresContext, 'sizes' | 'camera' | 'render'> & { invalidate: () => void, advance: () => void }
+    contextParts: Pick<TresContext, 'sizes' | 'camera' | 'loop'>
   },
 ) {
   const webGLRendererConstructorParameters = computed<WebGLRendererParameters>(() => ({
@@ -128,24 +135,69 @@ export function useRenderer(
     failIfMajorPerformanceCaveat: toValue(options.failIfMajorPerformanceCaveat),
   }))
 
-  const renderer = shallowRef<WebGLRenderer>(new WebGLRenderer(webGLRendererConstructorParameters.value))
+  const instance = shallowRef<WebGLRenderer>(new WebGLRenderer(webGLRendererConstructorParameters.value))
+
+  const amountOfFramesToRender = ref(0)
+  const maxFrames = 60
+  const canBeInvalidated = computed(() => toValue(options.renderMode) === 'on-demand' && amountOfFramesToRender.value === 0)
+
+  /**
+   * Invalidates the current frame when in on-demand render mode.
+   */
+  const invalidate = (amountOfFramesToInvalidate = 1) => {
+    if (!canBeInvalidated.value) {
+      if (toValue(options.renderMode) !== 'on-demand') { throw new Error('invalidate can only be called in on-demand render mode.') }
+
+      return
+    }
+
+    amountOfFramesToRender.value = Math.min(maxFrames, amountOfFramesToRender.value + amountOfFramesToInvalidate)
+  }
+
+  /**
+   * Advances one frame when in manual render mode.
+   */
+  const advance = () => {
+    if (toValue(options.renderMode) !== 'manual') {
+      throw new Error('advance can only be called in manual render mode.')
+    }
+
+    amountOfFramesToRender.value = 1
+  }
 
-  function invalidateOnDemand() {
-    if (options.renderMode === 'on-demand') {
+  const invalidateOnDemand = () => {
+    if (toValue(options.renderMode) === 'on-demand') {
       invalidate()
     }
   }
+
+  const isModeAlways = computed(() => toValue(options.renderMode) === 'always')
+
+  const onRender = createEventHook<WebGLRenderer>()
+
+  loop.register(() => {
+    if (camera.value && amountOfFramesToRender.value) {
+      instance.value.render(scene, camera.value)
+
+      onRender.trigger(instance.value)
+    }
+
+    amountOfFramesToRender.value = isModeAlways.value
+      ? 1
+      : Math.max(0, amountOfFramesToRender.value - 1)
+  }, 'render')
+
   // since the properties set via the constructor can't be updated dynamically,
   // the renderer is recreated once they change
   watch(webGLRendererConstructorParameters, () => {
-    renderer.value.dispose()
-    renderer.value = new WebGLRenderer(webGLRendererConstructorParameters.value)
+    instance.value.dispose()
+    instance.value = new WebGLRenderer(webGLRendererConstructorParameters.value)
 
     invalidateOnDemand()
   })
 
   watch([sizes.width, sizes.height], () => {
-    renderer.value.setSize(sizes.width.value, sizes.height.value)
+    instance.value.setSize(sizes.width.value, sizes.height.value)
     invalidateOnDemand()
   }, {
     immediate: true,
@@ -174,14 +226,12 @@ export function useRenderer(
 
   const threeDefaults = getThreeRendererDefaults()
 
-  const renderMode = toValue(options.renderMode)
-
-  if (renderMode === 'on-demand') {
+  if (toValue(options.renderMode) === 'on-demand') {
     // Invalidate for the first time
     invalidate()
   }
 
-  if (renderMode === 'manual') {
+  if (toValue(options.renderMode) === 'manual') {
     // Advance for the first time, setTimeout to make sure there is something to render
     setTimeout(() => {
       advance()
@@ -194,16 +244,16 @@ export function useRenderer(
     if (rendererPreset) {
       if (!(rendererPreset in rendererPresets)) { logError(`Renderer Preset must be one of these: ${Object.keys(rendererPresets).join(', ')}`) }
 
-      merge(renderer.value, rendererPresets[rendererPreset])
+      merge(instance.value, rendererPresets[rendererPreset])
     }
 
-    setPixelRatio(renderer.value, pixelRatio.value, toValue(options.dpr))
+    setPixelRatio(instance.value, pixelRatio.value, toValue(options.dpr))
 
     // Render mode
 
-    if (renderMode === 'always') {
+    if (isModeAlways.value) {
       // If the render mode is 'always', ensure there's always a frame pending
-      render.frames.value = Math.max(1, render.frames.value)
+      amountOfFramesToRender.value = Math.max(1, amountOfFramesToRender.value)
     }
 
     const getValue = <T>(option: MaybeRefOrGetter<T>, pathInThree: string): T | undefined => {
@@ -225,7 +275,7 @@ export function useRenderer(
     }
 
     const setValueOrDefault = <T>(option: MaybeRefOrGetter<T>, pathInThree: string) =>
-      set(renderer.value, pathInThree, getValue(option, pathInThree))
+      set(instance.value, pathInThree, getValue(option, pathInThree))
 
     setValueOrDefault(options.shadows, 'shadowMap.enabled')
     setValueOrDefault(options.toneMapping ?? ACESFilmicToneMapping, 'toneMapping')
@@ -239,7 +289,7 @@ export function useRenderer(
     const clearColor = getValue(options.clearColor, 'clearColor')
 
     if (clearColor) {
-      renderer.value.setClearColor(
+      instance.value.setClearColor(
         clearColor
           ? normalizeColor(clearColor)
           : new Color(0x000000), // default clear color is not easily/efficiently retrievable from three
@@ -248,13 +298,18 @@ export function useRenderer(
   })
 
   onUnmounted(() => {
-    renderer.value.dispose()
-    renderer.value.forceContextLoss()
+    instance.value.dispose()
+    instance.value.forceContextLoss()
   })
 
   return {
-    renderer,
+    instance,
+
+    advance,
+    onRender,
+    invalidate,
+    canBeInvalidated,
   }
 }
 
-export type UseRendererReturn = ReturnType<typeof useRenderer>
+export type UseRendererManagerReturn = ReturnType<typeof useRendererManager>

+ 3 - 3
src/composables/useSizes/index.ts

@@ -1,6 +1,6 @@
 import type { ComputedRef, MaybeRef, MaybeRefOrGetter, Ref } from 'vue'
-import { refDebounced, toValue, useElementSize, useWindowSize } from '@vueuse/core'
-import { computed, readonly } from 'vue'
+import { refDebounced, useElementSize, useWindowSize } from '@vueuse/core'
+import { computed, readonly, toValue } from 'vue'
 
 export interface SizesType {
   height: Readonly<Ref<number>>
@@ -27,4 +27,4 @@ export default function useSizes(
     width: debouncedReactiveWidth,
     aspectRatio,
   }
-}
+}

+ 9 - 83
src/composables/useTresContextProvider/index.ts

@@ -1,38 +1,19 @@
-import type { Camera, WebGLRenderer } from 'three'
+import type { Camera } from 'three'
 import type { ComputedRef, DeepReadonly, MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
 import type { RendererLoop } from '../../core/loop'
 import type { EmitEventFn, TresControl, TresObject, TresScene } from '../../types'
-import type { UseRendererOptions } from '../useRenderer'
+import type { UseRendererManagerReturn, UseRendererOptions } from '../useRenderer/useRendererManager'
 import { Raycaster } from 'three'
-import { computed, inject, onUnmounted, provide, readonly, ref, shallowRef } from 'vue'
+import { inject, onUnmounted, provide, readonly, ref, shallowRef } from 'vue'
 import { extend } from '../../core/catalogue'
 import { createRenderLoop } from '../../core/loop'
 
 import { useCamera } from '../useCamera'
-import { useRenderer } from '../useRenderer'
+import { useRendererManager } from '../useRenderer/useRendererManager'
 import useSizes, { type SizesType } from '../useSizes'
 import { type TresEventManager, useTresEventManager } from '../useTresEventManager'
 import { useTresReady } from '../useTresReady'
 
-export interface InternalState {
-  priority: Ref<number>
-  frames: Ref<number>
-  maxFrames: number
-}
-
-export interface RenderState {
-  /**
-   * If set to 'on-demand', the scene will only be rendered when the current frame is invalidated
-   * If set to 'manual', the scene will only be rendered when advance() is called
-   * If set to 'always', the scene will be rendered every frame
-   */
-  mode: Ref<'always' | 'on-demand' | 'manual'>
-  priority: Ref<number>
-  frames: Ref<number>
-  maxFrames: number
-  canBeInvalidated: ComputedRef<boolean>
-}
-
 export interface PerformanceState {
   maxFrames: number
   fps: {
@@ -53,20 +34,11 @@ export interface TresContext {
   camera: ComputedRef<Camera | undefined>
   cameras: DeepReadonly<Ref<Camera[]>>
   controls: Ref<TresControl | null>
-  renderer: ShallowRef<WebGLRenderer>
+  renderer: UseRendererManagerReturn
   raycaster: ShallowRef<Raycaster>
   perf: PerformanceState
-  render: RenderState
   // Loop
   loop: RendererLoop
-  /**
-   * Invalidates the current frame when renderMode === 'on-demand'
-   */
-  invalidate: () => void
-  /**
-   * Advance one frame when renderMode === 'manual'
-   */
-  advance: () => void
   // Camera
   registerCamera: (maybeCamera: unknown) => void
   setCameraActive: (cameraOrUuid: Camera | string) => void
@@ -93,7 +65,6 @@ export function useTresContextProvider({
   windowSize: MaybeRefOrGetter<boolean>
   rendererOptions: UseRendererOptions
   emit: EmitEventFn
-
 }): TresContext {
   const localScene = shallowRef<TresScene>(scene)
   const sizes = useSizes(windowSize, canvas)
@@ -106,37 +77,14 @@ export function useTresContextProvider({
     setCameraActive,
   } = useCamera({ sizes, scene })
 
-  // Render state
-
-  const render: RenderState = {
-    mode: ref(rendererOptions.renderMode || 'always') as Ref<'always' | 'on-demand' | 'manual'>,
-    priority: ref(0),
-    frames: ref(0),
-    maxFrames: 60,
-    canBeInvalidated: computed(() => render.mode.value === 'on-demand' && render.frames.value === 0),
-  }
+  const loop = createRenderLoop()
 
-  function invalidate(frames = 1) {
-    // Increase the frame count, ensuring not to exceed a maximum if desired
-    if (rendererOptions.renderMode === 'on-demand') {
-      render.frames.value = Math.min(render.maxFrames, render.frames.value + frames)
-    }
-  }
-
-  function advance() {
-    if (rendererOptions.renderMode === 'manual') {
-      render.frames.value = 1
-    }
-  }
-
-  const { renderer } = useRenderer(
+  const renderer = useRendererManager(
     {
       scene,
       canvas,
       options: rendererOptions,
-      emit,
-      // TODO: replace contextParts with full ctx at https://github.com/Tresjs/tres/issues/516
-      contextParts: { sizes, camera, render, invalidate, advance },
+      contextParts: { sizes, camera, loop },
     },
   )
 
@@ -160,14 +108,11 @@ export function useTresContextProvider({
         accumulator: [],
       },
     },
-    render,
-    advance,
     extend,
-    invalidate,
     registerCamera,
     setCameraActive,
     deregisterCamera,
-    loop: createRenderLoop(),
+    loop,
   }
 
   provide('useTres', ctx)
@@ -177,25 +122,6 @@ export function useTresContextProvider({
     root: ctx,
   }
 
-  // The loop
-
-  ctx.loop.register(() => {
-    if (camera.value && render.frames.value > 0) {
-      renderer.value.render(scene, camera.value)
-      emit('render', ctx.renderer.value)
-    }
-
-    // Reset priority
-    render.priority.value = 0
-
-    if (render.mode.value === 'always') {
-      render.frames.value = 1
-    }
-    else {
-      render.frames.value = Math.max(0, render.frames.value - 1)
-    }
-  }, 'render')
-
   const { on: onTresReady, cancel: cancelTresReady } = useTresReady(ctx)!
 
   ctx.loop.setReady(false)

+ 1 - 1
src/composables/useTresReady/index.ts

@@ -27,7 +27,7 @@ export function useTresReady(ctx?: TresContext) {
       return true
     }
     else {
-      const renderer = ctx.renderer.value
+      const renderer = ctx.renderer.instance.value
       const domElement = renderer?.domElement || { width: 0, height: 0 }
       return !!(renderer && domElement.width > 0 && domElement.height > 0)
     }

+ 0 - 2
src/core/loop.ts

@@ -22,8 +22,6 @@ export interface LoopCallbackWithCtx extends LoopCallback {
   controls: Ref<(EventDispatcher<object> & {
     enabled: boolean
   }) | null>
-  invalidate: Fn
-  advance: Fn
 }
 
 export type LoopCallbackFn = (params: LoopCallbackWithCtx) => void

+ 5 - 5
src/devtools/plugin.ts

@@ -188,15 +188,15 @@ export function registerTresDevtools(app: any, tres: TresContext) {
                   value: {
                     objects: instance.children.length,
                     memory: calculateMemoryUsage(instance),
-                    calls: tres.renderer.value.info.render.calls,
-                    triangles: tres.renderer.value.info.render.triangles,
-                    points: tres.renderer.value.info.render.points,
-                    lines: tres.renderer.value.info.render.lines,
+                    calls: tres.renderer.instance.value.info.render.calls,
+                    triangles: tres.renderer.instance.value.info.render.triangles,
+                    points: tres.renderer.instance.value.info.render.points,
+                    lines: tres.renderer.instance.value.info.render.lines,
                   },
                 },
                 {
                   key: 'Programs',
-                  value: tres.renderer.value.info.programs?.map(program => ({
+                  value: tres.renderer.instance.value.info.programs?.map(program => ({
                     ...program,
                     programName: program.name,
                   })) || [],

+ 3 - 3
src/utils/index.ts

@@ -436,10 +436,10 @@ export function prepareTresInstance<T extends TresObject>(obj: T, state: Partial
 export function invalidateInstance(instance: TresObject) {
   const ctx = instance?.__tres?.root
 
-  if (!ctx) { return }
+  if (!ctx?.renderer) { return }
 
-  if (ctx.render && ctx.render.canBeInvalidated.value) {
-    ctx.invalidate()
+  if (ctx.renderer.canBeInvalidated.value) {
+    ctx.renderer.invalidate()
   }
 }