Created
September 2, 2019 14:42
-
-
Save birkir/5f0724dff3efe5f8681d905fa505468a to your computer and use it in GitHub Desktop.
react native react-three-fiber
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Cube({ position }: { position?: number[] | THREE.Vector3 }) { | |
const mesh = useRef<THREE.Mesh>(); | |
const touched = useTouch(mesh); | |
return ( | |
<mesh ref={mesh} castShadow receiveShadow position={position}> | |
<boxGeometry attach="geometry" args={[1, 1, 1]} /> | |
<meshStandardMaterial | |
attach="material" | |
color={touched ? 0xff0000 : 0xffffff} | |
/> | |
</mesh> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { GLView } from 'expo-gl'; | |
import { Renderer } from 'expo-three'; | |
import React, { useCallback, useRef, useState } from 'react'; | |
import { | |
LayoutChangeEvent, | |
NativeTouchEvent, | |
PanResponder, | |
PixelRatio, | |
ViewStyle, | |
} from 'react-native'; | |
import { applyProps, render } from 'react-three-fiber'; | |
import * as THREE from 'three'; | |
import { useLoop } from '../../hooks/useLoop'; | |
import { | |
defaultSceneContext, | |
ISceneContext, | |
SceneContext, | |
} from './SceneContext'; | |
const Controls = require('./Controls'); | |
(window as any).performance = { | |
clearMarks() {}, | |
measure() {}, | |
clearMeasures() {}, | |
mark() {}, | |
}; | |
interface SceneProps { | |
children: React.ReactNode; | |
style?: ViewStyle; | |
camera?: Partial<THREE.OrthographicCamera & THREE.PerspectiveCamera>; | |
orthographic?: boolean; | |
updateDefaultCamera?: boolean; | |
pixelRatio?: number; | |
raycaster?: THREE.Raycaster; | |
controls?: boolean | any; | |
onCreated?(sceneContext: ISceneContext): void; | |
onPointerMissed?: () => void; | |
} | |
function isOrthographicCamera( | |
def: THREE.Camera | |
): def is THREE.OrthographicCamera { | |
return (def as THREE.OrthographicCamera).isOrthographicCamera; | |
} | |
export function Scene({ | |
children, | |
style, | |
controls, | |
orthographic, | |
updateDefaultCamera, | |
pixelRatio, | |
raycaster, | |
camera, | |
onCreated, | |
}: SceneProps) { | |
const state = useRef<ISceneContext>(defaultSceneContext); | |
const [ready, setReady] = useState(false); | |
const [mouse] = useState(() => new THREE.Vector2()); | |
const [defaultRaycaster] = useState(() => { | |
const ray = new THREE.Raycaster(); | |
if (raycaster) applyProps(ray, raycaster, {}); | |
return ray; | |
}); | |
const touchEvents = new Set<(name: string, e: NativeTouchEvent) => void>(); | |
const triggerTouchEvent = (name: string, e: NativeTouchEvent) => { | |
touchEvents.forEach(touchEvent => touchEvent(name, e)); | |
}; | |
const [panResponder] = useState(() => | |
PanResponder.create({ | |
onStartShouldSetPanResponder: (evt, gestureState) => true, | |
onStartShouldSetPanResponderCapture: (evt, gestureState) => true, | |
onMoveShouldSetPanResponder: (evt, gestureState) => true, | |
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true, | |
onPanResponderTerminationRequest: (evt, gestureState) => true, | |
onPanResponderGrant: e => { | |
if (state.current.controls) { | |
state.current.controls.onTouchStart(e.nativeEvent); | |
} | |
mouse.set(e.nativeEvent.locationX, e.nativeEvent.locationY); | |
triggerTouchEvent('pointerdown', e.nativeEvent); | |
}, | |
onPanResponderMove: e => { | |
if (state.current.controls) { | |
state.current.controls.onTouchMove(e.nativeEvent); | |
} | |
mouse.set(e.nativeEvent.locationX, e.nativeEvent.locationY); | |
triggerTouchEvent('pointermove', e.nativeEvent); | |
}, | |
onPanResponderRelease: e => { | |
if (state.current.controls) { | |
state.current.controls.onTouchEnd(e.nativeEvent); | |
} | |
mouse.set(e.nativeEvent.locationX, e.nativeEvent.locationY); | |
triggerTouchEvent('pointerup', e.nativeEvent); | |
}, | |
onPanResponderTerminate: e => { | |
if (state.current.controls) { | |
state.current.controls.onTouchEnd(e.nativeEvent); | |
} | |
mouse.set(e.nativeEvent.locationX, e.nativeEvent.locationY); | |
triggerTouchEvent('pointercancel', e.nativeEvent); | |
}, | |
}) | |
); | |
const onGLContextCreate = useCallback(gl => { | |
const scale = pixelRatio || PixelRatio.get(); | |
const width = gl.drawingBufferWidth / scale; | |
const height = gl.drawingBufferHeight / scale; | |
state.current.renderer = new Renderer({ | |
gl, | |
pixelRatio: scale, | |
width, | |
height, | |
}); | |
state.current.raycaster = defaultRaycaster; | |
state.current.onTouched = (fn: any) => { | |
touchEvents.add(fn); | |
return () => touchEvents.delete(fn); | |
}; | |
state.current.camera = orthographic | |
? new THREE.OrthographicCamera( | |
width / -2, | |
width / 2, | |
height / -2, | |
height / 2, | |
0.1, | |
1000 | |
) | |
: new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); | |
if (!orthographic) { | |
state.current.camera.position.z = 5; | |
} | |
if (camera) { | |
applyProps(state.current.camera, camera, {}); | |
} | |
state.current.scene = new THREE.Scene(); | |
state.current.subscribers = []; | |
state.current.gl = gl; | |
state.current.renderer.render( | |
state.current.scene as any, | |
state.current.camera as any | |
); | |
if (controls) { | |
state.current.controls = | |
controls !== true | |
? controls | |
: new Controls(state.current.camera, width, height); | |
} | |
render( | |
<SceneContext.Provider value={state}>{children}</SceneContext.Provider>, | |
state.current.scene, | |
state as any | |
); | |
setReady(true); | |
if (onCreated) { | |
onCreated(state.current); | |
} | |
}, []); | |
useLoop(() => { | |
const { gl, renderer, subscribers, scene, camera } = state.current; | |
subscribers.forEach(cb => cb()); | |
renderer.render(scene as any, camera as any); | |
gl.endFrameEXP(); | |
}, ready); | |
const onLayout = (e: LayoutChangeEvent) => { | |
if (!ready) { | |
return; | |
} | |
const { width, height, x, y } = e.nativeEvent.layout; | |
const { gl, camera, renderer } = state.current; | |
const scale = PixelRatio.get(); | |
gl.viewport(0, 0, width * scale, height * scale); | |
renderer.setSize(width, height); | |
renderer.setClearAlpha(0); | |
if (updateDefaultCamera !== false) { | |
if (isOrthographicCamera(camera)) { | |
(state.current.camera as THREE.OrthographicCamera).left = width / -2; | |
(state.current.camera as THREE.OrthographicCamera).right = width / 2; | |
(state.current.camera as THREE.OrthographicCamera).top = height / 2; | |
(state.current.camera as THREE.OrthographicCamera).bottom = height / -2; | |
} else { | |
(camera as THREE.PerspectiveCamera).aspect = width / height; | |
} | |
} | |
camera.updateProjectionMatrix(); | |
if (controls) { | |
this.state.controls.clientWidth = width; | |
this.state.controls.clientHeight = height; | |
} | |
}; | |
return ( | |
<GLView | |
{...panResponder.panHandlers} | |
onContextCreate={onGLContextCreate} | |
style={{ flex: 1, ...style }} | |
onLayout={onLayout} | |
/> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ExpoWebGLRenderingContext } from 'expo-gl'; | |
import { Renderer } from 'expo-three'; | |
import { createContext } from 'react'; | |
import { NativeTouchEvent } from 'react-native'; | |
import * as THREE from 'three'; | |
export type Camera = THREE.OrthographicCamera | THREE.PerspectiveCamera; | |
export type Intersection = THREE.Intersection & { | |
object: THREE.Object3D; | |
receivingObject: THREE.Object3D; | |
}; | |
export interface ISceneContext { | |
gl?: ExpoWebGLRenderingContext; | |
renderer?: Renderer; | |
scene?: THREE.Scene; | |
camera?: Camera; | |
raycaster?: THREE.Raycaster; | |
subscribers?: Array<(...args: any[]) => void>; | |
invalidateFrameloop: boolean; | |
controls?: any; | |
captured?: Intersection[]; | |
mouse?: THREE.Vector2; | |
onTouched?(fn: (type: string, e: NativeTouchEvent) => void): () => void; | |
} | |
export const defaultSceneContext = { | |
invalidateFrameloop: false, | |
}; | |
export const SceneContext = createContext< | |
React.MutableRefObject<ISceneContext> | |
>({ | |
current: defaultSceneContext, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useContext } from 'react'; | |
import { ISceneContext, SceneContext } from '../components/scene/SceneContext'; | |
export function useThree(): ISceneContext { | |
const { current } = useContext(SceneContext); | |
return current; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useContext, useEffect, useState } from 'react'; | |
import { NativeTouchEvent, PixelRatio } from 'react-native'; | |
import * as THREE from 'three'; | |
import { SceneContext } from '../components/scene/SceneContext'; | |
export function useTouch(ref: React.RefObject<THREE.Object3D>): boolean { | |
const [touch, setTouch] = useState<boolean>(); | |
const { current } = useContext(SceneContext); | |
const fn = (type: string, event: NativeTouchEvent) => { | |
if (type === 'pointerdown') { | |
const scale = PixelRatio.get(); | |
const left = 0; | |
const right = current.gl.drawingBufferWidth / scale; | |
const top = 0; | |
const bottom = current.gl.drawingBufferHeight / scale; | |
const x = ((event.locationX - left) / (right - left)) * 2 - 1; | |
const y = -((event.locationY - top) / (bottom - top)) * 2 + 1; | |
current.raycaster.setFromCamera(new THREE.Vector2(x, y), current.camera); | |
const intersects = current.raycaster.intersectObjects( | |
current.scene.children | |
); | |
for (let intersect of intersects) { | |
let receivingObject = intersect.object; | |
let object: THREE.Object3D | null = intersect.object; | |
// Bubble event up | |
while (object) { | |
if (object.uuid === ref.current.uuid) { | |
setTouch(true); | |
return; | |
} | |
object = object.parent; | |
} | |
} | |
setTouch(false); | |
} | |
if (type === 'pointerup') { | |
setTouch(false); | |
} | |
}; | |
useEffect(() => { | |
const destroy = current.onTouched(fn); | |
return () => destroy(); | |
}, []); | |
return touch; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment