Clean animation channels and samplers which do not animate #926
Replies: 2 comments
-
@kzhsw this is wonderful, thanks for sharing! I'd love to make this available in glTF Transform. Any idea why tools produce these samplers with two identical keyframes? Shouldn't that be the same as just including one, and/or is that just a result of resampling? One small note – it is probably necessary to update IBMs if the target node is a joint. See the quantize.ts implementation for an example: glTF-Transform/packages/functions/src/quantize.ts Lines 246 to 258 in 89108e0 |
Beta Was this translation helpful? Give feedback.
-
I tried to create a simplest animated cube model using blender with files attached below and failed to reproduce the non-animated channels.
It seems that the Maybe like this
export function cleanAnimation(_options = DEFAULT_OPTIONS) {
const options = {...DEFAULT_OPTIONS, ..._options};
return createTransform(NAME, (doc) => {
const root = doc.getRoot();
const animations = root.listAnimations();
/**
* @type {Map<import('@gltf-transform/core').Node, {
* rotation?: number[],
* scale?: number[],
* translation?: number[],
* }>}
*/
const map = new Map();
for (let i = 0, length = animations.length; i < length; i++) {
const animation = animations[i];
const channels = animation.listChannels();
for (let j = 0, cLength = channels.length; j < cLength; j++) {
const channel = channels[j];
const sampler = channel.getSampler();
// no sampler, remove it
if (!sampler) {
channel.dispose();
continue;
}
const input = sampler.getInput();
let inputCount;
// empty sampler and related channel should be removed
if (!input || !(inputCount = input.getCount())) {
sampler.dispose();
channel.dispose();
continue;
}
const output = sampler.getOutput();
// empty sampler and related channel should be removed
if (!output || !output.getCount()) {
sampler.dispose();
channel.dispose();
continue;
}
const targetNode = channel.getTargetNode();
if (!targetNode) {
cleanup(channel, sampler, animation, root);
continue;
}
let checkDiff = inputCount === 1;
const tmp1 = [];
const targetPath = channel.getTargetPath();
// for animation samplers full of duplicated frames, run `resample` first
if (inputCount === 2) {
const tmp2 = [];
if (targetPath === 'weights') {
const weights = targetNode.getWeights();
if (output.getCount() >= weights.length * 2) {
const array = output.getArray();
let offset = 0;
for (let k = 0; k < weights.length; k++) {
tmp1[k] = array[offset++];
}
for (let k = 0; k < weights.length; k++) {
tmp2[k] = array[offset++];
}
} else {
// maybe broken weights
continue;
}
} else {
output.getElement(0, tmp1);
output.getElement(1, tmp2);
}
checkDiff = MathUtils.eq(tmp1, tmp2, options.tolerance);
}
// no need to check, just keep it
if (!checkDiff) {
continue;
}
if (inputCount === 1) {
output.getElement(0, tmp1);
}
let isDiff = true;
switch (targetPath) {
case "rotation":
isDiff = MathUtils.eq(
tmp1, targetNode.getRotation(), options.tolerance);
break;
case "scale":
isDiff = MathUtils.eq(
tmp1, targetNode.getScale(), options.tolerance);
break;
case "translation":
isDiff = MathUtils.eq(
tmp1, targetNode.getTranslation(), options.tolerance);
break;
case "weights":
isDiff = MathUtils.eq(
tmp1, targetNode.getWeights(), options.tolerance);
break;
default:
// not supported, maybe KHR_animation_pointer
continue;
}
if (!isDiff) {
cleanup(channel, sampler, animation, root);
continue;
}
if (options.diff === 'keep') {
continue;
}
if (options.diff === 'remove') {
cleanup(channel, sampler, animation, root);
continue;
}
if (targetNode.getSkin()) {
if (targetPath === "rotation" ||
targetPath === "scale" ||
targetPath === "translation") {
let result = map.get(targetNode);
if (result) {
result[targetPath] = tmp1;
} else {
map.set(targetNode, {
[targetPath]: tmp1,
});
}
cleanup(channel, sampler, animation, root);
}
continue;
}
// options.diff === 'bake'
switch (targetPath) {
case "rotation":
targetNode.setRotation(tmp1);
break;
case "scale":
targetNode.setScale(tmp1);
break;
case "translation":
targetNode.setTranslation(tmp1);
break;
case "weights":
if (targetNode.getWeights().length === tmp1.length) {
targetNode.setWeights(tmp1);
}
break;
default:
// not supported
continue;
}
cleanup(channel, sampler, animation, root);
}
}
const cleanup = new Set();
map.forEach((value, node) => {
const transform = [];
MathUtils.compose(
node.getTranslation(),
node.getRotation(),
node.getScale(),
transform,
);
invert(transform, transform);
const transformNew = [];
MathUtils.compose(
value.translation || node.getTranslation(),
value.rotation || node.getRotation(),
value.scale || node.getScale(),
transformNew,
);
multiplyMat4(transform, transform, transformNew);
if (value.translation) {
node.setTranslation(value.translation);
}
if (value.rotation) {
node.setTranslation(value.rotation);
}
if (value.scale) {
node.setTranslation(value.scale);
}
const skin = node.getSkin();
node.setSkin(transformSkin(skin, transform));
cleanup.add(skin)
});
cleanup.forEach(skin => {
if (skin.listParents().filter(p => p !== root).length) {
skin.dispose();
}
});
});
}
function transformSkin(skin, transformMatrix) {
skin = skin.clone(); // quantize() does cleanup.
const inverseBindMatrices = skin.getInverseBindMatrices().clone();
const ibm = [];
for (let i = 0, count = inverseBindMatrices.getCount(); i < count; i++) {
inverseBindMatrices.getElement(i, ibm);
multiplyMat4(ibm, ibm, transformMatrix);
inverseBindMatrices.setElement(i, ibm);
}
return skin.setInverseBindMatrices(inverseBindMatrices);
} |
Beta Was this translation helpful? Give feedback.
-
Animation channels whose sampler has no frame would be removed.
Animation channels whose sampler has one frame or 2 identical frameswould be removed if frame values are the identical to targetNode.
Animation channels whose sampler has one frame or 2 identical frameswhere the frame values differ from targetNode would be configured to one of:
keep
to keep animation channels, samplers and targetNode unmodifiedremove
to remove animation channels and samplersbake
to set frame value to targetNode and remove animation channels and samplersFor best effort,
resample
should be applied before this.Maybe related: #501
TODO: bake it to skin if any.
Edit: animation could also be the parent node of a sampler.
The code
Beta Was this translation helpful? Give feedback.
All reactions