|
| 1 | +/** |
| 2 | + * Event compute by Garrett Johnson https://twitter.com/garrettkjohnson |
| 3 | + * https://discourse.threejs.org/t/how-to-use-three-raycaster-with-a-sphere-projected-envmap/56803/10 |
| 4 | + */ |
| 5 | + |
| 6 | +import * as THREE from 'three' |
| 7 | +import * as React from 'react' |
| 8 | +import { useFrame, useThree } from '@react-three/fiber' |
| 9 | +import { RenderCubeTexture, RenderCubeTextureApi } from './RenderCubeTexture' |
| 10 | + |
| 11 | +export type FisheyeProps = JSX.IntrinsicElements['mesh'] & { |
| 12 | + /** Zoom factor, 0..1, 0 */ |
| 13 | + zoom?: number |
| 14 | + /** Number of segments, 64 */ |
| 15 | + segments?: number |
| 16 | + /** Cubemap resolution (for each of the 6 takes), null === full screen resolution, default: 896 */ |
| 17 | + resolution?: number |
| 18 | + /** Children will be projected into the fisheye */ |
| 19 | + children: React.ReactNode |
| 20 | + /** Optional render priority, defaults to 1 */ |
| 21 | + renderPriority?: number |
| 22 | +} |
| 23 | + |
| 24 | +export function Fisheye({ |
| 25 | + renderPriority = 1, |
| 26 | + zoom = 0, |
| 27 | + segments = 64, |
| 28 | + children, |
| 29 | + resolution = 896, |
| 30 | + ...props |
| 31 | +}: FisheyeProps) { |
| 32 | + const sphere = React.useRef<THREE.Mesh>(null!) |
| 33 | + const cubeApi = React.useRef<RenderCubeTextureApi>(null!) |
| 34 | + |
| 35 | + // This isn't more than a simple sphere and a fixed orthographc camera |
| 36 | + // pointing at it. A virtual scene is portalled into the environment map |
| 37 | + // of its material. The cube-camera filming that scene is being synced to |
| 38 | + // the portals default camera with the <UpdateCubeCamera> component. |
| 39 | + |
| 40 | + const { width, height } = useThree((state) => state.size) |
| 41 | + const [orthoC] = React.useState(() => new THREE.OrthographicCamera()) |
| 42 | + |
| 43 | + React.useLayoutEffect(() => { |
| 44 | + orthoC.position.set(0, 0, 100) |
| 45 | + orthoC.zoom = 100 |
| 46 | + orthoC.left = width / -2 |
| 47 | + orthoC.right = width / 2 |
| 48 | + orthoC.top = height / 2 |
| 49 | + orthoC.bottom = height / -2 |
| 50 | + orthoC.updateProjectionMatrix() |
| 51 | + }, [width, height]) |
| 52 | + |
| 53 | + const radius = (Math.sqrt(width * width + height * height) / 100) * (0.5 + zoom / 2) |
| 54 | + const normal = new THREE.Vector3() |
| 55 | + const sph = new THREE.Sphere(new THREE.Vector3(), radius) |
| 56 | + const normalMatrix = new THREE.Matrix3() |
| 57 | + |
| 58 | + const compute = React.useCallback((event, state, prev) => { |
| 59 | + // Raycast from the render camera to the sphere and get the surface normal |
| 60 | + // of the point hit in world space of the sphere scene |
| 61 | + // We have to set the raycaster using the orthocam and pointer |
| 62 | + // to perform sphere interscetions. |
| 63 | + state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1) |
| 64 | + state.raycaster.setFromCamera(state.pointer, orthoC) |
| 65 | + if (!state.raycaster.ray.intersectSphere(sph, normal)) return |
| 66 | + else normal.normalize() |
| 67 | + // Get the matrix for transforming normals into world space |
| 68 | + normalMatrix.getNormalMatrix(cubeApi.current.camera.matrixWorld) |
| 69 | + // Get the ray |
| 70 | + cubeApi.current.camera.getWorldPosition(state.raycaster.ray.origin) |
| 71 | + state.raycaster.ray.direction.set(0, 0, 1).reflect(normal) |
| 72 | + state.raycaster.ray.direction.x *= -1 // flip across X to accommodate the "flip" of the env map |
| 73 | + state.raycaster.ray.direction.applyNormalMatrix(normalMatrix).multiplyScalar(-1) |
| 74 | + return undefined |
| 75 | + }, []) |
| 76 | + |
| 77 | + useFrame((state) => { |
| 78 | + // Take over rendering |
| 79 | + if (renderPriority) state.gl.render(sphere.current, orthoC) |
| 80 | + }, renderPriority) |
| 81 | + |
| 82 | + return ( |
| 83 | + <> |
| 84 | + <mesh ref={sphere} {...props} scale={radius}> |
| 85 | + <sphereGeometry args={[1, segments, segments]} /> |
| 86 | + <meshBasicMaterial> |
| 87 | + <RenderCubeTexture compute={compute} attach="envMap" flip resolution={resolution} ref={cubeApi}> |
| 88 | + {children} |
| 89 | + <UpdateCubeCamera api={cubeApi} /> |
| 90 | + </RenderCubeTexture> |
| 91 | + </meshBasicMaterial> |
| 92 | + </mesh> |
| 93 | + </> |
| 94 | + ) |
| 95 | +} |
| 96 | + |
| 97 | +function UpdateCubeCamera({ api }: { api: React.MutableRefObject<RenderCubeTextureApi> }) { |
| 98 | + const t = new THREE.Vector3() |
| 99 | + const r = new THREE.Quaternion() |
| 100 | + const s = new THREE.Vector3() |
| 101 | + const e = new THREE.Euler(0, Math.PI, 0) |
| 102 | + useFrame((state) => { |
| 103 | + // Read out the cameras whereabouts, state.camera is the one *within* the portal |
| 104 | + state.camera.matrixWorld.decompose(t, r, s) |
| 105 | + // Apply its position and rotation, flip the Y axis |
| 106 | + api.current.camera.position.copy(t) |
| 107 | + api.current.camera.quaternion.setFromEuler(e).premultiply(r) |
| 108 | + }) |
| 109 | + return null |
| 110 | +} |
1 commit comments
vercel[bot] commentedon Oct 10, 2023
Successfully deployed to the following URLs:
drei – ./
drei.react-spring.io
drei-git-master-pmndrs.vercel.app
drei-pmndrs.vercel.app
drei.vercel.app
drei.pmnd.rs