three
Version:
JavaScript 3D library
303 lines (227 loc) • 6.4 kB
JavaScript
import {
BufferGeometry,
Float32BufferAttribute,
Line,
LineBasicMaterial,
Object3D,
Points,
PointsMaterial
} from 'three';
/**
* Visualizes the motion path of an animated object based on position keyframes
* from an AnimationClip.
*
* ```js
* const clip = model.animations[ 0 ];
* const helper = new AnimationPathHelper( model, clip, object );
* scene.add( helper );
* ```
*
* @augments Object3D
* @three_import import { AnimationPathHelper } from 'three/addons/helpers/AnimationPathHelper.js';
*/
class AnimationPathHelper extends Object3D {
/**
* Constructs a new animation path helper.
*
* @param {Object3D} root - The root object containing the animation clips.
* @param {AnimationClip} clip - The animation clip containing position keyframes.
* @param {Object3D} object - The specific object to show the path for.
* @param {Object} [options={}] - Configuration options.
* @param {number|Color|string} [options.color=0x00ff00] - The path line color.
* @param {number|Color|string} [options.markerColor=0xff0000] - The keyframe marker color.
* @param {number} [options.divisions=100] - Number of samples for smooth path interpolation.
* @param {boolean} [options.showMarkers=true] - Whether to show markers at keyframe positions.
* @param {number} [options.markerSize=5] - Size of keyframe markers in pixels.
*/
constructor( root, clip, object, options = {} ) {
super();
const {
color = 0x00ff00,
markerColor = 0xff0000,
divisions = 100,
showMarkers = true,
markerSize = 5
} = options;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isAnimationPathHelper = true;
this.type = 'AnimationPathHelper';
/**
* The root object containing the animation clips.
*
* @type {Object3D}
*/
this.root = root;
/**
* The animation clip containing position keyframes.
*
* @type {AnimationClip}
*/
this.clip = clip;
/**
* The object whose path is being visualized.
*
* @type {Object3D}
*/
this.object = object;
/**
* Number of samples for smooth path interpolation.
*
* @type {number}
* @default 100
*/
this.divisions = divisions;
/**
* The position track for the object.
*
* @type {KeyframeTrack|null}
* @private
*/
this._track = this._findTrackForObject( object );
if ( this._track === null ) {
console.warn( 'AnimationPathHelper: No position track found for object', object.name );
return;
}
// Create line for path
const lineGeometry = new BufferGeometry();
const lineMaterial = new LineBasicMaterial( {
color: color,
toneMapped: false
} );
/**
* The line representing the animation path.
*
* @type {Line}
*/
this.line = new Line( lineGeometry, lineMaterial );
this.add( this.line );
// Create points for keyframe markers
if ( showMarkers ) {
const pointsGeometry = new BufferGeometry();
const pointsMaterial = new PointsMaterial( {
color: markerColor,
size: markerSize,
sizeAttenuation: false,
toneMapped: false
} );
/**
* Points marking keyframe positions.
*
* @type {Points|null}
*/
this.points = new Points( pointsGeometry, pointsMaterial );
this.add( this.points );
} else {
this.points = null;
}
// Sync matrix with object's parent
this.matrixAutoUpdate = false;
this._updateGeometry();
}
/**
* Finds the position track for the given object.
*
* @private
* @param {Object3D} object - The object to find the track for.
* @returns {KeyframeTrack|null} The position track, or null if not found.
*/
_findTrackForObject( object ) {
const targetName = object.uuid + '.position';
for ( const track of this.clip.tracks ) {
if ( track.name === targetName && track.getValueSize() === 3 ) {
return track;
}
}
return null;
}
/**
* Samples the track at regular intervals.
*
* @private
* @returns {Float32Array} Array of sampled positions.
*/
_sampleTrack() {
const track = this._track;
const interpolant = track.createInterpolant();
const duration = this.clip.duration;
const positions = [];
for ( let i = 0; i <= this.divisions; i ++ ) {
const t = ( i / this.divisions ) * duration;
const result = interpolant.evaluate( t );
positions.push( result[ 0 ], result[ 1 ], result[ 2 ] );
}
return new Float32Array( positions );
}
/**
* Updates the geometry with sampled path data.
*
* @private
*/
_updateGeometry() {
if ( this._track === null ) return;
// Update line geometry
const sampledPositions = this._sampleTrack();
this.line.geometry.setAttribute( 'position', new Float32BufferAttribute( sampledPositions, 3 ) );
this.line.geometry.computeBoundingSphere();
// Update keyframe markers
if ( this.points !== null ) {
this.points.geometry.setAttribute( 'position', new Float32BufferAttribute( new Float32Array( this._track.values ), 3 ) );
this.points.geometry.computeBoundingSphere();
}
}
/**
* Updates the helper's transform to match the object's parent.
*
* @param {boolean} force - Force matrix update.
*/
updateMatrixWorld( force ) {
// Position the helper at the object's parent so the path appears in correct local space
if ( this.object && this.object.parent ) {
this.object.parent.updateWorldMatrix( true, false );
this.matrix.copy( this.object.parent.matrixWorld );
} else {
this.matrix.identity();
}
this.matrixWorld.copy( this.matrix );
// Update children
for ( let i = 0; i < this.children.length; i ++ ) {
this.children[ i ].updateMatrixWorld( force );
}
}
/**
* Sets the path line color.
*
* @param {number|Color|string} color - The new color.
*/
setColor( color ) {
if ( this.line ) this.line.material.color.set( color );
}
/**
* Sets the keyframe marker color.
*
* @param {number|Color|string} color - The new color.
*/
setMarkerColor( color ) {
if ( this.points ) this.points.material.color.set( color );
}
/**
* Frees the GPU-related resources allocated by this instance.
*/
dispose() {
if ( this.line ) {
this.line.geometry.dispose();
this.line.material.dispose();
}
if ( this.points ) {
this.points.geometry.dispose();
this.points.material.dispose();
}
}
}
export { AnimationPathHelper };