forked from phenom/healer-man
84 lines
3.8 KiB
TypeScript
84 lines
3.8 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { Bone, BufferGeometry, Float32BufferAttribute, Group, Matrix4, MeshBasicMaterial, Skeleton, SkinnedMesh, Vector3 } from "three";
|
|
import { clone } from "three/examples/jsm/utils/SkeletonUtils.js";
|
|
import { shareClonedSkeletons } from "./sharedSkeletons";
|
|
|
|
function skinnedMesh(bone: Bone, inverse = new Matrix4()): SkinnedMesh {
|
|
const geometry = new BufferGeometry();
|
|
geometry.setAttribute("position", new Float32BufferAttribute([1, 0, 0], 3));
|
|
geometry.setAttribute("skinIndex", new Float32BufferAttribute([0, 0, 0, 0], 4));
|
|
geometry.setAttribute("skinWeight", new Float32BufferAttribute([1, 0, 0, 0], 4));
|
|
const mesh = new SkinnedMesh(geometry, new MeshBasicMaterial());
|
|
mesh.bind(new Skeleton([bone], [inverse]), new Matrix4());
|
|
return mesh;
|
|
}
|
|
|
|
function character(): Group {
|
|
const root = new Group();
|
|
const bone = new Bone();
|
|
bone.name = "spine";
|
|
root.add(bone, skinnedMesh(bone), skinnedMesh(bone));
|
|
return root;
|
|
}
|
|
|
|
describe("shared skeleton palettes in character clones", () => {
|
|
it("preserves posed vertices and each mesh's bind transform while sharing uploads", () => {
|
|
const root = character();
|
|
const first = root.children[1] as SkinnedMesh;
|
|
const second = root.children[2] as SkinnedMesh;
|
|
second.bindMatrix.makeTranslation(0, 2, 0);
|
|
root.children[0].rotation.z = 0.5;
|
|
root.updateMatrixWorld(true);
|
|
const poseBefore = [first, second].map((mesh) => mesh.applyBoneTransform(0, new Vector3(1, 0, 0)));
|
|
const bindBefore = second.bindMatrix.clone();
|
|
expect(shareClonedSkeletons(root)).toBe(1);
|
|
expect(first.skeleton).toBe(second.skeleton);
|
|
expect(second.bindMatrix.equals(bindBefore)).toBe(true);
|
|
[first, second].forEach((mesh, index) => {
|
|
expect(mesh.applyBoneTransform(0, new Vector3(1, 0, 0)).distanceTo(poseBefore[index])).toBeLessThan(1e-9);
|
|
});
|
|
});
|
|
|
|
it("keeps separate cloned characters and the loader source independently animated", () => {
|
|
const source = character();
|
|
const first = clone(source);
|
|
const second = clone(source);
|
|
shareClonedSkeletons(first);
|
|
shareClonedSkeletons(second);
|
|
const firstMesh = first.children[1] as SkinnedMesh;
|
|
const secondMesh = second.children[1] as SkinnedMesh;
|
|
expect(firstMesh.skeleton).not.toBe(secondMesh.skeleton);
|
|
expect(firstMesh.skeleton.bones[0]).not.toBe(secondMesh.skeleton.bones[0]);
|
|
first.children[0].position.y = 3;
|
|
first.updateMatrixWorld(true);
|
|
second.updateMatrixWorld(true);
|
|
expect(firstMesh.applyBoneTransform(0, new Vector3(1, 0, 0)).y).toBe(3);
|
|
expect(secondMesh.applyBoneTransform(0, new Vector3(1, 0, 0)).y).toBe(0);
|
|
expect(source.children[0].position.y).toBe(0);
|
|
});
|
|
|
|
it("retains different inverse bind matrices and different bone objects", () => {
|
|
const root = character();
|
|
const otherBone = new Bone();
|
|
otherBone.name = "spine";
|
|
root.add(skinnedMesh(root.children[0] as Bone, new Matrix4().makeTranslation(1, 0, 0)));
|
|
root.add(otherBone, skinnedMesh(otherBone));
|
|
expect(shareClonedSkeletons(root)).toBe(1);
|
|
const meshes = root.children.filter((node) => (node as SkinnedMesh).isSkinnedMesh) as SkinnedMesh[];
|
|
expect(new Set(meshes.map((mesh) => mesh.skeleton)).size).toBe(3);
|
|
});
|
|
|
|
it("disposes redundant bone textures once and tolerates already shared skeletons", () => {
|
|
const root = character();
|
|
const first = root.children[1] as SkinnedMesh;
|
|
const second = root.children[2] as SkinnedMesh;
|
|
second.skeleton.computeBoneTexture();
|
|
const dispose = vi.spyOn(second.skeleton.boneTexture!, "dispose");
|
|
expect(shareClonedSkeletons(root)).toBe(1);
|
|
expect(dispose).toHaveBeenCalledTimes(1);
|
|
expect(shareClonedSkeletons(root)).toBe(0);
|
|
expect(first.skeleton).toBe(second.skeleton);
|
|
expect(dispose).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|