1
0
Эх сурвалжийг харах

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
alvarosabu 2 долоо хоног өмнө
parent
commit
2a0786ecab

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

@@ -0,0 +1,76 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+import type { OrthographicCamera } from 'three'
+import { Vector3 } from 'three'
+import { TresLeches, useControls } from '@tresjs/leches'
+
+const { left, right, top, bottom, zoom, position, lookAt, near, far } = useControls({
+  position: new Vector3(1, 1, 1),
+  lookAt: new Vector3(0, 0, 0),
+  top: {
+    value: 500,
+    min: -1000,
+    max: 1000,
+    step: 1,
+  },
+  bottom: {
+    value: -500,
+    min: -1000,
+    max: 1000,
+    step: 1,
+  },
+  left: {
+    value: -500,
+    min: -1000,
+    max: 1000,
+    step: 1,
+  },
+  right: {
+    value: 500,
+    min: -100,
+    max: 1000,
+    step: 1,
+  },
+  zoom: {
+    value: 1,
+    min: 1,
+    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([left, right, top, bottom, zoom, near, far], () => {
+  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="[left, right, top, bottom, near, 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>

+ 27 - 51
playground/vue/src/pages/cameras/index.vue

@@ -1,31 +1,32 @@
 <script setup lang="ts">
 import { Box } from '@tresjs/cientos'
+import type { TresCamera } from '@tresjs/core'
 import { TresCanvas } from '@tresjs/core'
 import { TresLeches, useControls } from '@tresjs/leches'
-import { BasicShadowMap, NoToneMapping, OrthographicCamera, PerspectiveCamera, SRGBColorSpace } from 'three'
+import { OrthographicCamera, PerspectiveCamera } 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 perspectiveCamera = new PerspectiveCamera(75, 1, 0.1, 1000)
+const frustumSize = 10
+const orthographicCamera = new OrthographicCamera(
+  -frustumSize,
+  frustumSize,
+  frustumSize,
+  -frustumSize,
+  0.1,
+  1000,
+)
 
-const state = reactive({
-  cameraType: 'perspective',
-  camera: new PerspectiveCamera(75, 1, 0.1, 1000) as Cam,
-})
+perspectiveCamera.position.set(8, 8, 8)
+perspectiveCamera.lookAt(0, 0, 0)
+orthographicCamera.position.set(8, 8, 8)
+orthographicCamera.lookAt(0, 0, 0)
 
-state.camera.position.set(5, 5, 5)
-state.camera.lookAt(0, 0, 0)
+const currentCamera = ref<TresCamera>(perspectiveCamera)
 
-const { cameraType, manual } = useControls({
+const { cameraType } = useControls({
   cameraType: {
-    label: 'CameraType',
+    value: 'perspective',
     options: [{
       text: 'Perspective',
       value: 'perspective',
@@ -33,50 +34,25 @@ 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
+watch(cameraType, (newCameraType) => {
+  if (newCameraType === 'perspective') {
+    currentCamera.value = perspectiveCamera
   }
-  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)
+  else {
+    currentCamera.value = orthographicCamera
   }
-})
-
-const asyncTorus = ref(false)
-
-setTimeout(() => {
-  asyncTorus.value = true
-}, 1000)
+}, { immediate: true })
 </script>
 
 <template>
   <TresLeches />
   <TresCanvas
-    v-bind="gl"
-    ref="context"
-    :camera="state.camera"
+    clear-color="#82DBC5"
+    :camera="currentCamera"
   >
-    <!--     <TresPerspectiveCamera v-if="state.cameraType === 'perspective'" :position="[11, 11, 11]" />
-    <TresOrthographicCamera v-if="state.cameraType === 'orthographic'" :position="[11, 11, 11]" /> -->
     <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',

+ 11 - 7
src/components/TresCanvas.vue

@@ -5,7 +5,7 @@ import type {
   ToneMapping,
   WebGLRendererParameters,
 } from 'three'
-import type { App, Ref } from 'vue'
+import type { App, MaybeRef, Ref } from 'vue'
 import type { RendererPresetsType } from '../composables/useRenderer/const'
 import type { TresCamera, TresObject, TresScene } from '../types/'
 import { PerspectiveCamera, Scene } from 'three'
@@ -22,6 +22,7 @@ import {
   provide,
   ref,
   shallowRef,
+  toValue,
   watch,
   watchEffect,
 } from 'vue'
@@ -50,7 +51,7 @@ export interface TresCanvasProps
   dpr?: number | [number, number]
 
   // required by useTresContextProvider
-  camera?: TresCamera
+  camera?: MaybeRef<TresCamera>
   preset?: RendererPresetsType
   windowSize?: boolean
 
@@ -192,7 +193,8 @@ onMounted(() => {
     emit,
   })
 
-  const { registerCamera, camera, cameras, deregisterCamera } = context.value
+  const { camera } = context.value
+  const { registerCamera, cameras, activeCamera, deregisterCamera } = camera
 
   mountCustomRenderer(context.value)
 
@@ -219,10 +221,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 +234,7 @@ onMounted(() => {
     },
   )
 
-  if (!camera.value) {
+  if (!activeCamera.value) {
     addDefaultCamera()
   }
 

+ 1 - 1
src/composables/index.ts

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

+ 116 - 47
src/composables/useCamera/index.ts

@@ -1,85 +1,154 @@
-import type { OrthographicCamera } from 'three'
-import type { TresScene } from '../../types'
 import type { TresContext } from '../useTresContextProvider'
 
-import { Camera, PerspectiveCamera } from 'three'
+import type { ComputedRef, Ref } from 'vue'
 import { computed, onUnmounted, ref, watchEffect } from 'vue'
-import { isCamera } from '../../utils/is'
+import { isOrthographicCamera, isPerspectiveCamera } from '../../utils/is'
+import type { TresCamera } from '../../types'
 
-export const useCamera = ({ sizes }: Pick<TresContext, 'sizes'> & { scene: TresScene }) => {
-  // the computed does not trigger, when for example the camera position changes
-  const cameras = ref<Camera[]>([])
-  const camera = computed<Camera | undefined>(
-    () => cameras.value[0],
+/**
+ * Interface for the return value of the useCamera composable
+ */
+export interface UseCameraReturn {
+  /**
+   * The active camera
+   */
+  activeCamera: ComputedRef<TresCamera | undefined>
+  /**
+   * The list of cameras
+   */
+  cameras: Ref<TresCamera[]>
+  /**
+   * Register a camera
+   * @param camera - The camera to register
+   * @param active - Whether to set the camera as active
+   */
+  registerCamera: (camera: TresCamera, active?: boolean) => void
+  /**
+   * Deregister a camera
+   * @param camera - The camera to deregister
+   */
+  deregisterCamera: (camera: TresCamera) => void
+  /**
+   * Set the active camera
+   * @param cameraOrUuid - The camera or its UUID to set as active
+   */
+  setActiveCamera: (cameraOrUuid: string | TresCamera) => 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 useCamera = ({ sizes }: UseCameraParams): UseCameraReturn => {
+  // Store all registered cameras
+  const cameras = ref<TresCamera[]>([])
+  // Store the UUID of the active camera
+  const activeCameraUuid = ref<string | null>(null)
+
+  // Computed property that returns the active camera
+  const activeCamera = computed<TresCamera | undefined>(
+    () => cameras.value.find(camera => camera.uuid === activeCameraUuid.value),
   )
 
-  const setCameraActive = (cameraOrUuid: string | Camera) => {
-    const camera = cameraOrUuid instanceof Camera
-      ? cameraOrUuid
-      : cameras.value.find((camera: Camera) => camera.uuid === cameraOrUuid)
+  /**
+   * Set the active camera
+   * @param cameraOrUuid - The camera or its UUID to set as active
+   */
+  const setActiveCamera = (cameraOrUuid: string | TresCamera): void => {
+    const camera = cameras.value.find((camera: TresCamera) => camera.uuid === cameraOrUuid)
 
     if (!camera) { return }
 
+    // Move the active camera to the beginning of the array
     const otherCameras = cameras.value.filter(({ uuid }) => uuid !== camera.uuid)
     cameras.value = [camera, ...otherCameras]
+    activeCameraUuid.value = camera.uuid
   }
 
-  const registerCamera = (maybeCamera: unknown, active = false) => {
-    if (isCamera(maybeCamera)) {
-      const camera = maybeCamera
-      if (cameras.value.some(({ uuid }) => uuid === camera.uuid)) { return }
-
-      if (active) { setCameraActive(camera) }
-      else { cameras.value.push(camera) }
+  /**
+   * Register a camera
+   * @param camera - The camera to register
+   * @param active - Whether to set the camera as active
+   */
+  const registerCamera = (camera: TresCamera, active = false): void => {
+    // Skip if camera is already registered
+    // Skip if camera is already registered
+    if (cameras.value.some(({ uuid }) => uuid === camera.uuid)) { return }
+    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: TresCamera): void => {
+    cameras.value = cameras.value.filter(({ uuid }) => uuid !== camera.uuid)
+
+    // If the deregistered camera was active, clear the active camera
+    if (activeCameraUuid.value === camera.uuid) {
+      activeCameraUuid.value = null
     }
   }
 
+  /**
+   * 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: TresCamera & { manual?: boolean }) => {
+        // Skip if camera is marked as manual by the user
+        if (camera.manual) { return }
+
+        // Update perspective camera
+        if (isPerspectiveCamera(camera)) {
+          camera.aspect = sizes.aspectRatio.value
+          camera.updateProjectionMatrix()
+        }
+        // Update orthographic camera
+        else if (isOrthographicCamera(camera)) {
+          // Use a fixed frustum size for better visualization
+          const frustumSize = 10
+          const aspect = sizes.aspectRatio.value
+
+          camera.left = frustumSize * aspect / -2
+          camera.right = frustumSize * aspect / 2
+          camera.top = frustumSize / 2
+          camera.bottom = frustumSize / -2
+
+          // Ensure the camera is at a good position to see the scene
+          if (!camera.position.z) {
+            camera.position.z = 10
           }
+
           camera.updateProjectionMatrix()
         }
       })
     }
   })
 
+  // Clean up on unmount
   onUnmounted(() => {
     cameras.value = []
+    activeCameraUuid.value = null
   })
 
   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
-}

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

@@ -30,9 +30,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 +84,14 @@ 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)
+    const unprojectedPoint = new Vector3(event?.clientX, event?.clientY, 0).unproject(ctx.camera.activeCamera.value as TresCamera)
     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,

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

@@ -110,7 +110,7 @@ export function useRenderer(
     scene: Scene
     options: UseRendererOptions
     emit: EmitEventFn
-    contextParts: Pick<TresContext, 'sizes' | 'camera' | 'render'> & { invalidate: () => void, advance: () => void }
+    contextParts: Pick<TresContext, 'sizes' | 'render'> & { invalidate: () => void, advance: () => void }
   },
 ) {
   const webGLRendererConstructorParameters = computed<WebGLRendererParameters>(() => ({

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

@@ -1,14 +1,15 @@
-import type { Camera, WebGLRenderer } from 'three'
-import type { ComputedRef, DeepReadonly, MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
+import type { WebGLRenderer } from 'three'
+import type { ComputedRef, MaybeRef, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
 import type { RendererLoop } from '../../core/loop'
 import type { EmitEventFn, TresControl, TresObject, TresScene } from '../../types'
 import type { UseRendererOptions } from '../useRenderer'
 import { Raycaster } from 'three'
-import { computed, inject, onUnmounted, provide, readonly, ref, shallowRef } from 'vue'
+import { computed, 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 { useCamera } from '../useCamera/'
 import { useRenderer } from '../useRenderer'
 import useSizes, { type SizesType } from '../useSizes'
 import { type TresEventManager, useTresEventManager } from '../useTresEventManager'
@@ -50,8 +51,7 @@ 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: ShallowRef<WebGLRenderer>
   raycaster: ShallowRef<Raycaster>
@@ -67,10 +67,6 @@ export interface TresContext {
    * Advance one frame when renderMode === 'manual'
    */
   advance: () => void
-  // 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
@@ -98,13 +94,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 = useCamera({ sizes })
 
   // Render state
 
@@ -136,7 +126,7 @@ export function useTresContextProvider({
       options: rendererOptions,
       emit,
       // TODO: replace contextParts with full ctx at https://github.com/Tresjs/tres/issues/516
-      contextParts: { sizes, camera, render, invalidate, advance },
+      contextParts: { sizes, render, invalidate, advance },
     },
   )
 
@@ -144,7 +134,6 @@ export function useTresContextProvider({
     sizes,
     scene: localScene,
     camera,
-    cameras: readonly(cameras),
     renderer,
     raycaster: shallowRef(new Raycaster()),
     controls: ref(null),
@@ -164,9 +153,6 @@ export function useTresContextProvider({
     advance,
     extend,
     invalidate,
-    registerCamera,
-    setCameraActive,
-    deregisterCamera,
     loop: createRenderLoop(),
   }
 
@@ -180,8 +166,8 @@ export function useTresContextProvider({
   // The loop
 
   ctx.loop.register(() => {
-    if (camera.value && render.frames.value > 0) {
-      renderer.value.render(scene, camera.value)
+    if (camera.activeCamera.value && render.frames.value > 0) {
+      renderer.value.render(scene, camera.activeCamera.value)
       emit('render', ctx.renderer.value)
     }
 

+ 1 - 1
src/core/loop.ts

@@ -125,7 +125,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
 }
 

+ 11 - 3
src/core/nodeOps.ts

@@ -1,10 +1,10 @@
 import type { TresContext } from '../composables'
-import type { DisposeType, LocalState, TresInstance, TresObject, TresObject3D, TresPrimitive, WithMathProps } from '../types'
+import type { DisposeType, LocalState, TresCamera, TresInstance, TresObject, TresObject3D, TresPrimitive, WithMathProps } from '../types'
 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, isUndefined } from '../utils/is'
 import { createRetargetingProxy } from '../utils/primitive/createRetargetingProxy'
 import { catalogue } from './catalogue'
 
@@ -76,6 +76,7 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
 
     if (!obj) { return null }
 
+    // Opinionated default to avoid user issue not seeing anything if camera is on origin
     if (obj.isCamera) {
       if (!props?.position) {
         obj.position.set(3, 3, 3)
@@ -114,7 +115,10 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
       context.eventManager?.registerObject(child)
     }
 
-    context.registerCamera(child)
+    if (isCamera(child)) {
+      context.camera?.registerCamera(child as TresCamera)
+      context.camera?.setActiveCamera(child.uuid)
+    }
     // NOTE: Track onPointerMissed objects separate from the scene
     context.eventManager?.registerPointerMissedObject(child)
 
@@ -339,6 +343,10 @@ export const nodeOps: (context: TresContext) => RendererOptions<TresObject, Tres
     else if (!target.isColor && target.setScalar) { target.setScalar(value) }
     else { target.set(value) }
 
+    if (node.isCamera) {
+      node.updateProjectionMatrix()
+    }
+
     invalidateInstance(node as TresObject)
   }
 

+ 2 - 2
src/utils/index.ts

@@ -566,13 +566,13 @@ export function doRemoveDeregister(node: TresObject, context: TresContext) {
   // TODO: Refactor as `context.deregister`?
   // That would eliminate `context.deregisterCamera`.
   node.traverse?.((child: TresObject) => {
-    context.deregisterCamera(child)
+    context.camera.deregisterCamera(child)
     // deregisterAtPointerEventHandlerIfRequired?.(child as TresObject)
     context.eventManager?.deregisterPointerMissedObject(child)
   })
 
   // NOTE: Deregister `node`
-  context.deregisterCamera(node)
+  context.camera.deregisterCamera(node)
   /*  deregisterAtPointerEventHandlerIfRequired?.(node as TresObject) */
   invalidateInstance(node as TresObject)
 }

+ 19 - 1
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 { BufferGeometry, Camera, Fog, Light, Material, Object3D, OrthographicCamera, PerspectiveCamera, Scene } from 'three'
 
 /**
  * Type guard to check if a value is undefined
@@ -163,6 +163,24 @@ export function isCamera(value: unknown): value is Camera {
   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