Merge pull request #1870 from lbryio/three-v2

3D-viewer v2
This commit is contained in:
Sean Yesmunt 2018-08-22 15:10:06 -04:00 committed by GitHub
commit c077c9103d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 358 additions and 147 deletions

View file

@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
* New viewer for human-readable text files ([#1826](https://github.com/lbryio/lbry-desktop/pull/1826))
* CSV and JSON viewer ([#1410](https://github.com/lbryio/lbry-desktop/pull/1410))
* Recommended content on file viewer page ([#1845](https://github.com/lbryio/lbry-desktop/pull/1845))
* 3D File viewer improvements ([#1870](https://github.com/lbryio/lbry-desktop/pull/1870))
* Desktop notification when publish is completed ([#1892](https://github.com/lbryio/lbry-desktop/pull/1892))
### Changed

View file

@ -39,6 +39,7 @@
"classnames": "^2.2.5",
"codemirror": "^5.39.2",
"country-data": "^0.0.31",
"dat.gui": "^0.7.2",
"dom-scroll-into-view": "^1.2.1",
"electron-dl": "^1.11.0",
"electron-is-dev": "^0.3.0",

View file

@ -1,6 +1,9 @@
// @flow
import * as React from 'react';
import * as dat from 'dat.gui';
import classNames from 'classnames';
import LoadingScreen from 'component/common/loading-screen';
// ThreeJS
import * as THREE from './internal/three';
import detectWebGL from './internal/detector';
@ -11,123 +14,39 @@ import ThreeRenderer from './internal/renderer';
type Props = {
theme: string,
autoRotate: boolean,
source: {
fileType: string,
downloadPath: string,
},
};
class ThreeViewer extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
const { theme } = this.props;
// Main container
this.viewer = React.createRef();
// Object colors
this.materialColors = {
red: '#e74c3c',
blue: '#3498db',
green: '#44b098',
orange: '#f39c12',
type State = {
error: ?string,
isReady: boolean,
isLoading: boolean,
};
// Viewer themes
this.themes = {
dark: {
gridColor: '#414e5c',
groundColor: '#13233C',
backgroundColor: '#13233C',
centerLineColor: '#7f8c8d',
},
light: {
gridColor: '#7f8c8d',
groundColor: '#DDD',
backgroundColor: '#EEE',
centerLineColor: '#2F2F2F',
},
};
class ThreeViewer extends React.PureComponent<Props, State> {
static testWebgl = new Promise((resolve, reject) => {
if (detectWebGL()) resolve();
else reject();
});
// Select current theme
this.theme = this.themes[theme] || this.themes.light;
// State
this.state = {
error: null,
isReady: false,
isLoading: false,
};
}
componentDidMount() {
if (detectWebGL()) {
this.renderScene();
// Update render on resize window
window.addEventListener('resize', this.handleResize, false);
} else {
// No webgl support, handle Error...
// TODO: Use a better error message
this.setState({ error: "Sorry, your computer doesn't support WebGL." });
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize, false);
}
transformGroup(group) {
this.fitMeshToCamera(group);
this.createWireFrame(group);
this.updateControlsTarget(group.position);
}
createOrbitControls(camera, canvas) {
const { autoRotate } = this.props;
static createOrbitControls(camera, canvas) {
const controls = new THREE.OrbitControls(camera, canvas);
// Controls configuration
controls.enableDamping = true;
controls.dampingFactor = 0.75;
controls.enableZoom = true;
controls.minDistance = 1;
controls.maxDistance = 50;
controls.autoRotate = autoRotate;
controls.minDistance = 5;
controls.maxDistance = 14;
controls.autoRotate = false;
controls.enablePan = false;
controls.saveState();
return controls;
}
createGeometry(data) {
const geometry = new THREE.Geometry();
geometry.fromBufferGeometry(data);
geometry.computeBoundingBox();
geometry.computeVertexNormals();
geometry.center();
geometry.rotateX(-Math.PI / 2);
geometry.lookAt(new THREE.Vector3(0, 0, 1));
return geometry;
}
createWireFrame(group) {
const wireframeGeometry = new THREE.WireframeGeometry(group.geometry);
const wireframeMaterial = new THREE.LineBasicMaterial({
opacity: 0,
transparent: true,
linewidth: 1,
});
// Set material color
wireframeMaterial.color.set(this.materialColors.green);
this.wireframe = new THREE.LineSegments(wireframeGeometry, wireframeMaterial);
group.add(this.wireframe);
}
toggleWireFrame(show = false) {
this.wireframe.opacity = show ? 1 : 0;
this.mesh.material.opacity = show ? 0 : 1;
}
fitMeshToCamera(group) {
static fitMeshToCamera(group) {
const max = { x: 0, y: 0, z: 0 };
const min = { x: 0, y: 0, z: 0 };
@ -147,20 +66,214 @@ class ThreeViewer extends React.PureComponent<Props> {
const meshY = Math.abs(max.y - min.y);
const meshX = Math.abs(max.x - min.x);
const scaleFactor = 10 / Math.max(meshX, meshY);
group.scale.set(scaleFactor, scaleFactor, scaleFactor);
group.position.setY((meshY / 2) * scaleFactor);
// Reset object position
const box = new THREE.Box3().setFromObject(group);
// Update position
box.getCenter(group.position);
group.position.multiplyScalar(-1);
group.position.setY(group.position.y + meshY * scaleFactor);
}
/*
See: https://github.com/mrdoob/three.js/blob/dev/docs/scenes/js/material.js#L195
*/
static updateMaterial(material, geometry) {
material.vertexColors = +material.vertexColors; // Ensure number
material.side = +material.side; // Ensure number
material.needsUpdate = true;
// If Geometry needs update
if (geometry) {
geometry.verticesNeedUpdate = true;
geometry.normalsNeedUpdate = true;
geometry.colorsNeedUpdate = true;
}
}
constructor(props: Props) {
super(props);
const { theme } = this.props;
this.viewer = React.createRef();
this.guiContainer = React.createRef();
// Object defualt color
this.materialColor = '#44b098';
// Viewer themes
this.themes = {
dark: {
gridColor: '#414e5c',
groundColor: '#13233C',
backgroundColor: '#13233C',
centerLineColor: '#7f8c8d',
},
light: {
gridColor: '#7f8c8d',
groundColor: '#DDD',
backgroundColor: '#EEE',
centerLineColor: '#2F2F2F',
},
};
// Select current theme
this.theme = this.themes[theme] || this.themes.light;
// State
this.state = {
error: null,
isReady: false,
isLoading: false,
};
}
componentDidMount() {
ThreeViewer.testWebgl
.then(() => {
this.renderScene();
// Update render on resize window
window.addEventListener('resize', this.handleResize, false);
})
.catch(() => {
// No webgl support, handle Error...
// TODO: Use a better error message
this.setState({ error: "Sorry, your computer doesn't support WebGL." });
});
}
componentWillUnmount() {
// Remove event listeners
window.removeEventListener('resize', this.handleResize, false);
// Free memory
if (this.renderer && this.mesh) {
// Clean up group
this.scene.remove(this.mesh);
if (this.mesh.geometry) this.mesh.geometry.dispose();
if (this.mesh.material) this.mesh.material.dispose();
// Cleanup shared geometry
if (this.geometry) this.geometry.dispose();
if (this.bufferGeometry) this.bufferGeometry.dispose();
// Clean up shared material
if (this.material) this.material.dispose();
// Clean up grid
if (this.grid) {
this.grid.material.dispose();
this.grid.geometry.dispose();
}
// Clean up group items
this.mesh.traverse(child => {
if (child instanceof THREE.Mesh) {
if (child.geometry) child.geometry.dispose();
if (child.material) child.material.dispose();
}
});
// Clean up controls
if (this.controls) this.controls.dispose();
// It's unclear if we need this:
if (this.renderer) {
this.renderer.renderLists.dispose();
this.renderer.dispose();
}
// Stop animation
cancelAnimationFrame(this.frameID);
// Destroy GUI Controls
if (this.gui) this.gui.destroy();
// Empty objects
this.grid = null;
this.mesh = null;
this.renderer = null;
this.material = null;
this.geometry = null;
this.bufferGeometry = null;
}
}
transformGroup(group) {
ThreeViewer.fitMeshToCamera(group);
if (!this.targetCenter) {
const box = new THREE.Box3();
this.targetCenter = box.setFromObject(this.mesh).getCenter();
}
this.updateControlsTarget(this.targetCenter);
}
createInterfaceControls() {
if (this.guiContainer && this.mesh) {
this.gui = new dat.GUI({ autoPlace: false, name: 'controls' });
const config = {
color: this.materialColor,
};
config.reset = () => {
// Reset material color
config.color = this.materialColor;
// Reset material
this.material.color.set(config.color);
this.material.flatShading = true;
this.material.shininess = 30;
this.material.wireframe = false;
// Reset autoRotate
this.controls.autoRotate = false;
// Reset camera
this.restoreCamera();
};
const materialFolder = this.gui.addFolder('Material');
// Color picker
const colorPicker = materialFolder
.addColor(config, 'color')
.name('Color')
.listen();
colorPicker.onChange(color => {
this.material.color.set(color);
});
materialFolder
.add(this.material, 'shininess', 0, 100)
.name('Shininess')
.listen();
materialFolder
.add(this.material, 'flatShading')
.name('FlatShading')
.onChange(() => {
ThreeViewer.updateMaterial(this.material);
})
.listen();
materialFolder
.add(this.material, 'wireframe')
.name('Wireframe')
.listen();
const sceneFolder = this.gui.addFolder('Scene');
sceneFolder
.add(this.controls, 'autoRotate')
.name('Auto-Rotate')
.listen();
sceneFolder.add(config, 'reset').name('Reset');
this.guiContainer.current.appendChild(this.gui.domElement);
}
}
createGeometry(data) {
this.bufferGeometry = data;
this.bufferGeometry.computeBoundingBox();
this.bufferGeometry.center();
this.bufferGeometry.rotateX(-Math.PI / 2);
this.bufferGeometry.lookAt(new THREE.Vector3(0, 0, 1));
// Get geometry from bufferGeometry
this.geometry = new THREE.Geometry().fromBufferGeometry(this.bufferGeometry);
this.geometry.mergeVertices();
this.geometry.computeVertexNormals();
}
startLoader() {
const { source } = this.props;
@ -179,6 +292,8 @@ class ThreeViewer extends React.PureComponent<Props> {
handleReady = () => {
this.setState({ isReady: true, isLoading: false });
// GUI
this.createInterfaceControls();
};
handleError = () => {
@ -193,32 +308,29 @@ class ThreeViewer extends React.PureComponent<Props> {
this.renderer.setSize(width, height);
};
handleColorChange(color) {
if (!this.mesh) return;
const pickColor = this.materialColors[color] || this.materialColors.green;
this.mesh.material.color.set(pickColor);
this.wireframe.material.color.set(pickColor);
}
updateControlsTarget(point) {
this.controls.target.fromArray([point.x, point.y, point.z]);
this.controls.update();
}
restoreCamera() {
this.controls.reset();
this.camera.position.set(-9.5, 14, 11);
this.updateControlsTarget(this.targetCenter);
}
renderStl(data) {
const geometry = this.createGeometry(data);
const group = new THREE.Mesh(geometry, this.material);
// Assign name
group.name = 'objectGroup';
this.scene.add(group);
this.transformGroup(group);
this.mesh = group;
this.createGeometry(data);
this.mesh = new THREE.Mesh(this.geometry, this.material);
this.mesh.name = 'model';
this.scene.add(this.mesh);
this.transformGroup(this.mesh);
}
renderObj(event) {
const mesh = event.detail.loaderRootNode;
const group = new THREE.Group();
group.name = 'objGroup';
this.mesh = new THREE.Group();
this.mesh.name = 'model';
// Assign new material
mesh.traverse(child => {
@ -226,15 +338,18 @@ class ThreeViewer extends React.PureComponent<Props> {
// Get geometry from child
const geometry = new THREE.Geometry();
geometry.fromBufferGeometry(child.geometry);
geometry.mergeVertices();
geometry.computeVertexNormals();
// Create and regroup inner objects
const innerObj = new THREE.Mesh(geometry, this.material);
group.add(innerObj);
this.mesh.add(innerObj);
// Clean up geometry
geometry.dispose();
child.geometry.dispose();
}
});
this.scene.add(group);
this.transformGroup(group);
this.mesh = group;
this.scene.add(this.mesh);
this.transformGroup(this.mesh);
}
renderModel(fileType, parsedData) {
@ -254,6 +369,7 @@ class ThreeViewer extends React.PureComponent<Props> {
this.renderer = ThreeRenderer({
antialias: true,
shadowMap: true,
gammaCorrection: true,
});
this.scene = ThreeScene({
@ -268,54 +384,56 @@ class ThreeViewer extends React.PureComponent<Props> {
// Grid
this.grid = ThreeGrid({ size: 100, gridColor, centerLineColor });
this.scene.add(this.grid);
// Camera
this.camera = new THREE.PerspectiveCamera(80, width / height, 0.1, 1000);
this.camera.position.set(-9.5, 14, 11);
// Controls
this.controls = this.createOrbitControls(this.camera, canvas);
this.controls = ThreeViewer.createOrbitControls(this.camera, canvas);
// Set viewer size
this.renderer.setSize(width, height);
// Create model material
this.material = new THREE.MeshPhongMaterial({
opacity: 1,
transparent: true,
// depthWrite: true,
depthWrite: true,
flatShading: true,
vertexColors: THREE.FaceColors,
// Positive value pushes polygon further away
// polygonOffsetFactor: 1,
// polygonOffsetUnits: 1,
});
// Set material color
this.material.color.set(this.materialColors.green);
this.material.color.set(this.materialColor);
// Load file and render mesh
this.startLoader();
// Append canvas
viewer.appendChild(canvas);
const updateScene = () => {
requestAnimationFrame(updateScene);
this.controls.update();
this.frameID = requestAnimationFrame(updateScene);
if (this.controls.autoRotate) this.controls.update();
this.renderer.render(this.scene, this.camera);
};
updateScene();
// Append canvas
viewer.appendChild(canvas);
}
render() {
const { theme } = this.props;
const { error, isReady, isLoading } = this.state;
const loadingMessage = 'Loading 3D model.';
const loadingMessage = __('Loading 3D model.');
const showViewer = isReady && !error;
const showLoading = isLoading && !error;
// Adaptive theme for gui controls
const containerClass = classNames('gui-container', { light: theme === 'light' });
return (
<React.Fragment>
{error && <LoadingScreen status={error} spinner={false} />}
{showLoading && <LoadingScreen status={loadingMessage} spinner />}
<div ref={this.guiContainer} className={containerClass} />
<div
style={{ opacity: showViewer ? 1 : 0 }}
className="three-viewer file-render__viewer"

View file

@ -3,10 +3,8 @@ import { GridHelper, Color } from './three';
const ThreeGrid = ({ size, gridColor, centerLineColor }) => {
const divisions = size / 2;
const grid = new GridHelper(size, divisions, new Color(centerLineColor), new Color(gridColor));
grid.material.opacity = 0.4;
grid.material.transparent = true;
return grid;
};

View file

@ -1,14 +1,13 @@
import { WebGLRenderer } from './three';
const ThreeRenderer = ({ antialias, shadowMap }) => {
const renderer = new WebGLRenderer({
antialias,
});
const ThreeRenderer = ({ antialias, shadowMap, gammaCorrection }) => {
const renderer = new WebGLRenderer({ antialias });
// Renderer configuration
renderer.setPixelRatio(window.devicePixelRatio);
renderer.gammaInput = true;
renderer.gammaOutput = true;
renderer.shadowMap.enabled = shadowMap;
renderer.gammaInput = gammaCorrection || false;
renderer.gammaOutput = gammaCorrection || false;
renderer.shadowMap.enabled = shadowMap || false;
renderer.shadowMap.autoUpdate = false;
return renderer;
};

View file

@ -28,3 +28,4 @@
@import 'component/_search.scss';
@import 'component/_toggle.scss';
@import 'component/_search.scss';
@import 'component/_dat-gui.scss';

View file

@ -0,0 +1,89 @@
/*
* dat.gui component
*/
.gui-container {
position: absolute;
top: 0;
right: 0;
z-index: 2;
.dg.main {
padding: 0;
margin: 0;
overflow: inherit;
}
}
/*
* Light theme:
* https://github.com/liabru/dat-gui-light-theme
*/
.gui-container.light {
.dg.main.taller-than-window .close-button {
border-top: 1px solid #ddd;
}
.dg.main .close-button {
background-color: #e8e8e8;
}
.dg.main .close-button:hover {
background-color: #ddd;
}
.dg {
color: #555;
text-shadow: none !important;
}
.dg.main::-webkit-scrollbar {
background: #fafafa;
}
.dg.main::-webkit-scrollbar-thumb {
background: #bbb;
}
.dg li:not(.folder) {
background: #fafafa;
border-bottom: 1px solid #ddd;
}
.dg li.save-row .button {
text-shadow: none !important;
}
.dg li.title {
background: #e8e8e8
url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==)
6px 10px no-repeat;
}
.dg .cr.function:hover,
.dg .cr.boolean:hover {
background: #fff;
}
.dg .c input[type='text'] {
background: #e9e9e9;
}
.dg .c input[type='text']:hover {
background: #eee;
}
.dg .c input[type='text']:focus {
background: #eee;
color: #555;
}
.dg .c .slider {
background: #e9e9e9;
}
.dg .c .slider:hover {
background: #eee;
}
}

View file

@ -2514,6 +2514,10 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
dat.gui@^0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/dat.gui/-/dat.gui-0.7.2.tgz#57056f286d0f989e83f5adec196f24fd69c01ae3"
date-fns@^1.27.2:
version "1.29.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6"