Selaa lähdekoodia

refactor!: context camera is now a state (#1004)

* refactor!: context camera is now a state

BREAKING CHANGE: camera ctx property is now an object with the camera manager instead of the active camera

* fix: remove camera manual check

* chore: remove reset onUnmounted

Co-authored-by: Tino Koch <17991193+Tinoooo@users.noreply.github.com>

* chore: remove unused comment

Co-authored-by: Tino Koch <17991193+Tinoooo@users.noreply.github.com>

* chore: omit previous reordering of cameras

Co-authored-by: Tino Koch <17991193+Tinoooo@users.noreply.github.com>

* refactor: update camera handling to use Three.js Camera type

- Replaced instances of TresCamera with Three.js Camera type across the codebase for better compatibility and consistency.
- Updated camera management logic in useCamera composable and related components to reflect the new type.
- Simplified currentCamera logic to use computed properties instead of watch.

* refactor: enhance orthographic camera setup and controls

- Updated the orthographic camera initialization to correctly calculate frustum dimensions based on the aspect ratio.
- Introduced zoom functionality for the orthographic camera.
- Simplified the useControls setup by removing unnecessary properties and focusing on essential controls.
- Adjusted the camera update logic to reflect the new control structure, ensuring proper projection matrix updates.

* chore(playground): streamline camera implementation with TresJS components

- Removed manual camera initialization in favor of using TresJS components for perspective and orthographic cameras.
- Updated the template to conditionally render the appropriate camera based on the selected camera type.
- Enhanced readability and maintainability by leveraging TresJS's built-in camera properties and methods.

* refactor(playground): remove unused camera imports in index.vue

- Eliminated unused imports for OrthographicCamera and PerspectiveCamera from Three.js to streamline the code.
- This change enhances code clarity and reduces unnecessary dependencies, aligning with the recent updates to utilize TresJS components for camera management.

* refactor(useCamera): improve camera management logic (#1009)

---------

Co-authored-by: Tino Koch <17991193+Tinoooo@users.noreply.github.com>
Alvaro Saburido 1 kuukausi sitten
vanhempi
commit
d5daf5dace

+ 65 - 0
playground/vue/src/pages/cameras/OrthographicCamera.vue

@@ -0,0 +1,65 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import type { OrthographicCamera } from 'three'
+import { Vector3 } from 'three'
+import { TresLeches, useControls } from '@tresjs/leches'
+import { useWindowSize } from '@vueuse/core'
+
+const { width, height } = useWindowSize()
+const aspect = computed(() => width.value / height.value)
+
+const { zoom, position, lookAt, near, far, frustum } = useControls({
+  position: new Vector3(1, 1, 1),
+  lookAt: new Vector3(0, 0, 0),
+  frustum: {
+    value: 10,
+    min: 0,
+    max: 100,
+    step: 10,
+  },
+  zoom: {
+    value: 1,
+    min: -100,
+    max: 100,
+    step: 1,
+  },
+  near: {
+    value: -100,
+    min: -100,
+    max: 100,
+    step: 10,
+  },
+  far: {
+    value: 1000,
+    min: 0.01,
+    max: 1000,
+    step: 10,
+  },
+})
+const cameraRef = ref<OrthographicCamera>()
+
+watch([zoom, near, far, frustum], () => {
+  cameraRef.value?.updateProjectionMatrix()
+})
+</script>
+
+<template>
+  <TresLeches />
+  <TresCanvas clear-color="#82DBC5">
+    <TresOrthographicCamera
+      ref="cameraRef"
+      :position="[position.x, position.y, position.z]"
+      :look-at="[lookAt.x, lookAt.y, lookAt.z]"
+      :args="[-frustum * aspect, frustum * aspect, frustum, -frustum, 0.1, 1000]"
+      :zoom="zoom"
+      :near="near"
+      :far="far"
+    />
+    <TresGridHelper />
+    <TresMesh position-y="1">
+      <TresBoxGeometry />
+      <TresMeshNormalMaterial />
+    </TresMesh>
+    <TresAmbientLight :intensity="1" />
+  </TresCanvas>
+</template>

+ 63 - 0
playground/vue/src/pages/cameras/PerspectiveCamera.vue

@@ -0,0 +1,63 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import type { PerspectiveCamera } from 'three'
+import { Vector3 } from 'three'
+import { TresLeches, useControls } from '@tresjs/leches'
+
+const { position, lookAt, fov, near, far, zoom } = useControls({
+  position: new Vector3(8, 8, 8),
+  lookAt: new Vector3(0, 0, 0),
+  // TODO: For some reason, the PerspectiveCamera's fov prop is not updating when the value is changed.
+  fov: {
+    value: 45,
+    min: 1,
+    max: 180,
+    step: 1,
+  },
+  near: {
+    value: 0.1,
+    min: 0.01,
+    max: 100,
+    step: 0.01,
+  },
+  far: {
+    value: 1000,
+    min: 0.01,
+    max: 1000,
+    step: 0.01,
+  },
+  zoom: {
+    value: 1,
+    min: 0.01,
+    max: 10,
+    step: 0.01,
+  },
+})
+
+const cameraRef = ref<PerspectiveCamera>()
+
+const computedFov = computed(() => {
+  return Number(fov.value)
+})
+</script>
+
+<template>
+  <TresLeches />
+  <TresCanvas clear-color="#82DBC5">
+    <TresPerspectiveCamera
+      ref="cameraRef"
+      :position="[position.x, position.y, position.z]"
+      :look-at="[lookAt.x, lookAt.y, lookAt.z]"
+      :fov="computedFov"
+      :near="near"
+      :far="far"
+      :zoom="zoom"
+    />
+    <TresGridHelper />
+    <TresMesh position-y="1">
+      <TresBoxGeometry />
+      <TresMeshNormalMaterial />
+    </TresMesh>
+    <TresAmbientLight :intensity="1" />
+  </TresCanvas>
+</template>

+ 18 - 57
playground/vue/src/pages/cameras/index.vue

@@ -2,30 +2,11 @@
 import { Box } from '@tresjs/cientos'
 import { TresCanvas } from '@tresjs/core'
 import { TresLeches, useControls } from '@tresjs/leches'
-import { BasicShadowMap, NoToneMapping, OrthographicCamera, PerspectiveCamera, SRGBColorSpace } from 'three'
 import '@tresjs/leches/styles'
 
-const gl = {
-  clearColor: '#82DBC5',
-  shadows: true,
-  alpha: false,
-  shadowMapType: BasicShadowMap,
-  outputColorSpace: SRGBColorSpace,
-  toneMapping: NoToneMapping,
-}
-type Cam = (PerspectiveCamera | OrthographicCamera) & { manual?: boolean }
-
-const state = reactive({
-  cameraType: 'perspective',
-  camera: new PerspectiveCamera(75, 1, 0.1, 1000) as Cam,
-})
-
-state.camera.position.set(5, 5, 5)
-state.camera.lookAt(0, 0, 0)
-
-const { cameraType, manual } = useControls({
+const { cameraType } = useControls({
   cameraType: {
-    label: 'CameraType',
+    value: 'perspective',
     options: [{
       text: 'Perspective',
       value: 'perspective',
@@ -33,50 +14,30 @@ const { cameraType, manual } = useControls({
       text: 'Orthographic',
       value: 'orthographic',
     }],
-    value: state.cameraType,
   },
-  manual: false,
 })
-
-watch(() => [cameraType.value.value, manual.value.value], () => {
-  state.cameraType = cameraType.value.value
-  if (cameraType.value.value === 'perspective') {
-    state.camera = new PerspectiveCamera(75, 1, 0.1, 1000)
-  }
-  else if (cameraType.value.value === 'orthographic') {
-    state.camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1000)
-    state.camera.zoom = 20
-  }
-  state.camera.manual = manual.value.value
-  state.camera.position.set(5, 5, 5)
-  state.camera.lookAt(0, 0, 0)
-})
-
-const context = ref(null)
-
-watchEffect(() => {
-  if (context.value) {
-    // eslint-disable-next-line no-console
-    console.log(context.value)
-  }
-})
-
-const asyncTorus = ref(false)
-
-setTimeout(() => {
-  asyncTorus.value = true
-}, 1000)
 </script>
 
 <template>
   <TresLeches />
   <TresCanvas
-    v-bind="gl"
-    ref="context"
-    :camera="state.camera"
+    clear-color="#82DBC5"
   >
-    <!--     <TresPerspectiveCamera v-if="state.cameraType === 'perspective'" :position="[11, 11, 11]" />
-    <TresOrthographicCamera v-if="state.cameraType === 'orthographic'" :position="[11, 11, 11]" /> -->
+    <TresPerspectiveCamera
+      v-if="cameraType === 'perspective'"
+      :position="[8, 8, 8]"
+      :fov="75"
+      :near="0.1"
+      :far="1000"
+      :look-at="[0, 0, 0]"
+    />
+    <TresOrthographicCamera
+      v-else
+      :position="[8, 8, 8]"
+      :near="0.1"
+      :far="1000"
+      :look-at="[0, 0, 0]"
+    />
     <Box
       :position="[0, 1, 0]"
       :scale="[2, 2, 2]"

+ 2 - 2
playground/vue/src/pages/cameras/multipleCameras/TheCameraOperator.vue

@@ -5,10 +5,10 @@ const props = defineProps<{
   activeCameraUuid?: string
 }>()
 
-const { setCameraActive } = useTresContext()
+const { camera } = useTresContext()
 
 watchEffect(() => {
-  if (props.activeCameraUuid) { setCameraActive(props.activeCameraUuid) }
+  if (props.activeCameraUuid) { camera.setActiveCamera(props.activeCameraUuid) }
 })
 </script>
 

+ 67 - 76
playground/vue/src/pages/cameras/multipleCameras/index.vue

@@ -1,98 +1,89 @@
 <script setup lang="ts">
 import type { Camera } from 'three'
-import { OrbitControls } from '@tresjs/cientos'
 import { TresCanvas } from '@tresjs/core'
 import { TresLeches, useControls } from '@tresjs/leches'
 import TheCameraOperator from './TheCameraOperator.vue'
 import '@tresjs/leches/styles'
 
-const state = reactive({
-  clearColor: '#4f4f4f',
-  shadows: true,
-  alpha: false,
-})
-
 useControls('fpsgraph')
 
 const camera1 = shallowRef<Camera>()
 const camera2 = shallowRef<Camera>()
 const camera3 = shallowRef<Camera>()
 
-const activeCameraUuid = ref<string>()
+const cameraUuidList = computed(() => [
+  camera1.value?.uuid,
+  camera2.value?.uuid,
+  camera3.value?.uuid,
+])
 
-watchEffect(() => {
-  activeCameraUuid.value = camera1.value?.uuid
+const { cameras: activeCameraIndex } = useControls({
+  cameras: {
+    value: 0,
+    options: [
+      {
+        text: 'Camera 1',
+        value: 0,
+      },
+      {
+        text: 'Camera 2',
+        value: 1,
+      },
+      {
+        text: 'Camera 3',
+        value: 2,
+      },
+    ],
+  },
 })
 
-const camera3Exists = ref(false)
+const activeCameraUuid = computed(() => cameraUuidList.value[activeCameraIndex.value])
 </script>
 
 <template>
-  <div>
+  <TresLeches>
     {{ activeCameraUuid }}
-    <select v-model="activeCameraUuid">
-      <option :value="camera1?.uuid">
-        cam 1
-      </option>
-      <option :value="camera2?.uuid">
-        cam 2
-      </option>
-      <option
-        v-if="camera3Exists"
-        :value="camera3?.uuid"
-      >
-        cam 3
-      </option>
-    </select>
-    <input
-      v-model="camera3Exists"
-      type="checkbox"
-    />
-    <div class="w-1/2 aspect-video">
-      <TresCanvas v-bind="state">
-        <TheCameraOperator :active-camera-uuid="activeCameraUuid">
-          <TresPerspectiveCamera
-            ref="camera1"
-            :position="[5, 5, 5]"
-            :fov="45"
-            :near="0.1"
-            :far="1000"
-            :look-at="[0, 4, 0]"
-          />
-          <TresPerspectiveCamera
-            ref="camera2"
-            :position="[15, 5, 5]"
-            :fov="45"
-            :near="0.1"
-            :far="1000"
-            :look-at="[0, 4, 0]"
-          />
-          <TresPerspectiveCamera
-            v-if="camera3Exists"
-            ref="camera3"
-            :position="[-15, 8, 5]"
-            :fov="25"
-            :near="0.1"
-            :far="1000"
-            :look-at="[0, 4, 0]"
-          />
-        </TheCameraOperator>
-        <OrbitControls />
-        <TresAmbientLight :intensity="0.5" />
-        <TresMesh :position="[0, 4, 0]">
-          <TresBoxGeometry :args="[1, 1, 1]" />
-          <TresMeshToonMaterial color="cyan" />
-        </TresMesh>
+  </TresLeches>
 
-        <Suspense>
-          <PbrSphere />
-        </Suspense>
-        <TresDirectionalLight
-          :position="[0, 2, 4]"
-          :intensity="1"
-        />
-      </TresCanvas>
-    </div>
-    <TresLeches />
-  </div>
+  <TresCanvas clear-color="#4f4f4f">
+    <TheCameraOperator :active-camera-uuid="activeCameraUuid">
+      <TresPerspectiveCamera
+        ref="camera1"
+        :position="[5, 5, 5]"
+        :fov="45"
+        :near="0.1"
+        :far="1000"
+        :look-at="[0, 4, 0]"
+      />
+      <TresPerspectiveCamera
+        ref="camera2"
+        :position="[15, 5, 5]"
+        :fov="45"
+        :near="0.1"
+        :far="1000"
+        :look-at="[0, 4, 0]"
+      />
+      <TresPerspectiveCamera
+        ref="camera3"
+        :position="[-15, 8, 5]"
+        :fov="25"
+        :near="0.1"
+        :far="1000"
+        :look-at="[0, 4, 0]"
+      />
+    </TheCameraOperator>
+    <TresAmbientLight :intensity="0.5" />
+    <TresMesh :position="[0, 4, 0]">
+      <TresBoxGeometry :args="[1, 1, 1]" />
+      <TresMeshToonMaterial color="cyan" />
+    </TresMesh>
+
+    <Suspense>
+      <PbrSphere />
+    </Suspense>
+    <TresDirectionalLight
+      :position="[0, 2, 4]"
+      :intensity="1"
+    />
+  </TresCanvas>
 </template>

+ 10 - 0
playground/vue/src/router/routes/cameras.ts

@@ -4,6 +4,16 @@ export const cameraRoutes = [
     name: 'Cameras',
     component: () => import('../../pages/cameras/index.vue'),
   },
+  {
+    path: '/cameras/perspective-camera',
+    name: 'Perspective Camera',
+    component: () => import('../../pages/cameras/PerspectiveCamera.vue'),
+  },
+  {
+    path: '/cameras/orthographic-camera',
+    name: 'Orthographic Camera',
+    component: () => import('../../pages/cameras/OrthographicCamera.vue'),
+  },
   {
     path: '/cameras/no-camera',
     name: 'No Camera',

+ 12 - 7
src/components/TresCanvas.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import type {
+  Camera,
   ColorSpace,
   ShadowMapType,
   ToneMapping,
@@ -8,7 +9,7 @@ import type {
 } from 'three'
 import type { App, Ref } from 'vue'
 import type { RendererPresetsType } from '../composables/useRenderer/const'
-import type { TresCamera, TresObject, TresPointerEvent, TresScene } from '../types/'
+import type { TresObject, TresPointerEvent, TresScene } from '../types/'
 import { PerspectiveCamera, Scene } from 'three'
 import * as THREE from 'three'
 
@@ -23,6 +24,7 @@ import {
   provide,
   ref,
   shallowRef,
+  toValue,
   watch,
   watchEffect,
 } from 'vue'
@@ -52,7 +54,7 @@ export interface TresCanvasProps
   dpr?: number | [number, number]
 
   // required by useTresContextProvider
-  camera?: TresCamera
+  camera?: Camera
   preset?: RendererPresetsType
   windowSize?: boolean
 
@@ -192,7 +194,8 @@ onMounted(() => {
     rendererOptions: props,
   })
 
-  const { registerCamera, camera, cameras, deregisterCamera, renderer } = context.value
+  const { camera, renderer } = context.value
+  const { registerCamera, cameras, activeCamera, deregisterCamera } = camera
 
   mountCustomRenderer(context.value)
 
@@ -219,10 +222,12 @@ onMounted(() => {
   watch(
     () => props.camera,
     (newCamera, oldCamera) => {
-      if (newCamera) { registerCamera(newCamera) }
+      if (newCamera) {
+        registerCamera(toValue(newCamera), true)
+      }
       if (oldCamera) {
-        oldCamera.removeFromParent()
-        deregisterCamera(oldCamera)
+        toValue(oldCamera).removeFromParent()
+        deregisterCamera(toValue(oldCamera))
       }
     },
     {
@@ -230,7 +235,7 @@ onMounted(() => {
     },
   )
 
-  if (!camera.value) {
+  if (!activeCamera.value) {
     addDefaultCamera()
   }
 

+ 1 - 1
src/composables/index.ts

@@ -1,6 +1,6 @@
 import UseLoader from './useLoader/component.vue'
 
-export * from './useCamera/'
+export * from './useCamera'
 export * from './useGraph'
 export * from './useLoader'
 export * from './useLoop'

+ 77 - 51
src/composables/useCamera/index.ts

@@ -1,20 +1,61 @@
-import type { OrthographicCamera } from 'three'
-import type { TresScene } from '../../types'
 import type { TresContext } from '../useTresContextProvider'
 
-import { Camera, PerspectiveCamera } from 'three'
-import { computed, onUnmounted, ref, watchEffect } from 'vue'
-import { isCamera } from '../../utils/is'
+import type { ComputedRef, Ref } from 'vue'
+import { computed, ref, watchEffect } from 'vue'
+import { isCamera, isPerspectiveCamera } from '../../utils/is'
+import type { Camera } from 'three'
 
-export const useCamera = ({ sizes }: Pick<TresContext, 'sizes'> & { scene: TresScene }) => {
-  // the computed does not trigger, when for example the camera position changes
+/**
+ * Interface for the return value of the useCamera composable
+ */
+export interface UseCameraReturn {
+
+  activeCamera: ComputedRef<Camera | undefined>
+  /**
+   * The list of cameras
+   */
+  cameras: Ref<Camera[]>
+  /**
+   * Register a camera
+   * @param camera - The camera to register
+   * @param active - Whether to set the camera as active
+   */
+  registerCamera: (camera: Camera, active?: boolean) => void
+  /**
+   * Deregister a camera
+   * @param camera - The camera to deregister
+   */
+  deregisterCamera: (camera: Camera) => void
+  /**
+   * Set the active camera
+   * @param cameraOrUuid - The camera or its UUID to set as active
+   */
+  setActiveCamera: (cameraOrUuid: string | Camera) => void
+}
+
+/**
+ * Interface for the parameters of the useCamera composable
+ */
+interface UseCameraParams {
+  sizes: TresContext['sizes']
+}
+
+/**
+ * Composable for managing cameras in a Three.js scene
+ * @param params - The parameters for the composable
+ * @param params.sizes - The sizes object containing window dimensions
+ * @returns The camera management functions and state
+ */
+export const useCameraManager = ({ sizes }: UseCameraParams): UseCameraReturn => {
   const cameras = ref<Camera[]>([])
-  const camera = computed<Camera | undefined>(
-    () => cameras.value[0],
-  )
+  const activeCamera = computed<Camera | undefined>(() => cameras.value[0]) // the first camera is used to make sure there is always one camera active
 
-  const setCameraActive = (cameraOrUuid: string | Camera) => {
-    const camera = cameraOrUuid instanceof Camera
+  /**
+   * Set the active camera
+   * @param cameraOrUuid - The camera or its UUID to set as active
+   */
+  const setActiveCamera = (cameraOrUuid: string | Camera) => {
+    const camera = isCamera(cameraOrUuid)
       ? cameraOrUuid
       : cameras.value.find((camera: Camera) => camera.uuid === cameraOrUuid)
 
@@ -24,62 +65,47 @@ export const useCamera = ({ sizes }: Pick<TresContext, 'sizes'> & { scene: TresS
     cameras.value = [camera, ...otherCameras]
   }
 
-  const registerCamera = (maybeCamera: unknown, active = false) => {
-    if (isCamera(maybeCamera)) {
-      const camera = maybeCamera
-      if (cameras.value.some(({ uuid }) => uuid === camera.uuid)) { return }
+  /**
+   * Register a camera
+   * @param camera - The camera to register
+   * @param active - Whether to set the camera as active
+   */
+  const registerCamera = (camera: Camera, active = false): void => {
+    if (cameras.value.some(({ uuid }) => uuid === camera.uuid)) { return }
+    cameras.value.push(camera)
 
-      if (active) { setCameraActive(camera) }
-      else { cameras.value.push(camera) }
+    if (active) {
+      setActiveCamera(camera.uuid)
     }
   }
 
-  const deregisterCamera = (maybeCamera: unknown) => {
-    if (isCamera(maybeCamera)) {
-      const camera = maybeCamera
-      cameras.value = cameras.value.filter(({ uuid }) => uuid !== camera.uuid)
-    }
+  /**
+   * Deregister a camera
+   * @param camera - The camera to deregister
+   */
+  const deregisterCamera = (camera: Camera): void => {
+    cameras.value = cameras.value.filter(({ uuid }) => uuid !== camera.uuid)
   }
 
+  /**
+   * Update camera aspect ratios when the window size changes
+   */
   watchEffect(() => {
     if (sizes.aspectRatio.value) {
-      cameras.value.forEach((camera: Camera & { manual?: boolean }) => {
-        // NOTE: Don't mess with the camera if it belongs to the user.
-        // https://github.com/pmndrs/react-three-fiber/blob/0ef66a1d23bf16ecd457dde92b0517ceec9861c5/packages/fiber/src/core/utils.ts#L457
-        //
-        // To set camera as "manual":
-        // const myCamera = new PerspectiveCamera(); // or OrthographicCamera
-        // (myCamera as any).manual = true
-        if (!camera.manual && (camera instanceof PerspectiveCamera || isOrthographicCamera(camera))) {
-          if (camera instanceof PerspectiveCamera) {
-            camera.aspect = sizes.aspectRatio.value
-          }
-          else {
-            camera.left = sizes.width.value * -0.5
-            camera.right = sizes.width.value * 0.5
-            camera.top = sizes.height.value * 0.5
-            camera.bottom = sizes.height.value * -0.5
-          }
+      cameras.value.forEach((camera: Camera) => {
+        if (isPerspectiveCamera(camera)) {
+          camera.aspect = sizes.aspectRatio.value
           camera.updateProjectionMatrix()
         }
       })
     }
   })
 
-  onUnmounted(() => {
-    cameras.value = []
-  })
-
   return {
-    camera,
+    activeCamera,
     cameras,
     registerCamera,
     deregisterCamera,
-    setCameraActive,
+    setActiveCamera,
   }
 }
-
-function isOrthographicCamera(o: any): o is OrthographicCamera {
-  // eslint-disable-next-line no-prototype-builtins
-  return o.hasOwnProperty('isOrthographicCamera') && o.isOrthographicCamera
-}

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

@@ -1,12 +1,11 @@
+import { createEventHook, useElementBounding, usePointer } from '@vueuse/core'
+import { Vector2, Vector3 } from 'three'
+import { computed, onUnmounted, shallowRef } from 'vue'
 import type { EventHook } from '@vueuse/core'
-import type { DomEvent, TresCamera, TresEvent, TresInstance } from 'src/types'
 import type { Intersection, Object3D, Object3DEventMap } from 'three'
 import type { ShallowRef } from 'vue'
+import type { DomEvent, TresEvent, TresInstance } from '../../types'
 import type { TresContext } from '../useTresContextProvider'
-import { createEventHook, useElementBounding, usePointer } from '@vueuse/core'
-
-import { Vector2, Vector3 } from 'three'
-import { computed, onUnmounted, shallowRef } from 'vue'
 
 export const useRaycaster = (
   objectsWithEvents: ShallowRef<TresInstance[]>,
@@ -30,9 +29,9 @@ export const useRaycaster = (
   }
 
   const getIntersectsByRelativePointerPosition = ({ x, y }: { x: number, y: number }) => {
-    if (!ctx.camera.value) { return }
+    if (!ctx.camera.activeCamera.value) { return }
 
-    ctx.raycaster.value.setFromCamera(new Vector2(x, y), ctx.camera.value)
+    ctx.raycaster.value.setFromCamera(new Vector2(x, y), ctx.camera.activeCamera.value)
 
     intersects.value = ctx.raycaster.value.intersectObjects(objectsWithEvents.value as Object3D<Object3DEventMap>[], true)
     return intersects.value
@@ -84,14 +83,15 @@ export const useRaycaster = (
 
   const triggerEventHook = (eventHook: EventHook, event: PointerEvent | MouseEvent | WheelEvent) => {
     const eventProperties = copyMouseEventProperties(event)
-    const unprojectedPoint = new Vector3(event?.clientX, event?.clientY, 0).unproject(ctx.camera?.value as TresCamera)
+    if (!ctx.camera.activeCamera.value) { return }
+    const unprojectedPoint = new Vector3(event?.clientX, event?.clientY, 0).unproject(ctx.camera.activeCamera.value)
     eventHook.trigger({
       ...eventProperties,
       intersections: intersects.value,
       // The unprojectedPoint is wrong, math needs to be fixed
       unprojectedPoint,
       ray: ctx.raycaster?.value.ray,
-      camera: ctx.camera?.value,
+      camera: ctx.camera.activeCamera.value,
       sourceEvent: event,
       delta,
       stopPropagating: false,

+ 2 - 2
src/composables/useRenderer/useRendererManager.ts

@@ -176,8 +176,8 @@ export function useRendererManager(
   const onRender = createEventHook<WebGLRenderer>()
 
   loop.register(() => {
-    if (camera.value && amountOfFramesToRender.value) {
-      instance.value.render(scene, camera.value)
+    if (camera.activeCamera.value && amountOfFramesToRender.value) {
+      instance.value.render(scene, camera.activeCamera.value)
 
       onRender.trigger(instance.value)
     }

+ 10 - 23
src/composables/useTresContextProvider/index.ts

@@ -1,18 +1,20 @@
-import type { Camera } from 'three'
-import type { ComputedRef, DeepReadonly, MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
+import { Raycaster } from 'three'
+import type { MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
+import { whenever } from '@vueuse/core'
+
 import type { RendererLoop } from '../../core/loop'
 import type { TresControl, TresObject, TresScene } from '../../types'
 import type { UseRendererManagerReturn, UseRendererOptions } from '../useRenderer/useRendererManager'
-import { Raycaster } from 'three'
-import { inject, onUnmounted, provide, readonly, ref, shallowRef } from 'vue'
+import { inject, onUnmounted, provide, ref, shallowRef } from 'vue'
 import { extend } from '../../core/catalogue'
 import { createRenderLoop } from '../../core/loop'
 
-import { useCamera } from '../useCamera'
+import type { UseCameraReturn } from '../useCamera/'
+
+import { useCameraManager } from '../useCamera'
 import { useRendererManager } from '../useRenderer/useRendererManager'
 import useSizes, { type SizesType } from '../useSizes'
 import { type TresEventManager, useTresEventManager } from '../useTresEventManager'
-import { whenever } from '@vueuse/core'
 
 export interface PerformanceState {
   maxFrames: number
@@ -31,18 +33,13 @@ export interface TresContext {
   scene: ShallowRef<TresScene>
   sizes: SizesType
   extend: (objects: any) => void
-  camera: ComputedRef<Camera | undefined>
-  cameras: DeepReadonly<Ref<Camera[]>>
+  camera: UseCameraReturn
   controls: Ref<TresControl | null>
   renderer: UseRendererManagerReturn
   raycaster: ShallowRef<Raycaster>
   perf: PerformanceState
   // Loop
   loop: RendererLoop
-  // Camera
-  registerCamera: (maybeCamera: unknown) => void
-  setCameraActive: (cameraOrUuid: Camera | string) => void
-  deregisterCamera: (maybeCamera: unknown) => void
   eventManager?: TresEventManager
   // Events
   // Temporaly add the methods to the context, this should be handled later by the EventManager state on the context https://github.com/Tresjs/tres/issues/515
@@ -67,13 +64,7 @@ export function useTresContextProvider({
   const localScene = shallowRef<TresScene>(scene)
   const sizes = useSizes(windowSize, canvas)
 
-  const {
-    camera,
-    cameras,
-    registerCamera,
-    deregisterCamera,
-    setCameraActive,
-  } = useCamera({ sizes, scene })
+  const camera = useCameraManager({ sizes })
 
   const loop = createRenderLoop()
 
@@ -90,7 +81,6 @@ export function useTresContextProvider({
     sizes,
     scene: localScene,
     camera,
-    cameras: readonly(cameras),
     renderer,
     raycaster: shallowRef(new Raycaster()),
     controls: ref(null),
@@ -107,9 +97,6 @@ export function useTresContextProvider({
       },
     },
     extend,
-    registerCamera,
-    setCameraActive,
-    deregisterCamera,
     loop,
   }
 

+ 1 - 1
src/core/loop.ts

@@ -123,7 +123,7 @@ export function createRenderLoop(): RendererLoop {
     const delta = clock.getDelta()
     const elapsed = clock.getElapsedTime()
     const snapshotCtx = {
-      camera: unref(context.camera),
+      camera: unref(context.camera?.activeCamera),
       scene: unref(context.scene),
       renderer: unref(context.renderer),
       raycaster: unref(context.raycaster),

+ 4 - 2
src/core/nodeOps.test.ts

@@ -1546,8 +1546,10 @@ function mockTresObjectRootInObject(obj) {
 function mockTresContext() {
   return {
     scene: shallowRef(new Scene()),
-    registerCamera: () => {},
-    deregisterCamera: () => {},
+    camera: {
+      registerCamera: () => {},
+      deregisterCamera: () => {},
+    },
   } as unknown as TresContext
 }
 

+ 36 - 8
src/core/nodeOps.ts

@@ -4,7 +4,7 @@ import { BufferAttribute, Object3D } from 'three'
 import { isRef, type RendererOptions } from 'vue'
 import { attach, deepArrayEqual, doRemoveDeregister, doRemoveDetach, invalidateInstance, isHTMLTag, kebabToCamel, noop, prepareTresInstance, setPrimitiveObject, unboxTresPrimitive } from '../utils'
 import { logError } from '../utils/logger'
-import { isArray, isFunction, isObject, isObject3D, isScene, isUndefined } from '../utils/is'
+import { isArray, isCamera, isFunction, isObject, isObject3D, isScene, isTresInstance, isUndefined } from '../utils/is'
 import { createRetargetingProxy } from '../utils/primitive/createRetargetingProxy'
 import { catalogue } from './catalogue'
 
@@ -76,7 +76,8 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
 
     if (!obj) { return null }
 
-    if (obj.isCamera) {
+    // Opinionated default to avoid user issue not seeing anything if camera is on origin
+    if (isCamera(obj)) {
       if (!props?.position) {
         obj.position.set(3, 3, 3)
       }
@@ -86,7 +87,7 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
     }
 
     obj = prepareTresInstance(obj, {
-      ...obj.__tres,
+      ...(isTresInstance(obj) ? obj.__tres : {}),
       type: name,
       memoizedProps: props,
       eventCount: 0,
@@ -114,7 +115,9 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
       context.eventManager?.registerObject(child)
     }
 
-    context.registerCamera(child)
+    if (isCamera(child)) {
+      context.camera?.registerCamera(child)
+    }
     // NOTE: Track onPointerMissed objects separate from the scene
     context.eventManager?.registerPointerMissedObject(child)
 
@@ -288,10 +291,31 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
         && prevArgs.length
         && !deepArrayEqual(prevArgs, args)
       ) {
-        root = Object.assign(
-          prevNode,
-          new catalogue.value[instanceName](...nextValue),
-        )
+        // Create a new instance
+        const newInstance = new catalogue.value[instanceName](...nextValue)
+
+        // Get all property descriptors of the new instance
+        const descriptors = Object.getOwnPropertyDescriptors(newInstance)
+
+        // Only copy properties that are not readonly
+        Object.entries(descriptors).forEach(([key, descriptor]) => {
+          if (!descriptor.writable && !descriptor.set) {
+            return // Skip readonly properties
+          }
+
+          // Copy the value from new instance to previous node
+          if (key in prevNode) {
+            try {
+              (prevNode as unknown as Record<string, unknown>)[key] = newInstance[key]
+            }
+            catch (e) {
+              // Skip if property can't be set
+              console.warn(`Could not set property ${key} on ${instanceName}:`, e)
+            }
+          }
+        })
+
+        root = prevNode
       }
       return
     }
@@ -339,6 +363,10 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
     else if (!target.isColor && target.setScalar) { target.setScalar(value) }
     else { target.set(value) }
 
+    if (isCamera(node)) {
+      node.updateProjectionMatrix()
+    }
+
     invalidateInstance(node as TresObject)
   }
 

+ 1 - 1
src/types/index.ts

@@ -114,7 +114,7 @@ export interface IntersectionEvent<TSourceEvent> extends Intersection {
   /** The ray that pierced it */
   ray: THREE.Ray
   /** The camera that was used by the raycaster */
-  camera: TresCamera
+  camera: THREE.Camera
   /** stopPropagation will stop underlying handlers from firing */
   stopPropagation: () => void
   /** The original host event */

+ 7 - 3
src/utils/index.ts

@@ -4,7 +4,7 @@ import type { Material, Mesh, Object3D, Texture } from 'three'
 import type { TresContext } from '../composables/useTresContextProvider'
 import { DoubleSide, MathUtils, MeshBasicMaterial, Scene, Vector3 } from 'three'
 import { HightlightMesh } from '../devtools/highlight'
-import { isFunction, isNumber, isString, isTresPrimitive, isUndefined } from './is'
+import { isCamera, isFunction, isNumber, isString, isTresPrimitive, isUndefined } from './is'
 
 export * from './logger'
 export function toSetMethodName(key: string) {
@@ -566,13 +566,17 @@ export function doRemoveDeregister(node: TresObject, context: TresContext) {
   // TODO: Refactor as `context.deregister`?
   // That would eliminate `context.deregisterCamera`.
   node.traverse?.((child: TresObject) => {
-    context.deregisterCamera(child)
+    if (isCamera(child)) {
+      context.camera.deregisterCamera(child)
+    }
     // deregisterAtPointerEventHandlerIfRequired?.(child as TresObject)
     context.eventManager?.deregisterPointerMissedObject(child)
   })
 
   // NOTE: Deregister `node`
-  context.deregisterCamera(node)
+  if (isCamera(node)) {
+    context.camera.deregisterCamera(node)
+  }
   /*  deregisterAtPointerEventHandlerIfRequired?.(node as TresObject) */
   invalidateInstance(node as TresObject)
 }

+ 38 - 3
src/utils/is.ts

@@ -1,5 +1,5 @@
-import type { TresObject, TresPrimitive } from 'src/types'
-import type { BufferGeometry, Camera, Fog, Light, Material, Object3D, Scene } from 'three'
+import type { TresCamera, TresInstance, TresObject, TresPrimitive } from 'src/types'
+import type { BufferGeometry, Fog, Light, Material, Object3D, OrthographicCamera, PerspectiveCamera, Scene } from 'three'
 
 /**
  * Type guard to check if a value is undefined
@@ -159,10 +159,28 @@ export function isObject3D(value: unknown): value is Object3D {
  * }
  * ```
  */
-export function isCamera(value: unknown): value is Camera {
+export function isCamera(value: unknown): value is TresCamera {
   return isObject(value) && !!(value.isCamera)
 }
 
+/**
+ * Type guard to check if a value is a Three.js OrthographicCamera
+ * @param value - The value to check
+ * @returns True if the value is a Three.js OrthographicCamera instance, false otherwise
+ */
+export function isOrthographicCamera(value: unknown): value is OrthographicCamera {
+  return isObject(value) && !!(value.isOrthographicCamera)
+}
+
+/**
+ * Type guard to check if a value is a Three.js PerspectiveCamera
+ * @param value - The value to check
+ * @returns True if the value is a Three.js PerspectiveCamera instance, false otherwise
+ */
+export function isPerspectiveCamera(value: unknown): value is PerspectiveCamera {
+  return isObject(value) && !!(value.isPerspectiveCamera)
+}
+
 /**
  * Type guard to check if a value is a Three.js BufferGeometry
  * @param value - The value to check
@@ -299,3 +317,20 @@ export function isTresObject(value: unknown): value is TresObject {
 export function isTresPrimitive(value: unknown): value is TresPrimitive {
   return isObject(value) && !!(value.isPrimitive)
 }
+
+/**
+ * Type guard to check if a value is a TresInstance (has __tres property)
+ * @param value - The value to check
+ * @returns True if the value is a TresInstance (has __tres property), false otherwise
+ * @example
+ * ```ts
+ * const value = new THREE.Mesh()
+ * if (isTresInstance(value)) {
+ *   // TypeScript knows value is TresInstance here
+ *   // You can safely access value.__tres
+ * }
+ * ```
+ */
+export function isTresInstance(value: unknown): value is TresInstance {
+  return isTresObject(value) && '__tres' in value
+}