THREE.js - add a THREE.SkinnedMesh to a custom skeleton structure - javascript

I'm trying to define a model of the human body in THREE.js using the classes THREE.Bone, THREE.Skeleton and THREE.SkinnedMesh.
I defined a custom skeleton structure made of 12 body parts, each of which is a THREE.Bone instance, and used the .add() method to define parent / child relationships among them. Finally, I created a standard THREE.Object3D as the body root that is parent of the full skeleton.
Posting only part of the structure for conciseness:
// create person object
var body_root = new THREE.Object3D()
// create torso
var torso = new THREE.Bone();
torso.id = 1;
torso.name = "torso";
x_t = 0;
y_t = 0;
z_t = 0;
torso.position.set(x_t,y_t,z_t);
x_alpha = 0 * Math.PI;
y_alpha = 0 * Math.PI;
z_alpha = 0 * Math.PI;
torso.rotation.set(x_alpha,y_alpha,z_alpha);
// create right arm
var right_arm = new THREE.Bone();
right_arm.id = 2;
right_arm.name = "right_arm";
x_t = -TORSO_WIDTH / 2;
y_t = TORSO_HEIGHT;
z_t = 0;
right_arm.position.set(x_t,y_t,z_t);
x_alpha = 0 * Math.PI;
y_alpha = 0 * Math.PI;
z_alpha = 0 * Math.PI;
right_arm.rotation.set(x_alpha,y_alpha,z_alpha);
// add right_arm as child of torso
torso.add( right_arm );
This works just fine, and after loading the page I can access the model and traverse it correctly through the console.
However, when I try to render the skeleton in the scene things get tricky.
1. How can I add a THREE.SkinnedMesh for a custom skeleton structure?
In the documentation (check source code) a CylinderGeometry and a SkinnedMesh are created for all the bones jointly
var geometry = new THREE.CylinderGeometry( 5, 5, 5, 5, 15, 5, 30 );
var mesh = THREE.SkinnedMesh( geometry, material );
and then the bone structure is binded:
// See example from THREE.Skeleton for the armSkeleton
var rootBone = armSkeleton.bones[ 0 ];
mesh.add( rootBone );
// Bind the skeleton to the mesh
mesh.bind( armSkeleton );
This works perfectly for the simple example in the documentation (5 bones each one parent of the next one), but how can I adapt this example to a more complex structure in which some bones have multiple children? And how can I implement bones with different geometry? For instance I would like to implement joints like shoulder and elbow with a sphere for which I can only change rotation and body parts like arm and forearm with cylinders having different base radius.
Is it possible to define a SkinnedMesh for each joint independently and for a more complex structure than the one in the example? If so how do you link them all together?
2. Can I add a THREE.SkeletonHelper to the scene without defining a skinned mesh but using only the bones?
Since I don't know the answer to question 1 I decided to simply try to render the skeleton structure. This is done in other examples (such as this one) by creating a THREE.SkeletonHelper instance and adding that to the scene.
I tried passing the body_root variable (instead of the SkinnedMesh) to the constructor and the helper is created, but not rendered.
helper = new THREE.SkeletonHelper( body_root );
helper.material.linewidth = 3;
scene.add( helper );
In order to visualize the helper it needs to be binded to a mesh, even if the mesh is not added directly to the scene, i.e. adding the line mesh.bind(armSkeleton) visualizes the skeleton helper.
Is this possible at all to visualize the skeleton helper without defining a mesh? If so how?
NOTE on question 2:
I believe this should be possible, since the THREE.SkeletonHelper class defines internally its own geometry and material, so it should be possible to render it without needing the mesh of the skeleton:
THREE.SkeletonHelper = function ( object ) {
this.bones = this.getBoneList( object );
var geometry = new THREE.Geometry();
for ( var i = 0; i < this.bones.length; i ++ ) {
var bone = this.bones[ i ];
if ( bone.parent instanceof THREE.Bone ) {
geometry.vertices.push( new THREE.Vector3() );
geometry.vertices.push( new THREE.Vector3() );
geometry.colors.push( new THREE.Color( 0, 0, 1 ) );
geometry.colors.push( new THREE.Color( 0, 1, 0 ) );
}
}
geometry.dynamic = true;
var material = new THREE.LineBasicMaterial( { vertexColors: THREE.VertexColors, depthTest: false, depthWrite: false, transparent: true } );
THREE.LineSegments.call( this, geometry, material );
this.root = object;
this.matrix = object.matrixWorld;
this.matrixAutoUpdate = false;
this.update();
};

Related

Scaling a 2D SVG group object in three.js

I'm attempting to create a map of 2d SVG tiles in three.js. I have used SVGLoader() Like so (Keep in mind some brackets are for parent scopes that aren't shown. That is not the issue):
loader = new SVGLoader();
loader.load(
// resource URL
filePath,
// called when the resource is loaded
function ( data ) {
console.log("SVG file successfully loaded");
const paths = data.paths;
for ( let i = 0; i < paths.length; i ++ ) {
const path = paths[ i ];
const material = new THREE.MeshBasicMaterial( {
color: path.color,
side: THREE.DoubleSide,
depthWrite: false
} );
const shapes = SVGLoader.createShapes( path );
console.log(`Shapes length = ${shapes.length}`);
try{
for ( let j = 0; j < shapes.length; j ++ ) {
const shape = shapes[ j ];
const geometry = new THREE.ShapeGeometry( shape );
const testGeometry = new THREE.PlaneGeometry(2,2);
try{
const mesh = new THREE.Mesh(geometry, material );
group.add( mesh );
}catch(e){console.log(e)}
}
}catch(e){console.log(e)}
}
},
// called when loading is in progress
function ( xhr ) {
console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
},
// called when loading has errors
function ( error ) {
console.log( 'An error happened' );
}
);
return group;
}
Dismiss the fact that I surrounded alot of it in try{}catch(){}
I have also created grid lines and added it to my axis helper in the application that allows me to see where each cooordinate is, in relation to the X and Y axis.
This is how the svg appears on screen:
Application Output
I can't seem to figure out how to correlate the scale of the svg, with the individual grid lines. I have a feeling that Im going to have to dive deeper into the SVG loading script that I have above then scale each shape mesh specifically. I call the SVG group itself in the following code.
try{
//SVG returns a group, TGA returns a texture to be added to a material
var object1 = LOADER.textureLoader("TGA", './Art/tile1.tga', pGeometry);
var object2 = LOADER.textureLoader("SVG", '/Art/bitmap.svg');
const testMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
map: object1,
side: THREE.DoubleSide
});
//const useMesh = new THREE.Mesh(pGeometry, testMaterial);
//testing scaling the tile
try{
const worldScale = new THREE.Vector3();
object2.getWorldScale(worldScale);
console.log(`World ScaleX: ${worldScale.x} World ScaleY: ${worldScale.y} World ScaleZ: ${worldScale.z}`);
//object2.scale.set(2,2,0);
}catch(error){console.log(error)}
scene.add(object2);
}
Keep in mind that the SVG is object2 in this case. Some of the ideas to tackle this problem I have had is looking into what a world scale is, matrix4 transformations, and the scale methods of either the object3d parent properties or the bufferGeometry parent properties of this particular svg group object. I am also fully aware that three.js is designed for 3d graphics, however I would like to master 2d graphics programming in this library before I get into the 3d aspect of things. I also have a thought that the scale of the SVG group is distinctly different from the scale of the scene and its X Y and Z axis.
If this question has already been answered a link to the corresponding answer would be of great help to me.
Thank you for the time you take to answer this question.
I messed with the dimensions of the svg file itself in the editor I used to paint it and I got it to scale. Not exactly a solution in the code, however I guess the code is just closely tied to the data that the svg file provides and cant be altered too much.

ThreeJS: Add Two Materials on an Mesh Object

In ThreeJS, it is possible to add more than one material to an Object3D/Mesh according to its documentation. We can use a single Material or use an array of Material:
Mesh typescript file class declaration and contructor (from ThreeJS src code):
export class Mesh<
TGeometry extends BufferGeometry = BufferGeometry,
TMaterial extends Material | Material[] = Material | Material[] // ### Here: Material[] ###
> extends Object3D {
constructor(geometry?: TGeometry, material?: TMaterial);
Here is my problem which I can't seem to be able to solve...
When I use a single Material, my Mesh is displayed. However, when I use two materials as an array, my Mesh won't display. I also don't have any error print out in the console when rendering the scene.
My ultimate goal here is to be able to have to separate materials for the inner and the outer part of my object.
Here is my code:
export function init(radius: number = 18.0, innerColor: number = 0xFFFFFF, outerColor: number = 0x444444) {
var obj = new Object3D();
loader.load(
objPath,
function(object){
obj = object;
const mesh = obj.children[0] as Mesh;
// WORKING:
mesh.material = new MeshPhongMaterial({color: outerColor});
// NOT WORKING: Using two materials
// mesh.material = new Array<Material>(new MeshPhongMaterial({color: outerColor}), new MeshPhongMaterial({color:innerColor}));
mesh.scale.setLength(radius)
scene.add(mesh);
},
function (error){
console.log(error)
}
);
}
Why can't I manage to see my object when using two materials ?
I known there was the MeshFaceMaterial in previous version and the material array acceptance of the contructor is supposed to be a replacement for that in some sens.
ThreeJS version: r128
Any help would be appreciated !
The easiest way is to clone your mesh and assign two separate materials, one for the inside, another for the outside:
const meshOuter = obj.children[0] as THREE.Mesh;
const meshInner = meshOuter.clone();
// Outer mesh shows front side
meshOuter.material = new THREE.MeshPhongMaterial({
color: outerColor,
side: THREE.FrontSide
});
// Inner mesh shows back side
meshInner.material = new THREE.MeshPhongMaterial({
color: innerColor,
side: THREE.BackSide
});
// Scale inner mesh down just a bit to avoid z-fighting
meshInner.scale.multiplyScalar(0.99);

Three JS: Load an OBJ, translate to origin (center in scene), orbit

All I want to do is load an OBJ file and translate its coordinates to the world origins (0,0,0) so that orbit controls work perfectly (no Pivot points please).
I'd like to load random OBJ objects with different geometries/center points and have them translated automatically to the scene origin. In other words, a 'hard coded' translate solution for a specific model won't work
This has got to be one of the most common scenarios for Three JS (basic 3d object viewer), so I'm surprised I can't find a definitive solution on SO.
Unfortunately there are a lot of older answers with deprecated functions, so I would really appreciate a new answer even if there are similar solutions out there.
Things I've tried
the code below fits the object nicely to the camera, but doesn't solve the translation/orbiting problem.
// fit camera to object
var bBox = new THREE.Box3().setFromObject(scene);
var height = bBox.size().y;
var dist = height / (2 * Math.tan(camera.fov * Math.PI / 360));
var pos = scene.position;
// fudge factor so the object doesn't take up the whole view
camera.position.set(pos.x, pos.y, dist * 0.5);
camera.lookAt(pos);
Apparently the geometry.center() is good for translating an object's coordinates back to the origin, but the THREE.GeometryUtils.center has been replaced by geometry.center() and I keep getting errors when trying to use it.
when loading OBJs, geometry has now been replaced by bufferGeometry. I can't seem to cast the buffergeometry into geometry in order to use the center() function. do I have to place this in the object traverse > child loop like so? this seems unnecessarily complicated.
geometry = new THREE.Geometry().fromBufferGeometry( child.geometry );
My code is just a very simple OBJLoader.
var objLoader = new THREE.OBJLoader();
objLoader.setPath('assets/');
objLoader.load('BasketballNet_Skull.obj', function (object) {
object.traverse( function ( child ) {
if ( child instanceof THREE.Mesh ) {
child.material = material;
}
} );
scene.add(object);
});
(BTW first real question on SO so forgive any formatting / noob issues)
Why not object.geometry.center()?
var objLoader = new THREE.OBJLoader();
objLoader.setPath('assets/');
objLoader.load('BasketballNet_Skull.obj', function (object) {
object.traverse( function ( child ) {
if ( child instanceof THREE.Mesh ) {
child.material = material;
child.geometry.center();
}
} );
scene.add(object);
OK figured this out, using some very useful functions from Meshviewer Master, an older Three JS object viewer.
https://github.com/ideesculture/meshviewer
All credit to Gautier Michelin for this code
https://github.com/gautiermichelin
After loading the OBJ, you need to do 3 things:
1. Create a Bounding Box based on the OBJ
boundingbox = new THREE.BoundingBoxHelper(object, 0xff0000);
boundingbox.update();
sceneRadiusForCamera = Math.max(
boundingbox.box.max.y - boundingbox.box.min.y,
boundingbox.box.max.z - boundingbox.box.min.z,
boundingbox.box.max.x - boundingbox.box.min.x
)/2 * (1 + Math.sqrt(5)) ; // golden number to beautify display
2. Setup the Camera based on this bounding box / scene radius
function showFront() {
if (objectCopy !== undefined) objectCopy.rotation.z = 0;
controls.reset();
camera.position.z = 0;
camera.position.y = 0;
camera.position.x = sceneRadiusForCamera;
camera.lookAt(scene.position);
}
(the mesh viewer code also contains functions for viewing left, top, etc)
3. Reposition the OBJ to the scene origin
Like any centering exercise, the position is then the width and height divided by 2
function resetObjectPosition(){
boundingbox.update();
size.x = boundingbox.box.max.x - boundingbox.box.min.x;
size.y = boundingbox.box.max.y - boundingbox.box.min.y;
size.z = boundingbox.box.max.z - boundingbox.box.min.z;
// Repositioning object
objectCopy.position.x = -boundingbox.box.min.x - size.x/2;
objectCopy.position.y = -boundingbox.box.min.y - size.y/2;
objectCopy.position.z = -boundingbox.box.min.z - size.z/2;
boundingbox.update();
if (objectCopy !== undefined) objectCopy.rotation.z = 0;
}
From my understanding of your question, you want the objects that are added to the scene in the origin of the camera view. I believe the common way of achieving an object viewer solution is adding camera controls to your camera in the scene mostly THREE.OrbitControls and specifying the target for the camera as the object that you want to focus on. This makes the object focused to be in the center and the camera rotation and movement will be based on that object.

Three js children length

I created a three js object and added some children to it. then i changed the length of children to 0. then the objects have gone out of screen. Will that make the objects fully removed from the screen and memory?
var balls = new THREE.Object3D(); // parent
for creating childrens
var geometry = new THREE.SphereGeometry(5, 32, 32);
var material = new THREE.MeshPhongMaterial({color: 0x0f0ff0, shininess: 50, transparent: true, opacity: 1});
var sphere = new THREE.Mesh(geometry, material);
sphere.position.x = scale('some random value');
sphere.position.y = scale('some random value');
balls.add(sphere);
above steps repeated for more spheres
then in the console i wrote
balls.children = [];
this removes all the spheres from the scene. Will that removes all the sphere objects from the memory also??
Yes, when you have an array and then set array.length = 0; all the elements of the array will be deleted. When you type array.length = 2, all elements other than the first two elements will be deleted.
Javascript has a function called slice() which does a similar thing.
The correct way for deleting a child is calling remove(child) from its parent, and then use dispose() on children's material and geometry.
In your code:
var balls = new THREE.Object3D(); // parent
var geometry = new THREE.SphereGeometry(5, 32, 32);
var material = new THREE.MeshPhongMaterial({color: 0x0f0ff0, shininess: 50, transparent: true, opacity: 1});
var sphere = new THREE.Mesh(geometry, material);
sphere.position.x = scale('some random value');
sphere.position.y = scale('some random value');
balls.add(sphere);
// Do some work
balls.remove(sphere);
geometry.dispose();
material.dispose();
Dispose the material/geometry only when it is not used by other Mesh anymore.
From THREE.Object3D at remove(object, ...):
"Removes object as child of this object. An arbitrary number of objects may be removed."
From THREE.Geometry at dispose():
"Don't forget to call this method when you remove a geometry because it can cause memory leaks."
From THREE.Material at dispose():
"This disposes the material. Textures of a material don't get disposed. These needs to be disposed by Texture."
If you use textures, you must dispose these too.
(THREE.js r85).

How to merge two geometries or meshes using three.js r71?

Here I bumped to the problem since I need to merge two geometries (or meshes) to one. Using the earlier versions of three.js there was a nice function:
THREE.GeometryUtils.merge(pendulum, ball);
However, it is not on the new version anymore.
I tried to merge pendulum and ball with the following code:
ball is a mesh.
var ballGeo = new THREE.SphereGeometry(24,35,35);
var ballMat = new THREE.MeshPhongMaterial({color: 0xF7FE2E});
var ball = new THREE.Mesh(ballGeo, ballMat);
ball.position.set(0,0,0);
var pendulum = new THREE.CylinderGeometry(1, 1, 20, 16);
ball.updateMatrix();
pendulum.merge(ball.geometry, ball.matrix);
scene.add(pendulum);
After all, I got the following error:
THREE.Object3D.add: object not an instance of THREE.Object3D. THREE.CylinderGeometry {uuid: "688B0EB1-70F7-4C51-86DB-5B1B90A8A24C", name: "", type: "CylinderGeometry", vertices: Array[1332], colors: Array[0]…}THREE.error # three_r71.js:35THREE.Object3D.add # three_r71.js:7770(anonymous function) # pendulum.js:20
To explain Darius' answer more clearly (as I struggled with it, while trying to update a version of Mr Doob's procedural city to work with the Face3 boxes):
Essentially you are merging all of your Meshes into a single Geometry. So, if you, for instance, want to merge a box and sphere:
var box = new THREE.BoxGeometry(1, 1, 1);
var sphere = new THREE.SphereGeometry(.65, 32, 32);
...into a single geometry:
var singleGeometry = new THREE.Geometry();
...you would create a Mesh for each geometry:
var boxMesh = new THREE.Mesh(box);
var sphereMesh = new THREE.Mesh(sphere);
...then call the merge method of the single geometry for each, passing the geometry and matrix of each into the method:
boxMesh.updateMatrix(); // as needed
singleGeometry.merge(boxMesh.geometry, boxMesh.matrix);
sphereMesh.updateMatrix(); // as needed
singleGeometry.merge(sphereMesh.geometry, sphereMesh.matrix);
Once merged, create a mesh from the single geometry and add to the scene:
var material = new THREE.MeshPhongMaterial({color: 0xFF0000});
var mesh = new THREE.Mesh(singleGeometry, material);
scene.add(mesh);
A working example:
<!DOCTYPE html>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r77/three.js"></script>
<!-- OrbitControls.js is not versioned and may stop working with r77 -->
<script src='http://threejs.org/examples/js/controls/OrbitControls.js'></script>
<body style='margin: 0px; background-color: #bbbbbb; overflow: hidden;'>
<script>
// init renderer
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// init scene and camera
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 3000);
camera.position.z = 5;
var controls = new THREE.OrbitControls(camera)
// our code
var box = new THREE.BoxGeometry(1, 1, 1);
var sphere = new THREE.SphereGeometry(.65, 32, 32);
var singleGeometry = new THREE.Geometry();
var boxMesh = new THREE.Mesh(box);
var sphereMesh = new THREE.Mesh(sphere);
boxMesh.updateMatrix(); // as needed
singleGeometry.merge(boxMesh.geometry, boxMesh.matrix);
sphereMesh.updateMatrix(); // as needed
singleGeometry.merge(sphereMesh.geometry, sphereMesh.matrix);
var material = new THREE.MeshPhongMaterial({color: 0xFF0000});
var mesh = new THREE.Mesh(singleGeometry, material);
scene.add(mesh);
// a light
var light = new THREE.HemisphereLight(0xfffff0, 0x101020, 1.25);
light.position.set(0.75, 1, 0.25);
scene.add(light);
// render
requestAnimationFrame(function animate(){
requestAnimationFrame(animate);
renderer.render(scene, camera);
})
</script>
</body>
At least, that's how I am interpreting things; apologies to anyone if I have something wrong, as I am no where close to being a three.js expert (currently learning). I just had the "bad luck" to try my hand at customizing Mr. Doob's procedural city code, when the latest version breaks things (the merge stuff being one of them, the fact that three.js no longer uses quads for cube -ahem- box geometry the other - which has led to all kinds of fun getting the shading and such to work properly again).
Finally, I found a possible solution. I am posting since it could be useful for somebody else while I wasted a lot of hours. The tricky thing is about manipulating the concept of meshes and geometries:
var ballGeo = new THREE.SphereGeometry(10,35,35);
var material = new THREE.MeshPhongMaterial({color: 0xF7FE2E});
var ball = new THREE.Mesh(ballGeo, material);
var pendulumGeo = new THREE.CylinderGeometry(1, 1, 50, 16);
ball.updateMatrix();
pendulumGeo.merge(ball.geometry, ball.matrix);
var pendulum = new THREE.Mesh(pendulumGeo, material);
scene.add(pendulum);
The error message is right. CylinderGeometry is not an Object3D. Mesh is. A Mesh is constructed from a Geometry and a Material. A Mesh can be added to the scene, while a Geometry cannot.
In the newest versions of three.js, Geometry has two merge methods: merge and mergeMesh.
merge takes a mandatory argument geometry, and two optional arguments matrix and materialIndexOffset.
geom.mergeMesh(mesh) is basically a shorthand for geom.merge(mesh.geometry, mesh.matrix), as used in other answers. ('geom' and 'mesh' being arbitrary names for a Geometry and a Mesh, respectively.) The Material of the Mesh is ignored.
This is my ultimate compact version in four (or five) lines (as long as material is defined somewhere else) making use of mergeMesh:
var geom = new THREE.Geometry();
geom.mergeMesh(new THREE.Mesh(new THREE.BoxGeometry(2,20,2)));
geom.mergeMesh(new THREE.Mesh(new THREE.BoxGeometry(5,5,5)));
geom.mergeVertices(); // optional
scene.add(new THREE.Mesh(geom, material));
Edit: added optional extra line to remove duplicate vertices, which should help performance.
Edit 2: I'm using the newest version, 94.
The answers and code that I've seen posted here do not work because the second argument of the merge method is an integer, not a matrix. As far as I can tell, the merge method is not really functioning in a useful way. Therefore, I used the following approach to make a simple rocket with a nose cone.
import * as BufferGeometryUtils from '../three.js/examples/jsm/utils/BufferGeometryUtils.js'
lengthSegments = 2
radius = 5
radialSegments = 32
const bodyLength = dParamWithUnits['launchVehicleBodyLength'].value
const noseConeLength = dParamWithUnits['launchVehicleNoseConeLength'].value
// Create the vehicle's body
const launchVehicleBodyGeometry = new THREE.CylinderGeometry(radius, radius, bodyLength, radialSegments, lengthSegments, false)
launchVehicleBodyGeometry.name = "body"
// Create the nose cone
const launchVehicleNoseConeGeometry = new THREE.CylinderGeometry(0, radius, noseConeLength, radialSegments, lengthSegments, false)
launchVehicleNoseConeGeometry.name = "noseCone"
launchVehicleNoseConeGeometry.translate(0, (bodyLength+noseConeLength)/2, 0)
// Merge the nosecone into the body
const launchVehicleGeometry = BufferGeometryUtils.mergeBufferGeometries([launchVehicleBodyGeometry, launchVehicleNoseConeGeometry])
// Rotate the vehicle to horizontal
launchVehicleGeometry.rotateX(-Math.PI/2)
const launchVehicleMaterial = new THREE.MeshPhongMaterial( {color: 0x7f3f00})
const launchVehicleMesh = new THREE.Mesh(launchVehicleGeometry, launchVehicleMaterial)

Categories