Skip to content

Instantly share code, notes, and snippets.

@birkir
Created September 2, 2019 14:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save birkir/5f0724dff3efe5f8681d905fa505468a to your computer and use it in GitHub Desktop.
Save birkir/5f0724dff3efe5f8681d905fa505468a to your computer and use it in GitHub Desktop.
react native react-three-fiber
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>
);
}
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}
/>
);
}
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,
});
import { useContext } from 'react';
import { ISceneContext, SceneContext } from '../components/scene/SceneContext';
export function useThree(): ISceneContext {
const { current } = useContext(SceneContext);
return current;
}
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