manifold-3d
Version:
Geometry library for topological robustness
275 lines • 8.51 kB
JavaScript
// Copyright 2022-2025 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* Allow manifoldCAD models to specify glTF animations and morphs.
* @packageDocumentation
* @group ManifoldCAD
* @category Modelling
*/
import { Accessor, Animation, AnimationSampler, Document, Mesh as GLTFMesh, Node } from '@gltf-transform/core';
import { BaseGLTFNode } from "./gltf-node.js";
import { euler2quat } from "./math.js";
let animationMode = 'loop';
let animationDuration = 1;
let animationFPS = 30;
/**
* Set the animation repeat mode.
*
* @param mode 'loop' or 'ping-pong'
*/
export function setAnimationMode(mode) {
animationMode = mode;
}
/**
* Get the current animation repeat mode.
*/
export function getAnimationMode() {
return animationMode;
}
/**
* Set the duration of the animation, in seconds.
*
* @param duration in seconds.
*/
export function setAnimationDuration(duration) {
animationDuration = duration;
}
;
/**
* Get the current duruation of the animation, in seconds.
*/
export function getAnimationDuration() {
return animationDuration;
}
/**
* Set the animation frame rate.
*
* @param fps in frames per second.
*/
export function setAnimationFPS(fps) {
animationFPS = fps;
}
;
/**
* Get the current animation frame rate.
*/
export function getAnimationFPS() {
return animationFPS;
}
const manifold2morph = new Map();
let animation;
let timesAccessor;
let weightsAccessor;
let weightsSampler;
let hasAnimation;
export function cleanup() {
manifold2morph.clear();
animationMode = 'loop';
animationDuration = 1;
animationFPS = 30;
}
/**
*
* @internal
*/
export function addMotion(doc, type, node, out) {
const motion = node[type];
if (motion == null) {
return null;
}
if (typeof motion !== 'function') {
return motion;
}
const nFrames = timesAccessor.getCount();
const nEl = type == 'rotation' ? 4 : 3;
const frames = new Float32Array(nEl * nFrames);
for (let i = 0; i < nFrames; ++i) {
const x = i / (nFrames - 1);
const m = motion(animationMode !== 'ping-pong' ? x :
(1 - Math.cos(x * 2 * Math.PI)) / 2);
frames.set(nEl === 4 ? euler2quat(m) : m, nEl * i);
}
const framesAccessor = doc.createAccessor(node.name + ' ' + type + ' frames')
.setBuffer(doc.getRoot().listBuffers()[0])
.setArray(frames)
.setType(nEl === 4 ? Accessor.Type.VEC4 : Accessor.Type.VEC3);
const sampler = doc.createAnimationSampler()
.setInput(timesAccessor)
.setOutput(framesAccessor)
.setInterpolation('LINEAR');
const channel = doc.createAnimationChannel()
.setTargetPath(type)
.setTargetNode(out)
.setSampler(sampler);
animation.addSampler(sampler);
animation.addChannel(channel);
hasAnimation = true;
return motion(0);
}
/**
*
* @internal
*/
export function setMorph(doc, node, manifold) {
if (manifold2morph.has(manifold)) {
const channel = doc.createAnimationChannel()
.setTargetPath('weights')
.setTargetNode(node)
.setSampler(weightsSampler);
animation.addChannel(channel);
hasAnimation = true;
}
}
/**
*
* @internal
*/
export const getMorph = (manifold) => manifold2morph.get(manifold);
/**
*
* @internal
*/
export function morphStart(manifoldMesh, morph) {
const inputPositions = [];
if (morph == null) {
return inputPositions;
}
for (let i = 0; i < manifoldMesh.numVert; ++i) {
for (let j = 0; j < 3; ++j)
inputPositions[i * 3 + j] =
manifoldMesh.vertProperties[i * manifoldMesh.numProp + j];
}
if (morph.start) {
for (let i = 0; i < manifoldMesh.numVert; ++i) {
const vertProp = manifoldMesh.vertProperties;
const offset = i * manifoldMesh.numProp;
const pos = inputPositions.slice(offset, offset + 3);
morph.start(pos);
for (let j = 0; j < 3; ++j)
vertProp[offset + j] = pos[j];
}
}
return inputPositions;
}
/**
*
* @internal
*/
export function morphEnd(doc, manifoldMesh, mesh, inputPositions, morph) {
if (morph == null) {
return;
}
mesh.setWeights([0]);
mesh.listPrimitives().forEach((primitive, i) => {
if (morph.end) {
for (let i = 0; i < manifoldMesh.numVert; ++i) {
const pos = inputPositions.slice(3 * i, 3 * (i + 1));
morph.end(pos);
inputPositions.splice(3 * i, 3, ...pos);
}
}
const startPosition = primitive.getAttribute('POSITION').getArray();
const array = new Float32Array(startPosition.length);
const offset = manifoldMesh.runIndex[i];
for (let j = 0; j < array.length; ++j) {
array[j] = inputPositions[offset + j] - startPosition[j];
}
const morphAccessor = doc.createAccessor(mesh.getName() + ' morph target')
.setBuffer(doc.getRoot().listBuffers()[0])
.setArray(array)
.setType(Accessor.Type.VEC3);
const morphTarget = doc.createPrimitiveTarget().setAttribute('POSITION', morphAccessor);
primitive.addTarget(morphTarget);
});
}
/**
* Apply a morphing animation to the input manifold. Specify the start
* function which will be applied to the vertex positions of the first frame and
* linearly interpolated across the length of the overall animation. This
* animation will only be shown if this manifold is used directly on a GLTFNode.
*
* @param manifold The object to add morphing animation to.
* @param func A warping function to apply to the first animation frame.
*/
export function setMorphStart(manifold, func) {
const morph = manifold2morph.get(manifold);
if (morph != null) {
morph.start = func;
}
else {
manifold2morph.set(manifold, { start: func });
}
}
/**
* Apply a morphing animation to the input manifold. Specify the end
* function which will be applied to the vertex positions of the last frame and
* linearly interpolated across the length of the overall animation. This
* animation will only be shown if this manifold is used directly on a GLTFNode.
*
* @param manifold The object to add morphing animation to.
* @param func A warping function to apply to the last animation frame.
*/
export function setMorphEnd(manifold, func) {
const morph = manifold2morph.get(manifold);
if (morph != null) {
morph.end = func;
}
else {
manifold2morph.set(manifold, { end: func });
}
}
/**
*
* @internal
*/
export function addAnimationToDoc(doc) {
const buffer = doc.getRoot().listBuffers()[0];
animation = doc.createAnimation('');
hasAnimation = false;
const nFrames = Math.round(animationDuration * animationFPS) + 1;
const times = new Float32Array(nFrames);
const weights = new Float32Array(nFrames);
for (let i = 0; i < nFrames; ++i) {
const x = i / (nFrames - 1);
times[i] = x * animationDuration;
weights[i] =
animationMode !== 'ping-pong' ? x : (1 - Math.cos(x * 2 * Math.PI)) / 2;
}
timesAccessor = doc.createAccessor('animation times')
.setBuffer(buffer)
.setArray(times)
.setType(Accessor.Type.SCALAR);
weightsAccessor = doc.createAccessor('animation weights')
.setBuffer(buffer)
.setArray(weights)
.setType(Accessor.Type.SCALAR);
weightsSampler = doc.createAnimationSampler()
.setInput(timesAccessor)
.setOutput(weightsAccessor)
.setInterpolation('LINEAR');
animation.addSampler(weightsSampler);
}
/**
*
* @internal
*/
export function cleanupAnimationInDoc() {
if (!hasAnimation) {
timesAccessor.dispose();
weightsAccessor.dispose();
weightsSampler.dispose();
animation.dispose();
}
}
//# sourceMappingURL=animation.js.map