/** * Event compute by Garrett Johnson https://twitter.com/garrettkjohnson * https://discourse.threejs.org/t/how-to-use-three-raycaster-with-a-sphere-projected-envmap/56803/10 */ import * as THREE from 'three' import * as React from 'react' import { useFrame, useThree } from '@react-three/fiber' import { RenderCubeTexture, RenderCubeTextureApi } from './RenderCubeTexture' export type FisheyeProps = JSX.IntrinsicElements['mesh'] & { /** Zoom factor, 0..1, 0 */ zoom?: number /** Number of segments, 64 */ segments?: number /** Cubemap resolution (for each of the 6 takes), null === full screen resolution, default: 896 */ resolution?: number /** Children will be projected into the fisheye */ children: React.ReactNode /** Optional render priority, defaults to 1 */ renderPriority?: number } export function Fisheye({ renderPriority = 1, zoom = 0, segments = 64, children, resolution = 896, ...props }: FisheyeProps) { const sphere = React.useRef(null!) const cubeApi = React.useRef(null!) // This isn't more than a simple sphere and a fixed orthographc camera // pointing at it. A virtual scene is portalled into the environment map // of its material. The cube-camera filming that scene is being synced to // the portals default camera with the component. const { width, height } = useThree((state) => state.size) const [orthoC] = React.useState(() => new THREE.OrthographicCamera()) React.useLayoutEffect(() => { orthoC.position.set(0, 0, 100) orthoC.zoom = 100 orthoC.left = width / -2 orthoC.right = width / 2 orthoC.top = height / 2 orthoC.bottom = height / -2 orthoC.updateProjectionMatrix() }, [width, height]) const radius = (Math.sqrt(width * width + height * height) / 100) * (0.5 + zoom / 2) const normal = new THREE.Vector3() const sph = new THREE.Sphere(new THREE.Vector3(), radius) const normalMatrix = new THREE.Matrix3() const compute = React.useCallback((event, state, prev) => { // Raycast from the render camera to the sphere and get the surface normal // of the point hit in world space of the sphere scene // We have to set the raycaster using the orthocam and pointer // to perform sphere interscetions. state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) state.raycaster.setFromCamera(state.pointer, orthoC) if (!state.raycaster.ray.intersectSphere(sph, normal)) return else normal.normalize() // Get the matrix for transforming normals into world space normalMatrix.getNormalMatrix(cubeApi.current.camera.matrixWorld) // Get the ray cubeApi.current.camera.getWorldPosition(state.raycaster.ray.origin) state.raycaster.ray.direction.set(0, 0, 1).reflect(normal) state.raycaster.ray.direction.x *= -1 // flip across X to accommodate the "flip" of the env map state.raycaster.ray.direction.applyNormalMatrix(normalMatrix).multiplyScalar(-1) return undefined }, []) useFrame((state) => { // Take over rendering if (renderPriority) state.gl.render(sphere.current, orthoC) }, renderPriority) return ( <> {children} ) } function UpdateCubeCamera({ api }: { api: React.MutableRefObject }) { const t = new THREE.Vector3() const r = new THREE.Quaternion() const s = new THREE.Vector3() const e = new THREE.Euler(0, Math.PI, 0) useFrame((state) => { // Read out the cameras whereabouts, state.camera is the one *within* the portal state.camera.matrixWorld.decompose(t, r, s) // Apply its position and rotation, flip the Y axis api.current.camera.position.copy(t) api.current.camera.quaternion.setFromEuler(e).premultiply(r) }) return null }