Skip to content

Commit

Permalink
v3
Browse files Browse the repository at this point in the history
  • Loading branch information
brianzinn committed Jan 9, 2021
1 parent 381b4c5 commit dd6949a
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 155 deletions.
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ If you are using 3D models, include `@babylonjs/loaders` and ensure it is regist

# Usage Styles
`react-babylonjs` tries to remain unopinionated about how you integrate BabylonJS with React. This module provides a 100% declarative option and/or you can customise by adding code. There are lots of escape hatches where you can switch to imperative coding and direct access to objects.

![Connecting the pieces](https://raw.githubusercontent.com/brianzinn/react-babylonjs/master/media/react-babylonjs-boxes.gif)
## Connecting the pieces
If you are new to React or babylon.js (or both) there is some learning ahead. The babylon.js documentation site is really useful for understanding the basics of lighting, cameras, etc. This project aims to make easy to integrate those into React using JSX.

Here we re-use a `MovingBox` component that can be clicked or hovered. These reusable components can be used to compose a declarative scene just like regular React development.
Here we re-use a `SpinningBox` component that can be clicked or hovered. These reusable components can be used to compose a declarative scene. We are using hooks for the clicking, hovering and spinning.

![Connecting the pieces](https://raw.githubusercontent.com/brianzinn/react-babylonjs/master/media/react-babylonjs-boxes.gif)
```jsx
import React, { useRef, useState } from 'react'
import { Engine, Scene, useBeforeRender, useClick, useHover } from 'react-babylonjs'
Expand All @@ -38,7 +37,7 @@ import { Vector3, Color3 } from '@babylonjs/core'
const DefaultScale = new Vector3(1, 1, 1);
const BiggerScale = new Vector3(1.25, 1.25, 1.25);

const MovingBox = (props) => {
const SpinningBox = (props) => {
// access Babylon scene objects with same React hook as regular DOM elements
const boxRef = useRef(null);

Expand Down Expand Up @@ -70,14 +69,18 @@ const MovingBox = (props) => {
</box>);
}

export const SceneWithMovingBoxes = () => (
export const SceneWithSpinningBoxes = () => (
<div>
<Engine antialias adaptToDeviceRatio canvasId='babylonJS' >
<Scene>
<arcRotateCamera name="camera1" target={Vector3.Zero()} alpha={Math.PI / 2} beta={Math.PI / 4} radius={8} />
<hemisphericLight name='light1' intensity={0.7} direction={Vector3.Up()} />
<MovingBox name='left' color={Color3.FromHexString('#EEB5EB')} hoveredColor={Color3.FromHexString('#C26DBC')} position={new Vector3(-2, 0, 0)} />
<MovingBox name='right' color={Color3.FromHexString('#C8F4F9')} hoveredColor={Color3.FromHexString('#3CACAE')} position={new Vector3(2, 0, 0)} />
<SpinningBox name='left' position={new Vector3(-2, 0, 0)}
color={Color3.FromHexString('#EEB5EB')} hoveredColor={Color3.FromHexString('#C26DBC')}
/>
<SpinningBox name='right' position={new Vector3(2, 0, 0)}
color={Color3.FromHexString('#C8F4F9')} hoveredColor={Color3.FromHexString('#3CACAE')}
/>
</Scene>
</Engine>
</div>
Expand Down
42 changes: 15 additions & 27 deletions src/customComponents/Model.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,38 @@
import { AbstractMesh, ISceneLoaderProgressEvent, Vector3 } from "@babylonjs/core";
import React, { useEffect } from "react"
import { FiberAbstractMeshProps, FiberAbstractMeshPropsCtor } from "../generatedProps";
import { ILoadedModel, LoadedModel } from "../hooks/loaders/loadedModel";

import { SceneLoaderOptions, useSceneLoader } from "../hooks/loaders/useSceneLoader";

export type ModelProps = {
/**
* Only used on init. Will not update dynamically (scaling will update dynamically and override this)
*/
meshNames?: any
receiveShadows?: boolean
rootUrl: string
sceneFilename: string
pluginExtension?: string
position?: Vector3
scaling?: Vector3
rotation?: Vector3
showBoundingBox?: boolean
alwaysSelectAsActiveMesh?: boolean
reportProgress?: boolean

/**
* Only used on init. Will not update dynamically (scaling will update dynamically and override this)
*/
scaleToDimension?: number
onModelLoaded?: (model: ILoadedModel) => void
onModelError?: (model: LoadedModel) => void
onLoadProgress?: (event: ISceneLoaderProgressEvent) => void
onCreated?: (rootMesh: AbstractMesh) => void
}
} & FiberAbstractMeshProps & FiberAbstractMeshPropsCtor;

const Model = (props: ModelProps) => {
const Model: React.FC<ModelProps> = (props: ModelProps) => {
const { alwaysSelectAsActiveMesh, onModelLoaded, pluginExtension, rootUrl, receiveShadows, reportProgress, scaleToDimension, sceneFilename, ...rest } = props;
const options: SceneLoaderOptions = {
receiveShadows: props.receiveShadows,
scaleToDimension: props.scaleToDimension,
alwaysSelectAsActiveMesh: props.alwaysSelectAsActiveMesh,
reportProgress: props.reportProgress,
onModelLoaded: props.onModelLoaded
}
const sceneLoaderResults = useSceneLoader(props.rootUrl, props.sceneFilename, props.pluginExtension, options);

if (props.position) {
sceneLoaderResults.rootMesh!.position = props.position;
}

if (props.rotation) {
sceneLoaderResults.rootMesh!.rotation = props.rotation;
receiveShadows,
scaleToDimension,
alwaysSelectAsActiveMesh,
reportProgress,
onModelLoaded
}
const sceneLoaderResults = useSceneLoader(rootUrl, sceneFilename, pluginExtension, options);

useEffect(() => {
return () => {
Expand All @@ -52,8 +41,7 @@ const Model = (props: ModelProps) => {
}
}, []);

// TODO: return <abstractMesh /> and push ...rest of spread props with fromInstance = {rootMesh}
return null;
return <abstractMesh fromInstance={sceneLoaderResults.rootMesh!} {...rest} />;
}

export default Model
export default Model;
4 changes: 2 additions & 2 deletions src/customComponents/Skybox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ interface SkyboxProps {
}

const Skybox: React.FC<SkyboxProps> = (props: SkyboxProps) =>
<box name={name !== undefined ? `skybox-${name}` : 'skybox'} size={props.size ?? 100} infiniteDistance={true} renderingGroupId={0}>
<standardMaterial name={name !== undefined ? `skybox-material-${name}` : 'skybox-material'} backFaceCulling={false} disableLighting={true}>
<box name={props.name ? `skybox-${props.name}` : 'skybox'} size={props.size ?? 100} infiniteDistance={true} renderingGroupId={0}>
<standardMaterial name={props.name ? `skybox-material-${props.name}` : 'skybox-material'} backFaceCulling={false} disableLighting={true}>
<cubeTexture key={`cube-texture-${props.rootUrl}`} rootUrl={props.rootUrl} coordinatesMode={Texture.SKYBOX_MODE} assignTo={'reflectionTexture'} />
</standardMaterial>
</box>
Expand Down
155 changes: 74 additions & 81 deletions stories/babylonjs/Models/model-loader.stories.js
Original file line number Diff line number Diff line change
@@ -1,123 +1,116 @@
import React, { Component} from 'react';
import React, { Suspense, useRef, useContext, useMemo } from 'react';
import '@babylonjs/inspector';

import { Vector3, Color3 } from '@babylonjs/core';
import { ActionManager, SetValueAction } from '@babylonjs/core/Actions';

import { Engine, Scene } from '../../../dist/react-babylonjs';
import { Engine, Scene, useAssetManager, TaskType, useBeforeRender, AssetManagerContext, AssetManagerContextProvider } from '../../../dist/react-babylonjs';

import ScaledModelWithProgress from '../ScaledModelWithProgress';
import { Vector3 } from '@babylonjs/core';

// import '../../style.css';
import '../../style.css';

export default { title: 'Models' };

class WithModel extends Component {
constructor () {
super()
const baseUrl = 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/'

this.state = {
avocadoYPos: -1.5,
avocadoScaling: 3.0
}
const modelAssetTasks = [
{ taskType: TaskType.Mesh, rootUrl: `${baseUrl}BoomBox/glTF/`, sceneFilename: 'BoomBox.gltf', name: 'boombox' },
{ taskType: TaskType.Mesh, rootUrl: `${baseUrl}Avocado/glTF/`, sceneFilename: 'Avocado.gltf', name: 'avocado' }
];

this.moveAvocadoUp = this.moveAvocadoUp.bind(this)
this.moveAvocadoDown = this.moveAvocadoDown.bind(this)
this.increaseAvocadoSize = this.increaseAvocadoSize.bind(this)
this.decreaseAvocadoSize = this.decreaseAvocadoSize.bind(this)
this.onModelLoaded = this.onModelLoaded.bind(this)
}
const MyFallback = () => {
const boxRef = useRef();
const context = useContext(AssetManagerContext);
console.log('context in fallback:', context);

moveAvocadoDown () {
this.setState((state) => ({
...state,
avocadoYPos: state.avocadoYPos - 0.5
}))
}
useBeforeRender((scene) => {
if (boxRef.current) {
var deltaTimeInMillis = scene.getEngine().getDeltaTime();

moveAvocadoUp () {
this.setState((state) => ({
...state,
avocadoYPos: state.avocadoYPos + 0.5
}))
}
const rpm = 10;
boxRef.current.hostInstance.rotation.x = Math.PI / 4;
boxRef.current.hostInstance.rotation.y += ((rpm / 60) * Math.PI * 2 * (deltaTimeInMillis / 1000));
}
})

const eventData = context?.lastProgress?.eventData;

return <>
<adtFullscreenUi name='ui'>
<rectangle name="rect" height='50px' width='150px'>
<rectangle>
{eventData !== undefined &&
<textBlock text={`${eventData.totalCount-eventData.remainingCount}/${eventData.totalCount}`} fontStyle="bold" fontSize={20} color="white"/>
}
{eventData === undefined &&
<textBlock text='0/2' fontStyle="bold" fontSize={20} color="white"/>
}
</rectangle>
</rectangle>
</adtFullscreenUi>
<box ref={boxRef} name='fallback' size={2} />
</>
}

increaseAvocadoSize () {
this.setState((state) => ({
...state,
avocadoScaling: state.avocadoScaling + 0.1
}))
}
const MyModels = () => {
const [result] = useAssetManager(modelAssetTasks);

decreaseAvocadoSize () {
this.setState((state) => ({
...state,
avocadoScaling: state.avocadoScaling - 0.1
}))
}
useMemo(() => {
console.log('Loaded Tasks', result);
const boomboxTask = result.map['boombox'];
boomboxTask.loadedMeshes[0].position = new Vector3(2.5, 0, 0);
boomboxTask.loadedMeshes[1].scaling = new Vector3(20, 20, 20);

onModelLoaded = (model, sceneContext) => {
let mesh = model.meshes[1]
mesh.actionManager = new ActionManager(sceneContext.scene)
mesh.actionManager.registerAction(
new SetValueAction(
ActionManager.OnPointerOverTrigger,
mesh.material,
'wireframe',
true
)
)
mesh.actionManager.registerAction(
new SetValueAction(
ActionManager.OnPointerOutTrigger,
mesh.material,
'wireframe',
false
)
)
}
const avocadoTask = result.map['avocado'];
avocadoTask.loadedMeshes[0].position = new Vector3(-2.5, 0, 0);
avocadoTask.loadedMeshes[1].scaling = new Vector3(20, 20, 20);
});

return null;
}

const MyScene = () => {

render () {
let baseUrl = 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/'
return (
<Engine antialias adaptToDeviceRatio canvasId='babylonJS'>
<Scene>
<arcRotateCamera name='camera1' alpha={Math.PI / 2} beta={Math.PI / 2} radius={9.0} target={Vector3.Zero()} minZ={0.001} />
<hemisphericLight name='light1' intensity={0.7} direction={Vector3.Up()} />
return (
<Engine antialias adaptToDeviceRatio canvasId='babylonJS'>
<Scene>
<arcRotateCamera name='camera1' alpha={Math.PI / 2} beta={Math.PI / 2} radius={9.0} target={Vector3.Zero()} minZ={0.001} />
<hemisphericLight name='light1' intensity={0.7} direction={Vector3.Up()} />

<ScaledModelWithProgress rootUrl={`${baseUrl}BoomBox/glTF/`} sceneFilename='BoomBox.gltf' scaleTo={3}
progressBarColor={Color3.FromInts(255, 165, 0)} center={new Vector3(2.5, 0, 0)}
onModelLoaded={this.onModelLoaded}
/>
<AssetManagerContextProvider>

{/*<React.Suspense fallback={<box />}>
<Suspense fallback={<MyFallback />}>
<Model rootUrl={`${baseUrl}Avocado/glTF/`} sceneFilename='Avocado.gltf' />
</React.Suspense>
<MyModels />
*/}

{/* <SceneLoaderContextProvider>
<Suspense fallback= {<ProgressFallback progressBarColor={Color3.FromInts(255, 165, 0)} center={new Vector3(2.5, 0, 0)} rotation={Vector3.Zero()} scaleTo={3} />}>
<ScaledModelWithProgress position={sceneLoaderPosition} rootUrl={`${baseUrl}Avocado/glTF/`} sceneFilename='Avocado.gltf' />
</Suspense>
</SceneLoaderContextProvider> */}
</Suspense>
</AssetManagerContextProvider>
{/* <ScaledModelWithProgress rootUrl={`${baseUrl}Avocado/glTF/`} sceneFilename='Avocado.gltf'
scaleTo={this.state.avocadoScaling}
progressBarColor={Color3.FromInts(255, 165, 0)}
center={new Vector3(-2.5, this.state.avocadoYPos, 0)}
/> */}
</Scene>
</Engine>
)
</Scene>
</Engine>
)
}
}

export const ModelStory = () => (
export const ModelLoaderStory = () => (
<div style={{ flex: 1, display: 'flex' }}>
<WithModel />
<MyScene />
</div>
)

ModelStory.story = {
name: '3D-Model'
}
ModelLoaderStory.story = {
name: 'Asset Manager'
}
2 changes: 1 addition & 1 deletion stories/babylonjs/Models/model.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class WithModel extends Component {

onModelLoaded = (model, sceneContext) => {
let mesh = model.meshes[1]
mesh.actionManager = new ActionManager(sceneContext.scene)
mesh.actionManager = new ActionManager(mesh._scene)
mesh.actionManager.registerAction(
new SetValueAction(
ActionManager.OnPointerOverTrigger,
Expand Down
40 changes: 3 additions & 37 deletions stories/babylonjs/ScaledModelWithProgress.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,7 @@
import React, { Suspense, useContext, useEffect, useRef } from 'react'
import React, { Suspense, useContext } from 'react'
import { Vector3, Matrix, Color3 } from '@babylonjs/core/Maths/math'

import { useSceneLoader, useScene, SceneLoaderContextProvider, SceneLoaderContext, SceneContext } from '../../dist/react-babylonjs';

// try with later versions of RHL to get hooks working here:
// const [loadProgress, updateProgress] = useState(0)
const MyModel = (props) => {

const sceneLoaderResults = useSceneLoader(props.rootUrl, props.sceneFilename, undefined, {
scaleToDimension: props.scaleTo,
reportProgress: true
})

const scene = useScene();
const modelLoaded = useRef(false);
if (props.onModelLoaded && !modelLoaded.current) {
modelLoaded.current = true;
props.onModelLoaded(sceneLoaderResults, { scene });
}

if (props.position) {
sceneLoaderResults.rootMesh.position = props.position;
}

if (props.rotation) {
sceneLoaderResults.rootMesh.rotation = props.rotation;
}

useEffect(() => {
return () => {
console.log('disposing the sceneloader results.')
sceneLoaderResults.dispose();
}
}, []);

return null;
}
import { Model, SceneLoaderContextProvider, SceneLoaderContext } from '../../dist/react-babylonjs';

const ProgressFallback = (props) => {
const sceneLoaderContext = useContext(SceneLoaderContext);
Expand Down Expand Up @@ -67,7 +33,7 @@ const ScaledModelWithProgress = (props) => {
return (
<SceneLoaderContextProvider>
<Suspense fallback= {<ProgressFallback progressBarColor={props.progressBarColor} rotation={props.progressRotation ?? props.modelRotation} center={props.center} scaleTo={props.scaleTo} />}>
<MyModel position={props.center} rootUrl={props.rootUrl} sceneFilename={props.sceneFilename} scaleTo={props.scaleTo} rotation={props.modelRotation} onModelLoaded={props.onModelLoaded} />
<Model reportProgress position={props.center} rootUrl={props.rootUrl} sceneFilename={props.sceneFilename} scaleToDimension={props.scaleTo} rotation={props.modelRotation} onModelLoaded={props.onModelLoaded} />
</Suspense>
</SceneLoaderContextProvider>
)
Expand Down

0 comments on commit dd6949a

Please sign in to comment.