forked from cruise-automation/webviz
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add <GLTFScene>, loadGLB, and Duck example (cruise-automation#64)
- Loading branch information
Showing
12 changed files
with
421 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import {DuckScene} from './jsx/allLiveEditors'; | ||
|
||
# GLTFScene | ||
|
||
`<GLTFScene />` provides support for loading and rendering [glTF models](https://www.khronos.org/gltf/). Only **Binary glTF** (`.glb`, without external resources) and basic color textures are currently supported. | ||
|
||
## Props | ||
|
||
| Name | Type | Default | Description | | ||
| ------- | ---------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| `model` | `string | (() => Promise<Object>)` | | URL of a `.glb` file to load, or an async function resolving to a loaded model. (A `parseGLB` function is also exported if you want custom logic for loading the model.) The file must define a single `scene`. | | ||
| `pose` | `Pose` | zero position, identity orientation | position and orientation at which to render the scene | | ||
| `scale` | `Scale` | `{ x: 1, y: 1, z: 1 }` | scale factor | | ||
|
||
## Duck.glb | ||
|
||
This example shows a common sample model from [KhronosGroup/glTF-Sample-Models](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0). | ||
|
||
<DuckScene /> |
Binary file not shown.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// Copyright (c) 2019-present, GM Cruise LLC | ||
// | ||
// This source code is licensed under the Apache License, Version 2.0, | ||
// found in the LICENSE file in the root directory of this source tree. | ||
// You may not use this file except in compliance with the License. | ||
|
||
// #BEGIN EXAMPLE | ||
import React from "react"; | ||
|
||
import duckModel from "../Duck.glb"; // URL pointing to a .glb file | ||
import Worldview, { Axes, Grid, GLTFScene, DEFAULT_CAMERA_STATE } from "regl-worldview"; | ||
|
||
// #BEGIN EDITABLE | ||
function DuckScene() { | ||
return ( | ||
<Worldview | ||
defaultCameraState={{ | ||
...DEFAULT_CAMERA_STATE, | ||
distance: 15, | ||
thetaOffset: (-3 * Math.PI) / 4, | ||
}}> | ||
<Axes /> | ||
<Grid /> | ||
<GLTFScene model={duckModel}> | ||
{{ | ||
pose: { | ||
position: { x: 0, y: 0, z: 0 }, | ||
orientation: { x: 0, y: 0, z: 0, w: 1 }, | ||
}, | ||
scale: { x: 3, y: 3, z: 3 }, | ||
}} | ||
</GLTFScene> | ||
</Worldview> | ||
); | ||
} | ||
// #END EXAMPLE | ||
|
||
export default DuckScene; |
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
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
// @flow | ||
|
||
// Copyright (c) 2019-present, GM Cruise LLC | ||
// | ||
// This source code is licensed under the Apache License, Version 2.0, | ||
// found in the LICENSE file in the root directory of this source tree. | ||
// You may not use this file except in compliance with the License. | ||
|
||
import { mat4 } from "gl-matrix"; | ||
import React from "react"; | ||
|
||
import parseGLB from "../utils/parseGLB"; | ||
|
||
import { Command, pointToVec3, orientationToVec4, type Pose, type Scale, WorldviewReactContext } from ".."; | ||
|
||
function glConstantToRegl(value: ?number): ?string { | ||
if (value === undefined) { | ||
return undefined; | ||
} | ||
// prettier-ignore | ||
switch (value) { | ||
// min/mag filters | ||
case WebGLRenderingContext.NEAREST: return "nearest"; | ||
case WebGLRenderingContext.LINEAR: return "linear"; | ||
case WebGLRenderingContext.NEAREST_MIPMAP_NEAREST: return "nearest mipmap nearest"; | ||
case WebGLRenderingContext.NEAREST_MIPMAP_LINEAR: return "nearest mipmap linear"; | ||
case WebGLRenderingContext.LINEAR_MIPMAP_NEAREST: return "linear mipmap nearest"; | ||
case WebGLRenderingContext.LINEAR_MIPMAP_LINEAR: return "linear mipmap linear"; | ||
// texture wrapping modes | ||
case WebGLRenderingContext.REPEAT: return "repeat"; | ||
case WebGLRenderingContext.CLAMP_TO_EDGE: return "clamp"; | ||
case WebGLRenderingContext.MIRRORED_REPEAT: return "mirror"; | ||
} | ||
throw new Error(`unhandled constant value ${JSON.stringify(value)}`); | ||
} | ||
|
||
const drawModel = (regl) => { | ||
const command = regl({ | ||
primitive: "triangles", | ||
uniforms: { | ||
baseColorTexture: regl.prop("baseColorTexture"), | ||
nodeMatrix: regl.prop("nodeMatrix"), | ||
poseMatrix: regl.context("poseMatrix"), | ||
"light.direction": [0, 0, -1], | ||
"light.ambientIntensity": 0.5, | ||
"light.diffuseIntensity": 0.5, | ||
}, | ||
attributes: { | ||
position: regl.prop("positions"), | ||
normal: regl.prop("normals"), | ||
texCoord: regl.prop("texCoords"), | ||
}, | ||
elements: regl.prop("indices"), | ||
vert: ` | ||
uniform mat4 projection, view; | ||
uniform mat4 nodeMatrix; | ||
uniform mat4 poseMatrix; | ||
attribute vec3 position, normal; | ||
varying vec3 vNormal; | ||
attribute vec2 texCoord; | ||
varying vec2 vTexCoord; | ||
void main() { | ||
// using the projection matrix for normals breaks lighting for orthographic mode | ||
mat4 mv = view * poseMatrix * nodeMatrix; | ||
vNormal = normalize((mv * vec4(normal, 0)).xyz); | ||
vTexCoord = texCoord; | ||
gl_Position = projection * mv * vec4(position, 1); | ||
} | ||
`, | ||
frag: ` | ||
precision mediump float; | ||
uniform sampler2D baseColorTexture; | ||
varying mediump vec2 vTexCoord; | ||
varying mediump vec3 vNormal; | ||
// Basic directional lighting from: | ||
// http://ogldev.atspace.co.uk/www/tutorial18/tutorial18.html | ||
struct DirectionalLight { | ||
mediump vec3 direction; | ||
lowp float ambientIntensity; | ||
lowp float diffuseIntensity; | ||
}; | ||
uniform DirectionalLight light; | ||
void main() { | ||
vec3 baseColor = texture2D(baseColorTexture, vTexCoord).rgb; | ||
float diffuse = light.diffuseIntensity * max(0.0, dot(vNormal, -light.direction)); | ||
gl_FragColor = vec4((light.ambientIntensity + diffuse) * baseColor, 1); | ||
} | ||
`, | ||
}); | ||
|
||
// Build the draw calls needed to draw the model. This only needs to happen once, since they | ||
// are the same each time, with only poseMatrix changing. | ||
let drawCalls; | ||
function prepareDrawCallsIfNeeded(model) { | ||
if (drawCalls) { | ||
return; | ||
} | ||
|
||
// upload textures to the GPU | ||
const textures = model.json.textures.map((textureInfo) => { | ||
const sampler = model.json.samplers[textureInfo.sampler]; | ||
const bitmap: ImageBitmap = model.images[textureInfo.source]; | ||
const texture = regl.texture({ | ||
data: bitmap, | ||
min: glConstantToRegl(sampler.minFilter), | ||
mag: glConstantToRegl(sampler.magFilter), | ||
wrapS: glConstantToRegl(sampler.wrapS), | ||
wrapT: glConstantToRegl(sampler.wrapT), | ||
}); | ||
bitmap.close(); | ||
return texture; | ||
}); | ||
drawCalls = []; | ||
|
||
// helper to draw the primitives comprising a mesh | ||
function drawMesh(mesh, nodeMatrix) { | ||
for (const primitive of mesh.primitives) { | ||
const material = model.json.materials[primitive.material]; | ||
const texInfo = material.pbrMetallicRoughness.baseColorTexture; | ||
drawCalls.push({ | ||
indices: model.accessors[primitive.indices], | ||
positions: model.accessors[primitive.attributes.POSITION], | ||
normals: model.accessors[primitive.attributes.NORMAL], | ||
texCoords: model.accessors[primitive.attributes[`TEXCOORD_${texInfo.texCoord || 0}`]], | ||
baseColorTexture: textures[texInfo.index], | ||
nodeMatrix, | ||
}); | ||
} | ||
} | ||
|
||
// helper to draw all the meshes contained in a node and its child nodes | ||
function drawNode(node, parentMatrix) { | ||
const nodeMatrix = node.matrix | ||
? mat4.clone(node.matrix) | ||
: mat4.fromRotationTranslationScale( | ||
mat4.create(), | ||
node.rotation || [0, 0, 0, 1], | ||
node.translation || [0, 0, 0], | ||
node.scale || [1, 1, 1] | ||
); | ||
mat4.mul(nodeMatrix, parentMatrix, nodeMatrix); | ||
if (node.mesh != null) { | ||
drawMesh(model.json.meshes[node.mesh], nodeMatrix); | ||
} | ||
if (node.children) { | ||
for (const childIdx of node.children) { | ||
drawNode(model.json.nodes[childIdx], nodeMatrix); | ||
} | ||
} | ||
} | ||
|
||
// finally, draw each of the main scene's nodes | ||
for (const nodeIdx of model.json.scenes[model.json.scene].nodes) { | ||
const rootTransform = mat4.create(); | ||
mat4.rotateX(rootTransform, rootTransform, Math.PI / 2); | ||
mat4.rotateY(rootTransform, rootTransform, Math.PI / 2); | ||
drawNode(model.json.nodes[nodeIdx], rootTransform); | ||
} | ||
} | ||
|
||
// create a regl command to set the context for each draw call | ||
const withPoseMatrix = regl({ | ||
context: { | ||
poseMatrix: (context, props) => | ||
mat4.fromRotationTranslationScale( | ||
mat4.create(), | ||
orientationToVec4(props.pose.orientation), | ||
pointToVec3(props.pose.position), | ||
props.scale ? pointToVec3(props.scale) : [1, 1, 1] | ||
), | ||
}, | ||
}); | ||
|
||
return (props) => { | ||
prepareDrawCallsIfNeeded(props.model); | ||
withPoseMatrix(props, () => { | ||
command(drawCalls); | ||
}); | ||
}; | ||
}; | ||
|
||
type Props = {| | ||
model: string | (() => Promise<Object>), | ||
children: {| | ||
pose: Pose, | ||
scale: Scale, | ||
|}, | ||
|}; | ||
|
||
export default class GLTFScene extends React.Component<Props, {| loadedModel: ?Object |}> { | ||
state = { | ||
loadedModel: undefined, | ||
}; | ||
_context = undefined; | ||
|
||
async _loadModel(): Promise<Object> { | ||
const { model } = this.props; | ||
if (typeof model === "function") { | ||
return model(); | ||
} else if (typeof model === "string") { | ||
const response = await fetch(model); | ||
if (!response.ok) { | ||
throw new Error(`failed to fetch GLB: ${response.status}`); | ||
} | ||
return parseGLB(await response.arrayBuffer()); | ||
} | ||
/*:: (model: empty) */ | ||
throw new Error(`unsupported model prop: ${typeof model}`); | ||
} | ||
|
||
componentDidMount() { | ||
this._loadModel() | ||
.then((loadedModel) => { | ||
this.setState({ loadedModel }); | ||
if (this._context) { | ||
this._context.onDirty(); | ||
} | ||
}) | ||
.catch((err) => { | ||
console.error("error loading GLB model:", err); | ||
}); | ||
} | ||
|
||
render() { | ||
const { loadedModel } = this.state; | ||
if (!loadedModel) { | ||
return null; | ||
} | ||
return ( | ||
<WorldviewReactContext.Consumer> | ||
{(context) => { | ||
this._context = context; | ||
return <Command reglCommand={drawModel} drawProps={{ model: loadedModel, ...this.props.children }} />; | ||
}} | ||
</WorldviewReactContext.Consumer> | ||
); | ||
} | ||
} |
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
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
Oops, something went wrong.