ソースを参照

feat: 982 refactor userenderer (#1022)

* refactor(TresCanvas): refactor prop types

* chore(playground): add controls to basic playground

* feat(playground): refactor useRenderer

* refactor(useRendererManager): remove unused watchEffect for render mode

* refactor(useRendererManager): replace watch with watchEffect to track instance.value

* refactor: making renderer instance a plain object (#1028)

* refactor: streamline renderer management and event handling

- Replaced the `instance` reference with `renderer` in `useRendererManager` for clarity and consistency.
- Updated event handling in `TresCanvas.vue` to utilize the new `onReady` method instead of `whenever`, improving readability.
- Cleaned up unused imports across multiple files, enhancing code maintainability.
- Adjusted inspector state handling to reflect the new renderer structure, ensuring accurate memory and rendering statistics.

* refactor: tiny performance improvement (maybe)

* refactor: tiny type improvement

* refactor: separate types for props and composable

- Updated references to `renderer.instance.value` to `renderer.instance` for consistency and clarity in `useLoop`, `TresCanvas`, and related components.
- Enhanced type definitions by introducing `RendererOptions` to encapsulate WebGL context options, improving type safety and maintainability.
- Cleaned up unused imports and adjusted prop types in `useTres` and `useTresContextProvider` for better alignment with the new renderer structure.
- This refactor aims to simplify the renderer management and improve overall code readability.

* fix(loop): update renderer reference in snapshot context

- Changed the renderer reference in the snapshot context from `context.renderer.instance` to `context.renderer` for consistency with recent refactors.
- This adjustment aligns with the updated renderer management structure, enhancing clarity and maintainability of the rendering loop.

---------

Co-authored-by: Tino Koch <17991193+Tinoooo@users.noreply.github.com>
Alvaro Saburido 1 ヶ月 前
コミット
cc8b752b71

+ 1 - 1
docs/api/composables.md

@@ -121,7 +121,7 @@ You can take over the render call by using the `render` method.
 const { render } = useLoop()
 
 render(({ renderer, scene, camera }) => {
-  renderer.instance.value.render(scene, camera)
+  renderer.instance.render(scene, camera)
 })
 ```
 

+ 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.instance.value
+  rendererRef.value = renderer.instance
 }
 
 const isRendererDprClamped = (renderer: WebGLRenderer) => {

+ 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.instance.value.render(scene, camera)
+  renderer.instance.render(scene, camera)
 })
 
 const { isRenderPaused, unregisterRender } = useControls({

+ 66 - 5
playground/vue/src/pages/basic/index.vue

@@ -1,17 +1,78 @@
 <script setup lang="ts">
 import { TresCanvas } from '@tresjs/core'
+import type { ShadowMapType, ToneMapping } from 'three'
+import { ACESFilmicToneMapping, AgXToneMapping, BasicShadowMap, CineonToneMapping, LinearToneMapping, NeutralToneMapping, NoToneMapping, PCFShadowMap, PCFSoftShadowMap, ReinhardToneMapping, VSMShadowMap } from 'three'
 import { OrbitControls } from '@tresjs/cientos'
+import { TresLeches, useControls } from '@tresjs/leches'
+import '@tresjs/leches/styles'
+
+const { clearColor, clearAlpha, toneMapping, shadows, shadowMapType } = useControls({
+  clearColor: '#82DBC5',
+  clearAlpha: {
+    value: 1,
+    min: 0,
+    max: 1,
+    step: 0.01,
+    label: 'Clear Alpha',
+  },
+  toneMapping: {
+    value: ACESFilmicToneMapping,
+    options: [
+      { text: 'No Tone Mapping', value: NoToneMapping },
+      { text: 'Linear', value: LinearToneMapping },
+      { text: 'Reinhard', value: ReinhardToneMapping },
+      { text: 'Cineon', value: CineonToneMapping },
+      { text: 'ACES Filmic', value: ACESFilmicToneMapping },
+      { text: 'AgX', value: AgXToneMapping }, // New in Three.js r155
+      { text: 'Neutral', value: NeutralToneMapping },
+    ],
+  },
+  shadows: true,
+  shadowMapType: {
+    value: PCFSoftShadowMap,
+    options: [
+      { text: 'Basic', value: BasicShadowMap },
+      { text: 'PCF', value: PCFShadowMap },
+      { text: 'PCF Soft', value: PCFSoftShadowMap },
+      { text: 'VSM', value: VSMShadowMap },
+    ],
+  },
+})
+
+const formattedToneMapping = computed(() => {
+  return Number(toneMapping.value) as ToneMapping
+})
+
+const formattedShadowMapType = computed(() => {
+  return Number(shadowMapType.value) as ShadowMapType
+})
 </script>
 
 <template>
-  <TresCanvas clear-color="#82DBC5">
-    <TresPerspectiveCamera :position="[8, 8, 8]" />
+  <TresLeches />
+  <TresCanvas
+    :clear-color="clearColor"
+    :clear-alpha="clearAlpha"
+    :tone-mapping="formattedToneMapping"
+    :shadows="shadows"
+    :shadow-map-type="formattedShadowMapType"
+  >
+    <TresPerspectiveCamera :position="[5, 5, 5]" />
     <OrbitControls />
-    <TresGridHelper />
-    <TresMesh position-y="1">
+    <TresMesh :position="[0, 1, 0]" cast-shadow>
       <TresBoxGeometry />
-      <TresMeshNormalMaterial />
+      <TresMeshStandardMaterial color="teal" :opacity="0.5" transparent />
+    </TresMesh>
+
+    <TresMesh
+      :rotation="[-Math.PI / 2, 0, 0]"
+      receive-shadow
+    >
+      <TresPlaneGeometry :args="[10, 10, 10, 10]" />
+      <TresMeshToonMaterial />
     </TresMesh>
+    <!-- Add lighting to see the edges better -->
+    <TresDirectionalLight :position="[1, 1, -1]" cast-shadow :intensity="2" />
     <TresAmbientLight :intensity="1" />
   </TresCanvas>
 </template>

+ 41 - 46
src/components/TresCanvas.vue

@@ -1,16 +1,11 @@
 <script setup lang="ts">
 import type {
-  ColorSpace,
-  ShadowMapType,
-  ToneMapping,
   WebGLRenderer,
-  WebGLRendererParameters,
 } from 'three'
 import type { App, Ref } from 'vue'
-import type { PointerEvent } from '@pmndrs/pointer-events'
-import type { RendererPresetsType } from '../composables/useRenderer/const'
 import type { TresCamera, TresObject, TresScene } from '../types/'
-import { PerspectiveCamera, Scene } from 'three'
+import { ACESFilmicToneMapping, PCFSoftShadowMap, PerspectiveCamera, Scene } from 'three'
+import type { PointerEvent } from '@pmndrs/pointer-events'
 import * as THREE from 'three'
 
 import {
@@ -29,59 +24,37 @@ import {
   watchEffect,
 } from 'vue'
 import pkg from '../../package.json'
-import {
-  type TresContext,
-  useTresContextProvider,
-} from '../composables'
+import type { RendererOptions, TresContext } from '../composables'
+import { useTresContextProvider } from '../composables'
 import { extend } from '../core/catalogue'
 import { nodeOps } from '../core/nodeOps'
 
 import { disposeObject3D } from '../utils/'
 import { registerTresDevtools } from '../devtools'
-import { whenever } from '@vueuse/core'
 import type { TresPointerEventName } from '../utils/pointerEvents'
 
-export interface TresCanvasProps
-  extends Omit<WebGLRendererParameters, 'canvas'> {
-  // required by for useRenderer
-  shadows?: boolean
-  clearColor?: string
-  toneMapping?: ToneMapping
-  shadowMapType?: ShadowMapType
-  useLegacyLights?: boolean
-  outputColorSpace?: ColorSpace
-  toneMappingExposure?: number
-  renderMode?: 'always' | 'on-demand' | 'manual'
-  dpr?: number | [number, number]
-
-  // required by useTresContextProvider
-  camera?: TresCamera
-  preset?: RendererPresetsType
-  windowSize?: boolean
-
-  // Misc opt-out flags
-  enableProvideBridge?: boolean
-}
-
 const props = withDefaults(defineProps<TresCanvasProps>(), {
   alpha: undefined,
   depth: undefined,
   shadows: undefined,
   stencil: undefined,
-  antialias: undefined,
+  antialias: true,
   windowSize: undefined,
   useLegacyLights: undefined,
   preserveDrawingBuffer: undefined,
   logarithmicDepthBuffer: undefined,
   failIfMajorPerformanceCaveat: undefined,
   renderMode: 'always',
+  clearColor: '#000000',
+  clearAlpha: 1,
   enableProvideBridge: true,
+  toneMapping: ACESFilmicToneMapping,
+  shadowMapType: PCFSoftShadowMap,
 })
 
 const emit = defineEmits<{
   ready: [context: TresContext]
   render: [renderer: WebGLRenderer]
-
   pointermissed: [event: PointerEvent<MouseEvent>]
 } & {
   // all pointer events are supported because they bubble up
@@ -92,7 +65,7 @@ const slots = defineSlots<{
   default: () => any
 }>()
 
-const canvas = ref<HTMLCanvasElement>()
+const canvasRef = ref<HTMLCanvasElement>()
 
 /*
  `scene` is defined here and not in `useTresContextProvider` because the custom
@@ -154,9 +127,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.instance.value.dispose()
-    context.renderer.instance.value.renderLists.dispose()
-    context.renderer.instance.value.forceContextLoss()
+    context.renderer.instance.dispose()
+    context.renderer.instance.renderLists.dispose()
+    context.renderer.instance.forceContextLoss()
   }
   (scene.value as TresScene).__tres = {
     root: context,
@@ -178,7 +151,7 @@ const unmountCanvas = () => {
 }
 
 onMounted(() => {
-  const existingCanvas = canvas as Ref<HTMLCanvasElement>
+  const existingCanvas = canvasRef as Ref<HTMLCanvasElement>
 
   context.value = useTresContextProvider({
     scene: scene.value as TresScene,
@@ -240,20 +213,42 @@ onMounted(() => {
     emit('render', renderer)
   })
 
+  renderer.onReady(() => {
+    emit('ready', context.value!)
+  })
+
   // HMR support
   if (import.meta.hot && context.value) { import.meta.hot.on('vite:afterUpdate', () => handleHMR(context.value as TresContext)) }
 })
 
-whenever(() => context.value?.renderer.isReady, () => {
-  if (context.value) { emit('ready', context.value) }
-}, { once: true })
-
 onUnmounted(unmountCanvas)
 </script>
 
+<script lang="ts">
+export interface TresCanvasProps extends RendererOptions {
+  /**
+   * Custom camera instance to use as main camera
+   * If not provided, a default PerspectiveCamera will be created
+   */
+  camera?: TresCamera
+  /**
+   * Whether the canvas should be sized to the window
+   * When true, canvas will be fixed positioned and full viewport size
+   * @default false
+   */
+  windowSize?: boolean
+  /**
+   * Whether to enable the provide/inject bridge between Vue and TresJS
+   * When true, Vue's provide/inject will work across the TresJS boundary
+   * @default true
+   */
+  enableProvideBridge?: boolean
+}
+</script>
+
 <template>
   <canvas
-    ref="canvas"
+    ref="canvasRef"
     :data-scene="scene.uuid"
     :class="$attrs.class"
     :data-tres="`tresjs ${pkg.version}`"

+ 0 - 21
src/composables/useRenderer/const.ts

@@ -1,21 +0,0 @@
-import { ACESFilmicToneMapping, NoToneMapping, PCFSoftShadowMap, SRGBColorSpace } from 'three'
-
-export const rendererPresets = {
-  realistic: {
-    shadows: true,
-    physicallyCorrectLights: true,
-    outputColorSpace: SRGBColorSpace,
-    toneMapping: ACESFilmicToneMapping,
-    toneMappingExposure: 3,
-    shadowMap: {
-      enabled: true,
-      type: PCFSoftShadowMap,
-    },
-  },
-  flat: {
-    toneMapping: NoToneMapping,
-    toneMappingExposure: 1,
-  },
-}
-
-export type RendererPresetsType = keyof typeof rendererPresets

+ 232 - 172
src/composables/useRenderer/useRendererManager.ts

@@ -1,29 +1,19 @@
-import type { ColorSpace, Scene, ShadowMapType, ToneMapping, WebGLRendererParameters } from 'three'
-import type { TresColor } from '../../types'
+import type { ColorRepresentation, ColorSpace, Object3D, Scene, ShadowMapType, ToneMapping } from 'three'
 
 import type { TresContext } from '../useTresContextProvider'
 
-import type { RendererPresetsType } from './const'
 import {
   createEventHook,
-  type MaybeRefOrGetter,
   unrefElement,
   useDevicePixelRatio,
 } from '@vueuse/core'
-import { ACESFilmicToneMapping, Color, WebGLRenderer } from 'three'
-import { computed, type MaybeRef, onUnmounted, readonly, ref, shallowRef, toValue, triggerRef, watch, watchEffect } from 'vue'
+import { Material, Mesh, WebGLRenderer } from 'three'
+import { computed, type MaybeRef, onUnmounted, type Reactive, readonly, ref, 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'
-import { get, merge, set, setPixelRatio } from '../../utils'
+import { setPixelRatio } from '../../utils'
 
-import { normalizeColor } from '../../utils/normalize'
-import { logError } from '../../utils/logger'
-import { rendererPresets } from './const'
-
-type TransformToMaybeRefOrGetter<T> = {
-  [K in keyof T]: MaybeRefOrGetter<T[K]> | MaybeRefOrGetter<T[K]>;
-}
+import { logWarning } from '../../utils/logger'
 
 /**
  * If set to 'on-demand', the scene will only be rendered when the current frame is invalidated
@@ -32,78 +22,164 @@ type TransformToMaybeRefOrGetter<T> = {
  */
 export type RenderMode = 'always' | 'on-demand' | 'manual'
 
-export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRendererParameters> {
+export interface RendererOptions {
   /**
-   * Enable shadows in the Renderer
-   *
-   * @default false
+   * WebGL Context options (Readonly because they are passed to the renderer constructor)
+   * They can't be changed after the renderer is created because they are passed to the canvas context
    */
-  shadows?: MaybeRefOrGetter<boolean>
+  antialias?: boolean
+  /**
+   * Enables stencil buffer with 8 bits.
+   * Required for stencil-based operations like shadow volumes or post-processing effects.
+   * @readonly
+   * @default true
+   */
+  stencil?: boolean
 
   /**
-   * Set the shadow map type
-   * Can be PCFShadowMap, PCFSoftShadowMap, BasicShadowMap, VSMShadowMap
-   * [see](https://threejs.org/docs/?q=we#api/en/constants/Renderer)
-   *
-   * @default PCFSoftShadowMap
+   * Enables depth buffer with at least 16 bits.
+   * Required for proper 3D rendering and depth testing.
+   * @readonly
+   * @default true
    */
-  shadowMapType?: MaybeRefOrGetter<ShadowMapType>
+  depth?: boolean
 
   /**
-   * Whether to use physically correct lighting mode.
-   * See the [lights / physical example](https://threejs.org/examples/#webgl_lights_physical).
-   *
+   * Sets the shader precision of the renderer.
+   * @see {@link https://threejs.org/docs/#api/en/renderers/WebGLRenderer}
+   * @default 'highp'
+   */
+  precision?: 'highp' | 'mediump' | 'lowp'
+
+  /**
+   * Enables logarithmic depth buffer. Useful for scenes with large differences in scale.
+   * Helps prevent z-fighting in scenes with objects very close and very far from the camera.
+   * @readonly
    * @default false
-   * @deprecated Use {@link WebGLRenderer.useLegacyLights useLegacyLights} instead.
    */
-  physicallyCorrectLights?: MaybeRefOrGetter<boolean>
+  logarithmicDepthBuffer?: boolean
   /**
-   * Whether to use legacy lighting mode.
-   *
-   * @type {MaybeRefOrGetter<boolean>}
-   * @memberof UseRendererOptions
+   * Preserves the buffers until manually cleared or overwritten.
+   * Needed for screenshots or when reading pixels from the canvas.
+   * Warning: This may impact performance.
+   * @readonly
+   * @default false
    */
-  useLegacyLights?: MaybeRefOrGetter<boolean>
+  preserveDrawingBuffer?: boolean
   /**
-   * Defines the output encoding of the renderer.
-   * Can be LinearSRGBColorSpace, SRGBColorSpace
-   *
-   * @default LinearSRGBColorSpace
+   * Power preference for the renderer.
+    * Power preference for the renderer.
+    * - `default`: Automatically chooses the most suitable power setting.
+    * - `high-performance`: Prioritizes rendering performance.
+    * - `low-power`: Tries to reduce power usage.
+   * @see {@link https://threejs.org/docs/#api/en/renderers/WebGLRenderer}
+   * @default 'default'
+   * @readonly
    */
-  outputColorSpace?: MaybeRefOrGetter<ColorSpace>
-
+  powerPreference?: WebGLPowerPreference
+  /**
+     * Whether to create the WebGL context with an alpha buffer.
+     * This is a WebGL context option that must be set during context creation and cannot be changed later.
+     * When true, the canvas can be transparent, showing content behind it.
+     * @readonly
+     * @default false
+     */
+  alpha?: boolean
   /**
-   * Defines the tone mapping used by the renderer.
-   * Can be NoToneMapping, LinearToneMapping,
-   * ReinhardToneMapping, Uncharted2ToneMapping,
-   * CineonToneMapping, ACESFilmicToneMapping,
-   * CustomToneMapping
-   *
-   * @default ACESFilmicToneMapping
+   * Whether to premultiply the alpha of the canvas.
+   * @see {@link https://threejs.org/docs/#api/en/renderers/WebGLRenderer}
+   * @default true
+   */
+  premultipliedAlpha?: boolean
+  /**
+   * Whether to fail if the major performance caveat is detected.
+   * @see {@link https://threejs.org/docs/#api/en/renderers/WebGLRenderer}
+   * @default false
+   */
+  failIfMajorPerformanceCaveat?: boolean
+  /**
+   * WebGL options with set methods
+   * @see {@link https://threejs.org/docs/#api/en/renderers/WebGLRenderer}
    */
-  toneMapping?: MaybeRefOrGetter<ToneMapping>
-
   /**
-   * Defines the tone mapping exposure used by the renderer.
-   *
+   * Clear color for the canvas
+   * Can include alpha value (e.g. '#00808000' for fully transparent teal)
+   */
+  clearColor?: ColorRepresentation
+  /**
+   * The opacity of the clear color (0-1)
+   * Controls the transparency of the clear color
    * @default 1
    */
-  toneMappingExposure?: MaybeRefOrGetter<number>
-
+  clearAlpha?: number
   /**
-   * The color value to use when clearing the canvas.
-   *
-   * @default 0x000000
+   * Enable shadow rendering in the scene
+   * @default false
    */
-  clearColor?: MaybeRefOrGetter<TresColor>
-  windowSize?: MaybeRefOrGetter<boolean | string>
-  preset?: MaybeRefOrGetter<RendererPresetsType>
-  renderMode?: MaybeRef<RenderMode>
+  shadows?: boolean
   /**
-   * A `number` sets the renderer's device pixel ratio.
-   * `[number, number]` clamp's the renderer's device pixel ratio.
+   * Tone mapping technique to use for the scene
+   * - `NoToneMapping`: No tone mapping is applied.
+   * - `LinearToneMapping`: Linear tone mapping.
+   * - `ReinhardToneMapping`: Reinhard tone mapping.
+   * - `CineonToneMapping`: Cineon tone mapping.
+   * - `ACESFilmicToneMapping`: ACES Filmic tone mapping.
+   * - `AgXToneMapping`: AgX tone mapping.
+   * - `NeutralToneMapping`: Neutral tone mapping.
+   * @see {@link https://threejs.org/docs/#api/en/constants/Renderer}
+   * @default ACESFilmicToneMapping (Opinionated default by TresJS)
    */
-  dpr?: MaybeRefOrGetter<number | [number, number]>
+  toneMapping?: ToneMapping
+  /**
+   * Type of shadow map to use for shadow calculations
+   * - `BasicShadowMap`: Basic shadow map.
+   * - `PCFShadowMap`: Percentage-Closer Filtering shadow map.
+   * - `PCFSoftShadowMap`: Percentage-Closer Filtering soft shadow map.
+   * - `VSMShadowMap`: Variance shadow map.
+   * @see {@link https://threejs.org/docs/#api/en/constants/Renderer}
+   * @default PCFSoftShadowMap (Opinionated default by TresJS)
+   */
+  shadowMapType?: ShadowMapType
+  /**
+   * Whether to use legacy lights system instead of the new one
+   * @deprecated Use `useLegacyLights: false` for the new lighting system
+   */
+  useLegacyLights?: boolean
+  /**
+   * Color space for the output render
+   * @see {@link https://threejs.org/docs/#api/en/constants/Renderer}
+   */
+  outputColorSpace?: ColorSpace
+  /**
+   * Exposure level of tone mapping
+   * @default 1
+   */
+  toneMappingExposure?: number
+  /**
+   * Rendering mode for the canvas
+   * - 'always': Renders every frame
+   * - 'on-demand': Renders only when changes are detected
+   * - 'manual': Renders only when explicitly called
+   * @default 'always'
+   */
+  renderMode?: 'always' | 'on-demand' | 'manual'
+  /**
+   * Device Pixel Ratio for the renderer
+   * Can be a single number or a tuple defining a range [min, max]
+   */
+  dpr?: number | [number, number]
+  /**
+   * Custom WebGL renderer instance
+   * Allows using a pre-configured renderer instead of creating a new one
+   */
+  // renderer?: (ctx: TresRendererSetupContext) => Promise<TresRenderer> | TresRenderers
+}
+
+export interface UseRendererOptions {
+  scene: Scene
+  canvas: MaybeRef<HTMLCanvasElement>
+  options: RendererOptions
+  contextParts: Pick<TresContext, 'sizes' | 'camera' | 'loop'>
 }
 
 export function useRendererManager(
@@ -116,30 +192,25 @@ export function useRendererManager(
   {
     scene: Scene
     canvas: MaybeRef<HTMLCanvasElement>
-    options: UseRendererOptions
+    options: Reactive<RendererOptions>
     contextParts: Pick<TresContext, 'sizes' | 'camera' | 'loop'>
   },
 ) {
-  const webGLRendererConstructorParameters = computed<WebGLRendererParameters>(() => ({
-    alpha: toValue(options.alpha) ?? true,
-    depth: toValue(options.depth),
+  const renderer = new WebGLRenderer({
+    ...options,
     canvas: unrefElement(canvas),
-    context: toValue(options.context),
-    stencil: toValue(options.stencil),
-    antialias: toValue(options.antialias) ?? true,
-    precision: toValue(options.precision),
-    powerPreference: toValue(options.powerPreference),
-    premultipliedAlpha: toValue(options.premultipliedAlpha),
-    preserveDrawingBuffer: toValue(options.preserveDrawingBuffer),
-    logarithmicDepthBuffer: toValue(options.logarithmicDepthBuffer),
-    failIfMajorPerformanceCaveat: toValue(options.failIfMajorPerformanceCaveat),
-  }))
-
-  const instance = shallowRef<WebGLRenderer>(new WebGLRenderer(webGLRendererConstructorParameters.value))
-
-  const amountOfFramesToRender = ref(0)
+  })
+
+  const frames = ref(0)
   const maxFrames = 60
-  const canBeInvalidated = computed(() => toValue(options.renderMode) === 'on-demand' && amountOfFramesToRender.value === 0)
+  const canBeInvalidated = computed(() => toValue(options.renderMode) === 'on-demand' && frames.value === 0)
+
+  const forceMaterialUpdate = () =>
+    scene.traverse((child: Object3D) => {
+      if (child instanceof Mesh && child.material instanceof Material) {
+        child.material.needsUpdate = true
+      }
+    })
 
   /**
    * Invalidates the current frame when in on-demand render mode.
@@ -149,7 +220,7 @@ export function useRendererManager(
       return
     }
 
-    amountOfFramesToRender.value = Math.min(maxFrames, amountOfFramesToRender.value + amountOfFramesToInvalidate)
+    frames.value = Math.min(maxFrames, frames.value + amountOfFramesToInvalidate)
   }
 
   /**
@@ -160,7 +231,7 @@ export function useRendererManager(
       throw new Error('advance can only be called in manual render mode.')
     }
 
-    amountOfFramesToRender.value = 1
+    frames.value = 1
   }
 
   const invalidateOnDemand = () => {
@@ -174,61 +245,43 @@ export function useRendererManager(
   const renderEventHook = createEventHook<WebGLRenderer>()
 
   loop.register(() => {
-    if (camera.activeCamera.value && amountOfFramesToRender.value) {
-      instance.value.render(scene, camera.activeCamera.value)
+    if (camera.activeCamera.value && frames.value) {
+      renderer.render(scene, camera.activeCamera.value)
 
-      renderEventHook.trigger(instance.value)
+      renderEventHook.trigger(renderer)
     }
 
-    amountOfFramesToRender.value = isModeAlways.value
+    frames.value = isModeAlways.value
       ? 1
-      : Math.max(0, amountOfFramesToRender.value - 1)
+      : Math.max(0, frames.value - 1)
   }, 'render')
 
-  // since the properties set via the constructor can't be updated dynamically,
-  // the renderer is recreated once they change
-  watch(webGLRendererConstructorParameters, () => {
-    instance.value.dispose()
-    instance.value = new WebGLRenderer(webGLRendererConstructorParameters.value)
-
-    invalidateOnDemand()
-  })
-
   const isReady = computed(() =>
-    !!(instance.value.domElement.width && instance.value.domElement.height),
+    !!(renderer.domElement.width && renderer.domElement.height),
   )
 
+  const readyEventHook = createEventHook<WebGLRenderer>()
+  let hasTriggeredReady = false
+
+  // Watch the sizes and invalidate the renderer when they change
   watch([sizes.width, sizes.height], () => {
-    instance.value.setSize(sizes.width.value, sizes.height.value)
-    invalidateOnDemand()
+    renderer.setSize(sizes.width.value, sizes.height.value)
 
-    triggerRef(instance)
+    if (!hasTriggeredReady && renderer.domElement.width && renderer.domElement.height) {
+      readyEventHook.trigger(renderer)
+      hasTriggeredReady = true
+    }
+
+    invalidateOnDemand()
   }, {
     immediate: true,
   })
 
-  watch(() => options.clearColor, invalidateOnDemand)
-
   const { pixelRatio } = useDevicePixelRatio()
 
-  const getThreeRendererDefaults = () => {
-    const plainRenderer = new WebGLRenderer()
-
-    const defaults = {
-      shadowMap: {
-        enabled: plainRenderer.shadowMap.enabled,
-        type: plainRenderer.shadowMap.type,
-      },
-      toneMapping: plainRenderer.toneMapping,
-      toneMappingExposure: plainRenderer.toneMappingExposure,
-      outputColorSpace: plainRenderer.outputColorSpace,
-    }
-    plainRenderer.dispose()
-
-    return defaults
-  }
-
-  const threeDefaults = getThreeRendererDefaults()
+  watchEffect(() => {
+    setPixelRatio(renderer, pixelRatio.value, toValue(options.dpr))
+  })
 
   if (toValue(options.renderMode) === 'on-demand') {
     // Invalidate for the first time
@@ -242,78 +295,85 @@ export function useRendererManager(
     }, 100)
   }
 
-  watchEffect(() => {
-    const rendererPreset = toValue(options.preset)
-
-    if (rendererPreset) {
-      if (!(rendererPreset in rendererPresets)) { logError(`Renderer Preset must be one of these: ${Object.keys(rendererPresets).join(', ')}`) }
-
-      merge(instance.value, rendererPresets[rendererPreset])
-    }
+  const clearColorAndAlpha = computed(() => {
+    const clearColor = toValue(options.clearColor)
+    const clearAlpha = toValue(options.clearAlpha)
 
-    setPixelRatio(instance.value, pixelRatio.value, toValue(options.dpr))
+    const isClearColorWithAlpha = typeof clearColor === 'string' && clearColor.length === 9 && clearColor.startsWith('#')
 
-    // Render mode
-
-    if (isModeAlways.value) {
-      // If the render mode is 'always', ensure there's always a frame pending
-      amountOfFramesToRender.value = Math.max(1, amountOfFramesToRender.value)
+    if (isClearColorWithAlpha && clearAlpha !== undefined) {
+      logWarning(`clearColor with alpha (e.g. ${clearColor}) and clearAlpha cannot both be set, using clearColor as source of truth`)
     }
 
-    const getValue = <T>(option: MaybeRefOrGetter<T>, pathInThree: string): T | undefined => {
-      const value = toValue(option)
-
-      const getValueFromPreset = () => {
-        if (!rendererPreset) { return }
-
-        return get(rendererPresets[rendererPreset], pathInThree)
+    if (isClearColorWithAlpha) {
+      return {
+        alpha: Number.parseInt(clearColor.slice(7, 9), 16) / 255,
+        color: clearColor.slice(0, 7),
       }
+    }
 
-      if (value !== undefined) { return value }
-
-      const valueInPreset = getValueFromPreset() as T
-
-      if (valueInPreset !== undefined) { return valueInPreset }
-
-      return get(threeDefaults, pathInThree)
+    return {
+      alpha: clearAlpha,
+      color: clearColor,
     }
+  })
 
-    const setValueOrDefault = <T>(option: MaybeRefOrGetter<T>, pathInThree: string) =>
-      set(instance.value, pathInThree, getValue(option, pathInThree))
+  // Watchers for updatable renderer options at runtime
+  watchEffect(() => {
+    const value = clearColorAndAlpha.value
+    if (value.color === undefined || value.alpha === undefined) { return }
+    renderer.setClearColor(value.color, value.alpha)
+  })
 
-    setValueOrDefault(options.shadows, 'shadowMap.enabled')
-    setValueOrDefault(options.toneMapping ?? ACESFilmicToneMapping, 'toneMapping')
-    setValueOrDefault(options.shadowMapType, 'shadowMap.type')
+  watchEffect(() => {
+    const value = options.toneMapping
+    if (value) {
+      renderer.toneMapping = value
+    }
+  })
 
-    if (revision < 150) { setValueOrDefault(!options.useLegacyLights, 'physicallyCorrectLights') }
+  watchEffect(() => {
+    const value = options.toneMappingExposure
+    if (value) {
+      renderer.toneMappingExposure = value
+    }
+  })
 
-    setValueOrDefault(options.outputColorSpace, 'outputColorSpace')
-    setValueOrDefault(options.toneMappingExposure, 'toneMappingExposure')
+  watchEffect(() => {
+    const value = options.outputColorSpace
+    if (value) {
+      renderer.outputColorSpace = value
+    }
+  })
 
-    const clearColor = getValue(options.clearColor, 'clearColor')
+  watchEffect(() => {
+    const value = options.shadows
+    if (value === undefined) { return }
+    renderer.shadowMap.enabled = value
+    forceMaterialUpdate()
+  })
 
-    if (clearColor) {
-      instance.value.setClearColor(
-        clearColor
-          ? normalizeColor(clearColor)
-          : new Color(0x000000), // default clear color is not easily/efficiently retrievable from three
-      )
-    }
+  watchEffect(() => {
+    const value = options.shadowMapType
+    if (value === undefined) { return }
+    renderer.shadowMap.type = value
+    forceMaterialUpdate()
   })
 
   onUnmounted(() => {
-    instance.value.dispose()
-    instance.value.forceContextLoss()
+    renderer.dispose()
+    renderer.forceContextLoss()
   })
 
   return {
-    instance,
+    instance: renderer,
     isReady: readonly(isReady),
     advance,
     onRender: renderEventHook.on,
+    onReady: readyEventHook.on,
     invalidate,
     canBeInvalidated,
-    amountOfFramesToRender,
+    frames,
   }
 }
 

+ 2 - 2
src/composables/useTres/index.ts

@@ -1,4 +1,4 @@
-import { type ComputedRef, toValue } from 'vue'
+import type { ComputedRef } from 'vue'
 import type { TresContext } from '../useTresContextProvider'
 import { useTresContext } from '../useTresContextProvider'
 import type { Camera, WebGLRenderer } from 'three'
@@ -41,7 +41,7 @@ export function useTres(): TresPartialContext {
 
   return {
     scene,
-    renderer: toValue(renderer.instance),
+    renderer: renderer.instance,
     camera: camera.activeCamera,
     sizes,
     controls,

+ 5 - 8
src/composables/useTresContextProvider/index.ts

@@ -1,9 +1,8 @@
 import type { MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
-import { whenever } from '@vueuse/core'
 
 import type { RendererLoop } from '../../core/loop'
 import type { TresControl, TresScene } from '../../types'
-import type { UseRendererManagerReturn, UseRendererOptions } from '../useRenderer/useRendererManager'
+import type { RendererOptions, UseRendererManagerReturn } from '../useRenderer/useRendererManager'
 import { inject, onUnmounted, provide, ref, shallowRef } from 'vue'
 import { extend } from '../../core/catalogue'
 import { createRenderLoop } from '../../core/loop'
@@ -13,6 +12,7 @@ import type { UseCameraReturn } from '../useCamera/'
 import { useCameraManager } from '../useCamera'
 import { useRendererManager } from '../useRenderer/useRendererManager'
 import useSizes, { type SizesType } from '../useSizes'
+import type { TresCanvasProps } from '../../components/TresCanvas.vue'
 import { useEventManager } from '../useEventManager'
 
 export interface TresContext {
@@ -35,7 +35,7 @@ export function useTresContextProvider({
   scene: TresScene
   canvas: MaybeRef<HTMLCanvasElement>
   windowSize: MaybeRefOrGetter<boolean>
-  rendererOptions: UseRendererOptions
+  rendererOptions: TresCanvasProps
 }): TresContext {
   const localScene = shallowRef<TresScene>(scene)
   const sizes = useSizes(windowSize, canvas)
@@ -48,7 +48,7 @@ export function useTresContextProvider({
     {
       scene,
       canvas,
-      options: rendererOptions,
+      options: rendererOptions as RendererOptions,
       contextParts: { sizes, camera, loop },
     },
   )
@@ -79,11 +79,8 @@ export function useTresContextProvider({
   ctx.loop.setReady(false)
   ctx.loop.start()
 
-  whenever(renderer.isReady, () => { // TODO #994 This does not belong here, see https://github.com/Tresjs/tres/issues/595
+  renderer.onReady(() => {
     ctx.loop.setReady(true)
-  }, {
-    once: true,
-    immediate: true,
   })
 
   onUnmounted(() => {

+ 1 - 1
src/core/loop.ts

@@ -125,7 +125,7 @@ export function createRenderLoop(): RendererLoop {
     const snapshotCtx = {
       camera: unref(context.camera?.activeCamera),
       scene: unref(context.scene),
-      renderer: unref(context.renderer),
+      renderer: context.renderer,
       raycaster: unref(context.raycaster),
       controls: unref(context.controls),
       invalidate: context.invalidate,

+ 5 - 5
src/devtools/inspectorHandlers.ts

@@ -279,15 +279,15 @@ export const inspectorStateHandler = (tres: TresContext, { highlightMesh, prevIn
               value: {
                 objects: instance.children.length,
                 memory: calculateMemoryUsage(instance),
-                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,
+                calls: tres.renderer.instance.info.render.calls,
+                triangles: tres.renderer.instance.info.render.triangles,
+                points: tres.renderer.instance.info.render.points,
+                lines: tres.renderer.instance.info.render.lines,
               },
             },
             {
               key: 'Programs',
-              value: tres.renderer.instance.value.info.programs?.map(program => ({
+              value: tres.renderer.instance.info.programs?.map(program => ({
                 ...program,
                 programName: program.name,
               })) || [],