Skip to content

Commit

Permalink
feat: allow ortho and pers cams to film into fbos
Browse files Browse the repository at this point in the history
  • Loading branch information
drcmda committed Oct 23, 2022
1 parent 566b703 commit b46d9a1
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 69 deletions.
39 changes: 39 additions & 0 deletions README.md
Expand Up @@ -209,6 +209,21 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de

[![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/camera-perspectivecamera--perspective-camera-scene-st)

```tsx
type Props = Omit<JSX.IntrinsicElements['perspectiveCamera'], 'children'> & {
/** Registers the camera as the system default, fiber will start rendering with it */
makeDefault?: boolean
/** Making it manual will stop responsiveness and you have to calculate aspect ratio yourself. */
manual?: boolean
/** The contents will either follow the camera, or be hidden when filming if you pass a function */
children?: React.ReactNode | ((texture: THREE.Texture) => React.ReactNode)
/** Number of frames to render, 0 */
frames?: number
/** Resolution of the FBO, 256 */
resolution?: number
}
```
A responsive [THREE.PerspectiveCamera](https://threejs.org/docs/#api/en/cameras/PerspectiveCamera) that can set itself as the default.
```jsx
Expand All @@ -230,6 +245,18 @@ You can also drive it manually, it won't be responsive and you have to calculate
<PerspectiveCamera manual aspect={...} onUpdate={(c) => c.updateProjectionMatrix()}>
```
You can use the PerspectiveCamera to film contents into a RenderTarget, similar to CubeCamera. As a child you must provide a render-function which receives the texture as its first argument. The result of that function will _not follow the camera_, instead it will be set invisible while the the FBO renders so as to avoid issues where the meshes that receive the texture are interrering.
```jsx
<PerspectiveCamera position={[0, 0, 10]}>
{(texture) => (
<mesh geometry={plane}>
<meshBasicMaterial map={texture} />
</mesh>
)}
</PerspectiveCamera>
```
#### OrthographicCamera
[![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/camera-orthographiccamera--orthographic-camera-scene-st)
Expand All @@ -242,6 +269,18 @@ A responsive [THREE.OrthographicCamera](https://threejs.org/docs/#api/en/cameras
</OrthographicCamera>
```
You can use the OrthographicCamera to film contents into a RenderTarget, it has the same API as OrthographicCamera.
```jsx
<OrthographicCamera position={[0, 0, 10]}>
{(texture) => (
<mesh geometry={plane}>
<meshBasicMaterial map={texture} />
</mesh>
)}
</OrthographicCamera>
```
#### CubeCamera
[![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.pmnd.rs/?path=/story/camera-cubecamera--default-story)
Expand Down
108 changes: 70 additions & 38 deletions src/core/OrthographicCamera.tsx
@@ -1,48 +1,80 @@
import * as THREE from 'three'
import * as React from 'react'
import { OrthographicCamera as OrthographicCameraImpl } from 'three'
import { useThree } from '@react-three/fiber'
import { useThree, useFrame } from '@react-three/fiber'
import mergeRefs from 'react-merge-refs'
import { useFBO } from './useFBO'

type Props = JSX.IntrinsicElements['orthographicCamera'] & {
const isFunction = (node: any): node is Function => typeof node === 'function'

type Props = Omit<JSX.IntrinsicElements['orthographicCamera'], 'children'> & {
/** Registers the camera as the system default, fiber will start rendering with it */
makeDefault?: boolean
/** Making it manual will stop responsiveness and you have to calculate aspect ratio yourself. */
manual?: boolean
children?: React.ReactNode
/** The contents will either follow the camera, or be hidden when filming if you pass a function */
children?: React.ReactNode | ((texture: THREE.Texture) => React.ReactNode)
/** Number of frames to render, Infinity */
frames?: number
/** Resolution of the FBO, 256 */
resolution?: number
}

export const OrthographicCamera = React.forwardRef(({ makeDefault, ...props }: Props, ref) => {
const set = useThree(({ set }) => set)
const camera = useThree(({ camera }) => camera)
const size = useThree(({ size }) => size)
const cameraRef = React.useRef<OrthographicCameraImpl>(null!)
export const OrthographicCamera = React.forwardRef(
({ resolution = 256, frames = Infinity, children, makeDefault, ...props }: Props, ref) => {
const set = useThree(({ set }) => set)
const camera = useThree(({ camera }) => camera)
const size = useThree(({ size }) => size)
const cameraRef = React.useRef<OrthographicCameraImpl>(null!)
const groupRef = React.useRef<THREE.Group>(null!)
const fbo = useFBO(resolution)

React.useLayoutEffect(() => {
if (!props.manual) {
cameraRef.current.updateProjectionMatrix()
}
}, [size, props])

React.useLayoutEffect(() => {
if (!props.manual) {
React.useLayoutEffect(() => {
cameraRef.current.updateProjectionMatrix()
}
}, [size, props])

React.useLayoutEffect(() => {
cameraRef.current.updateProjectionMatrix()
})

React.useLayoutEffect(() => {
if (makeDefault) {
const oldCam = camera
set(() => ({ camera: cameraRef.current! }))
return () => set(() => ({ camera: oldCam }))
}
// The camera should not be part of the dependency list because this components camera is a stable reference
// that must exchange the default, and clean up after itself on unmount.
}, [cameraRef, makeDefault, set])

return (
<orthographicCamera
left={size.width / -2}
right={size.width / 2}
top={size.height / 2}
bottom={size.height / -2}
ref={mergeRefs([cameraRef, ref])}
{...props}
/>
)
})
})

React.useLayoutEffect(() => {
if (makeDefault) {
const oldCam = camera
set(() => ({ camera: cameraRef.current! }))
return () => set(() => ({ camera: oldCam }))
}
// The camera should not be part of the dependency list because this components camera is a stable reference
// that must exchange the default, and clean up after itself on unmount.
}, [cameraRef, makeDefault, set])

let count = 0
const functional = isFunction(children)
useFrame((state) => {
if (functional && (frames === Infinity || count < frames)) {
groupRef.current.visible = false
state.gl.setRenderTarget(fbo)
state.gl.render(state.scene, cameraRef.current)
state.gl.setRenderTarget(null)
groupRef.current.visible = true
}
})

return (
<>
<orthographicCamera
left={size.width / -2}
right={size.width / 2}
top={size.height / 2}
bottom={size.height / -2}
ref={mergeRefs([cameraRef, ref])}
{...props}
>
{!functional && children}
</orthographicCamera>
<group ref={groupRef}>{functional && children(fbo.texture)}</group>
</>
)
}
)
96 changes: 65 additions & 31 deletions src/core/PerspectiveCamera.tsx
@@ -1,39 +1,73 @@
import * as THREE from 'three'
import * as React from 'react'
import { PerspectiveCamera as PerspectiveCameraImpl } from 'three'
import { useThree } from '@react-three/fiber'
import { useFrame, useThree } from '@react-three/fiber'
import mergeRefs from 'react-merge-refs'
import { useFBO } from './useFBO'

type Props = JSX.IntrinsicElements['perspectiveCamera'] & {
const isFunction = (node: any): node is Function => typeof node === 'function'

type Props = Omit<JSX.IntrinsicElements['perspectiveCamera'], 'children'> & {
/** Registers the camera as the system default, fiber will start rendering with it */
makeDefault?: boolean
/** Making it manual will stop responsiveness and you have to calculate aspect ratio yourself. */
manual?: boolean
children?: React.ReactNode
/** The contents will either follow the camera, or be hidden when filming if you pass a function */
children?: React.ReactNode | ((texture: THREE.Texture) => React.ReactNode)
/** Number of frames to render, Infinity */
frames?: number
/** Resolution of the FBO, 256 */
resolution?: number
}

export const PerspectiveCamera = React.forwardRef(({ makeDefault, ...props }: Props, ref) => {
const set = useThree(({ set }) => set)
const camera = useThree(({ camera }) => camera)
const size = useThree(({ size }) => size)
const cameraRef = React.useRef<PerspectiveCameraImpl>(null!)

React.useLayoutEffect(() => {
if (!props.manual) {
cameraRef.current.aspect = size.width / size.height
}
}, [size, props])

React.useLayoutEffect(() => {
cameraRef.current.updateProjectionMatrix()
})

React.useLayoutEffect(() => {
if (makeDefault) {
const oldCam = camera
set(() => ({ camera: cameraRef.current! }))
return () => set(() => ({ camera: oldCam }))
}
// The camera should not be part of the dependency list because this components camera is a stable reference
// that must exchange the default, and clean up after itself on unmount.
}, [cameraRef, makeDefault, set])

return <perspectiveCamera ref={mergeRefs([cameraRef, ref])} {...props} />
})
export const PerspectiveCamera = React.forwardRef(
({ resolution = 256, frames = Infinity, makeDefault, children, ...props }: Props, ref) => {
const set = useThree(({ set }) => set)
const camera = useThree(({ camera }) => camera)
const size = useThree(({ size }) => size)
const cameraRef = React.useRef<PerspectiveCameraImpl>(null!)
const groupRef = React.useRef<THREE.Group>(null!)
const fbo = useFBO(resolution)

React.useLayoutEffect(() => {
if (!props.manual) {
cameraRef.current.aspect = size.width / size.height
}
}, [size, props])

React.useLayoutEffect(() => {
cameraRef.current.updateProjectionMatrix()
})

let count = 0
const functional = isFunction(children)
useFrame((state) => {
if (functional && (frames === Infinity || count < frames)) {
groupRef.current.visible = false
state.gl.setRenderTarget(fbo)
state.gl.render(state.scene, cameraRef.current)
state.gl.setRenderTarget(null)
groupRef.current.visible = true
}
})

React.useLayoutEffect(() => {
if (makeDefault) {
const oldCam = camera
set(() => ({ camera: cameraRef.current! }))
return () => set(() => ({ camera: oldCam }))
}
// The camera should not be part of the dependency list because this components camera is a stable reference
// that must exchange the default, and clean up after itself on unmount.
}, [cameraRef, makeDefault, set])

return (
<>
<perspectiveCamera ref={mergeRefs([cameraRef, ref])} {...props}>
{!functional && children}
</perspectiveCamera>
<group ref={groupRef}>{functional && children(fbo.texture)}</group>
</>
)
}
)

1 comment on commit b46d9a1

@vercel
Copy link

@vercel vercel bot commented on b46d9a1 Oct 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.