disfigure
Version:
A library to rig non-rigged 3D models
462 lines (283 loc) • 12.3 kB
JavaScript
// disfigure
//
// Definitions of man, woman and child with motion methods.
import { Euler, Group, MathUtils, Matrix3, Mesh, MeshPhysicalNodeMaterial, PlaneGeometry, Vector3 } from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { uniform, vec3 } from 'three/tsl';
import { compileClothing } from './clothes.js';
import { tslNormalNode, tslPositionNode } from './motion.js';
import { Space } from './space.js';
import { everybody, scene } from './world.js';
var loader = new GLTFLoader();
// path to models as GLB files
const MODEL_PATH = import.meta.url.replace( '/src/body.js', '/assets/models/' );
// dummy vars
var _v = new Vector3();
var toDeg = x => x * 180 / Math.PI,
toRad = x => x / 180 * Math.PI,
toRound = x => Math.round( 100*x )/100;
function getset( object, name, axis, sign ) {
Object.defineProperty( object, name, {
get() {
return toDeg( sign*object.rotation[ axis ]);
},
set( value ) {
object.rotation[ axis ] = toRad( sign*value );
object.quaternion.setFromEuler( object.rotation, false );
}
} );
}
class Joint extends Group {
constructor( model, parent, space, bendAxis, turnAxis, tiltAxis, bendSign, turnSign, tiltSign, order='XZY' ) {
super();
this.model = model;
this.model.joints.push( this );
this.space = space;
this.umatrix = uniform( new Matrix3() );
this.rotation.reorder( order );
getset( this, 'bend', bendAxis, bendSign );
getset( this, 'turn', turnAxis, turnSign );
getset( this, 'tilt', tiltAxis, tiltSign );
getset( this, 'foreward', bendAxis, bendSign );
getset( this, 'straddle', tiltAxis, tiltSign );
( parent??model ).add( this );
}
attach( mesh, x, y, z ) {
if ( mesh.parent ) mesh = mesh.clone();
if ( typeof x !== 'undefined' )
mesh.position.set( x, y, z );
this.add( mesh );
}
point( x, y, z ) {
_v.set( x, y, z );
return this.localToWorld( _v );
}
lockTo( localX, localY, localZ, globalX, globalY, globalZ ) {
this.model.position.set( 0, 0, 0 );
_v = this.point( localX, localY, localZ ); // local
this.model.position.sub( _v );
_v.set( globalX, globalY, globalZ ); // global
this.model.position.add( _v );
} // Joint.lockTo
} // Joint
var dummyGeometry = new PlaneGeometry(),
_uid = 1;
class Disfigure extends Mesh {
constructor( figure, height ) {
var url = MODEL_PATH+figure.URL,
space = figure.SPACE,
geometryHeight = figure.HEIGHT;
super( dummyGeometry );
// unique number for each body, used to make their motions different
this.url = url;
this.uid = _uid;
_uid += 1 + 10*Math.random();
this.castShadow = true;
this.receiveShadow = true;
this.joints = [];
this.height = height??geometryHeight;
this.scale.setScalar( this.height / geometryHeight );
loader.load( this.url, ( gltf )=>{
this.geometry = gltf.scene.children[ 0 ].geometry;
} );
// create the space around the model
this.space = new Space( space );
this.torso = new Joint( this, null, this.space.torso, 'x', 'y', 'z', 1, 1, -1 );
this.waist = new Joint( this, this.torso, this.space.waist, 'x', 'y', 'z', 1, 1, -1 );
this.chest = new Joint( this, this.waist, this.space.chest, 'x', 'y', 'z', 1, 1, -1 );
this.head = new Joint( this, this.chest, this.space.head, 'x', 'y', 'z', 1, 1, -1 );
this.l_leg = new Joint( this, this.torso, this.space.l_leg, 'x', 'y', 'z', -1, 1, 1, 'ZYX' );
this.l_thigh = new Joint( this, this.l_leg, this.space.l_thigh, 'x', 'y', 'z', 0, 1, 0 );
this.l_knee = new Joint( this, this.l_thigh, this.space.l_knee, 'x', 'y', 'z', 1, 0, -1 );
this.l_shin = new Joint( this, this.l_knee, this.space.l_shin, 'x', 'y', 'z', 0, 1, 0 );
this.l_ankle = new Joint( this, this.l_shin, this.space.l_ankle, 'x', 'y', 'z', 1, 0, 1 );
this.l_foot = new Joint( this, this.l_ankle, this.space.l_foot, 'x', 'y', 'z', 1, 0, 0 );
this.r_leg = new Joint( this, this.torso, this.space.r_leg, 'x', 'y', 'z', -1, -1, -1, 'ZYX' );
this.r_thigh = new Joint( this, this.r_leg, this.space.r_thigh, 'x', 'y', 'z', 0, -1, 0 );
this.r_knee = new Joint( this, this.r_thigh, this.space.r_knee, 'x', 'y', 'z', 1, 0, 1 );
this.r_shin = new Joint( this, this.r_knee, this.space.r_shin, 'x', 'y', 'z', 0, -1, 0 );
this.r_ankle = new Joint( this, this.r_shin, this.space.r_ankle, 'x', 'y', 'z', 1, 0, -1 );
this.r_foot = new Joint( this, this.r_ankle, this.space.r_foot, 'x', 'y', 'z', 1, 0, 0 );
this.l_arm = new Joint( this, this.chest, this.space.l_arm, 'y', 'x', 'z', -1, 1, -1, 'ZYX' );
this.l_elbow = new Joint( this, this.l_arm, this.space.l_elbow, 'y', 'x', 'z', -1, 0, 0 );
this.l_forearm = new Joint( this, this.l_elbow, this.space.l_forearm, 'z', 'x', 'y', 0, 1, 0 );
this.l_wrist = new Joint( this, this.l_forearm, this.space.l_wrist, 'z', 'x', 'y', -1, 0, -1 );
this.r_arm = new Joint( this, this.chest, this.space.r_arm, 'y', 'x', 'z', 1, 1, 1, 'ZYX' );
this.r_elbow = new Joint( this, this.r_arm, this.space.r_elbow, 'y', 'x', 'z', 1, 0, 0 );
this.r_forearm = new Joint( this, this.r_elbow, this.space.r_forearm, 'z', 'x', 'y', 0, 1, 0 );
this.r_wrist = new Joint( this, this.r_forearm, this.space.r_wrist, 'z', 'x', 'y', 1, 0, 1 );
// sets the materials of the model hooking them to TSL functions
this.material = new MeshPhysicalNodeMaterial( {
positionNode: tslPositionNode( this ),
normalNode: tslNormalNode( this ),
colorNode: vec3( 0.99, 0.65, 0.49 ),
metalness: 0,
roughness: 0.6,
} );
this.position.y = 0;
this.castShadow = true;
this.receiveShadow = true;
// register the model
everybody.push( this );
if ( scene ) scene.add( this );
this.l_arm.straddle = this.r_arm.straddle = 65;
this.l_elbow.bend = this.r_elbow.bend = 20;
// define bones positions
for ( var name in this.space )
if ( this[ name ])
this[ name ].position.copy( this.space[ name ].pivot );
// fix positions, because they are accumulated
this.reposition( this.torso );
} // Disfigure.constructor
reposition( bone ) {
for ( var child of bone.children )
this.reposition( child );
bone.position.sub( bone.parent.position );
} // Skeleton.reposition
update( ) {
for ( var joint of this.joints ) {
var s = joint.matrix.elements;
joint.umatrix.value.set( s[ 0 ], s[ 1 ], s[ 2 ], s[ 4 ], s[ 5 ], s[ 6 ], s[ 8 ], s[ 9 ], s[ 10 ]);
}
} // Disfigure.update
get posture() {
var angles = [];
for ( var joint of this.joints )
angles.push( joint.rotation.x, joint.rotation.y, joint.rotation.z );
var position = [ ...this.position ];
var rotation = [ ...this.rotation ];
return {
version: 8,
position: position.map( x=>toRound( x ) ),
rotation: rotation,
angles: angles.map( x=>toRound( toDeg( x ) ) ) };
} // Disfigure.posture
get postureString() {
return JSON.stringify( this.posture );
} // Disfigure.postureString
set posture( data ) {
if ( data.version !=8 )
console.error( 'Incompatible posture version' );
var angles = data.angles.map( x=>toRad( x ) );
this.position.set( ...data.position );
this.rotation.set( ...data.rotation );
var i = 0;
for ( var joint of this.joints )
joint.rotation.set( angles[ i++ ], angles[ i++ ], angles[ i++ ]);
this.update();
this.updateMatrixWorld( true );
} // Disfigure.posture
blend( postureA, postureB, k ) {
function lerp( a, b ) {
var c = [];
for ( var i=0; i<a.length; i++ )
c[ i ] = MathUtils.lerp( a[ i ], b[ i ], k );
return c;
}
var eulerA = new Euler( ...postureA.rotation ),
eulerB = new Euler( ...postureB.rotation );
eulerA.reorder( eulerB.order );
var posture = {
version: postureA.version,
position: lerp( postureA.position, postureB.position ),
rotation: new Euler(
...lerp([ eulerA.x, eulerA.y, eulerA.z ],
[ eulerB.x, eulerB.y, eulerB.z ])
),
angles: lerp( postureA.angles, postureB.angles ),
};
this.posture = posture;
} // Disfigure.blend
dress( clothingData ) {
var clothes = compileClothing( clothingData ).toVar();
this.material.colorNode = clothes[ 0 ].xyz;
this.material.roughnessNode = clothes[ 1 ].x;
this.material.metalnessNode = clothes[ 1 ].y;
} // Disfigure.dress
} // Disfigure
class Man extends Disfigure {
static URL = 'man.glb';
static HEIGHT = 1.795;
static SPACE = {
// TORSO
head: [[ 0, 1.566, -0.066 ], [ 1.495, 1.647 ], 30 ],
chest: [[ 0, 1.177, -0.014 ], [ 0.777, 1.658 ], 0, [ 0.072, 0.538 ]],
waist: [[ 0, 1.014, -0.016 ], [ 0.547, 1.498 ]],
torso: [[ 0, 1.014, -0.016 ], [ -3, -2 ]],
// LEGS
leg: [[ 0.074, 0.970, -0.034 ], [ -0.004, 0.004 ], [ 1.229, 0.782 ]],
thigh: [[ 0.070, 0.737, -0.034 ], [ 1.247, 0.242 ]],
knee: [[ 0.090, 0.504, -0.041 ], [ 0.603, 0.382 ], 20 ],
ankle: [[ 0.074, 0.082, -0.002 ], [ 0.165, 0.008 ], -10 ],
shin: [[ 0.092, 0.360, -0.052 ], [ 0.762, -0.027 ]],
foot: [[ 0.074, 0.026, 0.022 ], [ 0.190, -0.342 ], 120 ],
// ARMS
elbow: [[ 0.427, 1.453, -0.072 ], [ 0.413, 0.467 ]],
forearm: [[ 0.550, 1.453, -0.068 ], [ 0.083, 0.879 ]],
wrist: [[ 0.673, 1.462, -0.072 ], [ 0.635, 0.722 ]],
arm: [[ 0.153, 1.408, -0.072 ], [ 0.054, 0.269 ], [ 1.067, 1.616 ]],
};
constructor( height ) {
super( Man, height );
this.l_leg.straddle = this.r_leg.straddle = 5;
this.l_ankle.tilt = this.r_ankle.tilt = -5;
this.l_ankle.bend = this.r_ankle.bend = 3;
} // Man.constructor
} // Man
class Woman extends Disfigure {
static URL = 'woman.glb';
static HEIGHT = 1.691;
static SPACE = {
// TORSO
head: [[ 0.001, 1.471, -0.049 ], [ 1.395, 1.551 ], 30 ],
chest: [[ 0.001, 1.114, -0.012 ], [ 0.737, 1.568 ], 0, [ 0.069, 0.509 ]],
waist: [[ 0.001, 0.961, -0.014 ], [ 0.589, 1.417 ]],
torso: [[ 0.001, 0.961, -0.014 ], [ -1.696, -1.694 ]],
// LEGS
leg: [[ 0.071, 0.920, -0.031 ], [ -0.002, 0.005 ], [ 1.163, 0.742 ]],
thigh: [[ 0.076, 0.7, -0.031 ], [ 1.180, 0.233 ]],
knee: [[ 0.086, 0.480, -0.037 ], [ 0.573, 0.365 ], 20 ],
shin: [[ 0.088, 0.337, -0.047 ], [ 0.724, -0.059 ]],
ankle: [[ 0.076, 0.083, -0.005 ], [ 0.161, 0.014 ], -10 ],
foot: [[ 0.076, 0.031, 0.022 ], [ 0.184, -0.316 ], 120 ],
// ARMS
elbow: [[ 0.404, 1.375, -0.066 ], [ 0.390, 0.441 ]],
forearm: [[ 0.506, 1.375, -0.063 ], [ 0.093, 0.805 ]],
wrist: [[ 0.608, 1.375, -0.056 ], [ 0.581, 0.644 ]],
arm: [[ 0.137, 1.338, -0.066 ], [ 0.052, 0.233 ], [ 1.011, 1.519 ]],
};
constructor( height ) {
super( Woman, height );
this.l_leg.straddle = this.r_leg.straddle = -2.9;
this.l_ankle.tilt = this.r_ankle.tilt = 2.9;
this.l_ankle.bend = this.r_ankle.bend = 3;
} // Woman.constructor
} // Woman
class Child extends Disfigure {
static URL = 'child.glb';
static HEIGHT = 1.352;
static SPACE = {
// TORSO
head: [[ 0, 1.149, -0.058 ], [ 1.091, 1.209 ], 30 ],
chest: [[ 0, 0.865, -0.013 ], [ 0.566, 1.236 ], 0, [ 0.054, 0.406 ]],
waist: [[ 0, 0.717, -0.024 ], [ 0.385, 1.130 ]],
torso: [[ 0, 0.717, -0.024 ], [ -1.354, -1.353 ]],
// LEGS
leg: [[ 0.054, 0.704, -0.027 ], [ -0.001, 0.001 ], [ 0.845, 0.581 ], 1 ],
thigh: [[ 0.062, 0.547, -0.021 ], [ 0.946, 0.189 ]],
knee: [[ 0.068, 0.389, -0.031 ], [ 0.468, 0.299 ], 20 ],
shin: [[ 0.069, 0.272, -0.048 ], [ 0.581, -0.045 ]],
ankle: [[ 0.073, 0.065, -0.033 ], [ 0.109, 0.044 ], -10 ],
foot: [[ 0.073, 0.027, -0.006 ], [ 0.112, -0.271 ], 120 ],
// ARMS
elbow: [[ 0.337, 1.072, -0.090 ], [ 0.311, 0.369 ]],
forearm: [[ 0.438, 1.074, -0.094 ], [ 0.073, 0.642 ]],
wrist: [[ 0.538, 1.084, -0.091 ], [ 0.519, 0.553 ]],
arm: [[ 0.108, 1.072, -0.068 ], [ 0.041, 0.185 ], [ 0.811, 1.217 ]],
};
constructor( height ) {
super( Child, height );
this.l_ankle.bend = this.r_ankle.bend = 3;
} // Child.constructor
} // Child
export { Man, Woman, Child, Joint };