Phase 1.1-1.3 : bridge GLB → SkinnedMesh
Premier jalon Phase 1 : conversion d'un GltfData (format legacy 11-joints PlayerAnimator) vers SkinnedMesh Epic Fight (biped ~20 joints). Files : - rig/bridge/GlbJointAliasTable.java : table mapping statique PlayerAnimator → biped EF (body/torso→Chest, leftUpperArm→Arm_L, leftLowerArm→Elbow_L, etc). Fallback Root pour inconnus. Bypass si nom déjà biped (Root/Torso/Chest/Head ou suffixe _R/_L). - rig/bridge/GltfToSkinnedMesh.java : convert(GltfData, AssetAccessor<Armature>) → SkinnedMesh. Pré-calcule jointIdMap, boucle vertices (pos/normal/uv + drop 4th joint à plus faible poids + renormalise 3 restants), groupe indices par primitive (material) en VanillaMeshPartDefinition. Note : animations GLB ignorées (scope Phase 4 JSON EF authored). Compile BUILD SUCCESSFUL maintenu.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||
*/
|
||||
|
||||
package com.tiedup.remake.rig.bridge;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
/**
|
||||
* Table d'alias runtime pour mapper les noms de joints des GLB legacy TiedUp
|
||||
* (riggés via PlayerAnimator / bendy-lib avec noms type {@code leftUpperArm})
|
||||
* vers le skeleton biped Epic Fight utilisé par RIG ({@code Arm_L}, etc.).
|
||||
*
|
||||
* <p>Voir {@code docs/plans/rig/ARCHITECTURE.md §6.3} pour la source de vérité
|
||||
* du mapping. Tout joint inconnu après lookup doit être loggé WARN par le
|
||||
* caller et fallback sur {@code Root}.</p>
|
||||
*
|
||||
* <p><b>Cas spécial "body/torso"</b> — le GLB legacy a souvent un unique joint
|
||||
* couvrant l'ensemble du torse. On le mappe sur {@code Chest} par défaut
|
||||
* (meilleur fit pour les items bondage majoritairement attachés au haut du
|
||||
* corps : harnais, menottes de poitrine, collier). Si un item a besoin
|
||||
* d'attachement à {@code Torso} (ceinture), le modeler devra renommer son
|
||||
* joint en {@code waist} explicitement.</p>
|
||||
*/
|
||||
public final class GlbJointAliasTable {
|
||||
|
||||
/**
|
||||
* Mapping direct PlayerAnimator → biped EF. Les clés sont la forme
|
||||
* lowercase EXACTE des noms exportés par les GLB legacy.
|
||||
*/
|
||||
private static final Map<String, String> ALIAS = ImmutableMap.<String, String>builder()
|
||||
// Torso region
|
||||
.put("body", "Chest")
|
||||
.put("torso", "Chest")
|
||||
.put("chest", "Chest")
|
||||
.put("waist", "Torso")
|
||||
.put("hip", "Torso")
|
||||
|
||||
// Head
|
||||
.put("head", "Head")
|
||||
|
||||
// Arms left
|
||||
.put("leftshoulder", "Shoulder_L")
|
||||
.put("leftupperarm", "Arm_L")
|
||||
.put("leftarm", "Arm_L")
|
||||
.put("leftlowerarm", "Elbow_L")
|
||||
.put("leftforearm", "Elbow_L")
|
||||
.put("leftelbow", "Elbow_L")
|
||||
.put("lefthand", "Hand_L")
|
||||
|
||||
// Arms right
|
||||
.put("rightshoulder", "Shoulder_R")
|
||||
.put("rightupperarm", "Arm_R")
|
||||
.put("rightarm", "Arm_R")
|
||||
.put("rightlowerarm", "Elbow_R")
|
||||
.put("rightforearm", "Elbow_R")
|
||||
.put("rightelbow", "Elbow_R")
|
||||
.put("righthand", "Hand_R")
|
||||
|
||||
// Legs left
|
||||
.put("leftupperleg", "Thigh_L")
|
||||
.put("leftleg", "Thigh_L")
|
||||
.put("leftlowerleg", "Knee_L")
|
||||
.put("leftknee", "Knee_L")
|
||||
.put("leftfoot", "Leg_L")
|
||||
|
||||
// Legs right
|
||||
.put("rightupperleg", "Thigh_R")
|
||||
.put("rightleg", "Thigh_R")
|
||||
.put("rightlowerleg", "Knee_R")
|
||||
.put("rightknee", "Knee_R")
|
||||
.put("rightfoot", "Leg_R")
|
||||
|
||||
// Root fallback (déjà nommé Root dans GLB modernes)
|
||||
.put("root", "Root")
|
||||
.put("armature", "Root")
|
||||
.build();
|
||||
|
||||
private GlbJointAliasTable() {}
|
||||
|
||||
/**
|
||||
* Traduit un nom de joint GLB legacy vers le nom biped EF équivalent.
|
||||
* Case-insensitive. Les noms déjà au format biped EF (ex: {@code Arm_L}) sont
|
||||
* retournés tels quels après vérification.
|
||||
*
|
||||
* @param gltfJointName nom tel qu'exporté dans le GLB (jointNames[])
|
||||
* @return nom biped EF (ex: {@code Arm_L}), ou null si inconnu
|
||||
*/
|
||||
@Nullable
|
||||
public static String mapGltfJointName(String gltfJointName) {
|
||||
if (gltfJointName == null || gltfJointName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Direct hit sur le biped EF (GLB moderne déjà bien rigged).
|
||||
if (isBipedJointName(gltfJointName)) {
|
||||
return gltfJointName;
|
||||
}
|
||||
|
||||
return ALIAS.get(gltfJointName.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un nom est déjà au format biped EF. Utilisé pour court-circuiter
|
||||
* l'alias lookup sur les GLB modernes.
|
||||
*/
|
||||
public static boolean isBipedJointName(String name) {
|
||||
// Heuristique : les noms biped EF sont en PascalCase avec suffixe _R/_L,
|
||||
// ou parmi {Root, Torso, Chest, Head}.
|
||||
return switch (name) {
|
||||
case "Root", "Torso", "Chest", "Head" -> true;
|
||||
default -> name.endsWith("_R") || name.endsWith("_L");
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||
*/
|
||||
|
||||
package com.tiedup.remake.rig.bridge;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.ints.IntList;
|
||||
|
||||
import com.tiedup.remake.client.gltf.GltfData;
|
||||
import com.tiedup.remake.client.gltf.GltfData.Primitive;
|
||||
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||
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.Vec2f;
|
||||
import com.tiedup.remake.rig.math.Vec3f;
|
||||
import com.tiedup.remake.rig.mesh.MeshPartDefinition;
|
||||
import com.tiedup.remake.rig.mesh.SingleGroupVertexBuilder;
|
||||
import com.tiedup.remake.rig.mesh.SkinnedMesh;
|
||||
import com.tiedup.remake.rig.mesh.transformer.VanillaModelTransformer.VanillaMeshPartDefinition;
|
||||
|
||||
/**
|
||||
* Pont Phase 1 : convertit un {@link GltfData} (format GLB legacy TiedUp
|
||||
* riggé 11-joints PlayerAnimator) en {@link SkinnedMesh} Epic Fight
|
||||
* (biped ~20 joints).
|
||||
*
|
||||
* <p>Algorithme (voir {@code docs/plans/rig/MIGRATION.md §1.2.1}) :</p>
|
||||
* <ol>
|
||||
* <li>Pré-calculer le mapping {@code gltfJointIdx → bipedJointId} via
|
||||
* {@link GlbJointAliasTable} + {@link Armature#searchJointByName}.</li>
|
||||
* <li>Pour chaque vertex :
|
||||
* <ul>
|
||||
* <li>Position / normal / UV depuis {@link GltfData}</li>
|
||||
* <li>Retenir les 3 joints de plus fort poids parmi les 4 glTF</li>
|
||||
* <li>Renormaliser les poids retenus pour sommer à 1.0</li>
|
||||
* <li>Construire le {@link SingleGroupVertexBuilder}</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>Grouper les indices par {@link Primitive} en autant de
|
||||
* {@link MeshPartDefinition}.</li>
|
||||
* <li>{@link SingleGroupVertexBuilder#loadVertexInformation(List, Map)}
|
||||
* construit le {@link SkinnedMesh}.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Les animations éventuellement embarquées dans le GLB sont <b>ignorées</b> —
|
||||
* les animations passent par JSON EF natif via {@code JsonAssetLoader}.</p>
|
||||
*/
|
||||
public final class GltfToSkinnedMesh {
|
||||
|
||||
private static final float WEIGHT_EPSILON = 1.0e-4F;
|
||||
|
||||
private GltfToSkinnedMesh() {}
|
||||
|
||||
/**
|
||||
* Convertit un GLB parsé en {@link SkinnedMesh} utilisable par le pipeline
|
||||
* de rendu RIG.
|
||||
*
|
||||
* @param data données GLB parsées par {@code GlbParser.parse(...)}
|
||||
* @param armature armature biped EF cible (doit être déjà chargée)
|
||||
* @return SkinnedMesh prêt à être rendu
|
||||
* @throws IllegalStateException si {@code armature} est null
|
||||
*/
|
||||
public static SkinnedMesh convert(GltfData data, AssetAccessor<? extends Armature> armature) {
|
||||
if (armature == null || armature.get() == null) {
|
||||
throw new IllegalStateException(
|
||||
"Armature not loaded — GltfToSkinnedMesh.convert() called before resource reload completed"
|
||||
);
|
||||
}
|
||||
|
||||
Armature arm = armature.get();
|
||||
int[] jointIdMap = buildJointIdMap(data.jointNames(), arm);
|
||||
|
||||
int vertexCount = data.vertexCount();
|
||||
float[] positions = data.positions();
|
||||
float[] normals = data.normals();
|
||||
float[] texCoords = data.texCoords();
|
||||
int[] joints = data.joints();
|
||||
float[] weights = data.weights();
|
||||
|
||||
List<SingleGroupVertexBuilder> vertices = new ArrayList<>(vertexCount);
|
||||
for (int i = 0; i < vertexCount; i++) {
|
||||
vertices.add(buildVertex(i, positions, normals, texCoords, joints, weights, jointIdMap));
|
||||
}
|
||||
|
||||
Map<MeshPartDefinition, IntList> partIndices = buildPartIndices(data.primitives());
|
||||
|
||||
return SingleGroupVertexBuilder.loadVertexInformation(vertices, partIndices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit le mapping {@code gltfJointIdx → bipedJointId} une seule fois
|
||||
* avant la boucle vertex. Les noms inconnus retombent sur la racine
|
||||
* {@code Root} (id 0) avec un log WARN.
|
||||
*/
|
||||
private static int[] buildJointIdMap(String[] gltfJointNames, Armature arm) {
|
||||
int[] map = new int[gltfJointNames.length];
|
||||
int unknownCount = 0;
|
||||
int aliasedCount = 0;
|
||||
int rootId = arm.rootJoint != null ? arm.rootJoint.getId() : 0;
|
||||
|
||||
for (int i = 0; i < gltfJointNames.length; i++) {
|
||||
String gltfName = gltfJointNames[i];
|
||||
String bipedName = GlbJointAliasTable.mapGltfJointName(gltfName);
|
||||
|
||||
if (bipedName == null) {
|
||||
TiedUpRigConstants.LOGGER.warn(
|
||||
"GltfToSkinnedMesh: unknown joint '{}' — fallback to Root",
|
||||
gltfName
|
||||
);
|
||||
map[i] = rootId;
|
||||
unknownCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
Joint joint = arm.searchJointByName(bipedName);
|
||||
if (joint == null) {
|
||||
TiedUpRigConstants.LOGGER.warn(
|
||||
"GltfToSkinnedMesh: biped joint '{}' (aliased from '{}') not found in armature — fallback to Root",
|
||||
bipedName, gltfName
|
||||
);
|
||||
map[i] = rootId;
|
||||
unknownCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
map[i] = joint.getId();
|
||||
if (!gltfName.equals(bipedName)) {
|
||||
aliasedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
TiedUpRigConstants.LOGGER.info(
|
||||
"GltfToSkinnedMesh: {} joints mapped ({} via alias, {} unknown→Root)",
|
||||
gltfJointNames.length, aliasedCount, unknownCount
|
||||
);
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit un vertex individuel : position/normal/UV depuis les arrays
|
||||
* flattened, puis sélection des 3 plus forts poids (drop du 4e) + renormalisation.
|
||||
*/
|
||||
private static SingleGroupVertexBuilder buildVertex(
|
||||
int i,
|
||||
float[] positions, float[] normals, float[] texCoords,
|
||||
int[] joints, float[] weights,
|
||||
int[] jointIdMap) {
|
||||
|
||||
SingleGroupVertexBuilder vb = new SingleGroupVertexBuilder();
|
||||
vb.setPosition(new Vec3f(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]));
|
||||
vb.setNormal(new Vec3f(normals[i * 3], normals[i * 3 + 1], normals[i * 3 + 2]));
|
||||
vb.setTextureCoordinate(new Vec2f(texCoords[i * 2], texCoords[i * 2 + 1]));
|
||||
|
||||
// Récupère les 4 joints/poids glTF, sélectionne les 3 plus forts.
|
||||
int[] rawJoints = new int[4];
|
||||
float[] rawWeights = new float[4];
|
||||
for (int k = 0; k < 4; k++) {
|
||||
rawJoints[k] = joints[i * 4 + k];
|
||||
rawWeights[k] = weights[i * 4 + k];
|
||||
}
|
||||
|
||||
// Trouve l'index du plus faible poids (à drop).
|
||||
int minIdx = 0;
|
||||
for (int k = 1; k < 4; k++) {
|
||||
if (rawWeights[k] < rawWeights[minIdx]) {
|
||||
minIdx = k;
|
||||
}
|
||||
}
|
||||
|
||||
// Build les 3 retenus + compte effective.
|
||||
float w0 = 0, w1 = 0, w2 = 0;
|
||||
int id0 = 0, id1 = 0, id2 = 0;
|
||||
int effectiveCount = 0;
|
||||
int slot = 0;
|
||||
for (int k = 0; k < 4; k++) {
|
||||
if (k == minIdx) continue;
|
||||
float w = rawWeights[k];
|
||||
int id = jointIdMap[rawJoints[k]];
|
||||
switch (slot) {
|
||||
case 0 -> { w0 = w; id0 = id; }
|
||||
case 1 -> { w1 = w; id1 = id; }
|
||||
case 2 -> { w2 = w; id2 = id; }
|
||||
}
|
||||
if (w > WEIGHT_EPSILON) effectiveCount++;
|
||||
slot++;
|
||||
}
|
||||
|
||||
// Renormalise les 3 poids pour qu'ils somment à 1.0.
|
||||
float sum = w0 + w1 + w2;
|
||||
if (sum > WEIGHT_EPSILON) {
|
||||
float inv = 1.0F / sum;
|
||||
w0 *= inv; w1 *= inv; w2 *= inv;
|
||||
} else {
|
||||
// Vertex sans skinning (tout-zéro ou bugué) — attache au Root avec poids 1.
|
||||
w0 = 1.0F; w1 = 0; w2 = 0;
|
||||
id0 = 0; id1 = 0; id2 = 0;
|
||||
effectiveCount = 1;
|
||||
}
|
||||
|
||||
vb.setEffectiveJointIDs(new Vec3f(id0, id1, id2));
|
||||
vb.setEffectiveJointWeights(new Vec3f(w0, w1, w2));
|
||||
vb.setEffectiveJointNumber(Math.max(1, effectiveCount));
|
||||
return vb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Groupe les indices par primitive (= material dans Blender) → une
|
||||
* {@link VanillaMeshPartDefinition} par primitive. Le partName est pris sur
|
||||
* le {@code materialName} si défini, sinon un nom synthétique
|
||||
* {@code "part_N"}.
|
||||
*/
|
||||
private static Map<MeshPartDefinition, IntList> buildPartIndices(List<Primitive> primitives) {
|
||||
Map<MeshPartDefinition, IntList> partIndices = new HashMap<>();
|
||||
int fallbackCounter = 0;
|
||||
|
||||
for (Primitive prim : primitives) {
|
||||
String partName = prim.materialName();
|
||||
if (partName == null || partName.isEmpty()) {
|
||||
partName = "part_" + fallbackCounter++;
|
||||
}
|
||||
|
||||
MeshPartDefinition partDef = VanillaMeshPartDefinition.of(partName);
|
||||
IntList indexList = new IntArrayList(prim.indices().length);
|
||||
for (int idx : prim.indices()) {
|
||||
indexList.add(idx);
|
||||
}
|
||||
partIndices.put(partDef, indexList);
|
||||
}
|
||||
|
||||
return partIndices;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user