├── .gitignore ├── .watchmanconfig ├── App.tsx ├── app.json ├── assets ├── icon.png └── splash.png ├── babel.config.js ├── components └── Scene.tsx ├── hooks ├── useLoop.ts └── useRender.ts ├── package.json ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | *.orig.* 9 | web-build/ 10 | web-report/ 11 | .DS_Store -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Scene from './components/Scene' 3 | import useRender from './hooks/useRender' 4 | 5 | function Donut({ position = undefined }) { 6 | const rotation = React.useRef(0) 7 | const mesh = React.useRef() 8 | 9 | useRender(() => { 10 | const t = rotation.current 11 | mesh.current.rotation.set(t, t, t) 12 | rotation.current = t + 0.01 13 | }, []) 14 | 15 | return ( 16 | 17 | 18 | 24 | 25 | ) 26 | } 27 | 28 | function Lights() { 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | } 42 | 43 | export default function App() { 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Native Three Fiber", 4 | "slug": "native-three-fiber", 5 | "privacy": "public", 6 | "sdkVersion": "33.0.0", 7 | "platforms": ["ios", "android", "web"], 8 | "version": "1.0.0", 9 | "orientation": "default", 10 | "icon": "./assets/icon.png", 11 | "splash": { 12 | "image": "./assets/splash.png", 13 | "resizeMode": "contain", 14 | "backgroundColor": "#ffffff" 15 | }, 16 | "updates": { 17 | "fallbackToCacheTimeout": 0 18 | }, 19 | "assetBundlePatterns": ["**/*"], 20 | "ios": { 21 | "supportsTablet": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattblackdev/native-three-fiber/6a4e5a59bfc672136cb0368573eeadf1e77d0980/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattblackdev/native-three-fiber/6a4e5a59bfc672136cb0368573eeadf1e77d0980/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /components/Scene.tsx: -------------------------------------------------------------------------------- 1 | import { Renderer } from 'expo-three' 2 | import React from 'react' 3 | import { GLView, ExpoWebGLRenderingContext } from 'expo-gl' 4 | import { PixelRatio } from 'react-native' 5 | import * as THREE from 'three' 6 | import { render } from 'react-three-fiber' 7 | 8 | import useLoop from '../hooks/useLoop' 9 | 10 | interface ISceneContext { 11 | gl?: ExpoWebGLRenderingContext 12 | renderer?: Renderer 13 | scene?: THREE.Scene 14 | camera?: THREE.PerspectiveCamera 15 | subscribers?: Array<(...args: any[]) => void> 16 | } 17 | 18 | export const SceneContext = React.createContext< 19 | React.MutableRefObject 20 | >({ current: {} }) 21 | 22 | export default function Scene({ children }) { 23 | const state = React.useRef({}) 24 | const [ready, setReady] = React.useState(false) 25 | 26 | const onGLContextCreate = React.useCallback(gl => { 27 | const scale = PixelRatio.get() 28 | const width = gl.drawingBufferWidth / scale 29 | const height = gl.drawingBufferHeight / scale 30 | 31 | state.current.renderer = new Renderer({ 32 | gl, 33 | pixelRatio: scale, 34 | width, 35 | height, 36 | }) 37 | 38 | state.current.camera = new THREE.PerspectiveCamera( 39 | 75, 40 | width / height, 41 | 0.1, 42 | 1000, 43 | ) 44 | state.current.camera.position.z = 50 45 | state.current.scene = new THREE.Scene() 46 | state.current.subscribers = [] 47 | state.current.gl = gl 48 | 49 | state.current.renderer.render(state.current.scene, state.current.camera) 50 | render( 51 | {children}, 52 | state.current.scene, 53 | state, 54 | ) 55 | setReady(true) 56 | }, []) 57 | 58 | useLoop(() => { 59 | const { gl, renderer, subscribers, scene, camera } = state.current 60 | subscribers.forEach(cb => cb()) 61 | renderer.render(scene, camera) 62 | gl.endFrameEXP() 63 | }, ready) 64 | 65 | return ( 66 | { 70 | setReady(ready => !ready) 71 | }} 72 | onLayout={e => { 73 | if (!ready) return 74 | const { width, height } = e.nativeEvent.layout 75 | const { gl, camera, renderer } = state.current 76 | const scale = PixelRatio.get() 77 | 78 | gl.viewport(0, 0, width * scale, height * scale) 79 | renderer.setSize(width, height) 80 | camera.aspect = width / height 81 | camera.updateProjectionMatrix() 82 | }} 83 | /> 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /hooks/useLoop.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class Looper { 4 | subscribers = [] 5 | loopId = null 6 | 7 | loop = (time: number) => { 8 | if (this.loopId) { 9 | this.subscribers.forEach(callback => { 10 | callback(time) 11 | }) 12 | } 13 | 14 | this.loopId = requestAnimationFrame(this.loop) 15 | } 16 | 17 | start() { 18 | if (!this.loopId) { 19 | this.loop(0) 20 | } 21 | } 22 | 23 | stop() { 24 | if (this.loopId) { 25 | cancelAnimationFrame(this.loopId) 26 | this.loopId = null 27 | } 28 | } 29 | 30 | subscribe(callback: (time: number) => void) { 31 | if (this.subscribers.indexOf(callback) === -1) 32 | this.subscribers.push(callback) 33 | } 34 | 35 | unsubscribe(callback: (time: number) => void) { 36 | this.subscribers = this.subscribers.filter(s => s !== callback) 37 | } 38 | } 39 | 40 | export default function useLoop( 41 | callback: (time: number) => void, 42 | invalidate?: boolean, 43 | ) { 44 | const looper = React.useRef(new Looper()) 45 | 46 | React.useEffect(() => { 47 | looper.current.subscribe(callback) 48 | return () => looper.current.unsubscribe(callback) 49 | }, []) 50 | 51 | React.useEffect(() => { 52 | if (invalidate === undefined) return 53 | 54 | if (invalidate) { 55 | looper.current.start() 56 | } else { 57 | looper.current.stop() 58 | } 59 | }, [invalidate]) 60 | 61 | return looper.current 62 | } 63 | -------------------------------------------------------------------------------- /hooks/useRender.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SceneContext } from '../components/Scene' 3 | 4 | export default function useRender(cb: () => void, deps: [] = []) { 5 | const state = React.useContext(SceneContext) 6 | React.useEffect(() => { 7 | const { subscribers } = state.current 8 | subscribers.push(cb) 9 | return () => (state.current.subscribers = subscribers.filter(i => i !== cb)) 10 | }, deps) 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "expo": "^33.0.0", 12 | "expo-file-system": "~5.0.1", 13 | "expo-gl": "^5.0.1", 14 | "expo-graphics": "^1.1.0", 15 | "expo-three": "^3.0.0-alpha.8", 16 | "lodash-es": "^4.17.11", 17 | "react": "16.8.3", 18 | "react-dom": "^16.8.6", 19 | "react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz", 20 | "react-native-web": "^0.11.4", 21 | "react-reconciler": "^0.20.4", 22 | "react-three-fiber": "^2.2.10", 23 | "scheduler": "0.13.3", 24 | "three": "^0.106.2" 25 | }, 26 | "devDependencies": { 27 | "@types/react": "^16.8.19", 28 | "@types/react-native": "^0.57.60", 29 | "@types/three": "^0.103.2", 30 | "babel-preset-expo": "^5.1.1", 31 | "typescript": "^3.4.5" 32 | }, 33 | "private": true, 34 | "prettier": { 35 | "semi": false, 36 | "singleQuote": true, 37 | "trailingComma": "all" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "noEmit": true, 5 | "lib": ["dom", "esnext"], 6 | "jsx": "react-native", 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "skipLibCheck": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------