├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── rollup.config.ts ├── src ├── babylonjs-hook.tsx ├── engine.tsx └── scene.tsx ├── tsconfig.json └── tslint.json /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: make_release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | npmVersion: 7 | description: 'NPM Version ( | major | minor | patch | premajor | preminor | prepatch | prerelease | ...)' 8 | required: true 9 | default: 'patch' 10 | # push: 11 | # branches: [ master ] 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-node@v1 20 | with: 21 | node-version: '12.x' 22 | registry-url: 'https://registry.npmjs.org' 23 | scope: brianzinn 24 | 25 | - name: Configure git 26 | run: | 27 | git config user.email "github@wakeskate.com" 28 | git config user.name "Brian Zinn" 29 | 30 | - name: install-build 31 | run: | 32 | yarn install --frozen-lockfile 33 | yarn build 34 | - run: echo "version -- ${{ github.event.inputs.npmVersion }}" 35 | - run: npm version ${{ github.event.inputs.npmVersion }} -m "release %s :package:" 36 | - run: git push && git push --tags 37 | - name: publish-npm 38 | run: npm publish --access public 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babylonjs-hook 2 | 3 | Future plans are to add useful hooks for attaching cameras and a provider for Babylon scene object. Additionally will be including hooks for loading resources/models that support Suspense. 4 | 5 | [![NPM version](http://img.shields.io/npm/v/babylonjs-hook.svg?style=flat-square)](https://www.npmjs.com/package/babylonjs-hook) 6 | [![NPM downloads](http://img.shields.io/npm/dm/babylonjs-hook.svg?style=flat-square)](https://www.npmjs.com/package/babylonjs-hook) 7 | 8 | ## How to Install 9 | ```sh 10 | $ cd 11 | $ npm i babylonjs-hook 12 | ``` 13 | OR 14 | ```sh 15 | $ cd 16 | $ yarn add babylonjs-hook 17 | ``` 18 | 19 | Basic Usage: 20 | ```jsx 21 | import React from 'react'; 22 | import { FreeCamera, Vector3, HemisphericLight, MeshBuilder } from '@babylonjs/core'; 23 | import SceneComponent from 'babylonjs-hook'; 24 | import './App.css'; 25 | 26 | let box; 27 | 28 | const onSceneReady = scene => { 29 | // This creates and positions a free camera (non-mesh) 30 | var camera = new FreeCamera("camera1", new Vector3(0, 5, -10), scene); 31 | 32 | // This targets the camera to scene origin 33 | camera.setTarget(Vector3.Zero()); 34 | 35 | const canvas = scene.getEngine().getRenderingCanvas(); 36 | 37 | // This attaches the camera to the canvas 38 | camera.attachControl(canvas, true); 39 | 40 | // This creates a light, aiming 0,1,0 - to the sky (non-mesh) 41 | var light = new HemisphericLight("light", new Vector3(0, 1, 0), scene); 42 | 43 | // Default intensity is 1. Let's dim the light a small amount 44 | light.intensity = 0.7; 45 | 46 | // Our built-in 'box' shape. 47 | box = MeshBuilder.CreateBox("box", {size: 2}, scene); 48 | 49 | // Move the box upward 1/2 its height 50 | box.position.y = 1; 51 | 52 | // Our built-in 'ground' shape. 53 | MeshBuilder.CreateGround("ground", {width: 6, height: 6}, scene); 54 | } 55 | 56 | /** 57 | * Will run on every frame render. We are spinning the box on y-axis. 58 | */ 59 | const onRender = scene => { 60 | if (box !== undefined) { 61 | var deltaTimeInMillis = scene.getEngine().getDeltaTime(); 62 | 63 | const rpm = 10; 64 | box.rotation.y += ((rpm / 60) * Math.PI * 2 * (deltaTimeInMillis / 1000)); 65 | } 66 | } 67 | 68 | function App() { 69 | 70 | return ( 71 |
72 |
73 | 74 |
75 |
76 | ); 77 | } 78 | 79 | export default App; 80 | ``` 81 | 82 | Codesandbox example from above extended with `useScene` hook and ways of connecting DOM and GUI: 83 | [codesandbox example](https://codesandbox.io/s/codesandbox-react-tsx-forked-l45zvu?file=/src/App.tsx) 84 | 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babylonjs-hook", 3 | "version": "0.1.1", 4 | "description": "react babylonjs hook", 5 | "keywords": [ 6 | "react", 7 | "babylonjs", 8 | "hook" 9 | ], 10 | "type": "module", 11 | "main": "dist/babylonjs-hook.es5.js", 12 | "module": "dist/babylonjs-hook.es5.js", 13 | "typings": "dist/types/babylonjs-hook.d.ts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "author": "Brian Zinn ", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/brianzinn/babylonjs-hook" 21 | }, 22 | "scripts": { 23 | "lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.tsx'", 24 | "prebuild": "rimraf dist", 25 | "build": "rollup -c rollup.config.ts && tsc -d --emitDeclarationOnly --declarationDir dist/types", 26 | "start": "rollup -c rollup.config.ts -w", 27 | "precommit": "lint-staged" 28 | }, 29 | "lint-staged": { 30 | "src/**/*.ts": [ 31 | "prettier --write", 32 | "git add" 33 | ] 34 | }, 35 | "prettier": { 36 | "semi": false, 37 | "singleQuote": true, 38 | "trailingComma": "es5" 39 | }, 40 | "devDependencies": { 41 | "@babylonjs/core": "^4.2.0", 42 | "@rollup/plugin-json": "^4.1.0", 43 | "@rollup/plugin-node-resolve": "^11.1.0", 44 | "@rollup/plugin-typescript": "^8.1.0", 45 | "@types/node": "^12.0.8", 46 | "@types/react": "^17.0.0", 47 | "cross-env": "^6.0.0", 48 | "lint-staged": "^9.0.0", 49 | "prettier": "^1.14.3", 50 | "react": "^17.0.1", 51 | "rimraf": "^3.0.0", 52 | "rollup": "^2.36.2", 53 | "rollup-plugin-commonjs": "^10.0.0", 54 | "ts-node": "^8.3.0", 55 | "tslint": "^5.11.0", 56 | "tslint-config-prettier": "^1.15.0", 57 | "tslint-config-standard": "^8.0.1", 58 | "typescript": "^4.4.3" 59 | }, 60 | "peerDependencies": { 61 | "@babylonjs/core": "4.x||>5.0.0-rc||5.x", 62 | "react": ">=16" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import json from '@rollup/plugin-json'; 4 | import pkg from './package.json'; 5 | 6 | // can fix this when TypeScript 4.2 is released (currently in beta). 7 | // https://github.com/rollup/plugins/issues/287 8 | 9 | const libraryName = 'babylonjs-hook'; 10 | 11 | // The nexted imports are otherwise not considered external. 12 | // ie: import { ... } from '@babylonjs/core/scene' 13 | const replace = { 14 | '@babylonjs/core': /@babylonjs\/core.*/ 15 | } 16 | 17 | const external = Object.keys(pkg.peerDependencies || {}) 18 | .reduce((prev, cur) => { 19 | prev.push(replace[cur] !== undefined ? replace[cur] : cur); 20 | return prev; 21 | }, []); 22 | console.log('external:', external.join(',')) 23 | 24 | export default { 25 | input: `src/${libraryName}.tsx`, 26 | output: [ 27 | { 28 | file: pkg.module, 29 | format: 'es', 30 | sourcemap: true 31 | }, 32 | ], 33 | // Indicate here external modules you don't wanna include in your bundle (i.e.: '@babylonjs/*') 34 | external, 35 | watch: { 36 | include: 'src/**', 37 | }, 38 | plugins: [ 39 | // Compile TypeScript files 40 | typescript({ 41 | outDir: 'dist' 42 | }), 43 | // Allow json resolution 44 | json(), 45 | // Allow node_modules resolution, so you can use 'external' to control 46 | // which external modules to include in the bundle 47 | // https://github.com/rollup/plugins/tree/master/packages/node-resolve#usage 48 | resolve() 49 | ], 50 | } 51 | -------------------------------------------------------------------------------- /src/babylonjs-hook.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext, useRef, useState } from 'react'; 2 | import { Camera } from '@babylonjs/core/Cameras/camera.js'; 3 | import { Engine } from '@babylonjs/core/Engines/engine.js'; 4 | import { EngineOptions } from '@babylonjs/core/Engines/thinEngine.js'; 5 | import { EventState, Observer } from '@babylonjs/core/Misc/observable.js'; 6 | import { Scene, SceneOptions } from '@babylonjs/core/scene.js'; 7 | import { Nullable } from '@babylonjs/core/types.js'; 8 | import { SceneContext, SceneContextType } from './scene'; 9 | import { EngineCanvasContext, EngineCanvasContextType } from './engine'; 10 | 11 | export * from './engine'; 12 | export * from './scene'; 13 | 14 | export type BabylonjsProps = { 15 | antialias?: boolean 16 | engineOptions?: EngineOptions 17 | adaptToDeviceRatio?: boolean 18 | renderChildrenWhenReady?: boolean 19 | sceneOptions?: SceneOptions 20 | onSceneReady: (scene: Scene) => void 21 | /** 22 | * Automatically trigger engine resize when the canvas resizes (default: true) 23 | */ 24 | observeCanvasResize?: boolean 25 | onRender?: (scene: Scene) => void 26 | children?: React.ReactNode 27 | }; 28 | 29 | export type OnFrameRenderFn = (eventData: Scene, eventState: EventState) => void 30 | 31 | /** 32 | * Register a callback for before the scene renders. 33 | * 34 | * @param callback called using onBeforeRender functionality of scene 35 | * @param mask the mask used to filter observers 36 | * @param insertFirst if true will be inserted at first position, if false (default) will be last position. 37 | * @param callOnce only call the callback once 38 | */ 39 | export const useBeforeRender = (callback: OnFrameRenderFn, mask?: number, insertFirst?: boolean, callOnce?: boolean): void => { 40 | const { scene } = useContext(SceneContext); 41 | 42 | useEffect(() => { 43 | if (scene === null) { 44 | return; 45 | } 46 | 47 | const unregisterOnFirstCall: boolean = callOnce === true; 48 | const sceneObserver: Nullable> = scene.onBeforeRenderObservable.add(callback, mask, insertFirst, undefined, unregisterOnFirstCall); 49 | 50 | if (unregisterOnFirstCall !== true) { 51 | return () => { 52 | scene.onBeforeRenderObservable.remove(sceneObserver); 53 | } 54 | } 55 | }) 56 | } 57 | 58 | /** 59 | * Register a callback for after the scene renders. 60 | * 61 | * @param callback called using onBeforeRender functionality of scene 62 | * @param mask the mask used to filter observers 63 | * @param insertFirst if true will be inserted at first position, if false (default) will be last position. 64 | * @param callOnce only call the callback once 65 | */ 66 | export const useAfterRender = (callback: OnFrameRenderFn, mask?: number, insertFirst?: boolean, callOnce?: boolean): void => { 67 | const { scene } = useContext(SceneContext); 68 | 69 | useEffect(() => { 70 | if (scene === null) { 71 | return; 72 | } 73 | 74 | const unregisterOnFirstCall: boolean = callOnce === true; 75 | const sceneObserver: Nullable> = scene.onAfterRenderObservable.add(callback, mask, insertFirst, undefined, unregisterOnFirstCall); 76 | 77 | if (unregisterOnFirstCall !== true) { 78 | return () => { 79 | scene.onAfterRenderObservable.remove(sceneObserver); 80 | } 81 | } 82 | }) 83 | } 84 | 85 | /** 86 | * Handles creating a camera and attaching/disposing. 87 | * TODO: add new 4.2 parameters: useCtrlForPanning & panningMouseButton 88 | * @param createCameraFn function that creates and returns a camera 89 | * @param autoAttach Attach the input controls (default true) 90 | * @param noPreventDefault Events caught by controls should call prevent default 91 | * @param useCtrlForPanning (ArcRotateCamera only) 92 | * @param panningMoustButton (ArcRotateCamera only) 93 | */ 94 | export const useCamera = (createCameraFn: (scene: Scene) => T, autoAttach: boolean = true, noPreventDefault: boolean = true/*, useCtrlForPanning: boolean = false, panningMouseButton: number*/): Nullable => { 95 | const { scene } = useContext(SceneContext); 96 | const cameraRef = useRef>(null); 97 | 98 | useEffect(() => { 99 | if (scene === null) { 100 | console.warn('cannot create camera (scene not ready)'); 101 | return; 102 | } 103 | 104 | const camera: T = createCameraFn(scene); 105 | if (autoAttach === true) { 106 | const canvas: HTMLCanvasElement = scene.getEngine().getRenderingCanvas()!; 107 | 108 | // This attaches the camera to the canvas - adding extra parameters breaks backwards compatibility 109 | // https://github.com/BabylonJS/Babylon.js/pull/9192 (keep canvas to work with < 4.2 beta-13) 110 | // TODO: look at parameters of other camera types for attaching - likely need an 'options' parameter instead. 111 | // if (camera instanceof ArcRotateCamera) { 112 | // camera.attachControl(noPreventDefault, useCtrlForPanning, panningMouseButton) 113 | camera.attachControl(canvas, noPreventDefault); 114 | } 115 | cameraRef.current = camera; 116 | 117 | return () => { 118 | if (autoAttach === true) { 119 | // canvas is only needed for < 4.1 120 | const canvas: HTMLCanvasElement = scene.getEngine().getRenderingCanvas()!; 121 | camera.detachControl(canvas); 122 | } 123 | camera.dispose(); 124 | } 125 | }, [scene]); 126 | 127 | return cameraRef.current; 128 | } 129 | 130 | export default (props: BabylonjsProps & React.CanvasHTMLAttributes) => { 131 | const reactCanvas = useRef>(null); 132 | const { antialias, engineOptions, adaptToDeviceRatio, sceneOptions, onRender, onSceneReady, renderChildrenWhenReady, children, ...rest } = props; 133 | 134 | const [sceneContext, setSceneContext] = useState({ 135 | scene: null, 136 | sceneReady: false 137 | }); 138 | 139 | const [engineContext, setEngineContext] = useState({ 140 | engine: null, 141 | canvas: null 142 | }); 143 | 144 | useEffect(() => { 145 | if (reactCanvas.current) { 146 | const engine = new Engine(reactCanvas.current, antialias, engineOptions, adaptToDeviceRatio); 147 | setEngineContext(() => ({ 148 | engine, 149 | canvas: reactCanvas.current 150 | })); 151 | 152 | let resizeObserver: Nullable = null; 153 | 154 | const scene = new Scene(engine, sceneOptions); 155 | 156 | if (props.observeCanvasResize !== false && window.ResizeObserver) { 157 | resizeObserver = new ResizeObserver(() => { 158 | engine.resize(); 159 | if (scene.activeCamera /* needed for rendering */) { 160 | // render to prevent flickering on resize 161 | if (typeof onRender === 'function') { 162 | onRender(scene); 163 | } 164 | scene.render(); 165 | } 166 | }); 167 | resizeObserver.observe(reactCanvas.current); 168 | } 169 | 170 | const sceneIsReady = scene.isReady(); 171 | if (sceneIsReady) { 172 | props.onSceneReady(scene); 173 | } else { 174 | scene.onReadyObservable.addOnce((scene) => { 175 | props.onSceneReady(scene); 176 | setSceneContext(() => ({ 177 | canvas: reactCanvas.current, 178 | scene, 179 | engine, 180 | sceneReady: true, 181 | })); 182 | }); 183 | } 184 | 185 | engine.runRenderLoop(() => { 186 | if (scene.activeCamera) { 187 | if (typeof onRender === 'function') { 188 | onRender(scene); 189 | } 190 | scene.render(); 191 | } else { 192 | // @babylonjs/core throws an error if you attempt to render with no active camera. 193 | // if we attach as a child React component we have frames with no active camera. 194 | console.warn('no active camera..'); 195 | } 196 | }) 197 | 198 | const resize = () => { 199 | scene.getEngine().resize(); 200 | } 201 | 202 | if (window) { 203 | window.addEventListener('resize', resize); 204 | } 205 | 206 | setSceneContext(() => ({ 207 | canvas: reactCanvas.current, 208 | scene, 209 | engine, 210 | sceneReady: sceneIsReady, 211 | })); 212 | 213 | return () => { 214 | // cleanup 215 | if (resizeObserver !== null) { 216 | resizeObserver.disconnect(); 217 | } 218 | 219 | if (window) { 220 | window.removeEventListener('resize', resize); 221 | } 222 | 223 | scene.getEngine().dispose(); 224 | } 225 | } 226 | }, [reactCanvas]); 227 | 228 | return ( 229 | <> 230 | 231 | 232 | 233 | {(renderChildrenWhenReady !== true || (renderChildrenWhenReady === true && sceneContext.sceneReady)) && 234 | children 235 | } 236 | 237 | 238 | 239 | ); 240 | } 241 | -------------------------------------------------------------------------------- /src/engine.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from'react'; 2 | import { Engine } from '@babylonjs/core/Engines/engine.js'; 3 | import { Nullable } from '@babylonjs/core/types.js'; 4 | 5 | export type EngineCanvasContextType = { 6 | engine: Nullable 7 | canvas: Nullable 8 | }; 9 | 10 | export const EngineCanvasContext = createContext({ 11 | engine: null, 12 | canvas: null 13 | }); 14 | 15 | type Omit = Pick>; 16 | 17 | export function withEngineCanvasContext< 18 | P extends { engineCanvasContext: EngineCanvasContextType }, 19 | R = Omit 20 | >( 21 | Component: React.ComponentClass

| React.FunctionComponent

22 | ): React.FunctionComponent { 23 | return function BoundComponent(props: R) { 24 | return ( 25 | 26 | {ctx => } 27 | 28 | ); 29 | }; 30 | } 31 | 32 | /** 33 | * Get the engine from the context. 34 | */ 35 | export const useEngine = (): Nullable => useContext(EngineCanvasContext).engine; 36 | 37 | /** 38 | * Get the canvas DOM element from the context. 39 | */ 40 | export const useCanvas = (): Nullable => useContext(EngineCanvasContext).canvas; 41 | -------------------------------------------------------------------------------- /src/scene.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import { Scene } from '@babylonjs/core/scene.js'; 3 | import { Nullable } from '@babylonjs/core/types.js'; 4 | 5 | export type SceneContextType = { 6 | scene: Nullable 7 | sceneReady: boolean 8 | }; 9 | 10 | export const SceneContext = createContext({ 11 | scene: null, 12 | sceneReady: false 13 | }); 14 | 15 | /** 16 | * Get the scene from the context. 17 | */ 18 | export const useScene = (): Nullable => useContext(SceneContext).scene; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "module": "es2015", 6 | "jsx": "react", 7 | "lib": [ 8 | "dom", 9 | "dom.iterable", 10 | "esnext" 11 | ], 12 | "strict": true, 13 | "declaration": true, 14 | "rootDir": "src", 15 | "skipLibCheck": true, 16 | "esModuleInterop": true, 17 | "isolatedModules": true, 18 | "resolveJsonModule": true, 19 | }, 20 | "include": ["src"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-standard", "tslint-config-prettier"] 3 | } 4 | --------------------------------------------------------------------------------