Editing part sizes of a 3d model - three.js

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)

Related

Three.js + webxr animations not playing from .glb file

I am unable to get the animation clips from the .glb file to play. The .glb model loads and is displayed on the screen but the animation does not play.
The animations play fine when viewing the object in the three.js editor.
The .glb has 1 animation clip associated with it.
Any Help would be much appreciated!!!
import {
Mesh,
AmbientLight,
Clock,
AnimationMixer,
PerspectiveCamera,
Scene,
WebGLRenderer,
MeshBasicMaterial,
RingGeometry,
sRGBEncoding
} from 'three';
import {ARButton} from 'three/examples/jsm/webxr/ARButton';
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js'
class App {
constructor() {
this.clock = new Clock();
this.camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.camera.position.set(0, 1.6, 3);
this.scene = new Scene();
this.renderer = new WebGLRenderer({
antialias: true,
alpha: true
});
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.outputEncoding = sRGBEncoding;
document.body.appendChild(this.renderer.domElement);
this.initXR();
this.initScene();
window.addEventListener('resize', this.onWindowResize.bind(this), false);
this.renderer.setAnimationLoop(this.render.bind(this));
}
initXR() {
this.renderer.xr.enabled = true;
document.body.appendChild(ARButton.createButton(this.renderer, {requiredFeatures: ['hit-test']}));
this.hitTestSourceRequested = false;
this.hitTestSource = null;
this.controller = this.renderer.xr.getController(0);
this.controller.addEventListener('select', this.onSelect.bind(this));
}
initScene() {
let geometry = new RingGeometry(0.08, 0.10, 32).rotateX(-Math.PI / 2);
let material = new MeshBasicMaterial();
this.reticle = new Mesh(geometry, material);
this.reticle.matrixAutoUpdate = false;
this.reticle.visible = false;
this.scene.add(this.reticle);
const loader = new GLTFLoader();
loader.load('./models/stylized_character.glb', gltf => {
this.myObj = gltf;
gltf.scene.scale.set(.25, .25, .25);
gltf.scene.visible = true;
this.scene.add(gltf.scene);
//todo: this doesnt seem to work
this.mixer = new AnimationMixer(gltf);
var action = this.mixer.clipAction(gltf.animations[0]);
action.play();
});
var aLight = new AmbientLight(0xffffff, 1);
this.scene.add(aLight)
}
render(_, frame) {
const delta = this.clock.getDelta();
if (this.mixer) {
this.mixer.update(delta);
}
if (frame) {
if (this.hitTestSourceRequested === false) {
this.requestHitTestSource();
}
if (this.hitTestSource) {
this.getHitTestResults(frame);
}
}
this.renderer.render(this.scene, this.camera);
}
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.render(this.scene, this.camera);
}
onSelect() {
if (this.reticle.visible) {
this.myObj.scene.position.setFromMatrixPosition(this.reticle.matrix);
this.myObj.scene.visible = true;
}
}
async requestHitTestSource() {
const session = this.renderer.xr.getSession();
session.addEventListener('end', () => {
this.hitTestSourceRequested = false;
this.hitTestSource = null;
});
const referenceSpace = await session.requestReferenceSpace('viewer');
this.hitTestSource = await session.requestHitTestSource({space: referenceSpace, entityTypes: ['plane']});
this.hitTestSourceRequested = true;
}
getHitTestResults(frame) {
const hitTestResults = frame.getHitTestResults(this.hitTestSource);
if (hitTestResults.length) {
const hit = hitTestResults[0];
const pose = hit.getPose(this.renderer.xr.getReferenceSpace());
this.reticle.visible = true;
this.reticle.matrix.fromArray(pose.transform.matrix);
} else {
this.reticle.visible = false;
}
}
}
window.addEventListener('DOMContentLoaded', () => {
new App();
});

How to use Threejs to create a boundary for the Mesh load by fbxloador

The created boundary's scale and rotation are totally different with the import fbxmodel.
Hi, I have loaded a fbx model into the scene by fbxLoador.
const addFbxModel = (modelName: string, position: Vector3) => {
const fbxLoader = new FBXLoader();
fbxLoader.load(
`../../src/assets/models/${modelName}.fbx`,
(fbxObject: Object3D) => {
fbxObject.position.set(position.x, position.y, position.z);
console.log(fbxObject.scale);
fbxObject.scale.set(0.01, 0.01, 0.01);
const material = new MeshBasicMaterial({ color: 0x008080 });
fbxObject.traverse((object) => {
if (object instanceof Mesh) {
object.material = material;
}
});
scene.add(fbxObject);
updateRenderer();
updateCamera();
render();
},
(xhr) => {
console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
},
(error) => {
console.log(error);
}
);
};
And now i want to add a click function on this model which will highlight it by showing it's boundary box.
const onclick = (event: MouseEvent) => {
event.preventDefault();
if (renderBox.value) {
const mouse = new Vector2();
mouse.x = (event.offsetX / renderBox.value.clientWidth) * 2 - 1;
mouse.y = -(event.offsetY / renderBox.value.clientHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
const intersect = intersects[0];
const object = intersect.object;
createBoundary(object);
}
}
};
const createBoundary = (object: Object3D) => {
if (object instanceof Mesh) {
console.log(object.geometry, object.scale, object.rotation);
const edges = new EdgesGeometry(object.geometry);
const material = new LineBasicMaterial({ color: 0xffffff });
const wireframe = new LineSegments(edges, material);
wireframe.scale.copy(object.scale);
wireframe.rotation.copy(object.rotation);
console.log(wireframe.scale);
scene.add(wireframe);
}
};
But now the boundary's scale and rotation are totally different with the fbxmodel.
And also the boundary is too complex, is it possible to create one only include the outline and the top point.
Thank you. :)

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

Drag&Drop element into canvas from HTML

I've got a very trivial task to drag&drop the element from a gallery into the Three.js canvas.
Nothing seems tricky until I face the problem that when I drag the item and add it to the scene I cannot update the item's coordinates until I finish the drag&drop event
I already played around with all events that watch the mouse event mousemove, drag, dragover but the element is just stuck at the initial coordinates, the ones I applied in the dragenter event
export const params = {
devicePixelRatio: Math.min(window.devicePixelRatio, 2),
size: getSizeParams(),
grid: {
size: 20,
divisions: 20,
},
}
const itemProtos = ['Box', 'Sphere', 'Cone']
export const canvas = document.querySelector(`#canvas`)
const raycaster = new Raycaster()
const pointer = new Vector2()
const scene = new Scene()
const camera = new PerspectiveCamera(75, params.size.width / params.size.height, 0.1, 100)
camera.position.z = 5
camera.position.y = 2
/**
* Variable for Drag&Drop - just created object that's being moved around
*/
let newObjectType = null
let newObject = null
/**
* Groups
*/
export const itemGroup = new Group()
scene.add(itemGroup)
/**
* Grid
*/
export const gridHelper = new GridHelper(params.grid.size, params.grid.divisions)
scene.add(gridHelper)
/**
* Renderer
*/
const renderer = new WebGLRenderer({
canvas,
antialias: true,
})
renderer.setSize(params.size.width, params.size.height)
renderer.setPixelRatio(params.devicePixelRatio)
/**
* Resizing updates to fit the screen
*/
window.addEventListener('resize', () => {
params.size = getSizeParams()
camera.aspect = params.size.width / params.size.height
camera.updateProjectionMatrix()
renderer.setSize(params.size.width, params.size.height)
renderer.setPixelRatio()
})
canvas.addEventListener('mouseenter', () => {
canvas.style.cursor = 'grab'
})
/**
* Controls
*/
const orbitControls = new OrbitControls(camera, canvas)
orbitControls.enableDamping = true
orbitControls.addEventListener('start', () => {
canvas.style.cursor = 'grabbing'
})
orbitControls.addEventListener('end', () => {
canvas.style.cursor = 'grab'
})
const tick = () => {
orbitControls.update()
requestAnimationFrame(tick)
renderer.render(scene, camera)
}
window.onload = tick
/**
* Raycaster functions
*/
const refreshMouseCoords = (event) => {
pointer.x = (event.clientX / params.size.width) * 2 - 1
pointer.y = -(event.clientY / params.size.height) * 2 + 1
}
let currentIntersect = null
let currentPick = null
canvas.addEventListener('mousemove', (event) => {
refreshMouseCoords(event)
raycaster.setFromCamera(pointer, camera)
const intersects = raycaster.intersectObjects(itemGroup.children, false)
if(intersects.length && intersects[0].object instanceof Mesh) {
if(!currentIntersect) {
canvas.style.cursor = 'all-scroll'
intersects[0].object.material.color.set('red')
console.log(`mouse enter`)
}
if(currentIntersect && currentIntersect !== intersects[0].object) {
currentIntersect.material.color.set('blue')
intersects[0].object.material.color.set('red')
}
currentIntersect = intersects[0].object
} else {
if(currentIntersect) {
console.log(`mouse leave`)
currentIntersect.material.color.set('blue')
canvas.style.cursor = 'grab'
}
currentIntersect = null
}
moveItem(currentPick)
})
/**
* Function to move items around GridHelper
*/
const moveItem = (item) => {
const intersectsGround = raycaster.intersectObject(gridHelper, false)
if(item && intersectsGround[0]) {
item.position.z = intersectsGround[0].point.z
item.position.x = intersectsGround[0].point.x
}
}
canvas.addEventListener('mousedown', (event) => {
event.preventDefault()
refreshMouseCoords(event)
raycaster.setFromCamera(pointer, camera)
const intersects = raycaster.intersectObjects(itemGroup.children, false)
if(intersects.length && intersects[0].object instanceof Mesh) {
currentPick = intersects[0].object
}
if(currentIntersect) {
canvas.style.cursor = 'all-scroll'
orbitControls.enabled = false
}
})
canvas.addEventListener('mouseup', () => {
if(currentIntersect) {
canvas.style.cursor = 'all-scroll'
orbitControls.enabled = true
}
if(currentPick) {
currentPick = null
}
})
// *** Drag&Drop *** //
const gallery = document.querySelector(`#gallery`)
setGallery(itemProtos, gallery)
canvas.addEventListener('dragenter', (event) => {
event.preventDefault()
console.log('Drag&Drop: dragenter')
refreshMouseCoords(event)
raycaster.setFromCamera(pointer, camera)
const intersects = raycaster.intersectObject(gridHelper)
if (intersects.length && newObjectType) {
add3DEl(intersects[0].point, newObjectType, itemGroup)
}
})
canvas.addEventListener('dragover', (event) => {
event.preventDefault()
event.stopPropagation()
if(newObject) {
moveItem(newObject)
}
})
function setGallery(itemProtos, gallery) {
for (let i of itemProtos) {
const el = createProto()
gallery.appendChild(el)
el.addEventListener('dragstart', function (event) {
event.dataTransfer.setData('text/plain', i)
newObjectType = i
})
el.addEventListener('dragend', function () {
newObjectType = null
newObject = null
})
}
}
function add3DEl({ x, z }, type = 'Box', scene) {
const geometry = new itemObjects[`${type}Geometry`]()
const material = new MeshBasicMaterial({ color: 0x0000ff }) // Shared material for all items
material.wireframe = true
const el = new Mesh(geometry, material)
el.position.x = x
el.position.y = type == 'Sphere' ? 1 : .5
el.position.z = z
el.userData.name = `${type}_${Date.now()}`
newObject = el
scene.add(el)
}
Here you can find a playgroud with what I've got so far: Playground

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?

Resources