three
Version:
JavaScript 3D library
264 lines (176 loc) • 9.05 kB
JavaScript
import { Fn, add, uniform } from '../tsl/TSLBase.js';
import { attribute } from '../core/AttributeNode.js';
import { OnObjectUpdate } from '../utils/EventNode.js';
import { normalLocal } from './Normal.js';
import { positionLocal, positionPrevious } from './Position.js';
import { tangentLocal } from './Tangent.js';
import { reference, referenceBuffer } from './ReferenceNode.js';
import { buffer } from './BufferNode.js';
import { storage } from './StorageBufferNode.js';
import { instanceIndex } from '../core/IndexNode.js';
import { InstancedBufferAttribute } from '../../core/InstancedBufferAttribute.js';
const _skeletonsUpdated = /*@__PURE__*/ new WeakMap();
const _previousBoneMatricesData = /*@__PURE__*/ new WeakMap();
/**
* Computes the skinned position by applying bone matrices based on weights.
*
* @param {Node} boneMatrices - The bone matrices buffer or storage node.
* @param {Node<vec3>} position - The vertex position to transform.
* @param {Node<mat4>} bindMatrix - The bind matrix node.
* @param {Node<mat4>} bindMatrixInverse - The inverse bind matrix node.
* @param {Node<uvec4>} skinIndex - The skin index attribute.
* @param {Node<vec4>} skinWeight - The skin weight attribute.
* @returns {Node<vec3>} The skinned position.
*/
function getSkinnedPosition( boneMatrices, position, bindMatrix, bindMatrixInverse, skinIndex, skinWeight ) {
const boneMatX = boneMatrices.element( skinIndex.x );
const boneMatY = boneMatrices.element( skinIndex.y );
const boneMatZ = boneMatrices.element( skinIndex.z );
const boneMatW = boneMatrices.element( skinIndex.w );
// POSITION
const skinVertex = bindMatrix.mul( position );
const skinned = add(
boneMatX.mul( skinWeight.x ).mul( skinVertex ),
boneMatY.mul( skinWeight.y ).mul( skinVertex ),
boneMatZ.mul( skinWeight.z ).mul( skinVertex ),
boneMatW.mul( skinWeight.w ).mul( skinVertex )
);
return bindMatrixInverse.mul( skinned ).xyz;
}
/**
* Computes the skinned normal and tangent vectors by applying bone matrices based on weights.
*
* @param {Node} boneMatrices - The bone matrices buffer or storage node.
* @param {Node<vec3>} normal - The normal vector in local space.
* @param {Node<vec3>} tangent - The tangent vector in local space.
* @param {Node<mat4>} bindMatrix - The bind matrix node.
* @param {Node<mat4>} bindMatrixInverse - The inverse bind matrix node.
* @param {Node<uvec4>} skinIndex - The skin index attribute.
* @param {Node<vec4>} skinWeight - The skin weight attribute.
* @returns {{skinNormal: Node<vec3>, skinTangent: Node<vec3>}} The skinned normal and tangent.
*/
function getSkinnedNormalAndTangent( boneMatrices, normal, tangent, bindMatrix, bindMatrixInverse, skinIndex, skinWeight ) {
const boneMatX = boneMatrices.element( skinIndex.x );
const boneMatY = boneMatrices.element( skinIndex.y );
const boneMatZ = boneMatrices.element( skinIndex.z );
const boneMatW = boneMatrices.element( skinIndex.w );
// NORMAL and TANGENT
let skinMatrix = add(
skinWeight.x.mul( boneMatX ),
skinWeight.y.mul( boneMatY ),
skinWeight.z.mul( boneMatZ ),
skinWeight.w.mul( boneMatW )
);
skinMatrix = bindMatrixInverse.mul( skinMatrix ).mul( bindMatrix );
const skinNormal = skinMatrix.transformDirection( normal ).xyz;
const skinTangent = skinMatrix.transformDirection( tangent ).xyz;
return { skinNormal, skinTangent };
}
/**
* Retrieves or initializes the previous frame skinned position node for motion vectors.
* Uses a WeakMap to cache previous frame bone matrix arrays and their TSL buffer nodes.
*
* @param {SkinnedMesh} skinnedMesh - The skinned mesh.
* @param {Node<mat4>} bindMatrixNode - The bind matrix node.
* @param {Node<mat4>} bindMatrixInverseNode - The inverse bind matrix node.
* @param {Node<uvec4>} skinIndexNode - The skin index attribute.
* @param {Node<vec4>} skinWeightNode - The skin weight attribute.
* @returns {Node<vec3>} The skinned position from the previous frame.
*/
function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ) {
const skeleton = skinnedMesh.skeleton;
let data = _previousBoneMatricesData.get( skeleton );
if ( data === undefined ) {
skeleton.update();
const previousBoneMatrices = new Float32Array( skeleton.boneMatrices );
data = {
previousBoneMatrices,
node: buffer( previousBoneMatrices, 'mat4', skeleton.bones.length )
};
_previousBoneMatricesData.set( skeleton, data );
}
return getSkinnedPosition( data.node, positionPrevious, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode );
}
/**
* TSL function representing the standard skeletal animation vertex shader setup.
* Transforms positionLocal, normalLocal, and tangentLocal in-place.
*
* @tsl
* @function
* @param {SkinnedMesh} skinnedMesh - The skinned mesh.
*/
export const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => {
const skinIndexNode = attribute( 'skinIndex', 'uvec4' );
const skinWeightNode = attribute( 'skinWeight', 'vec4' );
const bindMatrixNode = reference( 'bindMatrix', 'mat4' );
const bindMatrixInverseNode = reference( 'bindMatrixInverse', 'mat4' );
const boneMatricesNode = referenceBuffer( 'skeleton.boneMatrices', 'mat4', skinnedMesh.skeleton.bones.length );
OnObjectUpdate( ( { object, frameId } ) => {
const skeleton = object.skeleton;
if ( _skeletonsUpdated.get( skeleton ) !== frameId ) {
_skeletonsUpdated.set( skeleton, frameId );
const skeletonData = _previousBoneMatricesData.get( skeleton );
if ( skeletonData !== undefined ) {
skeletonData.previousBoneMatrices.set( skeleton.boneMatrices );
}
skeleton.update();
}
} );
if ( builder.needsPreviousData() ) {
const previousSkinnedPosition = getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode );
positionPrevious.assign( previousSkinnedPosition );
}
const skinPosition = getSkinnedPosition( boneMatricesNode, positionLocal, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode );
positionLocal.assign( skinPosition );
if ( builder.hasGeometryAttribute( 'normal' ) ) {
const { skinNormal, skinTangent } = getSkinnedNormalAndTangent( boneMatricesNode, normalLocal, tangentLocal, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode );
normalLocal.assign( skinNormal );
if ( builder.hasGeometryAttribute( 'tangent' ) ) {
tangentLocal.assign( skinTangent );
}
}
}, 'void' );
/**
* TSL function that computes skeletal animation for custom compute passes.
*
* @tsl
* @function
* @param {SkinnedMesh} skinnedMesh - The skinned mesh.
* @param {Node<vec3>} [toPosition=null] - The target position node to assign.
* @returns {Node<vec3>} The computed skinned position node.
*/
export const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ], builder ) => {
const positionNode = storage( new InstancedBufferAttribute( skinnedMesh.geometry.getAttribute( 'position' ).array, 3 ), 'vec3' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar();
const skinIndexNode = storage( new InstancedBufferAttribute( new Uint32Array( skinnedMesh.geometry.getAttribute( 'skinIndex' ).array ), 4 ), 'uvec4' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar();
const skinWeightNode = storage( new InstancedBufferAttribute( skinnedMesh.geometry.getAttribute( 'skinWeight' ).array, 4 ), 'vec4' ).setPBO( true ).toReadOnly().element( instanceIndex ).toVar();
const bindMatrixNode = uniform( skinnedMesh.bindMatrix, 'mat4' );
const bindMatrixInverseNode = uniform( skinnedMesh.bindMatrixInverse, 'mat4' );
const boneMatricesNode = buffer( skinnedMesh.skeleton.boneMatrices, 'mat4', skinnedMesh.skeleton.bones.length );
const skeleton = skinnedMesh.skeleton;
OnObjectUpdate( ( { frameId } ) => {
if ( _skeletonsUpdated.get( skeleton ) !== frameId ) {
_skeletonsUpdated.set( skeleton, frameId );
const state = _previousBoneMatricesData.get( skeleton );
if ( state !== undefined ) {
state.previousBoneMatrices.set( skeleton.boneMatrices );
}
skeleton.update();
}
} );
if ( builder.needsPreviousData() ) {
const previousSkinnedPosition = getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode );
positionPrevious.assign( previousSkinnedPosition );
}
const skinPosition = getSkinnedPosition( boneMatricesNode, positionNode, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode );
if ( toPosition !== null ) {
toPosition.assign( skinPosition );
}
if ( builder.hasGeometryAttribute( 'normal' ) ) {
const { skinNormal, skinTangent } = getSkinnedNormalAndTangent( boneMatricesNode, normalLocal, tangentLocal, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode );
normalLocal.assign( skinNormal );
if ( builder.hasGeometryAttribute( 'tangent' ) ) {
tangentLocal.assign( skinTangent );
}
}
return skinPosition;
} );