Skip to content

Commit b28adc7

Browse files
committed
feat(Controls): adding CameraControls
1 parent 175206d commit b28adc7

File tree

6 files changed

+148
-2
lines changed

6 files changed

+148
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { createPortal, useFrame } from '@react-three/fiber'
2+
import React, { useRef, useState } from 'react'
3+
import { Scene } from 'three'
4+
5+
import { Setup } from '../Setup'
6+
import { Box, CameraControls, PerspectiveCamera, Plane, useFBO } from '../../src'
7+
8+
import type { Camera } from 'three'
9+
import type { CameraControlsProps } from '../../src'
10+
11+
const args = {}
12+
13+
export const CameraControlsStory = (props: CameraControlsProps) => {
14+
const cameraControlRef = useRef<CameraControls | null>(null)
15+
16+
return (
17+
<>
18+
<CameraControls ref={cameraControlRef} {...props} />
19+
<Box
20+
onClick={() => {
21+
cameraControlRef.current?.rotate(Math.PI / 4, 0, true)
22+
}}
23+
>
24+
<meshBasicMaterial wireframe />
25+
</Box>
26+
</>
27+
)
28+
}
29+
30+
CameraControlsStory.args = args
31+
CameraControlsStory.storyName = 'Default'
32+
33+
export default {
34+
title: 'Controls/CameraControls',
35+
component: CameraControls,
36+
decorators: [(storyFn) => <Setup controls={false}>{storyFn()}</Setup>],
37+
}
38+
39+
const CustomCamera = (props: CameraControlsProps) => {
40+
/**
41+
* we will render our scene in a render target and use it as a map.
42+
*/
43+
const fbo = useFBO(400, 400)
44+
const virtualCamera = useRef<CameraControls['camera']>()
45+
const [virtualScene] = useState(() => new Scene())
46+
const cameraControlRef = useRef<CameraControls | null>(null)
47+
48+
useFrame(({ gl }) => {
49+
if (virtualCamera.current) {
50+
gl.setRenderTarget(fbo)
51+
gl.render(virtualScene, virtualCamera.current)
52+
53+
gl.setRenderTarget(null)
54+
}
55+
})
56+
57+
return (
58+
<>
59+
<Plane
60+
args={[4, 4, 4]}
61+
onClick={() => {
62+
cameraControlRef.current?.rotate(Math.PI / 4, 0, true)
63+
}}
64+
>
65+
<meshBasicMaterial map={fbo.texture} />
66+
</Plane>
67+
68+
{createPortal(
69+
<>
70+
<Box>
71+
<meshBasicMaterial wireframe />
72+
</Box>
73+
74+
<PerspectiveCamera name="FBO Camera" ref={virtualCamera} position={[0, 0, 5]} />
75+
<CameraControls ref={cameraControlRef} camera={virtualCamera.current} {...props} />
76+
77+
{/* @ts-ignore */}
78+
<color attach="background" args={['hotpink']} />
79+
</>,
80+
virtualScene
81+
)}
82+
</>
83+
)
84+
}
85+
86+
export const CustomCameraStory = (props: CameraControlsProps) => <CustomCamera {...props} />
87+
88+
CustomCameraStory.args = args
89+
CustomCameraStory.storyName = 'Custom Camera'

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ If available controls have damping enabled by default, they manage their own upd
337337
338338
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.
339339
340-
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)
340+
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)
341341
342342
All controls react to the default camera. If you have a `<PerspectiveCamera makeDefault />` 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: `<OrbitControls camera={MyCamera} />`.
343343

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,13 @@
5757
"@babel/runtime": "^7.11.2",
5858
"@react-spring/three": "^9.3.1",
5959
"@use-gesture/react": "^10.2.0",
60+
"camera-controls": "^1.37.6",
6061
"detect-gpu": "^5.0.5",
6162
"glsl-noise": "^0.0.0",
6263
"lodash.clamp": "^4.0.3",
6364
"lodash.omit": "^4.5.0",
6465
"lodash.pick": "^4.4.0",
66+
"maath": "^0.5.1",
6567
"meshline": "^3.1.6",
6668
"react-composer": "^5.0.3",
6769
"react-merge-refs": "^1.1.0",
@@ -71,7 +73,6 @@
7173
"three-stdlib": "^2.20.4",
7274
"troika-three-text": "^0.47.1",
7375
"utility-types": "^3.10.0",
74-
"maath": "^0.5.1",
7576
"zustand": "^3.5.13"
7677
},
7778
"devDependencies": {

src/core/CameraControls.tsx

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as THREE from 'three'
2+
import type { PerspectiveCamera, OrthographicCamera } from 'three'
3+
4+
import * as React from 'react'
5+
import { forwardRef, useMemo, useEffect } from 'react'
6+
import { extend, useFrame, useThree, ReactThreeFiber, EventManager } from '@react-three/fiber'
7+
8+
import CameraControlsImpl from 'camera-controls'
9+
10+
export type CameraControlsProps = Omit<
11+
ReactThreeFiber.Overwrite<
12+
ReactThreeFiber.Node<CameraControlsImpl, typeof CameraControlsImpl>,
13+
{
14+
camera?: PerspectiveCamera | OrthographicCamera
15+
domElement?: HTMLElement
16+
}
17+
>,
18+
'ref'
19+
>
20+
21+
export const CameraControls = forwardRef<CameraControlsImpl, CameraControlsProps>((props, ref) => {
22+
useMemo(() => {
23+
CameraControlsImpl.install({ THREE })
24+
extend({ CameraControlsImpl })
25+
}, [])
26+
27+
const { camera, domElement, ...restProps } = props
28+
29+
const defaultCamera = useThree((state) => state.camera)
30+
const gl = useThree((state) => state.gl)
31+
const invalidate = useThree((state) => state.invalidate)
32+
const events = useThree((state) => state.events) as EventManager<HTMLElement>
33+
34+
const explCamera = camera || defaultCamera
35+
const explDomElement = (domElement || events.connected || gl.domElement) as HTMLElement
36+
37+
const cameraControls = useMemo(() => new CameraControlsImpl(explCamera, explDomElement), [explCamera, explDomElement])
38+
39+
useFrame((state, delta) => {
40+
if (cameraControls.enabled) cameraControls.update(delta)
41+
}, -1)
42+
43+
useEffect(() => {
44+
return () => void cameraControls.dispose()
45+
}, [explDomElement, cameraControls, invalidate])
46+
47+
return <primitive ref={ref} object={cameraControls} {...restProps} />
48+
})
49+
50+
export type CameraControls = CameraControlsImpl

src/core/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export * from './ArcballControls'
3737
export * from './TransformControls'
3838
export * from './PointerLockControls'
3939
export * from './FirstPersonControls'
40+
export * from './CameraControls'
4041

4142
// Gizmos
4243
export * from './GizmoHelper'

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -4595,6 +4595,11 @@ camelcase@^6.0.0, camelcase@^6.2.0:
45954595
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
45964596
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
45974597

4598+
camera-controls@^1.37.6:
4599+
version "1.37.6"
4600+
resolved "https://registry.yarnpkg.com/camera-controls/-/camera-controls-1.37.6.tgz#d632f58e3b118921609908b53fbc328844d0e904"
4601+
integrity sha512-Fpppn3RwHgmGPfnjRVtK9AlpjcPdYo/6lFTqsSJ+gk9jRi48VmLFEBZ6uLLmTQiKiKjrs906ZMaAJW3fXIChdA==
4602+
45984603
caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001317:
45994604
version "1.0.30001322"
46004605
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001322.tgz#2e4c09d11e1e8f852767dab287069a8d0c29d623"

0 commit comments

Comments
 (0)