UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

240 lines (233 loc) 6.94 kB
import { m as macro } from '../../../macros2.js'; import { B as degreesFromRadians } from '../../../Common/Core/Math/index.js'; import { quat, vec3 } from 'gl-matrix'; const { vtkDebugMacro, vtkWarningMacro } = macro; /** * Create an animation channel * @param {glTFChannel} glTFChannel * @param {glTFChannel[]} glTFSamplers * @returns */ function createAnimationChannel(glTFChannel, glTFSamplers) { const path = glTFChannel.target.path; const node = glTFChannel.target.node; function applyAnimation(value) { let axisAngle; let w; let nq; switch (path) { case 'translation': node.setPosition(value[0], value[1], value[2]); break; case 'rotation': // Convert quaternion to axis-angle representation nq = quat.normalize(quat.create(), value); axisAngle = new Float64Array(3); w = quat.getAxisAngle(axisAngle, nq); // Apply rotation using rotateWXYZ node.rotateWXYZ(degreesFromRadians(w), axisAngle[0], axisAngle[1], axisAngle[2]); break; case 'scale': node.setScale(value[0], value[1], value[2]); break; default: vtkWarningMacro(`Unsupported animation path: ${path}`); } } function animate(currentTime) { const sampler = glTFSamplers[glTFChannel.sampler]; const value = sampler.evaluate(currentTime, path); applyAnimation(value); } return { ...glTFChannel, animate }; } /** * Create an animation sampler * @param {glTFSampler} glTFSampler * @returns */ function createAnimationSampler(glTFSampler) { let lastKeyframeIndex = 0; function findKeyframes(time) { let i1 = lastKeyframeIndex; while (i1 < glTFSampler.input.length - 1 && glTFSampler.input[i1] <= time) { i1++; } const i0 = Math.max(0, i1 - 1); lastKeyframeIndex = i0; return [glTFSampler.input[i0], glTFSampler.input[i1], i0, i1]; } function stepInterpolate(path, i0) { const startIndex = i0 * 3; const v0 = new Array(3); for (let i = 0; i < 3; ++i) { v0[i] = glTFSampler.output[startIndex + i]; } return v0; } function linearInterpolate(path, t0, t1, i0, i1, t) { const ratio = (t - t0) / (t1 - t0); const startIndex = i0 * 4; const endIndex = i1 * 4; const v0 = new Array(4); const v1 = new Array(4); for (let i = 0; i < 4; ++i) { v0[i] = glTFSampler.output[startIndex + i]; v1[i] = glTFSampler.output[endIndex + i]; } switch (path) { case 'translation': case 'scale': return vec3.lerp(vec3.create(), v0, v1, ratio); case 'rotation': return quat.slerp(quat.create(), v0, v1, ratio); default: vtkWarningMacro(`Unsupported animation path: ${path}`); return null; } } function cubicSplineInterpolate(path, t0, t1, i0, i1, time) { const dt = t1 - t0; const t = (time - t0) / dt; const t2 = t * t; const t3 = t2 * t; const p0 = glTFSampler.output[i0 * 3 + 1]; const m0 = dt * glTFSampler.output[i0 * 3 + 2]; const p1 = glTFSampler.output[i1 * 3 + 1]; const m1 = dt * glTFSampler.output[i1 * 3]; if (Array.isArray(p0)) { return p0.map((v, j) => { const a = 2 * t3 - 3 * t2 + 1; const b = t3 - 2 * t2 + t; const c = -2 * t3 + 3 * t2; const d = t3 - t2; return a * v + b * m0[j] + c * p1[j] + d * m1[j]; }); } const a = 2 * t3 - 3 * t2 + 1; const b = t3 - 2 * t2 + t; const c = -2 * t3 + 3 * t2; const d = t3 - t2; return a * p0 + b * m0 + c * p1 + d * m1; } function evaluate(time, path) { const [t0, t1, i0, i1] = findKeyframes(time); let result; switch (glTFSampler.interpolation) { case 'STEP': result = stepInterpolate(path, i0); break; case 'LINEAR': result = linearInterpolate(path, t0, t1, i0, i1, time); break; case 'CUBICSPLINE': result = cubicSplineInterpolate(path, t0, t1, i0, i1, time); break; default: vtkWarningMacro(`Unknown interpolation method: ${glTFSampler.interpolation}`); } return result; } return { ...glTFSampler, evaluate }; } /** * Create an animation * @param {glTFAnimation} glTFAnimation * @param {Map} nodes * @returns */ function createAnimation(glTFAnimation, nodes) { glTFAnimation.samplers = glTFAnimation.samplers.map(sampler => createAnimationSampler(sampler)); glTFAnimation.channels = glTFAnimation.channels.map(channel => { channel.target.node = nodes.get(`node-${channel.target.node}`); return createAnimationChannel(channel, glTFAnimation.samplers); }); function update(currentTime) { glTFAnimation.channels.forEach(channel => channel.animate(currentTime)); } return { ...glTFAnimation, update }; } /** * Create an animation mixer * @param {Map} nodes * @param {*} accessors * @returns */ function createAnimationMixer(nodes, accessors) { const animations = new Map(); const activeAnimations = new Map(); function addAnimation(glTFAnimation) { const annimation = createAnimation(glTFAnimation, nodes); animations.set(glTFAnimation.id, annimation); vtkDebugMacro(`Animation "${glTFAnimation.id}" added to mixer`); } function play(name) { let weight = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; if (!animations.has(name)) { vtkWarningMacro(`Animation "${name}" not found in mixer`); return; } activeAnimations.set(name, { animation: animations.get(name), weight, time: 0 }); vtkDebugMacro(`Playing animation "${name}" with weight ${weight}`); } function stop(name) { if (activeAnimations.delete(name)) { vtkWarningMacro(`Stopped animation "${name}"`); } else { vtkWarningMacro(`Animation "${name}" was not playing`); } } function stopAll() { activeAnimations.clear(); vtkWarningMacro('Stopped all animations'); } function update(deltaTime) { // Normalize weights const totalWeight = Array.from(activeAnimations.values()).reduce((sum, _ref) => { let { weight } = _ref; return sum + weight; }, 0); activeAnimations.forEach((_ref2, name) => { let { animation, weight, time } = _ref2; const normalizedWeight = totalWeight > 0 ? weight / totalWeight : 0; const newTime = time + deltaTime; activeAnimations.set(name, { animation, weight, time: newTime }); vtkDebugMacro(`Updating animation "${name}" at time ${newTime.toFixed(3)} with normalized weight ${normalizedWeight.toFixed(3)}`); animation.update(newTime, normalizedWeight); }); } return { addAnimation, play, stop, stopAll, update }; } export { createAnimation, createAnimationChannel, createAnimationMixer, createAnimationSampler };