React VR - Render Three.js Elements in Scene - three.js

I used the react-vr init App to spin up a sample project and had no problem generating spheres throughout my scene using:
{
data.map(node => return <Sphere key={`node-${node.id}`} radius={1} widthSegments={8} heightSegments={8} lit={true}
style={{color: "red", transform: [{translate: [node.x, node.y, node.z]}]
}}/>
}
But now I am looking to connect the Spheres via a simple Line geometry. I tried doing the below to create the line, but I don't know how to add to the scene. In plain three.js code I would simply scene.add(), but I'm not sure how that works in react-vr.
import {
LineBasicMaterial,
Line,
Geometry,
Vector3
} from 'three';
const lineMaterial = new LineBasicMaterial({color: 0xf0f0f0, transparent: true});
lineMaterial.opacity = .75;
const line = new Line(new Geometry(), lineMaterial);
line.geometry.vertices = [new Vector3(0, 0, 0), new Vector3(0, 0, 0)];
line.renderOrder = 10;

H/T to #cidicles and the react-vr docs! I create a new SceneGroup module in which it takes a three.js Group as an initializing variable and then when I'm ready in ./index.vr.js I call SceneGroupModule.drawLine(); to actually draw the Line and add to the scene.
./client.js
import {VRInstance} from 'react-vr-web';
import {
Scene,
Group
} from 'three';
function init(bundle, parent, options) {
const scene = new Scene();
const sceneGroupModule = new SceneGroupModule();
const vr = new VRInstance(bundle, 'App', parent, {
nativeModules: [sceneGroupModule],
scene: scene,
});
const group = new Group();
scene.add(group);
sceneGroupModule.init(group);
vr.render = () => {};
// Begin the animation loop
vr.start();
return vr;
}
window.ReactVR = {init};
export default class SceneGroupModule extends Module {
constructor() {
super('SceneGroupModule');
}
// Called directly after the module is created.
init(sceneGroup) {
this.sceneGroup = sceneGroup;
}
drawLine() {
const lineMaterial = new LineBasicMaterial({color: 0xf0f0f0, transparent: true});
lineMaterial.opacity = .75;
const line = new Line(new Geometry(), lineMaterial);
line.geometry.vertices = [new Vector3(0, 0, 0), new Vector3(0, 0, 0)];
line.geometry.vertices = [
new Vector3(10, 2, 5),
new Vector3(11, 3, 4)
];
line.geometry.verticesNeedUpdate = true;
this.sceneGroup.add(line);
}
}
./index.vr.js
import React from 'react';
import {
AppRegistry,
View,
NativeModules
} from 'react-vr';
// Native Module defined in vr/client.js
const SceneGroupModule = NativeModules.SceneGroupModule;
class App extends React.Component {
constructor(props) {
super(props);
this.state = {};
SceneGroupModule.drawLine();
}
render() {
return (
<View
style={{
transform: [{translate: [0, 0, -3]}],
layoutOrigin: [0.5, 0, 0],
alignItems: 'center',
}}
>
</View>
);
}
}
AppRegistry.registerComponent('App', () => App);

Related

Collada Kinematics Tweening to desired position with three.js and tween.js

The bounty expires in 6 days. Answers to this question are eligible for a +50 reputation bounty.
Bilal wants to draw more attention to this question.
I want to replicate the existent example of loading Collada kinematics and using them with my own model, to do so I have created a class as follows:
iimport { Object3D, MathUtils } from "three";
import { ColladaLoader } from "three/addons/loaders/ColladaLoader.js";
import yumi_path from "../assets/dae/yumi.dae";
import { Tween, Easing, update } from "#tweenjs/tween.js";
export class YuMiMotion {
constructor(scene, joint_vlaues) {
this.scene = scene;
this.tgt_jnt_vals = joint_vlaues;
this.loader = new ColladaLoader();
this.yumi_model = new Object3D();
this.kinematics;
this.kinematicsTween;
this.tweenParameters = {};
this.loadYuMi();
this.startMotion();
}
startMotion = () => {
if (this.kinematics == undefined) {
console.log("Kinematics Still not loaded!");
this.anim_frame_id1 = requestAnimationFrame(this.startMotion);
}
else {
console.log(this.kinematics);
this.setupTween();
cancelAnimationFrame(this.anim_frame_id1);
this.animate();
}
}
animate = () => {
update();
this.anim_frame_id2 = requestAnimationFrame(this.animate);
}
loadYuMi = async() => {
const onLoad = (result, yumi) => {
const model = result.scene.children[0];
yumi.add(model.clone(true));
yumi.traverse(function (child) {
if (child.isMesh) {
child.material.flatShading = true;
}
});
this.kinematics = result.kinematics;
};
await this.loader.load(yumi_path, (collada) =>
onLoad(collada, this.yumi_model, this.kinematics)
),
undefined,
function (error) {
console.log("An error happened", error);
};
this.yumi_model.position.set(0.0, 0.0, -6.0);
this.yumi_model.rotation.set(-Math.PI / 2, 0.0, -Math.PI / 2);
this.yumi_model.scale.set(20, 20, 20);
this.scene.add(this.yumi_model);
}
setupTween =() =>{
const duration = MathUtils.randInt( 1000, 5000 );
const target = {};
for (const prop in this.tgt_jnt_vals) {
const joint = this.kinematics.joints[ prop ];
const old = this.tweenParameters[ prop ];
const position = old ? old : joint.zeroPosition;
this.tweenParameters[ prop ] = position;
target[prop] = this.tgt_jnt_vals[prop]; //MathUtils.randInt( joint.limits.min, joint.limits.max );
// console.log('target:', prop, this.tgt_jnt_vals[prop]);
}
this.kinematicsTween = new Tween( this.tweenParameters ).to( target, duration ).easing( Easing.Quadratic.Out );
this.kinematicsTween.onUpdate( ( object )=> {
for ( const prop in this.kinematics.joints ) {
if ( prop in this.kinematics.joints) {
if ( ! this.kinematics.joints[ prop ].static ) {
this.kinematics.setJointValue( prop, object[ prop ] );
}
}
}
});
// console.log("Tween Parameters", this.tweenParameters);
// console.log("kinematics tween", this.kinematicsTween);
this.kinematicsTween.start();
setTimeout( this.setupTween, duration );
}
}
And I'm calling this class as follows:
let target_position = {
gripper_l_joint: 0.01,
gripper_l_joint_m: 0.01,
gripper_r_joint: 0.01,
gripper_r_joint_m: 0.01,
yumi_joint_1_l: 10.0,
yumi_joint_1_r: 10.0,
yumi_joint_2_l: 20.0,
yumi_joint_2_r: 20.0,
yumi_joint_3_l: 30.0,
yumi_joint_3_r: 30.0,
yumi_joint_4_l: 40.0,
yumi_joint_4_r: 40.0,
yumi_joint_5_l: 50.0,
yumi_joint_5_r: 50.0,
yumi_joint_6_l: 60.0,
yumi_joint_6_r: 60.0,
yumi_joint_7_l: 70.0,
yumi_joint_7_r: 70.0,
};
new YuMiMotion(this.scene, target_position);
I have created a repo to reproduce my example here.
Can you please tell me how can I Move my model properly to the desired joints position using Three.js and Tween.js? thanks in advance.

The model scale is too small when using in web application project

I downloaded this model from https://sketchfab.com/3d-models/dcb3a7d5b1ad4f948aa4945d6e378c8a , The model scale is appearing normal when open in Windows 3D Viewer, three.js glTF Viewer and Babylon.js View, but when load the model in three.js module, some model's scale is incorrect, for example.
three.js in Website
three.js glTF Viewer
Babylon.js Viewer
This model scale is correct, when open in another application, it scale correctly.
Model name : dog.glb
Source : https://github.com/craftzdog/craftzdog-homepage
three.js in Website
three.js glTF Viewer
Babylon.js Viewer
This model scale is incorrect and so tiny, but when open in another application, it scale correctly
Model name : แมวประเทศไทย
Source : https://sketchfab.com/3d-models/dcb3a7d5b1ad4f948aa4945d6e378c8a
Here is GLTF Loader Code
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
export function loadGLTFModel(
scene,
glbPath,
options = { receiveShadow: true, castShadow: true }
) {
const { receiveShadow, castShadow } = options
return new Promise((resolve, reject) => {
const loader = new GLTFLoader()
loader.load(
glbPath,
gltf => {
const obj = gltf.scene
obj.name = 'persian'
obj.position.x = 0
obj.position.y = 0
obj.receiveShadow = receiveShadow
obj.castShadow = castShadow
scene.add(obj)
obj.traverse(function (child) {
if (child.isMesh) {
child.castShadow = castShadow
child.receiveShadow = receiveShadow
}
})
resolve(obj)
},
undefined,
function (error) {
reject(error)
}
)
})
}
Here is Model Display Code
import { useState, useEffect, useRef, useCallback } from 'react'
import { Box, Spinner } from '#chakra-ui/react'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { loadGLTFModel } from '../lib/model'
function easeOutCirc(x) {
return Math.sqrt(1 - Math.pow(x - 1, 4))
}
const PersianCat = () => {
const refContainer = useRef()
const [loading, setLoading] = useState(true)
const [renderer, setRenderer] = useState()
const [_camera, setCamera] = useState()
const [target] = useState(new THREE.Vector3(-0.5, 1.2, 0))
const [initialCameraPosition] = useState(
new THREE.Vector3(
20 * Math.sin(0.2 * Math.PI),
10,
20 * Math.cos(0.2 * Math.PI)
)
)
const [scene] = useState(new THREE.Scene())
const [_controls, setControls] = useState()
// On component mount only one time.
/* eslint-disable react-hooks/exhaustive-deps */
useEffect(() => {
const { current: container } = refContainer
if (container && !renderer) {
const scW = container.clientWidth
const scH = container.clientHeight
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
})
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(scW, scH)
renderer.outputEncoding = THREE.sRGBEncoding
container.appendChild(renderer.domElement)
setRenderer(renderer)
// 640 -> 240
// 8 -> 6
const scale = scH * 0.005 + 4.8
const camera = new THREE.OrthographicCamera(
-scale,
scale,
scale,
-scale,
0.01,
50000
)
camera.position.copy(initialCameraPosition)
camera.lookAt(target)
setCamera(camera)
const ambientLight = new THREE.AmbientLight(0xcccccc, 1)
scene.add(ambientLight)
const controls = new OrbitControls(camera, renderer.domElement)
controls.autoRotate = true
controls.target = target
setControls(controls)
loadGLTFModel(scene, '/persian.glb', {
receiveShadow: false,
castShadow: false
}).then(() => {
animate()
setLoading(false)
})
let req = null
let frame = 0
const animate = () => {
req = requestAnimationFrame(animate)
frame = frame <= 100 ? frame + 1 : frame
if (frame <= 100) {
const p = initialCameraPosition
const rotSpeed = -easeOutCirc(frame / 120) * Math.PI * 20
camera.position.y = 10
camera.position.x =
p.x * Math.cos(rotSpeed) + p.z * Math.sin(rotSpeed)
camera.position.z =
p.z * Math.cos(rotSpeed) - p.x * Math.sin(rotSpeed)
camera.lookAt(target)
} else {
controls.update()
}
renderer.render(scene, camera)
}
return () => {
cancelAnimationFrame(req)
renderer.dispose()
}
}
}, [])
return (
<Box
ref={refContainer}
className="persian-cat"
m="auto"
at={['-20px', '-60px', '-120px']}
mb={['-40px', '-140px', '-200px']}
w={[280, 480, 640]}
h={[280, 480, 640]}
position="relative"
>
{loading && (
<Spinner
size="xl"
position="absolute"
left="50%"
top="50%"
ml="calc(0px - var(--spinner-size) / 2)"
mt="calc(0px - var(--spinner-size))"
/>
)}
</Box>
)
}
export default PersianCat

Updating three.js texture throughthe dat.gui dropdown

I have loaded different textures using textureLoader and I am trying to update them using dat.gui controls.
Why is the code below not working?
gui.add(mesh.position, "y", -1, 1, 0.1);
gui.add(mesh.material, "map", { alpha: alphaTexture, color: colorTexture, normal: normalTexture })
.onChange(() => {
mesh.material.needsUpdate = true;
console.log("updated");
});
It gives this error:
"Uncaught TypeError: m is undefined" [error][1]
After some tweaking, I found that the values of object(or array) in the third argument only supports string types, so passing a object as a value would not work.
This is the closest workaround that I could think of..
/* GUI options */
const guiOptions = {
mesh_material_map: "color",
};
/* Textures */
const textureLoader = new THREE.TextureLoader(loadingManager);
const colorTexture = textureLoader.load("/textures/door/color.jpg");
const alphaTexture = textureLoader.load("/textures/door/alpha.jpg");
const normalTexture = textureLoader.load("/textures/door/normal.jpg");
const guiTextureHash = {
color: colorTexture,
alpha: alphaTexture,
normal: normalTexture,
};
/* Add to gui */
gui.add(guiOptions, "mesh_material_map", Object.keys(guiTextureHash)).onChange((value) => {
mesh.material.map = guiTextureHash[value];
mesh.needsUpdate = true;
console.log("updated", value);
});
I found your topic looking for a texture picker. It's probably a little away from your starting point but could help some other. I finally made a simple texture picker with a dropdown selection key with dat.gui. The goal is to be able to change on the fly my matcap texture, going through an array of loaded texture.
const gui = new dat.GUI()
const textureLoader = new THREE.TextureLoader()
const myMatCap = [
textureLoader.load('./textures/matcaps/1.png'),
textureLoader.load('./textures/matcaps/2.png'),
textureLoader.load('./textures/matcaps/3.png')
]
const parameters = {
color: 0xff0000,
matCapTexture: 0
}
const updateAllMaterials = () => {
scene.traverse( (child)=>{
if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshMatcapMaterial) {
child.material.matcap = myMatCap[ parameters.matCapTexture]
child.material.needsUpdate = true
}
})
}
gui.add(parameters, 'matCapTexture', {
terracotta: 0,
grey: 1,
chrome: 2,
}).onFinishChange(()=>{
updateAllMaterials()
})
let mesh = new THREE.Mesh(
geometry,
new THREE.MeshMatcapMaterial({
side: THREE.DoubleSide,
matcap: myMatCap[ parameters.matCapTexture ]
})
);
scene.add(mesh)

What does "Cannot set property 'getStrokeStyle' of undefined" mean for a Three.js program?

I'm using SVGloader to load an SVG so I can map it on my OBJ file. But when gave it url to the svg file it generates an error
TypeError:Cannot set property 'getStrokeStyle' of undefined
I'm using Angular 8 and rendering a Obj file using THREE.js. I want to load an svg and map it on the obj file to add texture to that file, but as I told above it is generating an error and I don't know how to solve it.
Here is code file.
import { Component, AfterViewInit, ViewChild, Input, ElementRef } from '#angular/core';
import * as THREE from 'three';
import { OrbitControls } from '#avatsaev/three-orbitcontrols-ts';
import {OBJLoader} from 'three-obj-mtl-loader';
import {SVGLoader} from 'three-svg-loader';
#Component({
selector: 'app-scene',
templateUrl: './scene.component.html',
styleUrls: ['./scene.component.css']
})
export class SceneComponent implements AfterViewInit {
#Input() name: string;
#ViewChild('canvas', {static:true}) canvasRef: ElementRef;
renderer = new THREE.WebGLRenderer;
scene = null;
camera = null;
controls = null;
mesh = null;
light = null;
loader;
svgLoader;
private calculateAspectRatio(): number {
const height = this.canvas.clientHeight;
if (height === 0) {
return 0;
}
return this.canvas.clientWidth / this.canvas.clientHeight;
}
private get canvas(): HTMLCanvasElement {
return this.canvasRef.nativeElement;
}
constructor() {
// this.loader = new OBJLoader();
this.scene = new THREE.Scene();
this.loader = new OBJLoader();
this.svgLoader = new SVGLoader();
this.camera = new THREE.PerspectiveCamera(15, window.innerWidth / window.innerHeight, 0.1, 1000)
}
ngAfterViewInit() {
this.configScene();
this.configCamera();
this.configRenderer();
this.configControls();
this.createLight();
this.createMesh();
this.animate();
}
configScene() {
// this.scene.background = new THREE.Color( 0xdddddd );
}
configCamera() {
this.camera.aspect = this.calculateAspectRatio();
this.camera.updateProjectionMatrix();
this.camera.position.set( 0, 0, 3 );
this.camera.lookAt( this.scene.position );
}
configRenderer() {
this.renderer = new THREE.WebGLRenderer({
canvas: this.canvas,
antialias: true,
alpha: true
});
this.renderer.setPixelRatio(devicePixelRatio);
// setClearColor for transparent background
// i.e. scene or canvas background shows through
this.renderer.setClearColor( 0x000000, 0 );
this.renderer.setSize((window.innerWidth/2), (window.innerHeight/2));
window.addEventListener('resize', ()=>{
this.renderer.setSize((window.innerWidth/2), (window.innerHeight)/2);
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
})
console.log('clientWidth', this.canvas.clientWidth);
console.log('clientHeight', this.canvas.clientHeight);
}
configControls() {
this.controls = new OrbitControls(this.camera);
this.controls.autoRotate = false;
this.controls.enableZoom = false;
// this.controls.maxDistance = 5;
// this.controls.minDistance = 10;
this.controls.enablePan = false;
this.controls.update();
}
createLight() {
this.light = new THREE.PointLight( 0xffffff );
this.light.position.set( -10, 10, 10 );
this.scene.add( this.light );
}
createMesh() {
this.svgLoader.load('../../../../assets/abc.svg')
console.log("SVG Loader", this.svgLoader)
this.loader.load('../../../../assets/nonunified.obj', (object)=>{
object.traverse( function ( child ) {
if ( child instanceof THREE.Mesh ) {
child.geometry.center();
}
} );
this.scene.add(object)
},
// called when loading is in progresses
function (xhr) {
console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
},
// called when loading has errors
function ( error ) {
console.log( 'An error happened' );
}
)}
animate() {
window.requestAnimationFrame(() => this.animate());
this.controls.update();
this.renderer.render(this.scene, this.camera);
}
}
The SVGLoader is not for loading svg-files to be used as textures but for loading them as Geometry: https://threejs.org/docs/#examples/en/loaders/SVGLoader
If you want to use an svg-file as a texture, you should be able to use the TextureLoader like this:
obj.material.map = new TextureLoader().load('../../../../assets/abc.svg');
I'm not sure if you actually need to rasterize it to a canvas first, if the above doesn't work, try what is described here: How do you load and display SVG graphics in three.js?

Editing part sizes of a 3d model

I want to develop a web app to entering measurements of a man and displaying a 3d model with these measurements. I have chosen three.js to start it. And I downloaded a 3d model named standard-male-figure from clara.io. Here is my code to display human model.
import React, { Component } from "react";
import PropTypes from "prop-types";
import withStyles from "#material-ui/core/styles/withStyles";
import * as THREE from "three-full";
const styles = (/*theme*/) => ({
});
class ThreeDView extends Component {
constructor(props){
super(props);
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
this.renderScene - this.renderScene.bind(this);
this.animate = this.animate.bind(this);
}
componentDidMount() {
const width = this.mount.clientWidth;
const height = this.mount.clientHeight;
//ADD SCENE
this.scene = new THREE.Scene();
//ADD CAMERA
this.camera = new THREE.PerspectiveCamera(100,1);
this.camera.position.z = 12;
this.camera.position.y = 0;
this.camera.position.x = 0;
//ADD RENDERER
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setClearColor("#f0f0f0");
this.renderer.setSize(width, height);
this.mount.appendChild(this.renderer.domElement);
// MOUSE ROTATION
this.orbit = new THREE.OrbitControls(this.camera,this.renderer.domElement);
this.orbit.update();
//ADD LIGHTS
this.light = new THREE.PointLight(0xffffff,1.3);
this.light.position.z = 10;
this.light.position.y=20;
this.scene.add(this.light);
// ADD MAN FIGURE
const loader = new THREE.ColladaLoader();
loader.load("/models/standard-male-figure.dae",(manFigure)=>{
this.man = manFigure;
this.man.name = "man-figure";
this.man.scene.position.y = -10;
this.scene.add(this.man.scene);
},undefined,()=>alert("Loading failed"));
this.start();
}
componentWillUnmount() {
this.stop();
this.mount.removeChild(this.renderer.domElement);
}
start() {
if (!this.frameId) {
this.frameId = requestAnimationFrame(this.animate);
}
}
stop () {
cancelAnimationFrame(this.frameId);
}
animate () {
this.renderScene();
this.frameId = window.requestAnimationFrame(this.animate);
}
renderScene () {
this.orbit.update();
this.light.position.z = this.camera.position.z;
this.light.position.y=this.camera.position.y+20;
this.light.position.x=this.camera.position.x;
this.renderer.render(this.scene, this.camera);
}
render() {
return (
<div style={{ height: "640px" }} ref={(mount) => { this.mount = mount; }} >
</div>
);
}
}
ThreeDView.propTypes = {
values: PropTypes.object
};
/*
all values in inches
values = {
heightOfHand:10,
, etc..
}
*/
export default withStyles(styles)(ThreeDView);
values is measurements that user is entering. I have no idea about how to start updating 3d model with these measurements. Please give me a starting point or any advise to complete this. Thank You!.
Firstly you can get the current size and scale of your man
const box = new THREE.Box3().setFromObject(this.man)
const currentObjectSize = box.getSize()
const currentObjectScale = this.man.scale
and when an user update the size value (newSize), you can calculate a new scale for you man
const newScale = new THREE.Vector3(
currentObjectScale.x * newSize.x/currentObjectSize.x,
currentObjectScale.y * newSize.y/currentObjectSize.y,
currentObjectScale.z * newSize.z/currentObjectSize.z
)
and update your man with this scale
this.man.scale.set(newScale.x, newScale.y, newScale.z)

Resources