Pārlūkot izejas kodu

feat(renderer)!: complete renderer system overhaul

BREAKING CHANGE: This commit introduces significant changes to the renderer system:

- Removes renderer presets in favor of direct configuration
- Makes useTresContextProvider async
- `useRenderer` now only returns the renderer from context.
- Changes default renderer values
- Adds support for multiple renderer types (WebGPU, CSS2D, CSS3D, SVG) via prop callback
- Improves TypeScript types and documentation
- Refactors renderer creation and setup into separate modules (internal)
alvarosabu 3 mēneši atpakaļ
vecāks
revīzija
dc544fbbf9

+ 1 - 0
package.json

@@ -90,6 +90,7 @@
     "eslint-plugin-vue": "^9.32.0",
     "esno": "^4.8.0",
     "gsap": "^3.12.7",
+    "happy-dom": "^17.4.4",
     "jsdom": "^26.0.0",
     "kolorist": "^1.8.0",
     "ohmyfetch": "^0.4.21",

+ 2 - 2
playground/vue/package.json

@@ -9,9 +9,9 @@
     "preview": "vite preview"
   },
   "dependencies": {
-    "@tresjs/cientos": "4.1.0",
+    "@tresjs/cientos": "https://pkg.pr.new/@tresjs/cientos@d84eb13",
     "@tresjs/core": "workspace:^",
-    "@tresjs/leches": "https://pkg.pr.new/@tresjs/leches@9ad0cd3",
+    "@tresjs/leches": "https://pkg.pr.new/@tresjs/leches@b34e795",
     "vue-router": "^4.5.0"
   },
   "devDependencies": {

+ 1 - 12
playground/vue/src/components/BlenderCube.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import { useGLTF } from '@tresjs/cientos'
-import { dispose } from '@tresjs/core'
+import { dispose, useRenderer } from '@tresjs/core'
 import { useControls } from '@tresjs/leches'
 
 const { nodes } = await useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb', { draco: true })
@@ -8,17 +8,6 @@ const model = nodes.Cube
 
 model.position.set(0, 1, 0)
 
-useControls({
-  disposeBtn: {
-    label: 'Dispose',
-    type: 'button',
-    onClick: () => {
-      dispose(model)
-    },
-    size: 'sm',
-  },
-})
-
 onUnmounted(() => {
   dispose(model)
 })

+ 56 - 6
playground/vue/src/pages/advanced/manual/index.vue

@@ -1,22 +1,72 @@
 <script setup lang="ts">
 import { TresCanvas } from '@tresjs/core'
-import GraphPane from '../../../components/GraphPane.vue'
-
-import { useState } from '../../../composables/state'
+import { TresLeches, useControls } from '@tresjs/leches'
+import '@tresjs/leches/styles'
 import ManualExperience from './experience.vue'
+import { ACESFilmicToneMapping, AgXToneMapping, BasicShadowMap, CineonToneMapping, LinearToneMapping, NeutralToneMapping, NoToneMapping, PCFShadowMap, PCFSoftShadowMap, ReinhardToneMapping, VSMShadowMap } from 'three'
 
-const { renderingTimes } = useState()
+const renderingTimes = ref(0)
 
 function onRender() {
   renderingTimes.value = 1
 }
+
+useControls({
+  render: {
+    value: renderingTimes,
+    type: 'graph',
+    onUpdate: () => {
+      renderingTimes.value = 0
+    },
+  },
+})
+
+const { clearColor, alpha, antialias, toneMapping, shadows, shadowMapType } = useControls({
+  clearColor: '#82DBC5',
+  alpha: true,
+  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)
+})
+
+const formattedShadowMapType = computed(() => {
+  return Number(shadowMapType.value)
+})
 </script>
 
 <template>
-  <GraphPane />
+  <TresLeches />
   <TresCanvas
     render-mode="manual"
-    clear-color="#82DBC5"
+    :shadows="shadows"
+    :shadow-map-type="formattedShadowMapType"
+    :clear-color="clearColor"
+    :alpha="alpha"
+    :antialias="antialias"
+    :tone-mapping="formattedToneMapping"
     @render="onRender"
   >
     <ManualExperience />

+ 56 - 5
playground/vue/src/pages/advanced/on-demand/index.vue

@@ -1,21 +1,72 @@
 <script setup lang="ts">
 import { TresCanvas } from '@tresjs/core'
-import GraphPane from '../../../components/GraphPane.vue'
-import { useState } from '../../../composables/state'
 import OnDemandExperience from './experience.vue'
+import { TresLeches, useControls } from '@tresjs/leches'
+import '@tresjs/leches/styles'
+import { ACESFilmicToneMapping, AgXToneMapping, BasicShadowMap, CineonToneMapping, LinearToneMapping, NeutralToneMapping, NoToneMapping, PCFShadowMap, PCFSoftShadowMap, ReinhardToneMapping, VSMShadowMap } from 'three'
 
-const { renderingTimes } = useState()
+const renderingTimes = ref(0)
 
 function onRender() {
   renderingTimes.value = 1
 }
+
+useControls({
+  render: {
+    value: renderingTimes,
+    type: 'graph',
+    onUpdate: () => {
+      renderingTimes.value = 0
+    },
+  },
+})
+
+const { clearColor, alpha, antialias, toneMapping, shadows, shadowMapType } = useControls({
+  clearColor: '#82DBC5',
+  alpha: true,
+  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)
+})
+
+const formattedShadowMapType = computed(() => {
+  return Number(shadowMapType.value)
+})
 </script>
 
 <template>
-  <GraphPane />
+  <TresLeches />
   <TresCanvas
     render-mode="on-demand"
-    clear-color="#82DBC5"
+    :shadows="shadows"
+    :shadow-map-type="formattedShadowMapType"
+    :clear-color="clearColor"
+    :alpha="alpha"
+    :antialias="antialias"
+    :tone-mapping="formattedToneMapping"
     @render="onRender"
   >
     <OnDemandExperience />

+ 68 - 0
playground/vue/src/pages/advanced/webGPU/HologramCube.vue

@@ -0,0 +1,68 @@
+<script setup lang="ts">
+import { useGLTF } from '@tresjs/cientos'
+import { add, cameraProjectionMatrix, cameraViewMatrix, color, Fn, hash, mix, normalView, positionWorld, sin, timerGlobal, uniform, varying, vec3, vec4 } from 'three/tsl'
+import { AdditiveBlending, DoubleSide, MeshBasicNodeMaterial } from 'three/webgpu'
+
+const { nodes } = await useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb', { draco: true })
+const model = nodes.Cube
+
+/**
+ * Material
+ */
+const material = new MeshBasicNodeMaterial({
+  transparent: true,
+  side: DoubleSide,
+  depthWrite: false,
+  blending: AdditiveBlending,
+})
+
+// Position
+const glitchStrength = varying(0)
+
+material.vertexNode = Fn(() => {
+  const glitchTime = timerGlobal().sub(positionWorld.y.mul(0.5))
+
+  glitchStrength.assign(add(
+    sin(glitchTime),
+    sin(glitchTime.mul(3.45)),
+    sin(glitchTime.mul(8.76)),
+  ).div(3).smoothstep(0.3, 1))
+
+  const glitch = vec3(
+    hash(positionWorld.xz.abs().mul(9999)).sub(0.5),
+    0,
+    hash(positionWorld.yx.abs().mul(9999)).sub(0.5),
+  )
+
+  positionWorld.xyz.addAssign(glitch.mul(glitchStrength.mul(0.5)))
+
+  return cameraProjectionMatrix.mul(cameraViewMatrix).mul(positionWorld)
+})()
+
+// Color
+const colorInside = uniform(color('#ff6088'))
+const colorOutside = uniform(color('#4d55ff'))
+
+material.colorNode = Fn(() => {
+  const stripes = positionWorld.y.sub(timerGlobal(0.02)).mul(20).mod(1).pow(3)
+
+  const fresnel = normalView.dot(vec3(0, 0, 1)).abs().oneMinus()
+  const falloff = fresnel.smoothstep(0.8, 0.2)
+  const alpha = stripes.mul(fresnel).add(fresnel.mul(1.25)).mul(falloff)
+  const finalColor = mix(colorInside, colorOutside, fresnel.add(glitchStrength.mul(0.6)))
+
+  return vec4(finalColor, alpha)
+})()
+
+model.traverse((child) => {
+  if (child.isMesh) {
+    // const skinningMaterial = material.clone()
+    // skinningMaterial.positionNode = skinning(child)
+    child.material = material
+  }
+})
+</script>
+
+<template>
+  <primitive :object="model" />
+</template>

+ 39 - 0
playground/vue/src/pages/advanced/webGPU/index.vue

@@ -0,0 +1,39 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import { WebGPURenderer } from 'three/webgpu'
+import type { TresContext } from '@tresjs/core'
+import { OrbitControls } from '@tresjs/cientos'
+import HologramCube from './HologramCube.vue'
+
+const createWebGPURenderer = async (ctx: TresContext) => {
+  const renderer = new WebGPURenderer({
+    canvas: ctx.canvas.value,
+  })
+  // Initialize WebGPU context
+  await renderer.init()
+  renderer.setClearColor('#000000')
+
+  // Watch size changes
+  watch([ctx.sizes.width, ctx.sizes.height], () => {
+    renderer.setSize(ctx.sizes.width.value, ctx.sizes.height.value)
+  }, {
+    immediate: true,
+  })
+
+  return renderer
+}
+</script>
+
+<template>
+  <TresCanvas :renderer="createWebGPURenderer" clear-color="black">
+    <TresPerspectiveCamera
+      :position="[3, 3, 3]"
+      :look-at="[0, 0, 0]"
+    />
+    <Suspense>
+      <HologramCube />
+    </Suspense>
+    <OrbitControls />
+    <TresAmbientLight :intensity="1" />
+  </TresCanvas>
+</template>

+ 1 - 1
playground/vue/src/pages/basic/Responsiveness.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import TheBasic from '../basic/index.vue'
+import TheBasic from '../misc/BrownianDistribution.vue'
 </script>
 
 <template>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 34 - 98
playground/vue/src/pages/basic/index.vue


+ 5 - 0
playground/vue/src/router/routes/advanced.ts

@@ -54,4 +54,9 @@ export const advancedRoutes = [
     name: 'Memory Test: Tres Objects',
     component: () => import('../../pages/advanced/MemoryTresObjects.vue'),
   },
+  {
+    path: '/advanced/webgpu',
+    name: 'WebGPU',
+    component: () => import('../../pages/advanced/webGPU/index.vue'),
+  },
 ]

+ 103 - 100
pnpm-lock.yaml

@@ -66,6 +66,9 @@ importers:
       gsap:
         specifier: ^3.12.7
         version: 3.12.7
+      happy-dom:
+        specifier: ^17.4.4
+        version: 17.4.4
       jsdom:
         specifier: ^26.0.0
         version: 26.0.0
@@ -128,7 +131,7 @@ importers:
         version: 1.6.3(@algolia/client-search@5.20.3)(@types/node@22.13.5)(postcss@8.5.3)(search-insights@2.17.3)(typescript@5.7.3)
       vitest:
         specifier: 3.0.5
-        version: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(@vitest/ui@3.0.6)(jiti@2.4.2)(jsdom@26.0.0)(tsx@4.19.3)(yaml@2.7.0)
+        version: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(@vitest/ui@3.0.6)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@26.0.0)(tsx@4.19.3)(yaml@2.7.0)
       vue:
         specifier: 3.5.13
         version: 3.5.13(typescript@5.7.3)
@@ -164,14 +167,14 @@ importers:
   playground/vue:
     dependencies:
       '@tresjs/cientos':
-        specifier: 4.1.0
-        version: 4.1.0(@tresjs/core@)(@types/three@0.173.0)(three@0.173.0)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
+        specifier: https://pkg.pr.new/@tresjs/cientos@d84eb13
+        version: https://pkg.pr.new/@tresjs/cientos@d84eb13(@tresjs/core@)(@types/three@0.173.0)(three@0.173.0)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
       '@tresjs/core':
         specifier: workspace:^
         version: link:../..
       '@tresjs/leches':
-        specifier: https://pkg.pr.new/@tresjs/leches@9ad0cd3
-        version: https://pkg.pr.new/@tresjs/leches@9ad0cd3(magicast@0.3.5)(typescript@5.7.3)(unocss@65.5.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
+        specifier: https://pkg.pr.new/@tresjs/leches@b34e795
+        version: https://pkg.pr.new/@tresjs/leches@b34e795(magicast@0.3.5)(unocss@65.5.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
       vue-router:
         specifier: ^4.5.0
         version: 4.5.0(vue@3.5.13(typescript@5.7.3))
@@ -181,7 +184,7 @@ importers:
         version: 0.2.1(tweakpane@4.0.5)
       unplugin-auto-import:
         specifier: ^19.0.0
-        version: 19.1.0(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@12.7.0(typescript@5.7.3))
+        version: 19.1.0(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.7.3)))
       vite-plugin-glsl:
         specifier: ^1.3.1
         version: 1.3.1(rollup@4.34.8)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))
@@ -1663,6 +1666,14 @@ packages:
       three: '>=0.133'
       vue: '>=3.3'
 
+  '@tresjs/cientos@https://pkg.pr.new/@tresjs/cientos@d84eb13':
+    resolution: {tarball: https://pkg.pr.new/@tresjs/cientos@d84eb13}
+    version: 4.2.0
+    peerDependencies:
+      '@tresjs/core': '>=4.2.1'
+      three: '>=0.133'
+      vue: '>=3.3'
+
   '@tresjs/core@4.3.3':
     resolution: {integrity: sha512-AIFP0u5Hp/9LjifndcFEQWkucWYI72vpUAvJzeOArMdrGN/slKXf8XYP/GKm0BMbPQCu6/eg/LqghZO5tOQ81A==}
     peerDependencies:
@@ -1674,8 +1685,8 @@ packages:
     peerDependencies:
       eslint: 9.x
 
-  '@tresjs/leches@https://pkg.pr.new/@tresjs/leches@9ad0cd3':
-    resolution: {tarball: https://pkg.pr.new/@tresjs/leches@9ad0cd3}
+  '@tresjs/leches@https://pkg.pr.new/@tresjs/leches@b34e795':
+    resolution: {tarball: https://pkg.pr.new/@tresjs/leches@b34e795}
     version: 0.14.1
     peerDependencies:
       vue: '>=3.3.4'
@@ -1767,6 +1778,9 @@ packages:
   '@types/web-bluetooth@0.0.20':
     resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
 
+  '@types/web-bluetooth@0.0.21':
+    resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
+
   '@types/webxr@0.5.21':
     resolution: {integrity: sha512-geZIAtLzjGmgY2JUi6VxXdCrTb99A7yP49lxLr2Nm/uIK0PkkxcEi4OGhoGDO4pxCf3JwGz2GiJL2Ej4K2bKaA==}
 
@@ -1837,14 +1851,11 @@ packages:
     resolution: {integrity: sha512-XK9Y3Z1m3oPXQl5pVOYk6+pltsk70RHFvsAtTyFd5G5kAHzQS/em4/lL6/0IubU7rn2j+9eHeCVOiWXW9lnvYA==}
     engines: {node: '>=14'}
 
-  '@unocss/core@0.56.5':
-    resolution: {integrity: sha512-fx5VhOjSHn0HdV2D34pEwFMAHJcJQRTCp1xEE4GzxY1irXzaa+m2aYf5PZjmDxehiOC16IH7TO9FOWANXk1E0w==}
-
   '@unocss/core@65.5.0':
     resolution: {integrity: sha512-XYWdS09M2XOjZNDotGhI2TIW/6duLNiyssopwjCbv4AlPklF0bZI86SKI55syYDBt6GRadoQbuvUkhSiTV/hzQ==}
 
-  '@unocss/extractor-arbitrary-variants@0.56.5':
-    resolution: {integrity: sha512-p2pyzz/ONvc5CGcaB9OZvWE8qkRSgyuhaQqFQLdBFeUhveHC0CGP0iSnXwBgAFHWM7DJo4/JpWeZ+mBt0ogVLA==}
+  '@unocss/core@66.1.0-beta.7':
+    resolution: {integrity: sha512-l1/r+Jd9TbsRqR/geEdIV/Erzvs26GitTtMVsGcJfuaK1/WWOLtbSHRUDQAB/UpcOOWvuNuAv4UWsXX9Z0DFmw==}
 
   '@unocss/extractor-arbitrary-variants@65.5.0':
     resolution: {integrity: sha512-7K3gftOdkv9jbWvbkExTcx6/FDP2Xk/NSsOYTvR9oITLnLjmdQvp+9276WSnNfKF3frBl8ZcqpkC2EsuL2Yutw==}
@@ -1864,9 +1875,6 @@ packages:
   '@unocss/preset-icons@65.5.0':
     resolution: {integrity: sha512-lSwMNtj4nufpQDBFoioAM9S6hP8028lA9fLFM3Vw+KmI10/3TaZyOaCXJVH5UdsfNWexGGo/Qo+K1YFWfXLZ8A==}
 
-  '@unocss/preset-mini@0.56.5':
-    resolution: {integrity: sha512-/KhlThhs1ilauM7MwRSpahLbIPZ5VGeGvaUsU8+ZlNT3sis4yoVYkPtR14tL2IT6jhOU05N/uu3aBj+1bP8GjQ==}
-
   '@unocss/preset-mini@65.5.0':
     resolution: {integrity: sha512-oD2INmEgTOxmFsVceflv4Zs67vz9PRbpg3+CMsJLWgfX4UdQ1H4jZms72+g3N1hhXBvOFwvGvqGaMnrVMRk54g==}
 
@@ -1888,10 +1896,6 @@ packages:
   '@unocss/reset@65.5.0':
     resolution: {integrity: sha512-jADqiBAfOO9aZNpnsmxc7WX7vIIxyalcmCJ7fwdyPRmFhxZZ5ZoSYsHDt0Wfn/W2BRQkLjXWL0956nXH0lz79Q==}
 
-  '@unocss/rule-utils@0.56.5':
-    resolution: {integrity: sha512-CXIGHCIC9B8WUl9KbbFMSZHcsIgfmI/+X0bjBv6xrgBVC1EQ2Acq4PYnJIbaRGBRAhl9wYjNL7Zq2UWOdowHAw==}
-    engines: {node: '>=14'}
-
   '@unocss/rule-utils@65.5.0':
     resolution: {integrity: sha512-xT4N0EY1dl1mqY5gTKD0H/Fg6xApe7xbfNTUwctOu02DMeJhqv9BTqfoAihH/hzGSI69+FtzVtz7hUxTypfehA==}
     engines: {node: '>=14'}
@@ -2081,15 +2085,19 @@ packages:
   '@vue/test-utils@2.4.6':
     resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==}
 
-  '@vueuse/components@12.7.0':
-    resolution: {integrity: sha512-LbaKPOx9sTPRxI8ymJt3VCm2CifmC432yaXxCGbjkuKIh2jyNlXvE7sGrLm7kbC7WkBJnUXzm3K/cI1pIE8ueQ==}
-
-  '@vueuse/core@10.11.1':
-    resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
+  '@vueuse/components@13.0.0':
+    resolution: {integrity: sha512-rcGp3c5Yu4SVLGUhBXT0q227nduFx1HTKzJBQkPLpIhwG1SB8RZ5bbri9sbusGaFZB5CYc6jza5+gfSJ7YidIg==}
+    peerDependencies:
+      vue: ^3.5.0
 
   '@vueuse/core@12.7.0':
     resolution: {integrity: sha512-jtK5B7YjZXmkGNHjviyGO4s3ZtEhbzSgrbX+s5o+Lr8i2nYqNyHuPVOeTdM1/hZ5Tkxg/KktAuAVDDiHMraMVA==}
 
+  '@vueuse/core@13.0.0':
+    resolution: {integrity: sha512-rkgb4a8/0b234lMGCT29WkCjPfsX0oxrIRR7FDndRoW3FsaC9NBzefXg/9TLhAgwM11f49XnutshM4LzJBrQ5g==}
+    peerDependencies:
+      vue: ^3.5.0
+
   '@vueuse/integrations@12.7.0':
     resolution: {integrity: sha512-IEq7K4bCl7mn3uKJaWtNXnd1CAPaHLUMuyj5K1/k/pVcItt0VONZW8xiGxdIovJcQjkzOHjImhX5t6gija+0/g==}
     peerDependencies:
@@ -2131,23 +2139,25 @@ packages:
       universal-cookie:
         optional: true
 
-  '@vueuse/metadata@10.11.1':
-    resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
-
   '@vueuse/metadata@12.7.0':
     resolution: {integrity: sha512-4VvTH9mrjXqFN5LYa5YfqHVRI6j7R00Vy4995Rw7PQxyCL3z0Lli86iN4UemWqixxEvYfRjG+hF9wL8oLOn+3g==}
 
-  '@vueuse/motion@2.2.6':
-    resolution: {integrity: sha512-gKFktPtrdypSv44SaW1oBJKLBiP6kE5NcoQ6RsAU3InemESdiAutgQncfPe/rhLSLCtL4jTAhMmFfxoR6gm5LQ==}
+  '@vueuse/metadata@13.0.0':
+    resolution: {integrity: sha512-TRNksqmvtvqsuHf7bbgH9OSXEV2b6+M3BSN4LR5oxWKykOFT9gV78+C2/0++Pq9KCp9KQ1OQDPvGlWNQpOb2Mw==}
+
+  '@vueuse/motion@3.0.3':
+    resolution: {integrity: sha512-4B+ITsxCI9cojikvrpaJcLXyq0spj3sdlzXjzesWdMRd99hhtFI6OJ/1JsqwtF73YooLe0hUn/xDR6qCtmn5GQ==}
     peerDependencies:
       vue: '>=3.0.0'
 
-  '@vueuse/shared@10.11.1':
-    resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
-
   '@vueuse/shared@12.7.0':
     resolution: {integrity: sha512-coLlUw2HHKsm7rPN6WqHJQr18WymN4wkA/3ThFaJ4v4gWGWAQQGK+MJxLuJTBs4mojQiazlVWAKNJNpUWGRkNw==}
 
+  '@vueuse/shared@13.0.0':
+    resolution: {integrity: sha512-9MiHhAPw+sqCF/RLo8V6HsjRqEdNEWVpDLm2WBRW2G/kSQjb8X901sozXpSCaeLG0f7TEfMrT4XNaA5m1ez7Dg==}
+    peerDependencies:
+      vue: ^3.5.0
+
   '@webgpu/types@0.1.54':
     resolution: {integrity: sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg==}
 
@@ -3361,6 +3371,10 @@ packages:
     engines: {node: '>=0.4.7'}
     hasBin: true
 
+  happy-dom@17.4.4:
+    resolution: {integrity: sha512-/Pb0ctk3HTZ5xEL3BZ0hK1AqDSAUuRQitOmROPHhfUYEWpmTImwfD8vFDGADmMAX0JYgbcgxWoLFKtsWhcpuVA==}
+    engines: {node: '>=18.0.0'}
+
   has-flag@4.0.0:
     resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
     engines: {node: '>=8'}
@@ -5078,10 +5092,10 @@ packages:
     resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
     engines: {node: '>= 10.0.0'}
 
-  unocss-preset-scrollbar@0.3.1:
-    resolution: {integrity: sha512-LhvcQA1cfwq06sqAZY++1crrLsOf/IfOPdyCkMHVyywI9WCvMhxCJlCcrySlQI8/Y2VUjOpLBDWB0w3DXS5qRA==}
+  unocss-preset-scrollbar@3.2.0:
+    resolution: {integrity: sha512-j8BOoh2RgPm2U8XqEjMQ+XQk4YWYPH4T+yzv3fndxS+VpdizQinMvHmfsZGLN3yMv7I4O5Qi8fVTlQDhETyzbA==}
     peerDependencies:
-      unocss: '>= 0.31.13 < 1'
+      unocss: '>= 0.31.13'
 
   unocss@65.5.0:
     resolution: {integrity: sha512-dLTW89YK+5KCcB3vG/wxiwdpejkLLmZlK9hjWmP52sdeUFcmywc+/khD2/nid7or8dL3YCv1gwoyvnA7JRCwjA==}
@@ -5414,6 +5428,10 @@ packages:
     resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
     engines: {node: '>=18'}
 
+  whatwg-mimetype@3.0.0:
+    resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
+    engines: {node: '>=12'}
+
   whatwg-mimetype@4.0.0:
     resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
     engines: {node: '>=18'}
@@ -6911,9 +6929,9 @@ snapshots:
 
   '@tootallnate/quickjs-emscripten@0.23.0': {}
 
-  '@tresjs/cientos@4.1.0(@tresjs/core@)(@types/three@0.173.0)(three@0.173.0)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))':
+  '@tresjs/cientos@4.1.0(@tresjs/core@4.3.3(three@0.173.0)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))(@types/three@0.173.0)(three@0.173.0)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))':
     dependencies:
-      '@tresjs/core': 'link:'
+      '@tresjs/core': 4.3.3(three@0.173.0)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
       '@vueuse/core': 12.7.0(typescript@5.7.3)
       camera-controls: 2.10.0(three@0.173.0)
       stats-gl: 2.4.2(@types/three@0.173.0)(three@0.173.0)
@@ -6928,9 +6946,9 @@ snapshots:
       - react
       - typescript
 
-  '@tresjs/cientos@4.1.0(@tresjs/core@4.3.3(three@0.173.0)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))(@types/three@0.173.0)(three@0.173.0)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))':
+  '@tresjs/cientos@https://pkg.pr.new/@tresjs/cientos@d84eb13(@tresjs/core@)(@types/three@0.173.0)(three@0.173.0)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))':
     dependencies:
-      '@tresjs/core': 4.3.3(three@0.173.0)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
+      '@tresjs/core': 'link:'
       '@vueuse/core': 12.7.0(typescript@5.7.3)
       camera-controls: 2.10.0(three@0.173.0)
       stats-gl: 2.4.2(@types/three@0.173.0)(three@0.173.0)
@@ -6981,18 +6999,16 @@ snapshots:
       - typescript
       - vitest
 
-  '@tresjs/leches@https://pkg.pr.new/@tresjs/leches@9ad0cd3(magicast@0.3.5)(typescript@5.7.3)(unocss@65.5.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
+  '@tresjs/leches@https://pkg.pr.new/@tresjs/leches@b34e795(magicast@0.3.5)(unocss@65.5.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
     dependencies:
-      '@unocss/core': 65.5.0
-      '@vueuse/components': 12.7.0(typescript@5.7.3)
-      '@vueuse/motion': 2.2.6(magicast@0.3.5)(vue@3.5.13(typescript@5.7.3))
-      unocss-preset-scrollbar: 0.3.1(unocss@65.5.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)))
+      '@unocss/core': 66.1.0-beta.7
+      '@vueuse/components': 13.0.0(vue@3.5.13(typescript@5.7.3))
+      '@vueuse/motion': 3.0.3(magicast@0.3.5)(vue@3.5.13(typescript@5.7.3))
+      unocss-preset-scrollbar: 3.2.0(unocss@65.5.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)))
       vue: 3.5.13(typescript@5.7.3)
     transitivePeerDependencies:
-      - '@vue/composition-api'
       - magicast
       - supports-color
-      - typescript
       - unocss
 
   '@trysound/sax@0.2.0': {}
@@ -7076,6 +7092,8 @@ snapshots:
 
   '@types/web-bluetooth@0.0.20': {}
 
+  '@types/web-bluetooth@0.0.21': {}
+
   '@types/webxr@0.5.21': {}
 
   '@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)':
@@ -7188,13 +7206,9 @@ snapshots:
       '@unocss/core': 65.5.0
       unconfig: 7.0.0
 
-  '@unocss/core@0.56.5': {}
-
   '@unocss/core@65.5.0': {}
 
-  '@unocss/extractor-arbitrary-variants@0.56.5':
-    dependencies:
-      '@unocss/core': 0.56.5
+  '@unocss/core@66.1.0-beta.7': {}
 
   '@unocss/extractor-arbitrary-variants@65.5.0':
     dependencies:
@@ -7232,12 +7246,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@unocss/preset-mini@0.56.5':
-    dependencies:
-      '@unocss/core': 0.56.5
-      '@unocss/extractor-arbitrary-variants': 0.56.5
-      '@unocss/rule-utils': 0.56.5
-
   '@unocss/preset-mini@65.5.0':
     dependencies:
       '@unocss/core': 65.5.0
@@ -7274,10 +7282,6 @@ snapshots:
 
   '@unocss/reset@65.5.0': {}
 
-  '@unocss/rule-utils@0.56.5':
-    dependencies:
-      '@unocss/core': 0.56.5
-
   '@unocss/rule-utils@65.5.0':
     dependencies:
       '@unocss/core': 65.5.0
@@ -7332,7 +7336,7 @@ snapshots:
       magic-string: 0.30.17
       picocolors: 1.1.1
       std-env: 3.8.0
-      vitest: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(@vitest/ui@3.0.6)(jiti@2.4.2)(jsdom@26.0.0)(tsx@4.19.3)(yaml@2.7.0)
+      vitest: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(@vitest/ui@3.0.6)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@26.0.0)(tsx@4.19.3)(yaml@2.7.0)
 
   '@vitest/coverage-v8@3.0.6(vitest@3.0.5)':
     dependencies:
@@ -7348,7 +7352,7 @@ snapshots:
       std-env: 3.8.0
       test-exclude: 7.0.1
       tinyrainbow: 2.0.0
-      vitest: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(@vitest/ui@3.0.6)(jiti@2.4.2)(jsdom@26.0.0)(tsx@4.19.3)(yaml@2.7.0)
+      vitest: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(@vitest/ui@3.0.6)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@26.0.0)(tsx@4.19.3)(yaml@2.7.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -7358,7 +7362,7 @@ snapshots:
       eslint: 9.21.0(jiti@2.4.2)
     optionalDependencies:
       typescript: 5.7.3
-      vitest: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(@vitest/ui@3.0.6)(jiti@2.4.2)(jsdom@26.0.0)(tsx@4.19.3)(yaml@2.7.0)
+      vitest: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(@vitest/ui@3.0.6)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@26.0.0)(tsx@4.19.3)(yaml@2.7.0)
 
   '@vitest/expect@3.0.5':
     dependencies:
@@ -7407,7 +7411,7 @@ snapshots:
       sirv: 3.0.1
       tinyglobby: 0.2.12
       tinyrainbow: 2.0.0
-      vitest: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(@vitest/ui@3.0.6)(jiti@2.4.2)(jsdom@26.0.0)(tsx@4.19.3)(yaml@2.7.0)
+      vitest: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(@vitest/ui@3.0.6)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@26.0.0)(tsx@4.19.3)(yaml@2.7.0)
 
   '@vitest/utils@3.0.5':
     dependencies:
@@ -7585,23 +7589,11 @@ snapshots:
       js-beautify: 1.15.3
       vue-component-type-helpers: 2.2.2
 
-  '@vueuse/components@12.7.0(typescript@5.7.3)':
+  '@vueuse/components@13.0.0(vue@3.5.13(typescript@5.7.3))':
     dependencies:
-      '@vueuse/core': 12.7.0(typescript@5.7.3)
-      '@vueuse/shared': 12.7.0(typescript@5.7.3)
+      '@vueuse/core': 13.0.0(vue@3.5.13(typescript@5.7.3))
+      '@vueuse/shared': 13.0.0(vue@3.5.13(typescript@5.7.3))
       vue: 3.5.13(typescript@5.7.3)
-    transitivePeerDependencies:
-      - typescript
-
-  '@vueuse/core@10.11.1(vue@3.5.13(typescript@5.7.3))':
-    dependencies:
-      '@types/web-bluetooth': 0.0.20
-      '@vueuse/metadata': 10.11.1
-      '@vueuse/shared': 10.11.1(vue@3.5.13(typescript@5.7.3))
-      vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.3))
-    transitivePeerDependencies:
-      - '@vue/composition-api'
-      - vue
 
   '@vueuse/core@12.7.0(typescript@5.7.3)':
     dependencies:
@@ -7612,6 +7604,13 @@ snapshots:
     transitivePeerDependencies:
       - typescript
 
+  '@vueuse/core@13.0.0(vue@3.5.13(typescript@5.7.3))':
+    dependencies:
+      '@types/web-bluetooth': 0.0.21
+      '@vueuse/metadata': 13.0.0
+      '@vueuse/shared': 13.0.0(vue@3.5.13(typescript@5.7.3))
+      vue: 3.5.13(typescript@5.7.3)
+
   '@vueuse/integrations@12.7.0(focus-trap@7.6.4)(typescript@5.7.3)':
     dependencies:
       '@vueuse/core': 12.7.0(typescript@5.7.3)
@@ -7622,15 +7621,15 @@ snapshots:
     transitivePeerDependencies:
       - typescript
 
-  '@vueuse/metadata@10.11.1': {}
-
   '@vueuse/metadata@12.7.0': {}
 
-  '@vueuse/motion@2.2.6(magicast@0.3.5)(vue@3.5.13(typescript@5.7.3))':
+  '@vueuse/metadata@13.0.0': {}
+
+  '@vueuse/motion@3.0.3(magicast@0.3.5)(vue@3.5.13(typescript@5.7.3))':
     dependencies:
-      '@vueuse/core': 10.11.1(vue@3.5.13(typescript@5.7.3))
-      '@vueuse/shared': 10.11.1(vue@3.5.13(typescript@5.7.3))
-      csstype: 3.1.3
+      '@vueuse/core': 13.0.0(vue@3.5.13(typescript@5.7.3))
+      '@vueuse/shared': 13.0.0(vue@3.5.13(typescript@5.7.3))
+      defu: 6.1.4
       framesync: 6.1.2
       popmotion: 11.0.5
       style-value-types: 5.1.2
@@ -7638,23 +7637,19 @@ snapshots:
     optionalDependencies:
       '@nuxt/kit': 3.15.4(magicast@0.3.5)
     transitivePeerDependencies:
-      - '@vue/composition-api'
       - magicast
       - supports-color
 
-  '@vueuse/shared@10.11.1(vue@3.5.13(typescript@5.7.3))':
-    dependencies:
-      vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.3))
-    transitivePeerDependencies:
-      - '@vue/composition-api'
-      - vue
-
   '@vueuse/shared@12.7.0(typescript@5.7.3)':
     dependencies:
       vue: 3.5.13(typescript@5.7.3)
     transitivePeerDependencies:
       - typescript
 
+  '@vueuse/shared@13.0.0(vue@3.5.13(typescript@5.7.3))':
+    dependencies:
+      vue: 3.5.13(typescript@5.7.3)
+
   '@webgpu/types@0.1.54': {}
 
   abbrev@3.0.0: {}
@@ -9072,6 +9067,11 @@ snapshots:
     optionalDependencies:
       uglify-js: 3.19.3
 
+  happy-dom@17.4.4:
+    dependencies:
+      webidl-conversions: 7.0.0
+      whatwg-mimetype: 3.0.0
+
   has-flag@4.0.0: {}
 
   has-symbols@1.1.0: {}
@@ -10982,9 +10982,9 @@ snapshots:
 
   universalify@2.0.1: {}
 
-  unocss-preset-scrollbar@0.3.1(unocss@65.5.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))):
+  unocss-preset-scrollbar@3.2.0(unocss@65.5.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))):
     dependencies:
-      '@unocss/preset-mini': 0.56.5
+      '@unocss/preset-mini': 65.5.0
       unocss: 65.5.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))
 
   unocss@65.5.0(postcss@8.5.3)(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3)):
@@ -11013,7 +11013,7 @@ snapshots:
       - supports-color
       - vue
 
-  unplugin-auto-import@19.1.0(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@12.7.0(typescript@5.7.3)):
+  unplugin-auto-import@19.1.0(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.7.3))):
     dependencies:
       local-pkg: 1.0.0
       magic-string: 0.30.17
@@ -11023,7 +11023,7 @@ snapshots:
       unplugin-utils: 0.2.4
     optionalDependencies:
       '@nuxt/kit': 3.15.4(magicast@0.3.5)
-      '@vueuse/core': 12.7.0(typescript@5.7.3)
+      '@vueuse/core': 13.0.0(vue@3.5.13(typescript@5.7.3))
 
   unplugin-utils@0.2.4:
     dependencies:
@@ -11326,7 +11326,7 @@ snapshots:
       - typescript
       - universal-cookie
 
-  vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(@vitest/ui@3.0.6)(jiti@2.4.2)(jsdom@26.0.0)(tsx@4.19.3)(yaml@2.7.0):
+  vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(@vitest/ui@3.0.6)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@26.0.0)(tsx@4.19.3)(yaml@2.7.0):
     dependencies:
       '@vitest/expect': 3.0.5
       '@vitest/mocker': 3.0.5(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))
@@ -11352,6 +11352,7 @@ snapshots:
       '@types/debug': 4.1.12
       '@types/node': 22.13.5
       '@vitest/ui': 3.0.6(vitest@3.0.5)
+      happy-dom: 17.4.4
       jsdom: 26.0.0
     transitivePeerDependencies:
       - jiti
@@ -11425,6 +11426,8 @@ snapshots:
     dependencies:
       iconv-lite: 0.6.3
 
+  whatwg-mimetype@3.0.0: {}
+
   whatwg-mimetype@4.0.0: {}
 
   whatwg-url@14.1.1:

+ 180 - 39
src/components/TresCanvas.vue

@@ -1,15 +1,12 @@
 <script setup lang="ts">
-import type {
-  ColorSpace,
-  ShadowMapType,
-  ToneMapping,
-  WebGLRendererParameters,
-} from 'three'
-import type { App, Ref } from 'vue'
-import type { RendererPresetsType } from '../composables/useRenderer/const'
-import type { TresCamera, TresObject, TresScene } from '../types/'
-import { PerspectiveCamera, Scene } from 'three'
+import type { App, MaybeRefOrGetter, Ref } from 'vue'
+import type { TresObject, TresScene } from '../types/'
+import { ACESFilmicToneMapping, PCFSoftShadowMap, PerspectiveCamera, Scene } from 'three'
 import * as THREE from 'three'
+import type { TresContext } from '../composables'
+import { useTresContextProvider } from '../composables'
+import type { TransformToMaybeRefOrGetter, TresCamera, TresRenderer } from '../types'
+import type { ColorSpace, ShadowMapType, ToneMapping, WebGLRendererParameters } from 'three'
 
 import {
   createRenderer,
@@ -26,44 +23,18 @@ import {
   watchEffect,
 } from 'vue'
 import pkg from '../../package.json'
-import {
-  type TresContext,
-  useTresContextProvider,
-} from '../composables'
 import { extend } from '../core/catalogue'
 import { nodeOps } from '../core/nodeOps'
 
 import { registerTresDevtools } from '../devtools'
 import { disposeObject3D } from '../utils/'
 
-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,
@@ -71,6 +42,8 @@ const props = withDefaults(defineProps<TresCanvasProps>(), {
   failIfMajorPerformanceCaveat: undefined,
   renderMode: 'always',
   enableProvideBridge: true,
+  toneMapping: ACESFilmicToneMapping,
+  shadowMapType: PCFSoftShadowMap,
 })
 
 // Define emits for Pointer events, pass `emit` into useTresEventManager so we can emit events off of TresCanvas
@@ -181,10 +154,10 @@ const unmountCanvas = () => {
   mountCustomRenderer(context.value as TresContext, true)
 }
 
-onMounted(() => {
+onMounted(async () => {
   const existingCanvas = canvas as Ref<HTMLCanvasElement>
 
-  context.value = useTresContextProvider({
+  context.value = await useTresContextProvider({
     scene: scene.value as TresScene,
     canvas: existingCanvas,
     windowSize: props.windowSize ?? false,
@@ -241,6 +214,174 @@ onMounted(() => {
 onUnmounted(unmountCanvas)
 </script>
 
+<script lang="ts">
+export type WebGLRendererProps = TransformToMaybeRefOrGetter<Omit<WebGLRendererParameters, 'canvas'>>
+
+export interface TresCanvasProps extends /* @vue-ignore */ WebGLRendererProps {
+  /**
+   * 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
+   */
+  /**
+   * Enables antialiasing, smoothing out edges of 3D objects.
+   * Uses MSAA (Multisample Anti-Aliasing) when available.
+   * @readonly
+   * @default true (Opinionated default by TresJS)
+   */
+  antialias?: MaybeRefOrGetter<boolean>
+
+  /**
+   * Enables stencil buffer with 8 bits.
+   * Required for stencil-based operations like shadow volumes or post-processing effects.
+   * @readonly
+   * @default true
+   */
+  stencil?: MaybeRefOrGetter<boolean>
+
+  /**
+   * Enables depth buffer with at least 16 bits.
+   * Required for proper 3D rendering and depth testing.
+   * @readonly
+   * @default true
+   */
+  depth?: MaybeRefOrGetter<boolean>
+
+  /**
+   * 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
+   */
+  logarithmicDepthBuffer?: MaybeRefOrGetter<boolean>
+
+  /**
+   * 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
+   */
+  preserveDrawingBuffer?: MaybeRefOrGetter<boolean>
+
+  /**
+   * 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'
+   */
+  powerPreference?: MaybeRefOrGetter<WebGLPowerPreference>
+
+  /**
+   * WebGL options with set methods
+   * @see {@link https://threejs.org/docs/#api/en/renderers/WebGLRenderer}
+   */
+
+  /**
+   * Clear color for the canvas
+   *
+   */
+  clearColor?: MaybeRefOrGetter<string>
+
+  /**
+   * Whether to enable alpha blending
+   * @default false
+   */
+  alpha?: MaybeRefOrGetter<boolean>
+  /**
+   * Enable shadow rendering in the scene
+   * @default false
+   */
+  shadows?: MaybeRefOrGetter<boolean>
+
+  /**
+   * 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)
+   */
+  toneMapping?: MaybeRefOrGetter<ToneMapping | number>
+
+  /**
+   * 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?: MaybeRefOrGetter<ShadowMapType | number>
+
+  /**
+   * 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?: MaybeRefOrGetter<ColorSpace>
+
+  /**
+   * Exposure level of tone mapping
+   * @default 1
+   */
+  toneMappingExposure?: MaybeRefOrGetter<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?: MaybeRefOrGetter<'always' | 'on-demand' | 'manual'>
+
+  /**
+   * Device Pixel Ratio for the renderer
+   * Can be a single number or a tuple defining a range [min, max]
+   */
+  dpr?: MaybeRefOrGetter<number | [number, number]>
+
+  /**
+   * Custom WebGL renderer instance
+   * Allows using a pre-configured renderer instead of creating a new one
+   */
+  renderer?: (ctx: TresContext) => Promise<TresRenderer>
+
+  /**
+   * 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?: MaybeRefOrGetter<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?: MaybeRefOrGetter<boolean>
+}
+</script>
+
 <template>
   <canvas
     ref="canvas"

+ 2 - 0
src/components/index.ts

@@ -1,3 +1,5 @@
 import TresCanvas from './TresCanvas.vue'
+import type { TresCanvasProps } from './TresCanvas.vue'
 
 export { TresCanvas }
+export type { TresCanvasProps }

+ 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

+ 3 - 258
src/composables/useRenderer/index.ts

@@ -1,260 +1,5 @@
-import type { ColorSpace, Scene, ShadowMapType, ToneMapping, WebGLRendererParameters } from 'three'
-import type { EmitEventFn, TresColor } from '../../types'
+import { useTresContext } from '../../composables'
 
-import type { TresContext } from '../useTresContextProvider'
-
-import type { RendererPresetsType } from './const'
-import {
-  type MaybeRefOrGetter,
-  toValue,
-  unrefElement,
-  useDevicePixelRatio,
-} from '@vueuse/core'
-import { ACESFilmicToneMapping, Color, WebGLRenderer } from 'three'
-import { computed, type MaybeRef, onUnmounted, shallowRef, 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 { 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]>;
-}
-
-export interface UseRendererOptions extends TransformToMaybeRefOrGetter<WebGLRendererParameters> {
-  /**
-   * Enable shadows in the Renderer
-   *
-   * @default false
-   */
-  shadows?: MaybeRefOrGetter<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
-   */
-  shadowMapType?: MaybeRefOrGetter<ShadowMapType>
-
-  /**
-   * Whether to use physically correct lighting mode.
-   * See the [lights / physical example](https://threejs.org/examples/#webgl_lights_physical).
-   *
-   * @default false
-   * @deprecated Use {@link WebGLRenderer.useLegacyLights useLegacyLights} instead.
-   */
-  physicallyCorrectLights?: MaybeRefOrGetter<boolean>
-  /**
-   * Whether to use legacy lighting mode.
-   *
-   * @type {MaybeRefOrGetter<boolean>}
-   * @memberof UseRendererOptions
-   */
-  useLegacyLights?: MaybeRefOrGetter<boolean>
-  /**
-   * Defines the output encoding of the renderer.
-   * Can be LinearSRGBColorSpace, SRGBColorSpace
-   *
-   * @default LinearSRGBColorSpace
-   */
-  outputColorSpace?: MaybeRefOrGetter<ColorSpace>
-
-  /**
-   * Defines the tone mapping used by the renderer.
-   * Can be NoToneMapping, LinearToneMapping,
-   * ReinhardToneMapping, Uncharted2ToneMapping,
-   * CineonToneMapping, ACESFilmicToneMapping,
-   * CustomToneMapping
-   *
-   * @default ACESFilmicToneMapping
-   */
-  toneMapping?: MaybeRefOrGetter<ToneMapping>
-
-  /**
-   * Defines the tone mapping exposure used by the renderer.
-   *
-   * @default 1
-   */
-  toneMappingExposure?: MaybeRefOrGetter<number>
-
-  /**
-   * The color value to use when clearing the canvas.
-   *
-   * @default 0x000000
-   */
-  clearColor?: MaybeRefOrGetter<TresColor>
-  windowSize?: MaybeRefOrGetter<boolean | string>
-  preset?: MaybeRefOrGetter<RendererPresetsType>
-  renderMode?: MaybeRefOrGetter<'always' | 'on-demand' | 'manual'>
-  /**
-   * A `number` sets the renderer's device pixel ratio.
-   * `[number, number]` clamp's the renderer's device pixel ratio.
-   */
-  dpr?: MaybeRefOrGetter<number | [number, number]>
-}
-
-export function useRenderer(
-  {
-    canvas,
-    options,
-    contextParts: { sizes, render, invalidate, advance },
-  }:
-  {
-    canvas: MaybeRef<HTMLCanvasElement>
-    scene: Scene
-    options: UseRendererOptions
-    emit: EmitEventFn
-    contextParts: Pick<TresContext, 'sizes' | 'camera' | 'render'> & { invalidate: () => void, advance: () => void }
-  },
-) {
-  const webGLRendererConstructorParameters = computed<WebGLRendererParameters>(() => ({
-    alpha: toValue(options.alpha) ?? true,
-    depth: toValue(options.depth),
-    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 renderer = shallowRef<WebGLRenderer>(new WebGLRenderer(webGLRendererConstructorParameters.value))
-
-  function invalidateOnDemand() {
-    if (options.renderMode === 'on-demand') {
-      invalidate()
-    }
-  }
-  // 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)
-
-    invalidateOnDemand()
-  })
-
-  watch([sizes.width, sizes.height], () => {
-    renderer.value.setSize(sizes.width.value, sizes.height.value)
-    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()
-
-  const renderMode = toValue(options.renderMode)
-
-  if (renderMode === 'on-demand') {
-    // Invalidate for the first time
-    invalidate()
-  }
-
-  if (renderMode === 'manual') {
-    // Advance for the first time, setTimeout to make sure there is something to render
-    setTimeout(() => {
-      advance()
-    }, 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(renderer.value, rendererPresets[rendererPreset])
-    }
-
-    setPixelRatio(renderer.value, pixelRatio.value, toValue(options.dpr))
-
-    // Render mode
-
-    if (renderMode === 'always') {
-      // If the render mode is 'always', ensure there's always a frame pending
-      render.frames.value = Math.max(1, render.frames.value)
-    }
-
-    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 (value !== undefined) { return value }
-
-      const valueInPreset = getValueFromPreset() as T
-
-      if (valueInPreset !== undefined) { return valueInPreset }
-
-      return get(threeDefaults, pathInThree)
-    }
-
-    const setValueOrDefault = <T>(option: MaybeRefOrGetter<T>, pathInThree: string) =>
-      set(renderer.value, pathInThree, getValue(option, pathInThree))
-
-    setValueOrDefault(options.shadows, 'shadowMap.enabled')
-    setValueOrDefault(options.toneMapping ?? ACESFilmicToneMapping, 'toneMapping')
-    setValueOrDefault(options.shadowMapType, 'shadowMap.type')
-
-    if (revision < 150) { setValueOrDefault(!options.useLegacyLights, 'physicallyCorrectLights') }
-
-    setValueOrDefault(options.outputColorSpace, 'outputColorSpace')
-    setValueOrDefault(options.toneMappingExposure, 'toneMappingExposure')
-
-    const clearColor = getValue(options.clearColor, 'clearColor')
-
-    if (clearColor) {
-      renderer.value.setClearColor(
-        clearColor
-          ? normalizeColor(clearColor)
-          : new Color(0x000000), // default clear color is not easily/efficiently retrievable from three
-      )
-    }
-  })
-
-  onUnmounted(() => {
-    renderer.value.dispose()
-    renderer.value.forceContextLoss()
-  })
-
-  return {
-    renderer,
-  }
+export function useRenderer() {
+  return useTresContext().renderer
 }
-
-export type UseRendererReturn = ReturnType<typeof useRenderer>

+ 26 - 24
src/composables/useTresContextProvider/index.ts

@@ -1,20 +1,21 @@
-import type { Camera, WebGLRenderer } from 'three'
-import type { ComputedRef, DeepReadonly, MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
+import type { Camera } from 'three'
+import { Raycaster, WebGLRenderer } from 'three'
+import type { ComputedRef, DeepReadonly, 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 { EmitEventFn, Renderer, TresControl, TresObject, TresScene } from '../../types'
 import { useFps, useMemory, useRafFn } from '@vueuse/core'
-import { Raycaster } from 'three'
 import { computed, inject, onUnmounted, provide, readonly, ref, shallowRef } from 'vue'
 import { extend } from '../../core/catalogue'
 import { createRenderLoop } from '../../core/loop'
 import { calculateMemoryUsage } from '../../utils/perf'
 
 import { useCamera } from '../useCamera'
-import { useRenderer } from '../useRenderer'
 import useSizes, { type SizesType } from '../useSizes'
 import { type TresEventManager, useTresEventManager } from '../useTresEventManager'
 import { useTresReady } from '../useTresReady'
+import { createRenderer } from '../../core/createRenderer'
+import { setupWebGLRenderer } from '../../core/setupRenderer'
+import type { TresCanvasProps } from '../../components/TresCanvas.vue'
 
 export interface InternalState {
   priority: Ref<number>
@@ -55,7 +56,8 @@ export interface TresContext {
   camera: ComputedRef<Camera | undefined>
   cameras: DeepReadonly<Ref<Camera[]>>
   controls: Ref<TresControl | null>
-  renderer: ShallowRef<WebGLRenderer>
+  renderer: ShallowRef<Renderer>
+  canvas: Ref<HTMLCanvasElement>
   raycaster: ShallowRef<Raycaster>
   perf: PerformanceState
   render: RenderState
@@ -83,7 +85,7 @@ export interface TresContext {
   deregisterBlockingObjectAtPointerEventHandler?: (object: TresObject) => void
 }
 
-export function useTresContextProvider({
+export async function useTresContextProvider({
   scene,
   canvas,
   windowSize,
@@ -91,12 +93,12 @@ export function useTresContextProvider({
   emit,
 }: {
   scene: TresScene
-  canvas: MaybeRef<HTMLCanvasElement>
+  canvas: Ref<HTMLCanvasElement>
   windowSize: MaybeRefOrGetter<boolean>
-  rendererOptions: UseRendererOptions
+  rendererOptions: TresCanvasProps
   emit: EmitEventFn
 
-}): TresContext {
+}): Promise<TresContext> {
   const localScene = shallowRef<TresScene>(scene)
   const sizes = useSizes(windowSize, canvas)
 
@@ -131,24 +133,14 @@ export function useTresContextProvider({
     }
   }
 
-  const { renderer } = useRenderer(
-    {
-      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 },
-    },
-  )
-
   const ctx: TresContext = {
     sizes,
     scene: localScene,
     camera,
     cameras: readonly(cameras),
-    renderer,
+    renderer: shallowRef(null as unknown as Renderer),
     raycaster: shallowRef(new Raycaster()),
+    canvas,
     controls: ref(null),
     perf: {
       maxFrames: 160,
@@ -174,6 +166,14 @@ export function useTresContextProvider({
 
   provide('useTres', ctx)
 
+  // Renderer
+  const renderer = await createRenderer(ctx, rendererOptions)
+  // Only setup the renderer with Canvas props if it is a WebGLRenderer
+  if (renderer instanceof WebGLRenderer) {
+    setupWebGLRenderer(renderer, rendererOptions, ctx)
+  }
+  ctx.renderer.value = renderer
+
   // Add context to scene local state
   ctx.scene.value.__tres = {
     root: ctx,
@@ -183,7 +183,7 @@ export function useTresContextProvider({
 
   ctx.loop.register(() => {
     if (camera.value && render.frames.value > 0) {
-      renderer.value.render(scene, camera.value)
+      ctx.renderer.value.render(scene, camera.value)
       emit('render', ctx.renderer.value)
     }
 
@@ -210,6 +210,8 @@ export function useTresContextProvider({
   })
 
   onUnmounted(() => {
+    ctx.renderer.value.dispose()
+    ctx.renderer.value.forceContextLoss()
     cancelTresReady()
     ctx.loop.stop()
   })

+ 74 - 0
src/core/createRenderer.test.ts

@@ -0,0 +1,74 @@
+// @vitest-environment happy-dom
+
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { createRenderer } from './createRenderer'
+import type { TresContext } from '../composables'
+import { WebGLRenderer } from 'three'
+import type { TresRenderer } from '../types'
+
+// Mock both renderers
+vi.mock('three', () => ({
+  WebGLRenderer: vi.fn(),
+}))
+
+vi.mock('three/webgpu', () => ({
+  WebGPURenderer: vi.fn().mockImplementation(() => ({
+    init: vi.fn().mockResolvedValue(undefined),
+  })),
+}))
+
+describe('createRenderer', () => {
+  const mockContext: TresContext = {
+    canvas: {
+      value: document.createElement('canvas'),
+    },
+  } as TresContext
+
+  beforeEach(() => {
+    vi.clearAllMocks()
+  })
+
+  describe('options.renderer handling', () => {
+    it('should create a WebGLRenderer with default options when no renderer provided', async () => {
+      const renderer = await createRenderer(mockContext, {})
+      expect(renderer).toBeDefined()
+      expect(WebGLRenderer).toHaveBeenCalled()
+    })
+
+    it('should create a WebGLRenderer with custom options', async () => {
+      await createRenderer(mockContext, {
+        antialias: true,
+        depth: true,
+      })
+      expect(WebGLRenderer).toHaveBeenCalledWith({
+        antialias: true,
+        depth: true,
+        canvas: mockContext.canvas.value,
+      })
+    })
+
+    it('should call and return result if renderer is a function', async () => {
+      const mockWebGPURenderer = {
+        init: vi.fn().mockResolvedValue(undefined),
+      } as unknown as TresRenderer
+      const createWebGPURenderer = vi.fn().mockResolvedValue(mockWebGPURenderer)
+
+      const renderer = await createRenderer(mockContext, {
+        renderer: createWebGPURenderer,
+      })
+
+      expect(createWebGPURenderer).toHaveBeenCalledWith(mockContext)
+      expect(renderer).toBe(mockWebGPURenderer)
+    })
+
+    it('should return the renderer directly if provided as an instance', async () => {
+      const mockRenderer = { isCustomRenderer: true } as unknown as TresRenderer
+      const renderer = await createRenderer(mockContext, {
+        renderer: mockRenderer,
+      })
+
+      expect(WebGLRenderer).not.toHaveBeenCalled()
+      expect(renderer).toBe(mockRenderer)
+    })
+  })
+})

+ 23 - 0
src/core/createRenderer.ts

@@ -0,0 +1,23 @@
+import { WebGLRenderer } from 'three'
+import * as is from '../utils/is'
+import type { TresContext } from '../composables'
+import type { TresRenderer } from '../types'
+import type { TresCanvasProps } from '../components/TresCanvas.vue'
+
+export async function createRenderer(ctx: TresContext, options: TresCanvasProps): Promise<TresRenderer> {
+  if (options.renderer) {
+    const fnOrRenderer = options.renderer
+    if (typeof fnOrRenderer === 'function') {
+      return await fnOrRenderer(ctx)
+    }
+    return fnOrRenderer
+  }
+
+  const rendererConstructorArgs = {
+    ...(is.obj(options) ? options : {}),
+    canvas: ctx.canvas.value,
+  }
+  const renderer = new WebGLRenderer(rendererConstructorArgs)
+
+  return renderer
+}

+ 151 - 0
src/core/setupRenderer.ts

@@ -0,0 +1,151 @@
+import type { TresContext } from 'src/composables/useTresContextProvider'
+import type { ColorRepresentation, Object3D, WebGLRenderer } from 'three'
+import { watch } from 'vue'
+import { useDevicePixelRatio } from '@vueuse/core'
+import { setPixelRatio } from '../utils'
+
+import { Mesh } from 'three'
+
+interface PropertyHandler<T = unknown> {
+  set: (renderer: WebGLRenderer, value: T) => void
+  immediate?: boolean
+}
+
+interface DirectProperty {
+  key: keyof WebGLRenderer | 'shadowMap.enabled' | 'shadowMap.type' | 'physicallyCorrectLights'
+  immediate?: boolean
+}
+
+// Properties that can be set directly on the renderer
+const directProperties: Record<string, DirectProperty> = {
+  toneMapping: {
+    key: 'toneMapping',
+    immediate: true,
+  },
+  toneMappingExposure: {
+    key: 'toneMappingExposure',
+    immediate: true,
+  },
+  outputColorSpace: {
+    key: 'outputColorSpace',
+    immediate: true,
+  },
+  physicallyCorrectLights: {
+    key: 'physicallyCorrectLights',
+    immediate: true,
+  },
+  shadowMapType: {
+    key: 'shadowMap.type',
+    immediate: true,
+  },
+  shadows: {
+    key: 'shadowMap.enabled',
+    immediate: true,
+  },
+}
+
+// Properties that use setter methods
+const rendererPropertyHandlers: Record<string, PropertyHandler<ColorRepresentation | boolean>> = {
+  clearColor: {
+    set: (renderer, value) => renderer.setClearColor(value as ColorRepresentation),
+    immediate: true,
+  },
+  alpha: {
+    set: (renderer, value) => renderer.setClearAlpha(value as boolean ? 1 : 0),
+    immediate: true,
+  },
+}
+
+// Modified setup function to handle both types of properties
+export function setupWebGLRenderer(
+  initialRenderer: WebGLRenderer,
+  options: Record<string, any>,
+  ctx: TresContext,
+) {
+  const { pixelRatio } = useDevicePixelRatio()
+  const { invalidate } = ctx
+
+  function invalidateOnDemand() {
+    if (options.renderMode === 'on-demand') {
+      invalidate()
+    }
+  }
+
+  // Watch DPR changes
+  watch(() => options.dpr, (value) => {
+    if (!value) { return }
+    invalidateOnDemand()
+    setPixelRatio(initialRenderer, pixelRatio.value, value as number)
+  })
+
+  // Watch size changes
+  watch([ctx.sizes.width, ctx.sizes.height], () => {
+    initialRenderer.setSize(ctx.sizes.width.value, ctx.sizes.height.value)
+    invalidateOnDemand()
+  }, {
+    immediate: true,
+  })
+
+  // Watch properties that need setter methods
+  Object.entries(rendererPropertyHandlers).forEach(([key, handler]) => {
+    watch(
+      () => options[key],
+      (value) => {
+        if (value === undefined) { return }
+        handler.set(initialRenderer, value)
+        invalidateOnDemand()
+      },
+      { immediate: handler.immediate },
+    )
+  })
+
+  // Watch properties that can be set directly
+  Object.entries(directProperties).forEach(([key, prop]) => {
+    watch(
+      () => options[key],
+      (value) => {
+        if (value === undefined) { return }
+        // Handle nested properties (like shadowMap.type)
+        const parts = prop.key.split('.')
+
+        if (parts.length > 1) {
+          // Handle shadowMap properties specifically
+          if (parts[0] === 'shadowMap') {
+            const shadowMapKey = parts[1] as keyof typeof initialRenderer.shadowMap
+            initialRenderer.shadowMap[shadowMapKey] = value
+
+            // Update materials when shadow properties change
+            ctx.scene.value.traverse((child: Object3D) => {
+              if (child instanceof Mesh) {
+                const material = (child).material
+                if (material) {
+                  material.needsUpdate = true
+                }
+              }
+            })
+          }
+        }
+        else {
+          const key = prop.key as keyof WebGLRenderer
+          // Check instance property first, then prototype
+          const descriptor = Object.getOwnPropertyDescriptor(initialRenderer, key)
+            || Object.getOwnPropertyDescriptor(Object.getPrototypeOf(initialRenderer), key)
+
+          // Get safe properties from directProperties
+          const safeToSetProperties = Object.values(directProperties)
+            .map(prop => prop.key)
+            .filter(key => !key.includes('.')) // Filter out nested properties like shadowMap.type
+
+          if ((descriptor?.writable || safeToSetProperties.includes(key))) {
+            const rendererKey = key as keyof Omit<WebGLRenderer, 'coordinateSystem' | 'info'>
+            ;(initialRenderer as unknown as Record<string, unknown>)[rendererKey] = value
+          }
+        }
+        invalidateOnDemand()
+      },
+      { immediate: prop.immediate },
+    )
+  })
+
+  return initialRenderer
+}

+ 1 - 0
src/index.ts

@@ -10,6 +10,7 @@ export * from './core/catalogue'
 export * from './core/loop'
 export * from './directives'
 export * from './types'
+export * from './utils/logger'
 
 export interface TresOptions {
   extends?: Record<string, unknown>

+ 18 - 1
src/types/index.ts

@@ -1,8 +1,24 @@
 /* eslint-disable ts/method-signature-style */
 import type * as THREE from 'three'
 
-import type { DefineComponent, VNode, VNodeRef } from 'vue'
+import type { DefineComponent, MaybeRefOrGetter, VNode, VNodeRef } from 'vue'
 import type { TresContext } from '../composables/useTresContextProvider'
+import type { Camera, Scene, WebGLRenderer } from 'three'
+import type { CSS2DRenderer, CSS3DRenderer, SVGRenderer } from 'three-stdlib'
+import type { WebGPURenderer } from 'three/webgpu'
+
+export interface BaseRenderer {
+  render: (scene: Scene, camera: Camera) => void
+  setSize: (width: number, height: number) => void
+  dispose: () => void
+}
+
+// Union type of all possible renderers
+export type TresRenderer = WebGLRenderer | WebGPURenderer | CSS2DRenderer | CSS3DRenderer | SVGRenderer
+
+export type TransformToMaybeRefOrGetter<T> = {
+  [K in keyof T]: MaybeRefOrGetter<T[K]>
+}
 
 // Based on React Three Fiber types by Pmndrs
 // https://github.com/pmndrs/react-three-fiber/blob/v9/packages/fiber/src/three-types.ts
@@ -26,6 +42,7 @@ export interface TresCatalogue {
 export type EmitEventName = 'render' | 'ready' | 'click' | 'double-click' | 'context-menu' | 'pointer-move' | 'pointer-up' | 'pointer-down' | 'pointer-enter' | 'pointer-leave' | 'pointer-over' | 'pointer-out' | 'pointer-missed' | 'wheel'
 export type EmitEventFn = (event: EmitEventName, ...args: any[]) => void
 export type TresCamera = THREE.OrthographicCamera | THREE.PerspectiveCamera
+export type Renderer = { render: (scene: THREE.Scene, camera: THREE.Camera) => void, setSize: (width: number, height: number) => void } & Record<string | number | symbol, any>
 
 /**
  * Represents the properties of an instance.

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels