diff --git a/.storybook/stories/CameraControls.stories.tsx b/.storybook/stories/CameraControls.stories.tsx new file mode 100644 index 000000000..d297971d9 --- /dev/null +++ b/.storybook/stories/CameraControls.stories.tsx @@ -0,0 +1,89 @@ +import { createPortal, useFrame } from '@react-three/fiber' +import React, { useRef, useState } from 'react' +import { Scene } from 'three' + +import { Setup } from '../Setup' +import { Box, CameraControls, PerspectiveCamera, Plane, useFBO } from '../../src' + +import type { Camera } from 'three' +import type { CameraControlsProps } from '../../src' + +const args = {} + +export const CameraControlsStory = (props: CameraControlsProps) => { + const cameraControlRef = useRef(null) + + return ( + <> + + { + cameraControlRef.current?.rotate(Math.PI / 4, 0, true) + }} + > + + + + ) +} + +CameraControlsStory.args = args +CameraControlsStory.storyName = 'Default' + +export default { + title: 'Controls/CameraControls', + component: CameraControls, + decorators: [(storyFn) => {storyFn()}], +} + +const CustomCamera = (props: CameraControlsProps) => { + /** + * we will render our scene in a render target and use it as a map. + */ + const fbo = useFBO(400, 400) + const virtualCamera = useRef() + const [virtualScene] = useState(() => new Scene()) + const cameraControlRef = useRef(null) + + useFrame(({ gl }) => { + if (virtualCamera.current) { + gl.setRenderTarget(fbo) + gl.render(virtualScene, virtualCamera.current) + + gl.setRenderTarget(null) + } + }) + + return ( + <> + { + cameraControlRef.current?.rotate(Math.PI / 4, 0, true) + }} + > + + + + {createPortal( + <> + + + + + + + + {/* @ts-ignore */} + + , + virtualScene + )} + + ) +} + +export const CustomCameraStory = (props: CameraControlsProps) => + +CustomCameraStory.args = args +CustomCameraStory.storyName = 'Custom Camera' diff --git a/README.md b/README.md index 426858c68..0aa6aceb8 100644 --- a/README.md +++ b/README.md @@ -337,7 +337,7 @@ If available controls have damping enabled by default, they manage their own upd Some controls allow you to set `makeDefault`, similar to, for instance, PerspectiveCamera. This will set @react-three/fiber's `controls` field in the root store. This can make it easier in situations where you want controls to be known and other parts of the app could respond to it. Some drei controls already take it into account, like CameraShake, Gizmo and TransformControls. -Drei currently exports OrbitControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-orbitcontrols--orbit-controls-story), MapControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-mapcontrols--map-controls-scene-st), TrackballControls, ArcballControls, FlyControls, DeviceOrientationControls, PointerLockControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-pointerlockcontrols--pointer-lock-controls-scene-st), FirstPersonControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-firstpersoncontrols--first-person-controls-story) +Drei currently exports OrbitControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-orbitcontrols--orbit-controls-story), MapControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-mapcontrols--map-controls-scene-st), TrackballControls, ArcballControls, FlyControls, DeviceOrientationControls, PointerLockControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-pointerlockcontrols--pointer-lock-controls-scene-st), FirstPersonControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-firstpersoncontrols--first-person-controls-story) and [CameraControls](https://github.com/yomotsu/camera-controls) [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-cameracontrols--camera-controls-story) All controls react to the default camera. If you have a `` in your scene, they will control it. If you need to inject an imperative camera or one that isn't the default, use the `camera` prop: ``. diff --git a/package.json b/package.json index 1534cf7c3..e19c7202b 100644 --- a/package.json +++ b/package.json @@ -57,11 +57,13 @@ "@babel/runtime": "^7.11.2", "@react-spring/three": "^9.3.1", "@use-gesture/react": "^10.2.0", + "camera-controls": "^1.37.6", "detect-gpu": "^5.0.5", "glsl-noise": "^0.0.0", "lodash.clamp": "^4.0.3", "lodash.omit": "^4.5.0", "lodash.pick": "^4.4.0", + "maath": "^0.5.1", "meshline": "^3.1.6", "react-composer": "^5.0.3", "react-merge-refs": "^1.1.0", @@ -71,7 +73,6 @@ "three-stdlib": "^2.20.4", "troika-three-text": "^0.47.1", "utility-types": "^3.10.0", - "maath": "^0.5.1", "zustand": "^3.5.13" }, "devDependencies": { diff --git a/src/core/CameraControls.tsx b/src/core/CameraControls.tsx new file mode 100644 index 000000000..66b5a1e2b --- /dev/null +++ b/src/core/CameraControls.tsx @@ -0,0 +1,50 @@ +import * as THREE from 'three' +import type { PerspectiveCamera, OrthographicCamera } from 'three' + +import * as React from 'react' +import { forwardRef, useMemo, useEffect } from 'react' +import { extend, useFrame, useThree, ReactThreeFiber, EventManager } from '@react-three/fiber' + +import CameraControlsImpl from 'camera-controls' + +export type CameraControlsProps = Omit< + ReactThreeFiber.Overwrite< + ReactThreeFiber.Node, + { + camera?: PerspectiveCamera | OrthographicCamera + domElement?: HTMLElement + } + >, + 'ref' +> + +export const CameraControls = forwardRef((props, ref) => { + useMemo(() => { + CameraControlsImpl.install({ THREE }) + extend({ CameraControlsImpl }) + }, []) + + const { camera, domElement, ...restProps } = props + + const defaultCamera = useThree((state) => state.camera) + const gl = useThree((state) => state.gl) + const invalidate = useThree((state) => state.invalidate) + const events = useThree((state) => state.events) as EventManager + + const explCamera = camera || defaultCamera + const explDomElement = (domElement || events.connected || gl.domElement) as HTMLElement + + const cameraControls = useMemo(() => new CameraControlsImpl(explCamera, explDomElement), [explCamera, explDomElement]) + + useFrame((state, delta) => { + if (cameraControls.enabled) cameraControls.update(delta) + }, -1) + + useEffect(() => { + return () => void cameraControls.dispose() + }, [explDomElement, cameraControls, invalidate]) + + return +}) + +export type CameraControls = CameraControlsImpl diff --git a/src/core/index.ts b/src/core/index.ts index 3de3684e4..426a6979a 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -37,6 +37,7 @@ export * from './ArcballControls' export * from './TransformControls' export * from './PointerLockControls' export * from './FirstPersonControls' +export * from './CameraControls' // Gizmos export * from './GizmoHelper' diff --git a/yarn.lock b/yarn.lock index cbac85951..5b065c6f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4595,6 +4595,11 @@ camelcase@^6.0.0, camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +camera-controls@^1.37.6: + version "1.37.6" + resolved "https://registry.yarnpkg.com/camera-controls/-/camera-controls-1.37.6.tgz#d632f58e3b118921609908b53fbc328844d0e904" + integrity sha512-Fpppn3RwHgmGPfnjRVtK9AlpjcPdYo/6lFTqsSJ+gk9jRi48VmLFEBZ6uLLmTQiKiKjrs906ZMaAJW3fXIChdA== + caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001317: version "1.0.30001322" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001322.tgz#2e4c09d11e1e8f852767dab287069a8d0c29d623"