Phase 1.4 : tests unitaires bridge GLB→SkinnedMesh

8 tests GREEN :

GlbJointAliasTableTest (5) :
- mapLegacyPlayerAnimatorNames : body→Chest, leftUpperArm→Arm_L,
  leftLowerArm→Elbow_L, leftUpperLeg→Thigh_L, leftLowerLeg→Knee_L, etc.
- isCaseInsensitive : BODY/LeftUpperArm/leftupperarm tous remappés
- bypassBipedNames : Arm_L, Elbow_R, Head, Chest, Torso, Root non transformés
- unknownReturnsNull : null pour nom inconnu / vide / null
- isBipedJointNameDetection : _R/_L suffix + Root/Torso/Chest/Head

GltfToSkinnedMeshTest (3) :
- convertSyntheticGltfDoesNotThrow : 3 vertices + armature biped minimale (4
  joints manuels Root→Chest→{Arm_L,Arm_R}) → SkinnedMesh non null
- convertSyntheticGltfHasExpectedParts : partName dérivé du materialName de la
  primitive glTF
- convertThrowsOnNullArmature : IllegalStateException si armature null

Fixture : buildMinimalArmature() construit une hiérarchie 4 joints via Joint()
+ addSubJoints() + Armature(name, count, root, jointMap).bakeOriginMatrices().
buildSyntheticGltf() produit un triangle 3-vertices avec jointNames
(body, leftUpperArm, rightUpperArm) pour tester le mapping PlayerAnimator→EF.
This commit is contained in:
notevil
2026-04-22 19:08:05 +02:00
parent 94fcece05a
commit 29c4fddb90
2 changed files with 234 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
/*
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
*/
package com.tiedup.remake.rig.bridge;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class GlbJointAliasTableTest {
@Test
void mapLegacyPlayerAnimatorNames() {
assertEquals("Chest", GlbJointAliasTable.mapGltfJointName("body"));
assertEquals("Chest", GlbJointAliasTable.mapGltfJointName("torso"));
assertEquals("Head", GlbJointAliasTable.mapGltfJointName("head"));
assertEquals("Arm_L", GlbJointAliasTable.mapGltfJointName("leftUpperArm"));
assertEquals("Arm_L", GlbJointAliasTable.mapGltfJointName("leftArm"));
assertEquals("Elbow_L", GlbJointAliasTable.mapGltfJointName("leftLowerArm"));
assertEquals("Elbow_L", GlbJointAliasTable.mapGltfJointName("leftForearm"));
assertEquals("Arm_R", GlbJointAliasTable.mapGltfJointName("rightUpperArm"));
assertEquals("Elbow_R", GlbJointAliasTable.mapGltfJointName("rightLowerArm"));
assertEquals("Thigh_L", GlbJointAliasTable.mapGltfJointName("leftUpperLeg"));
assertEquals("Knee_L", GlbJointAliasTable.mapGltfJointName("leftLowerLeg"));
assertEquals("Thigh_R", GlbJointAliasTable.mapGltfJointName("rightUpperLeg"));
assertEquals("Knee_R", GlbJointAliasTable.mapGltfJointName("rightLowerLeg"));
}
@Test
void isCaseInsensitive() {
assertEquals("Chest", GlbJointAliasTable.mapGltfJointName("BODY"));
assertEquals("Arm_L", GlbJointAliasTable.mapGltfJointName("LeftUpperArm"));
assertEquals("Arm_L", GlbJointAliasTable.mapGltfJointName("leftupperarm"));
}
@Test
void bypassBipedNames() {
assertEquals("Arm_L", GlbJointAliasTable.mapGltfJointName("Arm_L"));
assertEquals("Elbow_R", GlbJointAliasTable.mapGltfJointName("Elbow_R"));
assertEquals("Head", GlbJointAliasTable.mapGltfJointName("Head"));
assertEquals("Chest", GlbJointAliasTable.mapGltfJointName("Chest"));
assertEquals("Torso", GlbJointAliasTable.mapGltfJointName("Torso"));
assertEquals("Root", GlbJointAliasTable.mapGltfJointName("Root"));
}
@Test
void unknownReturnsNull() {
assertNull(GlbJointAliasTable.mapGltfJointName("UnknownJoint"));
assertNull(GlbJointAliasTable.mapGltfJointName(""));
assertNull(GlbJointAliasTable.mapGltfJointName(null));
}
@Test
void isBipedJointNameDetection() {
assertTrue(GlbJointAliasTable.isBipedJointName("Arm_L"));
assertTrue(GlbJointAliasTable.isBipedJointName("Thigh_R"));
assertTrue(GlbJointAliasTable.isBipedJointName("Root"));
assertTrue(GlbJointAliasTable.isBipedJointName("Torso"));
assertTrue(GlbJointAliasTable.isBipedJointName("Head"));
// False cases
assertEquals(false, GlbJointAliasTable.isBipedJointName("leftUpperArm"));
assertEquals(false, GlbJointAliasTable.isBipedJointName("body"));
}
}

View File

@@ -0,0 +1,167 @@
/*
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
*/
package com.tiedup.remake.rig.bridge;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.junit.jupiter.api.Test;
import net.minecraft.resources.ResourceLocation;
import com.tiedup.remake.client.gltf.GltfData;
import com.tiedup.remake.client.gltf.GltfData.Primitive;
import com.tiedup.remake.rig.armature.Armature;
import com.tiedup.remake.rig.armature.Joint;
import com.tiedup.remake.rig.asset.AssetAccessor;
import com.tiedup.remake.rig.math.OpenMatrix4f;
import com.tiedup.remake.rig.mesh.SkinnedMesh;
class GltfToSkinnedMeshTest {
// ---------- Test fixtures ----------
/**
* Armature biped minimale pour test : Root → Chest → {Arm_L, Arm_R}.
* 4 joints avec IDs 0..3. Suffisant pour valider le mapping des noms legacy
* sans dépendre d'un resource reload.
*/
private static AssetAccessor<Armature> buildMinimalArmature() {
Joint root = new Joint("Root", 0, new OpenMatrix4f());
Joint chest = new Joint("Chest", 1, new OpenMatrix4f());
Joint armL = new Joint("Arm_L", 2, new OpenMatrix4f());
Joint armR = new Joint("Arm_R", 3, new OpenMatrix4f());
root.addSubJoints(chest);
chest.addSubJoints(armL, armR);
Map<String, Joint> jointMap = new LinkedHashMap<>();
jointMap.put("Root", root);
jointMap.put("Chest", chest);
jointMap.put("Arm_L", armL);
jointMap.put("Arm_R", armR);
Armature arm = new Armature("test_biped", 4, root, jointMap);
arm.bakeOriginMatrices();
return new AssetAccessor<>() {
@Override public Armature get() { return arm; }
@Override public ResourceLocation registryName() {
return ResourceLocation.fromNamespaceAndPath("test", "armature/test_biped");
}
@Override public boolean inRegistry() { return false; }
};
}
/**
* GltfData synthétique : 3 vertices (triangle), 3 joints glTF
* (body, leftUpperArm, rightUpperArm). Chaque vertex attaché à un joint
* différent avec poids 1.0 sur le joint principal + 0 sur les 3 autres.
* Permet de vérifier le mapping glTF→biped via le jointIdMap.
*/
private static GltfData buildSyntheticGltf() {
int vertexCount = 3;
float[] positions = {
0.0F, 0.0F, 0.0F, // v0 - attached to body/Chest
1.0F, 0.0F, 0.0F, // v1 - attached to leftUpperArm/Arm_L
-1.0F, 0.0F, 0.0F // v2 - attached to rightUpperArm/Arm_R
};
float[] normals = {
0.0F, 1.0F, 0.0F,
0.0F, 1.0F, 0.0F,
0.0F, 1.0F, 0.0F
};
float[] texCoords = {
0.5F, 0.5F,
1.0F, 0.5F,
0.0F, 0.5F
};
int[] indices = { 0, 1, 2 };
// 4 joints/weights par vertex. Le 4e est toujours 0 → sera drop.
int[] joints = {
0, 1, 2, 0, // v0
1, 0, 2, 0, // v1
2, 0, 1, 0 // v2
};
float[] weights = {
1.0F, 0.0F, 0.0F, 0.0F, // v0 : 100% body
1.0F, 0.0F, 0.0F, 0.0F, // v1 : 100% leftUpperArm
1.0F, 0.0F, 0.0F, 0.0F // v2 : 100% rightUpperArm
};
String[] jointNames = { "body", "leftUpperArm", "rightUpperArm" };
int[] parentJoints = { -1, 0, 0 };
Matrix4f[] invBind = {
new Matrix4f().identity(),
new Matrix4f().identity(),
new Matrix4f().identity()
};
Quaternionf[] restRot = {
new Quaternionf(),
new Quaternionf(),
new Quaternionf()
};
Vector3f[] restTrans = {
new Vector3f(),
new Vector3f(),
new Vector3f()
};
return new GltfData(
positions, normals, texCoords, indices,
joints, weights,
jointNames, parentJoints, invBind, restRot, restTrans,
restRot, // rawGltfRestRotations
null, // rawGltfAnimation
null, // animation
new LinkedHashMap<>(), // namedAnimations
new LinkedHashMap<>(), // rawNamedAnimations
List.of(new Primitive(indices, "material_0", false, null)),
vertexCount, jointNames.length
);
}
// ---------- Tests ----------
@Test
void convertSyntheticGltfDoesNotThrow() {
GltfData data = buildSyntheticGltf();
AssetAccessor<Armature> armature = buildMinimalArmature();
SkinnedMesh mesh = assertDoesNotThrow(() -> GltfToSkinnedMesh.convert(data, armature));
assertNotNull(mesh);
}
@Test
void convertSyntheticGltfHasExpectedParts() {
GltfData data = buildSyntheticGltf();
AssetAccessor<Armature> armature = buildMinimalArmature();
SkinnedMesh mesh = GltfToSkinnedMesh.convert(data, armature);
// Une seule primitive → une seule part (partName = "material_0")
assertNotNull(mesh);
assertEquals(1, mesh.getAllParts().size(), "Expected one part per primitive");
assertTrue(mesh.hasPart("material_0"), "Part name should match material name");
}
@Test
void convertThrowsOnNullArmature() {
GltfData data = buildSyntheticGltf();
assertThrows(IllegalStateException.class, () -> GltfToSkinnedMesh.convert(data, null));
}
}