Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/controllers/actors/weaponSprites.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,39 @@ describe('a held weapon\'s sprites (RenderLinkObject, ZzzCharacter.cpp:7027-8386
const sprites = new WeaponSprites(scene, object, [{ texture: 'impact03', bone: 0, scale: 0.75, light: [1, 1, 1] }]);
expect(sprites.nodes[0].parent).toBe(root);
});

it('ride bone 0 where a one-bone model keeps it, on its mesh node (the Divine Staff\'s row of glints)', () => {
const root = new Mesh('staff', scene);
const model = new TransformNode('model_staff', scene);
model.parent = root;
const shaft = new Mesh('shaft', scene);
shaft.parent = model;
shaft.position.set(0, -0.525, 0.409);
shaft.rotation.set(0, -Math.PI / 2, -Math.PI / 2);
const object = new ModelObject(scene);
object.load({ mesh: root, skeleton: null as unknown as Skeleton, animationGroups: [] as AnimationGroup[] });
const sprites = new WeaponSprites(scene, object, [
{ texture: 'flare01', bone: 0, offset: [-180, -40, 0], scale: 2, light: [1, 0.3, 0.1] },
{ texture: 'flare01', bone: 0, offset: [90, -40, 0], scale: 2, light: [1, 0.3, 0.1] },
]);
root.computeWorldMatrix(true);
model.computeWorldMatrix(true);
shaft.computeWorldMatrix(true);
const ends = sprites.nodes.map(node => {
node.computeWorldMatrix(true);
if (!node.parent) throw new Error('a sprite node has no parent');
return Vector3.TransformCoordinates(node.position, node.parent.getWorldMatrix());
});
// The two glints sit on the shaft's own x axis, 40 cm off it, as BoneTransform[0] puts them.
const along = Vector3.TransformNormal(new Vector3(2.7, 0, 0), shaft.getWorldMatrix());
const step = ends[1].subtract(ends[0]);
expect(step.x).toBeCloseTo(along.x);
expect(step.y).toBeCloseTo(along.y);
expect(step.z).toBeCloseTo(along.z);
expect(sprites.nodes[0].parent).not.toBe(root);
sprites.dispose();
expect(scene.transformNodes.some(n => n.name === 'weaponBone0')).toBe(false);
});
});

describe('a held sprite gated on its wearer', () => {
Expand Down
34 changes: 33 additions & 1 deletion src/controllers/actors/weaponSprites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,13 @@ export class WeaponSprites {
const gltf = model.gltf;
this.#scene = scene;
this.#meshes = gltf ? gltf.mesh.getChildMeshes(false) : [];
const boneZero = gltf && !gltf.skeleton ? this.#bakedBoneZero(scene, this.#meshes[0]) : null;
const at = (bone: number, name = 'weaponSprite') => {
const node = new TransformNode(name, scene);
node.parent = (bone >= 0 ? gltf?.skeleton?.bones[bone + 1]?.getTransformNode() : null) ?? gltf?.mesh ?? null;
node.parent =
(bone >= 0 ? (gltf?.skeleton?.bones[bone + 1]?.getTransformNode() ?? (bone === 0 ? boneZero : null)) : null) ??
gltf?.mesh ??
null;
return node;
};
this.nodes = specs.map(spec => {
Expand Down Expand Up @@ -110,9 +114,37 @@ export class WeaponSprites {
return false;
}

/** The bone-0 frame of a model with no skeleton, when one was needed (disposed with the sprites). */
#boneZero: TransformNode | null = null;

/**
* A one-bone model comes without a skeleton: its bone's rest transform sits on the mesh nodes instead
* (the Divine Staff of Archangel's is turned -90/-90 and moved off the root). `BoneTransform[0]` is that
* frame, not the root, or the staff's row of glints lies across the shaft and through the wearer.
* Null when the mesh sits on the root as it is.
*/
#bakedBoneZero(scene: Scene, mesh: AbstractMesh | undefined): TransformNode | null {
if (!mesh) return null;
const turned = mesh.rotationQuaternion
? Math.abs(mesh.rotationQuaternion.w) < 1 - 1e-6
: mesh.rotation.lengthSquared() > 1e-12;
const moved = mesh.position.lengthSquared() > 1e-12;
const scaled = Math.abs(mesh.scaling.x - 1) + Math.abs(mesh.scaling.y - 1) + Math.abs(mesh.scaling.z - 1) > 1e-6;
if (!turned && !moved && !scaled) return null;
const frame = new TransformNode('weaponBone0', scene);
frame.parent = mesh.parent;
frame.position.copyFrom(mesh.position);
if (mesh.rotationQuaternion) frame.rotationQuaternion = mesh.rotationQuaternion.clone();
else frame.rotation.copyFrom(mesh.rotation);
frame.scaling.copyFrom(mesh.scaling);
this.#boneZero = frame;
return frame;
}

dispose(): void {
this.#sprites.dispose();
for (const node of this.nodes) node.dispose();
for (const node of this.#emitAt) node.dispose();
this.#boneZero?.dispose();
}
}
Loading