diff --git a/README.md b/README.md index b9397f3de..3ec43601b 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
  • Hud
  • View
  • RenderTexture
  • +
  • RenderCubeTexture
  • Mask
  • MeshPortalMaterial
  • @@ -3583,6 +3584,63 @@ type Props = JSX.IntrinsicElements['texture'] & { ``` +#### RenderCubeTexture + +

    + Demo +

    + +This component allows you to render a live scene into a cubetexture which you can then apply to a material, for instance as an environment map (via the envMap property). The contents of it run inside a portal and are separate from the rest of the canvas, therefore you can have events in there, environment maps, etc. + +```tsx +export type RenderCubeTextureProps = Omit & { + /** Optional stencil buffer, defaults to false */ + stencilBuffer?: boolean + /** Optional depth buffer, defaults to true */ + depthBuffer?: boolean + /** Optional generate mipmaps, defaults to false */ + generateMipmaps?: boolean + /** Optional render priority, defaults to 0 */ + renderPriority?: number + /** Optional event priority, defaults to 0 */ + eventPriority?: number + /** Optional frame count, defaults to Infinity. If you set it to 1, it would only render a single frame, etc */ + frames?: number + /** Optional event compute, defaults to undefined */ + compute?: (event: any, state: any, previous: any) => false | undefined + /** Flip cubemap, see https://github.com/mrdoob/three.js/blob/master/src/renderers/WebGLCubeRenderTarget.js */ + flip?: boolean + /** Cubemap resolution (for each of the 6 takes), null === full screen resolution, default: 896 */ + resolution?: number + /** Children will be rendered into a portal */ + children: React.ReactNode + near?: number + far?: number + position?: ReactThreeFiber.Vector3 + rotation?: ReactThreeFiber.Euler + scale?: ReactThreeFiber.Vector3 + quaternion?: ReactThreeFiber.Quaternion + matrix?: ReactThreeFiber.Matrix4 + matrixAutoUpdate?: boolean +} + +export type RenderCubeTextureApi = { + scene: THREE.Scene + fbo: THREE.WebGLCubeRenderTarget + camera: THREE.CubeCamera +} +``` + +```jsx +const api = useRef(null!) +// ... + + + + + +``` + #### Mask

    diff --git a/src/core/RenderCubeTexture.tsx b/src/core/RenderCubeTexture.tsx new file mode 100644 index 000000000..0021ac868 --- /dev/null +++ b/src/core/RenderCubeTexture.tsx @@ -0,0 +1,149 @@ +import * as THREE from 'three' +import * as React from 'react' +import { ReactThreeFiber, createPortal, useFrame, useThree } from '@react-three/fiber' +import { ForwardRefComponent } from '../helpers/ts-utils' + +export type RenderCubeTextureProps = Omit & { + /** Optional stencil buffer, defaults to false */ + stencilBuffer?: boolean + /** Optional depth buffer, defaults to true */ + depthBuffer?: boolean + /** Optional generate mipmaps, defaults to false */ + generateMipmaps?: boolean + /** Optional render priority, defaults to 0 */ + renderPriority?: number + /** Optional event priority, defaults to 0 */ + eventPriority?: number + /** Optional frame count, defaults to Infinity. If you set it to 1, it would only render a single frame, etc */ + frames?: number + /** Optional event compute, defaults to undefined */ + compute?: (event: any, state: any, previous: any) => false | undefined + /** Flip cubemap, see https://github.com/mrdoob/three.js/blob/master/src/renderers/WebGLCubeRenderTarget.js */ + flip?: boolean + /** Cubemap resolution (for each of the 6 takes), null === full screen resolution, default: 896 */ + resolution?: number + /** Children will be rendered into a portal */ + children: React.ReactNode + near?: number + far?: number + position?: ReactThreeFiber.Vector3 + rotation?: ReactThreeFiber.Euler + scale?: ReactThreeFiber.Vector3 + quaternion?: ReactThreeFiber.Quaternion + matrix?: ReactThreeFiber.Matrix4 + matrixAutoUpdate?: boolean +} + +export type RenderCubeTextureApi = { + scene: THREE.Scene + fbo: THREE.WebGLCubeRenderTarget + camera: THREE.CubeCamera +} + +export const RenderCubeTexture: ForwardRefComponent = React.forwardRef( + ( + { + children, + compute, + renderPriority = -1, + eventPriority = 0, + frames = Infinity, + stencilBuffer = false, + depthBuffer = true, + generateMipmaps = false, + resolution = 896, + near = 0.1, + far = 1000, + flip = false, + position, + rotation, + scale, + quaternion, + matrix, + matrixAutoUpdate, + ...props + }, + forwardRef + ) => { + const { size, viewport } = useThree() + + const camera = React.useRef(null!) + const fbo = React.useMemo(() => { + const fbo = new THREE.WebGLCubeRenderTarget( + Math.max((resolution || size.width) * viewport.dpr, (resolution || size.height) * viewport.dpr), + { + stencilBuffer, + depthBuffer, + generateMipmaps, + } + ) + fbo.texture.isRenderTargetTexture = !flip + fbo.texture.flipY = true + fbo.texture.type = THREE.HalfFloatType + return fbo + }, [resolution, flip]) + + React.useEffect(() => { + return () => fbo.dispose() + }, [fbo]) + + const [vScene] = React.useState(() => new THREE.Scene()) + const uvCompute = React.useCallback((event, state, previous) => { + // https://github.com/pmndrs/react-three-fiber/pull/782 + // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides + state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) + state.raycaster.setFromCamera(state.pointer, state.camera) + }, []) + + React.useImperativeHandle(forwardRef, () => ({ scene: vScene, fbo, camera: camera.current }), [fbo]) + + return ( + <> + {createPortal( + + {children} + {/* Without an element that receives pointer events state.pointer will always be 0/0 */} + null} /> + , + vScene, + { events: { compute: compute || uvCompute, priority: eventPriority } } + )} + + + + ) + } +) + +// The container component has to be separate, it can not be inlined because "useFrame(state" when run inside createPortal will return +// the portals own state which includes user-land overrides (custom cameras etc), but if it is executed in 's render function +// it would return the default state. +function Container({ + frames, + renderPriority, + children, + camera, +}: { + frames: number + renderPriority: number + children: React.ReactNode + camera: React.MutableRefObject +}) { + let count = 0 + useFrame((state) => { + if (frames === Infinity || count < frames) { + camera.current.update(state.gl, state.scene) + count++ + } + }, renderPriority) + return <>{children} +} diff --git a/src/core/index.ts b/src/core/index.ts index b34aa0df6..15ab3c14a 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -138,6 +138,7 @@ export * from './PerformanceMonitor' // Portals export * from './RenderTexture' +export * from './RenderCubeTexture' export * from './Mask' export * from './Hud' export * from './MeshPortalMaterial'