UNPKG

nanogl-gltf

Version:
248 lines (247 loc) 9.13 kB
import { quat } from 'gl-matrix'; import Gltf2 from '../types/Gltf2'; import GltfTypes from '../types/GltfTypes'; function cubicSplineInterpolation(out, t, dt, v0, b0, a1, v1) { const t2 = t * t; const t3 = t * t2; const f0 = 2.0 * t3 - 3.0 * t2 + 1.0; const f1 = dt * (t3 - 2.0 * t2 + t); const f2 = 3.0 * t2 - 2.0 * t3; const f3 = dt * (t3 - t2); const ncomps = v0.length; for (let i = 0; i < ncomps; i++) { out[i] = f0 * v0[i] + f1 * b0[i] + f2 * v1[i] + f3 * a1[i]; } } class SampleInterval { constructor(input) { this.frame = 0; this.t0 = input.getRawScalar(0); this.t1 = input.getRawScalar(1); this.inBound = true; this._fMax = input.count - 1; } contain(t) { if (t < this.t0) return -1; if (t >= this.t1) return 1; return 0; } normalizedFrame() { return Math.min(Math.max(this.frame, 0), this._fMax); } } class Interpolator { constructor(sampler, numElements) { this.sampler = sampler; this.interval = new SampleInterval(sampler.input); this.numElements = numElements; } resolveInterval(t) { // TODO: optimize // test time bounds // binary search but test few frame around interval cache first const interval = this.interval; const input = this.sampler.input; const numFrames = input.count; const contain = interval.contain(t); // the current interval already contain t if (contain === 0) return; let frame; let t1; let t0; let inBound = true; if (contain > 0) { frame = interval.frame + 1; t1 = interval.t1; do { frame++; inBound = (frame < numFrames); t0 = t1; t1 = inBound ? input.getRawScalar(frame) : Number.MAX_VALUE; } while (t1 <= t); frame--; } else { frame = interval.frame; t0 = interval.t0; do { frame--; inBound = (frame >= 0); t1 = t0; t0 = inBound ? input.getRawScalar(frame) : -Number.MAX_VALUE; } while (t0 > t); } interval.frame = frame; interval.t0 = t0; interval.t1 = t1; interval.inBound = inBound; } evaluate(out, t) { //abstract } } class StepInterpolator extends Interpolator { evaluate(out, t) { this.resolveInterval(t); this.sampler.output.getValues(out, this.interval.normalizedFrame() * this.numElements, this.numElements); } } function LERP_N(out, a, b, p) { const n = a.length; const invp = 1.0 - p; for (let i = 0; i < n; i++) { out[i] = a[i] * invp + b[i] * p; } } function LERP1(out, a, b, p) { out[0] = a[0] * (1.0 - p) + b[0] * p; } function getLerpFunction(path, numComps) { switch (path) { case Gltf2.AnimationChannelTargetPath.WEIGHTS: return (numComps === 1) ? LERP1 : LERP_N; case Gltf2.AnimationChannelTargetPath.ROTATION: return quat.slerp; case Gltf2.AnimationChannelTargetPath.SCALE: case Gltf2.AnimationChannelTargetPath.TRANSLATION: default: return LERP_N; } } class LinearInterpolator extends Interpolator { constructor(sampler, path, numElements) { super(sampler, numElements); this.val0 = sampler.output.createElementHolderArray(numElements); this.val1 = sampler.output.createElementHolderArray(numElements); this.lerpFunc = getLerpFunction(path, this.val0.length); } evaluate(out, t) { this.resolveInterval(t); const output = this.sampler.output; const ne = this.numElements; if (this.interval.inBound) { const { t0, t1, frame } = this.interval; const p = (t - t0) / (t1 - t0); output.getValues(this.val0, ne * frame + 0, ne); output.getValues(this.val1, ne * frame + ne, ne); this.lerpFunc(out, this.val0, this.val1, p); } else { output.getValues(out, this.interval.normalizedFrame() * ne, ne); } } } // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation class CubicSplineInterpolator extends Interpolator { constructor(sampler, path, numElements) { super(sampler, numElements); this.val0 = sampler.output.createElementHolderArray(numElements); this.val1 = sampler.output.createElementHolderArray(numElements); this.val2 = sampler.output.createElementHolderArray(numElements); this.val3 = sampler.output.createElementHolderArray(numElements); this.assumeQuat = path === Gltf2.AnimationChannelTargetPath.ROTATION; } evaluate(out, t) { this.resolveInterval(t); const output = this.sampler.output; const ne = this.numElements; if (this.interval.inBound) { const { t0, t1, frame } = this.interval; const dt = t1 - t0; const p = (t - t0) / dt; output.getValues(this.val0, ne * (frame * 3 + 1), ne); output.getValues(this.val1, ne * (frame * 3 + 2), ne); output.getValues(this.val2, ne * (frame * 3 + 3), ne); output.getValues(this.val3, ne * (frame * 3 + 4), ne); cubicSplineInterpolation(out, p, dt, this.val0, this.val1, this.val2, this.val3); if (this.assumeQuat) { quat.normalize(out, out); } } else { output.getValues(out, ne * (this.interval.normalizedFrame() * 3 + 1), ne); } } } function InterpolatorFactory(sampler, path, numElements) { switch (sampler.interpolation) { case Gltf2.AnimationSamplerInterpolation.STEP: return new StepInterpolator(sampler, numElements); case Gltf2.AnimationSamplerInterpolation.LINEAR: return new LinearInterpolator(sampler, path, numElements); case Gltf2.AnimationSamplerInterpolation.CUBICSPLINE: return new CubicSplineInterpolator(sampler, path, numElements); default: throw new Error('GLTF : Unsupported sampler interpolation ' + sampler.interpolation); } } /** * A class providing a method to evaluate an AnimationSampler at a given time, using the proper interpolation algorithm. */ export class SamplerEvaluator { /** * @param sampler AnimationSampler to evaluate * @param path Node's property to animate * @param numElements Number of element to animate (1 for translation, rotation and scale ; number of morph targets for weights) */ constructor(sampler, path, numElements) { this.sampler = sampler; this.numElements = numElements; this.interpolator = InterpolatorFactory(sampler, path, numElements); } /** * Evaluate the proper value at a given time depending on the interpolation algorithm, and store the result in the out TypedArray * @param out TypedArray to store the result * @param t Time to evaluate */ evaluate(out, t) { this.interpolator.evaluate(out, t); } /** * Create a TypedArray to store the result of the AnimationSampler output Accessor */ createElementHolder() { return this.sampler.output.createElementHolderArray(this.numElements); } } /** * The AnimationSampler element is used to define the interpolation between keyframes of an AnimationChannel. */ export default class AnimationSampler { constructor() { this.gltftype = GltfTypes.ANIMATION_SAMPLER; /** * Start time of the Animation */ this.minTime = 0; /** * End time of the Animation */ this.maxTime = 0; } /** * Parse the AnimationSampler data, load the input and output Accessors, and set the minTime and maxTime with the length of input Accessor. * * Is async as it needs to wait for the input and output Accessors to be created. * @param gltfLoader GLTFLoader to use * @param data Data to parse */ async parse(gltfLoader, data) { this.input = await gltfLoader.getElement(GltfTypes.ACCESSOR, data.input); this.output = await gltfLoader.getElement(GltfTypes.ACCESSOR, data.output); this.interpolation = data.interpolation || Gltf2.AnimationSamplerInterpolation.LINEAR; this.minTime = this.input.getRawScalar(0); this.maxTime = this.input.getRawScalar(this.input.count - 1); } /** * Create a SamplerEvaluator for this AnimationSampler, with the given path and number of elements. * @param path Node's property to animate * @param numElements Number of element to animate (1 for translation, rotation and scale ; number of morph targets for weights) */ createEvaluator(path, numElements) { return new SamplerEvaluator(this, path, numElements); } }