@openhps/core
Version:
Open Hybrid Positioning System - Core component
254 lines (233 loc) • 8.79 kB
JavaScript
import { Mesh } from './Mesh.js';
import { Box3 } from '../math/Box3.js';
import { Matrix4 } from '../math/Matrix4.js';
import { Sphere } from '../math/Sphere.js';
import { Vector3 } from '../math/Vector3.js';
import { Vector4 } from '../math/Vector4.js';
import { Ray } from '../math/Ray.js';
import { AttachedBindMode, DetachedBindMode } from '../constants.js';
const _basePosition = /*@__PURE__*/new Vector3();
const _skinIndex = /*@__PURE__*/new Vector4();
const _skinWeight = /*@__PURE__*/new Vector4();
const _vector3 = /*@__PURE__*/new Vector3();
const _matrix4 = /*@__PURE__*/new Matrix4();
const _vertex = /*@__PURE__*/new Vector3();
const _sphere = /*@__PURE__*/new Sphere();
const _inverseMatrix = /*@__PURE__*/new Matrix4();
const _ray = /*@__PURE__*/new Ray();
/**
* A mesh that has a {@link Skeleton} that can then be used to animate the
* vertices of the geometry with skinning/skeleton animation.
*
* Next to a valid skeleton, the skinned mesh requires skin indices and weights
* as buffer attributes in its geometry. These attribute define which bones affect a single
* vertex to a certain extend.
*
* Typically skinned meshes are not created manually but loaders like {@link GLTFLoader}
* or {@link FBXLoader } import respective models.
*
* @augments Mesh
*/
class SkinnedMesh extends Mesh {
/**
* Constructs a new skinned mesh.
*
* @param {BufferGeometry} [geometry] - The mesh geometry.
* @param {Material|Array<Material>} [material] - The mesh material.
*/
constructor(geometry, material) {
super(geometry, material);
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isSkinnedMesh = true;
this.type = 'SkinnedMesh';
/**
* `AttachedBindMode` means the skinned mesh shares the same world space as the skeleton.
* This is not true when using `DetachedBindMode` which is useful when sharing a skeleton
* across multiple skinned meshes.
*
* @type {(AttachedBindMode|DetachedBindMode)}
* @default AttachedBindMode
*/
this.bindMode = AttachedBindMode;
/**
* The base matrix that is used for the bound bone transforms.
*
* @type {Matrix4}
*/
this.bindMatrix = new Matrix4();
/**
* The base matrix that is used for resetting the bound bone transforms.
*
* @type {Matrix4}
*/
this.bindMatrixInverse = new Matrix4();
/**
* The bounding box of the skinned mesh. Can be computed via {@link SkinnedMesh#computeBoundingBox}.
*
* @type {?Box3}
* @default null
*/
this.boundingBox = null;
/**
* The bounding sphere of the skinned mesh. Can be computed via {@link SkinnedMesh#computeBoundingSphere}.
*
* @type {?Sphere}
* @default null
*/
this.boundingSphere = null;
}
/**
* Computes the bounding box of the skinned mesh, and updates {@link SkinnedMesh#boundingBox}.
* The bounding box is not automatically computed by the engine; this method must be called by your app.
* If the skinned mesh is animated, the bounding box should be recomputed per frame in order to reflect
* the current animation state.
*/
computeBoundingBox() {
const geometry = this.geometry;
if (this.boundingBox === null) {
this.boundingBox = new Box3();
}
this.boundingBox.makeEmpty();
const positionAttribute = geometry.getAttribute('position');
for (let i = 0; i < positionAttribute.count; i++) {
this.getVertexPosition(i, _vertex);
this.boundingBox.expandByPoint(_vertex);
}
}
/**
* Computes the bounding sphere of the skinned mesh, and updates {@link SkinnedMesh#boundingSphere}.
* The bounding sphere is automatically computed by the engine once when it is needed, e.g., for ray casting
* and view frustum culling. If the skinned mesh is animated, the bounding sphere should be recomputed
* per frame in order to reflect the current animation state.
*/
computeBoundingSphere() {
const geometry = this.geometry;
if (this.boundingSphere === null) {
this.boundingSphere = new Sphere();
}
this.boundingSphere.makeEmpty();
const positionAttribute = geometry.getAttribute('position');
for (let i = 0; i < positionAttribute.count; i++) {
this.getVertexPosition(i, _vertex);
this.boundingSphere.expandByPoint(_vertex);
}
}
copy(source, recursive) {
super.copy(source, recursive);
this.bindMode = source.bindMode;
this.bindMatrix.copy(source.bindMatrix);
this.bindMatrixInverse.copy(source.bindMatrixInverse);
this.skeleton = source.skeleton;
if (source.boundingBox !== null) this.boundingBox = source.boundingBox.clone();
if (source.boundingSphere !== null) this.boundingSphere = source.boundingSphere.clone();
return this;
}
raycast(raycaster, intersects) {
const material = this.material;
const matrixWorld = this.matrixWorld;
if (material === undefined) return;
// test with bounding sphere in world space
if (this.boundingSphere === null) this.computeBoundingSphere();
_sphere.copy(this.boundingSphere);
_sphere.applyMatrix4(matrixWorld);
if (raycaster.ray.intersectsSphere(_sphere) === false) return;
// convert ray to local space of skinned mesh
_inverseMatrix.copy(matrixWorld).invert();
_ray.copy(raycaster.ray).applyMatrix4(_inverseMatrix);
// test with bounding box in local space
if (this.boundingBox !== null) {
if (_ray.intersectsBox(this.boundingBox) === false) return;
}
// test for intersections with geometry
this._computeIntersections(raycaster, intersects, _ray);
}
getVertexPosition(index, target) {
super.getVertexPosition(index, target);
this.applyBoneTransform(index, target);
return target;
}
/**
* Binds the given skeleton to the skinned mesh.
*
* @param {Skeleton} skeleton - The skeleton to bind.
* @param {Matrix4} [bindMatrix] - The bind matrix. If no bind matrix is provided,
* the skinned mesh's world matrix will be used instead.
*/
bind(skeleton, bindMatrix) {
this.skeleton = skeleton;
if (bindMatrix === undefined) {
this.updateMatrixWorld(true);
this.skeleton.calculateInverses();
bindMatrix = this.matrixWorld;
}
this.bindMatrix.copy(bindMatrix);
this.bindMatrixInverse.copy(bindMatrix).invert();
}
/**
* This method sets the skinned mesh in the rest pose).
*/
pose() {
this.skeleton.pose();
}
/**
* Normalizes the skin weights which are defined as a buffer attribute
* in the skinned mesh's geometry.
*/
normalizeSkinWeights() {
const vector = new Vector4();
const skinWeight = this.geometry.attributes.skinWeight;
for (let i = 0, l = skinWeight.count; i < l; i++) {
vector.fromBufferAttribute(skinWeight, i);
const scale = 1.0 / vector.manhattanLength();
if (scale !== Infinity) {
vector.multiplyScalar(scale);
} else {
vector.set(1, 0, 0, 0); // do something reasonable
}
skinWeight.setXYZW(i, vector.x, vector.y, vector.z, vector.w);
}
}
updateMatrixWorld(force) {
super.updateMatrixWorld(force);
if (this.bindMode === AttachedBindMode) {
this.bindMatrixInverse.copy(this.matrixWorld).invert();
} else if (this.bindMode === DetachedBindMode) {
this.bindMatrixInverse.copy(this.bindMatrix).invert();
} else {
console.warn('THREE.SkinnedMesh: Unrecognized bindMode: ' + this.bindMode);
}
}
/**
* Applies the bone transform associated with the given index to the given
* vertex position. Returns the updated vector.
*
* @param {number} index - The vertex index.
* @param {Vector3} target - The target object that is used to store the method's result.
* the skinned mesh's world matrix will be used instead.
* @return {Vector3} The updated vertex position.
*/
applyBoneTransform(index, target) {
const skeleton = this.skeleton;
const geometry = this.geometry;
_skinIndex.fromBufferAttribute(geometry.attributes.skinIndex, index);
_skinWeight.fromBufferAttribute(geometry.attributes.skinWeight, index);
_basePosition.copy(target).applyMatrix4(this.bindMatrix);
target.set(0, 0, 0);
for (let i = 0; i < 4; i++) {
const weight = _skinWeight.getComponent(i);
if (weight !== 0) {
const boneIndex = _skinIndex.getComponent(i);
_matrix4.multiplyMatrices(skeleton.bones[boneIndex].matrixWorld, skeleton.boneInverses[boneIndex]);
target.addScaledVector(_vector3.copy(_basePosition).applyMatrix4(_matrix4), weight);
}
}
return target.applyMatrix4(this.bindMatrixInverse);
}
}
export { SkinnedMesh };