-
Notifications
You must be signed in to change notification settings - Fork 730
/
Copy pathFisheye.tsx
110 lines (99 loc) · 4.1 KB
/
Fisheye.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/**
* 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<THREE.Mesh>(null!)
const cubeApi = React.useRef<RenderCubeTextureApi>(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 <UpdateCubeCamera> 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 (
<>
<mesh ref={sphere} {...props} scale={radius}>
<sphereGeometry args={[1, segments, segments]} />
<meshBasicMaterial>
<RenderCubeTexture compute={compute} attach="envMap" flip resolution={resolution} ref={cubeApi}>
{children}
<UpdateCubeCamera api={cubeApi} />
</RenderCubeTexture>
</meshBasicMaterial>
</mesh>
</>
)
}
function UpdateCubeCamera({ api }: { api: React.MutableRefObject<RenderCubeTextureApi> }) {
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
}