Remove internal phase comments and format code

Strip all Phase references, TODO/FUTURE roadmap notes, and internal
planning comments from the codebase. Run Prettier for consistent
formatting across all Java files.
This commit is contained in:
NotEvil
2026-04-12 01:24:49 +02:00
parent 73d70e212d
commit a71093ba9c
482 changed files with 8500 additions and 5155 deletions

View File

@@ -31,7 +31,7 @@ public final class GlbParser {
private static final int GLB_MAGIC = 0x46546C67; // "glTF"
private static final int GLB_VERSION = 2;
private static final int CHUNK_JSON = 0x4E4F534A; // "JSON"
private static final int CHUNK_BIN = 0x004E4942; // "BIN\0"
private static final int CHUNK_BIN = 0x004E4942; // "BIN\0"
private GlbParser() {}
@@ -43,9 +43,12 @@ public final class GlbParser {
* @return parsed GltfData
* @throws IOException if the file is malformed or I/O fails
*/
public static GltfData parse(InputStream input, String debugName) throws IOException {
public static GltfData parse(InputStream input, String debugName)
throws IOException {
byte[] allBytes = input.readAllBytes();
ByteBuffer buf = ByteBuffer.wrap(allBytes).order(ByteOrder.LITTLE_ENDIAN);
ByteBuffer buf = ByteBuffer.wrap(allBytes).order(
ByteOrder.LITTLE_ENDIAN
);
// -- Header --
int magic = buf.getInt();
@@ -54,7 +57,9 @@ public final class GlbParser {
}
int version = buf.getInt();
if (version != GLB_VERSION) {
throw new IOException("Unsupported GLB version " + version + " in " + debugName);
throw new IOException(
"Unsupported GLB version " + version + " in " + debugName
);
}
int totalLength = buf.getInt();
@@ -105,12 +110,18 @@ public final class GlbParser {
for (int j = 0; j < skinJoints.size(); j++) {
int nodeIdx = skinJoints.get(j).getAsInt();
JsonObject node = nodes.get(nodeIdx).getAsJsonObject();
String name = node.has("name") ? node.get("name").getAsString() : "joint_" + j;
String name = node.has("name")
? node.get("name").getAsString()
: "joint_" + j;
if (GltfBoneMapper.isKnownBone(name)) {
skinJointRemap[j] = filteredJointNodes.size();
filteredJointNodes.add(nodeIdx);
} else {
LOGGER.debug("[GltfPipeline] Skipping non-deforming bone: '{}' (node {})", name, nodeIdx);
LOGGER.debug(
"[GltfPipeline] Skipping non-deforming bone: '{}' (node {})",
name,
nodeIdx
);
}
}
@@ -134,14 +145,18 @@ public final class GlbParser {
int nodeIdx = filteredJointNodes.get(j);
JsonObject node = nodes.get(nodeIdx).getAsJsonObject();
jointNames[j] = node.has("name") ? node.get("name").getAsString() : "joint_" + j;
jointNames[j] = node.has("name")
? node.get("name").getAsString()
: "joint_" + j;
// Rest rotation
if (node.has("rotation")) {
JsonArray r = node.getAsJsonArray("rotation");
restRotations[j] = new Quaternionf(
r.get(0).getAsFloat(), r.get(1).getAsFloat(),
r.get(2).getAsFloat(), r.get(3).getAsFloat()
r.get(0).getAsFloat(),
r.get(1).getAsFloat(),
r.get(2).getAsFloat(),
r.get(3).getAsFloat()
);
} else {
restRotations[j] = new Quaternionf(); // identity
@@ -151,7 +166,9 @@ public final class GlbParser {
if (node.has("translation")) {
JsonArray t = node.getAsJsonArray("translation");
restTranslations[j] = new Vector3f(
t.get(0).getAsFloat(), t.get(1).getAsFloat(), t.get(2).getAsFloat()
t.get(0).getAsFloat(),
t.get(1).getAsFloat(),
t.get(2).getAsFloat()
);
} else {
restTranslations[j] = new Vector3f();
@@ -179,7 +196,12 @@ public final class GlbParser {
Matrix4f[] inverseBindMatrices = new Matrix4f[jointCount];
if (skin.has("inverseBindMatrices")) {
int ibmAccessor = skin.get("inverseBindMatrices").getAsInt();
float[] ibmData = GlbParserUtils.readFloatAccessor(accessors, bufferViews, binData, ibmAccessor);
float[] ibmData = GlbParserUtils.readFloatAccessor(
accessors,
bufferViews,
binData,
ibmAccessor
);
for (int origJ = 0; origJ < skinJoints.size(); origJ++) {
int newJ = skinJointRemap[origJ];
if (newJ >= 0) {
@@ -200,7 +222,9 @@ public final class GlbParser {
if (meshes != null) {
for (int mi = 0; mi < meshes.size(); mi++) {
JsonObject mesh = meshes.get(mi).getAsJsonObject();
String meshName = mesh.has("name") ? mesh.get("name").getAsString() : "";
String meshName = mesh.has("name")
? mesh.get("name").getAsString()
: "";
if (!"Player".equals(meshName)) {
targetMeshIdx = mi;
}
@@ -238,15 +262,27 @@ public final class GlbParser {
// -- Read this primitive's vertex data --
float[] primPositions = GlbParserUtils.readFloatAccessor(
accessors, bufferViews, binData,
accessors,
bufferViews,
binData,
attributes.get("POSITION").getAsInt()
);
float[] primNormals = attributes.has("NORMAL")
? GlbParserUtils.readFloatAccessor(accessors, bufferViews, binData, attributes.get("NORMAL").getAsInt())
? GlbParserUtils.readFloatAccessor(
accessors,
bufferViews,
binData,
attributes.get("NORMAL").getAsInt()
)
: new float[primPositions.length];
float[] primTexCoords = attributes.has("TEXCOORD_0")
? GlbParserUtils.readFloatAccessor(accessors, bufferViews, binData, attributes.get("TEXCOORD_0").getAsInt())
: new float[primPositions.length / 3 * 2];
? GlbParserUtils.readFloatAccessor(
accessors,
bufferViews,
binData,
attributes.get("TEXCOORD_0").getAsInt()
)
: new float[(primPositions.length / 3) * 2];
int primVertexCount = primPositions.length / 3;
@@ -254,13 +290,16 @@ public final class GlbParser {
int[] primIndices;
if (primitive.has("indices")) {
primIndices = GlbParserUtils.readIntAccessor(
accessors, bufferViews, binData,
accessors,
bufferViews,
binData,
primitive.get("indices").getAsInt()
);
} else {
// Non-indexed: generate sequential indices
primIndices = new int[primVertexCount];
for (int i = 0; i < primVertexCount; i++) primIndices[i] = i;
for (int i = 0; i < primVertexCount; i++) primIndices[i] =
i;
}
// Offset indices by cumulative vertex count from prior primitives
@@ -276,14 +315,19 @@ public final class GlbParser {
if (attributes.has("JOINTS_0")) {
primJoints = GlbParserUtils.readIntAccessor(
accessors, bufferViews, binData,
accessors,
bufferViews,
binData,
attributes.get("JOINTS_0").getAsInt()
);
// Remap vertex joint indices from original skin order to filtered order
for (int i = 0; i < primJoints.length; i++) {
int origIdx = primJoints[i];
if (origIdx >= 0 && origIdx < skinJointRemap.length) {
primJoints[i] = skinJointRemap[origIdx] >= 0 ? skinJointRemap[origIdx] : 0;
primJoints[i] =
skinJointRemap[origIdx] >= 0
? skinJointRemap[origIdx]
: 0;
} else {
primJoints[i] = 0;
}
@@ -291,7 +335,9 @@ public final class GlbParser {
}
if (attributes.has("WEIGHTS_0")) {
primWeights = GlbParserUtils.readFloatAccessor(
accessors, bufferViews, binData,
accessors,
bufferViews,
binData,
attributes.get("WEIGHTS_0").getAsInt()
);
}
@@ -304,10 +350,18 @@ public final class GlbParser {
matName = materialNames[matIdx];
}
}
boolean isTintable = matName != null && matName.startsWith("tintable_");
boolean isTintable =
matName != null && matName.startsWith("tintable_");
String tintChannel = isTintable ? matName : null;
parsedPrimitives.add(new GltfData.Primitive(primIndices, matName, isTintable, tintChannel));
parsedPrimitives.add(
new GltfData.Primitive(
primIndices,
matName,
isTintable,
tintChannel
)
);
allPositions.add(primPositions);
allNormals.add(primNormals);
@@ -327,16 +381,26 @@ public final class GlbParser {
// Build union of all primitive indices (for backward-compat indices() accessor)
int totalIndices = 0;
for (GltfData.Primitive p : parsedPrimitives) totalIndices += p.indices().length;
for (GltfData.Primitive p : parsedPrimitives)
totalIndices += p.indices().length;
indices = new int[totalIndices];
int offset = 0;
for (GltfData.Primitive p : parsedPrimitives) {
System.arraycopy(p.indices(), 0, indices, offset, p.indices().length);
System.arraycopy(
p.indices(),
0,
indices,
offset,
p.indices().length
);
offset += p.indices().length;
}
} else {
// Animation-only GLB: no mesh data
LOGGER.info("[GltfPipeline] No mesh found in '{}' (animation-only GLB)", debugName);
LOGGER.info(
"[GltfPipeline] No mesh found in '{}' (animation-only GLB)",
debugName
);
positions = new float[0];
normals = new float[0];
texCoords = new float[0];
@@ -352,12 +416,23 @@ public final class GlbParser {
if (animations != null) {
for (int ai = 0; ai < animations.size(); ai++) {
JsonObject anim = animations.get(ai).getAsJsonObject();
String animName = anim.has("name") ? anim.get("name").getAsString() : "animation_" + ai;
String animName = anim.has("name")
? anim.get("name").getAsString()
: "animation_" + ai;
// Strip the "ArmatureName|" prefix if present (Blender convention)
if (animName.contains("|")) {
animName = animName.substring(animName.lastIndexOf('|') + 1);
animName = animName.substring(
animName.lastIndexOf('|') + 1
);
}
GltfData.AnimationClip clip = parseAnimation(anim, accessors, bufferViews, binData, nodeToJoint, jointCount);
GltfData.AnimationClip clip = parseAnimation(
anim,
accessors,
bufferViews,
binData,
nodeToJoint,
jointCount
);
if (clip != null) {
allClips.put(animName, clip);
}
@@ -365,19 +440,39 @@ public final class GlbParser {
}
// Default animation = first clip (for backward compat)
GltfData.AnimationClip animClip = allClips.isEmpty() ? null : allClips.values().iterator().next();
GltfData.AnimationClip animClip = allClips.isEmpty()
? null
: allClips.values().iterator().next();
LOGGER.info("[GltfPipeline] Parsed '{}': vertices={}, indices={}, joints={}, animations={}",
debugName, vertexCount, indices.length, jointCount, allClips.size());
LOGGER.info(
"[GltfPipeline] Parsed '{}': vertices={}, indices={}, joints={}, animations={}",
debugName,
vertexCount,
indices.length,
jointCount,
allClips.size()
);
for (String name : allClips.keySet()) {
LOGGER.debug("[GltfPipeline] animation: '{}'", name);
}
for (int j = 0; j < jointCount; j++) {
Quaternionf rq = restRotations[j];
Vector3f rt = restTranslations[j];
LOGGER.debug(String.format("[GltfPipeline] joint[%d] = '%s', parent=%d, restQ=(%.3f,%.3f,%.3f,%.3f) restT=(%.3f,%.3f,%.3f)",
j, jointNames[j], parentJointIndices[j],
rq.x, rq.y, rq.z, rq.w, rt.x, rt.y, rt.z));
LOGGER.debug(
String.format(
"[GltfPipeline] joint[%d] = '%s', parent=%d, restQ=(%.3f,%.3f,%.3f,%.3f) restT=(%.3f,%.3f,%.3f)",
j,
jointNames[j],
parentJointIndices[j],
rq.x,
rq.y,
rq.z,
rq.w,
rt.x,
rt.y,
rt.z
)
);
}
// Log animation translation channels for default clip (BEFORE MC conversion)
@@ -387,16 +482,28 @@ public final class GlbParser {
if (j < animTrans.length && animTrans[j] != null) {
Vector3f at = animTrans[j][0]; // first frame
Vector3f rt = restTranslations[j];
LOGGER.debug(String.format(
"[GltfPipeline] joint[%d] '%s' has ANIM TRANSLATION: (%.4f,%.4f,%.4f) vs rest (%.4f,%.4f,%.4f) delta=(%.4f,%.4f,%.4f)",
j, jointNames[j],
at.x, at.y, at.z,
rt.x, rt.y, rt.z,
at.x - rt.x, at.y - rt.y, at.z - rt.z));
LOGGER.debug(
String.format(
"[GltfPipeline] joint[%d] '%s' has ANIM TRANSLATION: (%.4f,%.4f,%.4f) vs rest (%.4f,%.4f,%.4f) delta=(%.4f,%.4f,%.4f)",
j,
jointNames[j],
at.x,
at.y,
at.z,
rt.x,
rt.y,
rt.z,
at.x - rt.x,
at.y - rt.y,
at.z - rt.z
)
);
}
}
} else {
LOGGER.debug("[GltfPipeline] Default animation has NO translation channels");
LOGGER.debug(
"[GltfPipeline] Default animation has NO translation channels"
);
}
// Save raw glTF rotations BEFORE coordinate conversion (for pose converter)
@@ -409,10 +516,18 @@ public final class GlbParser {
// Build raw copies of ALL animation clips (before MC conversion)
Map<String, GltfData.AnimationClip> rawAllClips = new LinkedHashMap<>();
for (Map.Entry<String, GltfData.AnimationClip> entry : allClips.entrySet()) {
rawAllClips.put(entry.getKey(), GlbParserUtils.deepCopyClip(entry.getValue()));
for (Map.Entry<
String,
GltfData.AnimationClip
> entry : allClips.entrySet()) {
rawAllClips.put(
entry.getKey(),
GlbParserUtils.deepCopyClip(entry.getValue())
);
}
GltfData.AnimationClip rawAnimClip = rawAllClips.isEmpty() ? null : rawAllClips.values().iterator().next();
GltfData.AnimationClip rawAnimClip = rawAllClips.isEmpty()
? null
: rawAllClips.values().iterator().next();
// Convert from glTF coordinate system (Y-up, faces +Z) to MC (Y-up, faces -Z)
// This is a 180° rotation around Y: negate X and Z for all spatial data
@@ -420,22 +535,39 @@ public final class GlbParser {
for (GltfData.AnimationClip clip : allClips.values()) {
GlbParserUtils.convertAnimationToMinecraftSpace(clip, jointCount);
}
convertToMinecraftSpace(positions, normals, restTranslations, restRotations,
inverseBindMatrices, null, jointCount); // pass null — clips already converted above
LOGGER.debug("[GltfPipeline] Converted all data to Minecraft coordinate space");
convertToMinecraftSpace(
positions,
normals,
restTranslations,
restRotations,
inverseBindMatrices,
null,
jointCount
); // pass null — clips already converted above
LOGGER.debug(
"[GltfPipeline] Converted all data to Minecraft coordinate space"
);
return new GltfData(
positions, normals, texCoords,
indices, meshJoints, weights,
jointNames, parentJointIndices,
positions,
normals,
texCoords,
indices,
meshJoints,
weights,
jointNames,
parentJointIndices,
inverseBindMatrices,
restRotations, restTranslations,
restRotations,
restTranslations,
rawRestRotations,
rawAnimClip,
animClip,
allClips, rawAllClips,
allClips,
rawAllClips,
parsedPrimitives,
vertexCount, jointCount
vertexCount,
jointCount
);
}
@@ -443,9 +575,11 @@ public final class GlbParser {
private static GltfData.AnimationClip parseAnimation(
JsonObject animation,
JsonArray accessors, JsonArray bufferViews,
JsonArray accessors,
JsonArray bufferViews,
ByteBuffer binData,
int[] nodeToJoint, int jointCount
int[] nodeToJoint,
int jointCount
) {
JsonArray channels = animation.getAsJsonArray("channels");
JsonArray samplers = animation.getAsJsonArray("samplers");
@@ -465,27 +599,35 @@ public final class GlbParser {
String path = target.get("path").getAsString();
int nodeIdx = target.get("node").getAsInt();
if (nodeIdx >= nodeToJoint.length || nodeToJoint[nodeIdx] < 0) continue;
if (
nodeIdx >= nodeToJoint.length || nodeToJoint[nodeIdx] < 0
) continue;
int jointIdx = nodeToJoint[nodeIdx];
int samplerIdx = channel.get("sampler").getAsInt();
JsonObject sampler = samplers.get(samplerIdx).getAsJsonObject();
float[] times = GlbParserUtils.readFloatAccessor(
accessors, bufferViews, binData,
accessors,
bufferViews,
binData,
sampler.get("input").getAsInt()
);
if ("rotation".equals(path)) {
float[] quats = GlbParserUtils.readFloatAccessor(
accessors, bufferViews, binData,
accessors,
bufferViews,
binData,
sampler.get("output").getAsInt()
);
Quaternionf[] qArr = new Quaternionf[times.length];
for (int i = 0; i < times.length; i++) {
qArr[i] = new Quaternionf(
quats[i * 4], quats[i * 4 + 1],
quats[i * 4 + 2], quats[i * 4 + 3]
quats[i * 4],
quats[i * 4 + 1],
quats[i * 4 + 2],
quats[i * 4 + 3]
);
}
rotJoints.add(jointIdx);
@@ -493,13 +635,17 @@ public final class GlbParser {
rotValues.add(qArr);
} else if ("translation".equals(path)) {
float[] vecs = GlbParserUtils.readFloatAccessor(
accessors, bufferViews, binData,
accessors,
bufferViews,
binData,
sampler.get("output").getAsInt()
);
Vector3f[] tArr = new Vector3f[times.length];
for (int i = 0; i < times.length; i++) {
tArr[i] = new Vector3f(
vecs[i * 3], vecs[i * 3 + 1], vecs[i * 3 + 2]
vecs[i * 3],
vecs[i * 3 + 1],
vecs[i * 3 + 2]
);
}
transJoints.add(jointIdx);
@@ -523,7 +669,8 @@ public final class GlbParser {
Quaternionf[] vals = rotValues.get(i);
rotations[jIdx] = new Quaternionf[frameCount];
for (int f = 0; f < frameCount; f++) {
rotations[jIdx][f] = f < vals.length ? vals[f] : vals[vals.length - 1];
rotations[jIdx][f] =
f < vals.length ? vals[f] : vals[vals.length - 1];
}
}
@@ -534,19 +681,27 @@ public final class GlbParser {
Vector3f[] vals = transValues.get(i);
translations[jIdx] = new Vector3f[frameCount];
for (int f = 0; f < frameCount; f++) {
translations[jIdx][f] = f < vals.length
? new Vector3f(vals[f])
: new Vector3f(vals[vals.length - 1]);
translations[jIdx][f] =
f < vals.length
? new Vector3f(vals[f])
: new Vector3f(vals[vals.length - 1]);
}
}
// Log translation channels found
if (!transJoints.isEmpty()) {
LOGGER.debug("[GltfPipeline] Animation has {} translation channel(s)",
transJoints.size());
LOGGER.debug(
"[GltfPipeline] Animation has {} translation channel(s)",
transJoints.size()
);
}
return new GltfData.AnimationClip(timestamps, rotations, translations, frameCount);
return new GltfData.AnimationClip(
timestamps,
rotations,
translations,
frameCount
);
}
// ---- Coordinate system conversion ----
@@ -562,14 +717,17 @@ public final class GlbParser {
* For matrices: M → C * M * C where C = diag(-1, -1, 1, 1)
*/
private static void convertToMinecraftSpace(
float[] positions, float[] normals,
Vector3f[] restTranslations, Quaternionf[] restRotations,
float[] positions,
float[] normals,
Vector3f[] restTranslations,
Quaternionf[] restRotations,
Matrix4f[] inverseBindMatrices,
GltfData.AnimationClip animClip, int jointCount
GltfData.AnimationClip animClip,
int jointCount
) {
// Vertex positions: negate X and Y
for (int i = 0; i < positions.length; i += 3) {
positions[i] = -positions[i]; // X
positions[i] = -positions[i]; // X
positions[i + 1] = -positions[i + 1]; // Y
}

View File

@@ -77,8 +77,10 @@ public final class GlbParserUtils {
// ---- Accessor reading utilities ----
public static float[] readFloatAccessor(
JsonArray accessors, JsonArray bufferViews,
ByteBuffer binData, int accessorIdx
JsonArray accessors,
JsonArray bufferViews,
ByteBuffer binData,
int accessorIdx
) {
JsonObject accessor = accessors.get(accessorIdx).getAsJsonObject();
int count = accessor.get("count").getAsInt();
@@ -88,9 +90,14 @@ public final class GlbParserUtils {
int bvIdx = accessor.get("bufferView").getAsInt();
JsonObject bv = bufferViews.get(bvIdx).getAsJsonObject();
int byteOffset = (bv.has("byteOffset") ? bv.get("byteOffset").getAsInt() : 0)
+ (accessor.has("byteOffset") ? accessor.get("byteOffset").getAsInt() : 0);
int byteStride = bv.has("byteStride") ? bv.get("byteStride").getAsInt() : 0;
int byteOffset =
(bv.has("byteOffset") ? bv.get("byteOffset").getAsInt() : 0) +
(accessor.has("byteOffset")
? accessor.get("byteOffset").getAsInt()
: 0);
int byteStride = bv.has("byteStride")
? bv.get("byteStride").getAsInt()
: 0;
int totalElements = count * components;
float[] result = new float[totalElements];
@@ -102,7 +109,10 @@ public final class GlbParserUtils {
int pos = byteOffset + i * stride;
for (int c = 0; c < components; c++) {
binData.position(pos + c * componentSize);
result[i * components + c] = readComponentAsFloat(binData, componentType);
result[i * components + c] = readComponentAsFloat(
binData,
componentType
);
}
}
@@ -110,8 +120,10 @@ public final class GlbParserUtils {
}
public static int[] readIntAccessor(
JsonArray accessors, JsonArray bufferViews,
ByteBuffer binData, int accessorIdx
JsonArray accessors,
JsonArray bufferViews,
ByteBuffer binData,
int accessorIdx
) {
JsonObject accessor = accessors.get(accessorIdx).getAsJsonObject();
int count = accessor.get("count").getAsInt();
@@ -121,9 +133,14 @@ public final class GlbParserUtils {
int bvIdx = accessor.get("bufferView").getAsInt();
JsonObject bv = bufferViews.get(bvIdx).getAsJsonObject();
int byteOffset = (bv.has("byteOffset") ? bv.get("byteOffset").getAsInt() : 0)
+ (accessor.has("byteOffset") ? accessor.get("byteOffset").getAsInt() : 0);
int byteStride = bv.has("byteStride") ? bv.get("byteStride").getAsInt() : 0;
int byteOffset =
(bv.has("byteOffset") ? bv.get("byteOffset").getAsInt() : 0) +
(accessor.has("byteOffset")
? accessor.get("byteOffset").getAsInt()
: 0);
int byteStride = bv.has("byteStride")
? bv.get("byteStride").getAsInt()
: 0;
int totalElements = count * components;
int[] result = new int[totalElements];
@@ -135,22 +152,31 @@ public final class GlbParserUtils {
int pos = byteOffset + i * stride;
for (int c = 0; c < components; c++) {
binData.position(pos + c * componentSize);
result[i * components + c] = readComponentAsInt(binData, componentType);
result[i * components + c] = readComponentAsInt(
binData,
componentType
);
}
}
return result;
}
public static float readComponentAsFloat(ByteBuffer buf, int componentType) {
public static float readComponentAsFloat(
ByteBuffer buf,
int componentType
) {
return switch (componentType) {
case FLOAT -> buf.getFloat();
case BYTE -> buf.get() / 127.0f;
case UNSIGNED_BYTE -> (buf.get() & 0xFF) / 255.0f;
case SHORT -> buf.getShort() / 32767.0f;
case UNSIGNED_SHORT -> (buf.getShort() & 0xFFFF) / 65535.0f;
case UNSIGNED_INT -> (buf.getInt() & 0xFFFFFFFFL) / (float) 0xFFFFFFFFL;
default -> throw new IllegalArgumentException("Unknown component type: " + componentType);
case UNSIGNED_INT -> (buf.getInt() & 0xFFFFFFFFL) /
(float) 0xFFFFFFFFL;
default -> throw new IllegalArgumentException(
"Unknown component type: " + componentType
);
};
}
@@ -162,7 +188,9 @@ public final class GlbParserUtils {
case UNSIGNED_SHORT -> buf.getShort() & 0xFFFF;
case UNSIGNED_INT -> buf.getInt();
case FLOAT -> (int) buf.getFloat();
default -> throw new IllegalArgumentException("Unknown component type: " + componentType);
default -> throw new IllegalArgumentException(
"Unknown component type: " + componentType
);
};
}
@@ -173,7 +201,9 @@ public final class GlbParserUtils {
case "VEC3" -> 3;
case "VEC4" -> 4;
case "MAT4" -> 16;
default -> throw new IllegalArgumentException("Unknown accessor type: " + type);
default -> throw new IllegalArgumentException(
"Unknown accessor type: " + type
);
};
}
@@ -182,7 +212,9 @@ public final class GlbParserUtils {
case BYTE, UNSIGNED_BYTE -> 1;
case SHORT, UNSIGNED_SHORT -> 2;
case UNSIGNED_INT, FLOAT -> 4;
default -> throw new IllegalArgumentException("Unknown component type: " + componentType);
default -> throw new IllegalArgumentException(
"Unknown component type: " + componentType
);
};
}
@@ -191,13 +223,18 @@ public final class GlbParserUtils {
/**
* Deep-copy an AnimationClip (preserves original data before MC conversion).
*/
public static GltfData.AnimationClip deepCopyClip(GltfData.AnimationClip clip) {
Quaternionf[][] rawRotations = new Quaternionf[clip.rotations().length][];
public static GltfData.AnimationClip deepCopyClip(
GltfData.AnimationClip clip
) {
Quaternionf[][] rawRotations =
new Quaternionf[clip.rotations().length][];
for (int j = 0; j < clip.rotations().length; j++) {
if (clip.rotations()[j] != null) {
rawRotations[j] = new Quaternionf[clip.rotations()[j].length];
for (int f = 0; f < clip.rotations()[j].length; f++) {
rawRotations[j][f] = new Quaternionf(clip.rotations()[j][f]);
rawRotations[j][f] = new Quaternionf(
clip.rotations()[j][f]
);
}
}
}
@@ -206,15 +243,20 @@ public final class GlbParserUtils {
rawTranslations = new Vector3f[clip.translations().length][];
for (int j = 0; j < clip.translations().length; j++) {
if (clip.translations()[j] != null) {
rawTranslations[j] = new Vector3f[clip.translations()[j].length];
rawTranslations[j] =
new Vector3f[clip.translations()[j].length];
for (int f = 0; f < clip.translations()[j].length; f++) {
rawTranslations[j][f] = new Vector3f(clip.translations()[j][f]);
rawTranslations[j][f] = new Vector3f(
clip.translations()[j][f]
);
}
}
}
}
return new GltfData.AnimationClip(
clip.timestamps().clone(), rawRotations, rawTranslations,
clip.timestamps().clone(),
rawRotations,
rawTranslations,
clip.frameCount()
);
}
@@ -225,7 +267,10 @@ public final class GlbParserUtils {
* Convert an animation clip's rotations and translations to MC space.
* Negate qx/qy for rotations and negate tx/ty for translations.
*/
public static void convertAnimationToMinecraftSpace(GltfData.AnimationClip clip, int jointCount) {
public static void convertAnimationToMinecraftSpace(
GltfData.AnimationClip clip,
int jointCount
) {
if (clip == null) return;
Quaternionf[][] rotations = clip.rotations();

View File

@@ -13,7 +13,6 @@ import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.jetbrains.annotations.Nullable;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.resources.ResourceLocation;
@@ -22,6 +21,7 @@ import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
/**
* V2 Animation Applier -- manages dual-layer animation for V2 bondage items.
@@ -59,22 +59,23 @@ public final class GltfAnimationApplier {
* Keyed by "animSource#context#ownedPartsHash".
* Same GLB + same context + same owned parts = same KeyframeAnimation.
*/
private static final Map<String, KeyframeAnimation> itemAnimCache = new ConcurrentHashMap<>();
private static final Map<String, KeyframeAnimation> itemAnimCache =
new ConcurrentHashMap<>();
/**
* Track which composite state is currently active per entity, to avoid redundant replays.
* Keyed by entity UUID, value is "animSource|context|sortedParts".
*/
private static final Map<UUID, String> activeStateKeys = new ConcurrentHashMap<>();
private static final Map<UUID, String> activeStateKeys =
new ConcurrentHashMap<>();
/** Track cache keys where GLB loading failed, to avoid per-tick retries. */
private static final Set<String> failedLoadKeys = ConcurrentHashMap.newKeySet();
private static final Set<String> failedLoadKeys =
ConcurrentHashMap.newKeySet();
private GltfAnimationApplier() {}
// ========================================
// INIT (legacy)
// ========================================
/**
* Legacy init method -- called by GltfClientSetup.
@@ -84,9 +85,7 @@ public final class GltfAnimationApplier {
// No-op: animation layers are managed by BondageAnimationManager
}
// ========================================
// V2 DUAL-LAYER API
// ========================================
/**
* Apply the full V2 animation state: context layer + item layer.
@@ -113,12 +112,17 @@ public final class GltfAnimationApplier {
* @param ownership bone ownership: which parts this item owns vs other items
* @return true if the item layer animation was applied successfully
*/
public static boolean applyV2Animation(LivingEntity entity, ResourceLocation modelLoc,
@Nullable ResourceLocation animationSource,
AnimationContext context, RegionBoneMapper.BoneOwnership ownership) {
public static boolean applyV2Animation(
LivingEntity entity,
ResourceLocation modelLoc,
@Nullable ResourceLocation animationSource,
AnimationContext context,
RegionBoneMapper.BoneOwnership ownership
) {
if (entity == null || modelLoc == null) return false;
ResourceLocation animSource = animationSource != null ? animationSource : modelLoc;
ResourceLocation animSource =
animationSource != null ? animationSource : modelLoc;
// Cache key includes both owned and enabled parts for full disambiguation
String ownedKey = canonicalPartsKey(ownership.thisParts());
String enabledKey = canonicalPartsKey(ownership.enabledParts());
@@ -135,7 +139,9 @@ public final class GltfAnimationApplier {
// Parts owned by ANY item (this or others) are disabled on the context layer.
// Only free parts remain enabled on context.
KeyframeAnimation contextAnim = ContextAnimationFactory.create(
context, ownership.disabledOnContext());
context,
ownership.disabledOnContext()
);
if (contextAnim != null) {
BondageAnimationManager.playContext(entity, contextAnim);
}
@@ -151,18 +157,31 @@ public final class GltfAnimationApplier {
KeyframeAnimation itemAnim = itemAnimCache.get(itemCacheKey);
if (itemAnim == null) {
GltfData animData = GlbAnimationResolver.resolveAnimationData(modelLoc, animationSource);
GltfData animData = GlbAnimationResolver.resolveAnimationData(
modelLoc,
animationSource
);
if (animData == null) {
LOGGER.warn("[GltfPipeline] Failed to load animation GLB: {}", animSource);
LOGGER.warn(
"[GltfPipeline] Failed to load animation GLB: {}",
animSource
);
failedLoadKeys.add(itemCacheKey);
activeStateKeys.put(entity.getUUID(), stateKey);
return false;
}
// Resolve which named animation to use (with fallback chain + variant selection)
String glbAnimName = GlbAnimationResolver.resolve(animData, context);
String glbAnimName = GlbAnimationResolver.resolve(
animData,
context
);
// Pass both owned parts and enabled parts (owned + free) for selective enabling
itemAnim = GltfPoseConverter.convertSelective(
animData, glbAnimName, ownership.thisParts(), ownership.enabledParts());
animData,
glbAnimName,
ownership.thisParts(),
ownership.enabledParts()
);
itemAnimCache.put(itemCacheKey, itemAnim);
}
@@ -185,16 +204,24 @@ public final class GltfAnimationApplier {
* @param allOwnedParts union of all owned parts across all items
* @return true if the composite animation was applied
*/
public static boolean applyMultiItemV2Animation(LivingEntity entity,
List<RegionBoneMapper.V2ItemAnimInfo> items,
AnimationContext context, Set<String> allOwnedParts) {
public static boolean applyMultiItemV2Animation(
LivingEntity entity,
List<RegionBoneMapper.V2ItemAnimInfo> items,
AnimationContext context,
Set<String> allOwnedParts
) {
if (entity == null || items.isEmpty()) return false;
// Build composite state key
StringBuilder keyBuilder = new StringBuilder();
for (RegionBoneMapper.V2ItemAnimInfo item : items) {
ResourceLocation src = item.animSource() != null ? item.animSource() : item.modelLoc();
keyBuilder.append(src).append(':').append(canonicalPartsKey(item.ownedParts())).append(';');
ResourceLocation src =
item.animSource() != null ? item.animSource() : item.modelLoc();
keyBuilder
.append(src)
.append(':')
.append(canonicalPartsKey(item.ownedParts()))
.append(';');
}
keyBuilder.append(context.name());
String stateKey = keyBuilder.toString();
@@ -205,7 +232,10 @@ public final class GltfAnimationApplier {
}
// === Layer 1: Context animation ===
KeyframeAnimation contextAnim = ContextAnimationFactory.create(context, allOwnedParts);
KeyframeAnimation contextAnim = ContextAnimationFactory.create(
context,
allOwnedParts
);
if (contextAnim != null) {
BondageAnimationManager.playContext(entity, contextAnim);
}
@@ -222,7 +252,8 @@ public final class GltfAnimationApplier {
if (compositeAnim == null) {
KeyframeAnimation.AnimationBuilder builder =
new KeyframeAnimation.AnimationBuilder(
dev.kosmx.playerAnim.core.data.AnimationFormat.JSON_EMOTECRAFT);
dev.kosmx.playerAnim.core.data.AnimationFormat.JSON_EMOTECRAFT
);
builder.beginTick = 0;
builder.endTick = 1;
builder.stopTick = 1;
@@ -234,15 +265,27 @@ public final class GltfAnimationApplier {
for (int i = 0; i < items.size(); i++) {
RegionBoneMapper.V2ItemAnimInfo item = items.get(i);
ResourceLocation animSource = item.animSource() != null ? item.animSource() : item.modelLoc();
ResourceLocation animSource =
item.animSource() != null
? item.animSource()
: item.modelLoc();
GltfData animData = GlbAnimationResolver.resolveAnimationData(item.modelLoc(), item.animSource());
GltfData animData = GlbAnimationResolver.resolveAnimationData(
item.modelLoc(),
item.animSource()
);
if (animData == null) {
LOGGER.warn("[GltfPipeline] Failed to load GLB for multi-item: {}", animSource);
LOGGER.warn(
"[GltfPipeline] Failed to load GLB for multi-item: {}",
animSource
);
continue;
}
String glbAnimName = GlbAnimationResolver.resolve(animData, context);
String glbAnimName = GlbAnimationResolver.resolve(
animData,
context
);
GltfData.AnimationClip rawClip;
if (glbAnimName != null) {
rawClip = animData.getRawAnimation(glbAnimName);
@@ -257,7 +300,9 @@ public final class GltfAnimationApplier {
// if the item declares per-animation bone filtering.
Set<String> effectiveParts = item.ownedParts();
if (glbAnimName != null && !item.animationBones().isEmpty()) {
Set<String> override = item.animationBones().get(glbAnimName);
Set<String> override = item
.animationBones()
.get(glbAnimName);
if (override != null) {
Set<String> filtered = new HashSet<>(override);
filtered.retainAll(item.ownedParts());
@@ -268,11 +313,20 @@ public final class GltfAnimationApplier {
}
GltfPoseConverter.addBonesToBuilder(
builder, animData, rawClip, effectiveParts);
builder,
animData,
rawClip,
effectiveParts
);
anyLoaded = true;
LOGGER.debug("[GltfPipeline] Multi-item: {} -> owned={}, effective={}, anim={}",
animSource, item.ownedParts(), effectiveParts, glbAnimName);
LOGGER.debug(
"[GltfPipeline] Multi-item: {} -> owned={}, effective={}, anim={}",
animSource,
item.ownedParts(),
effectiveParts,
glbAnimName
);
}
if (!anyLoaded) {
@@ -284,9 +338,19 @@ public final class GltfAnimationApplier {
// Enable only owned parts on the item layer.
// Free parts (head, body, etc. not owned by any item) are disabled here
// so they pass through to the context layer / vanilla animation.
String[] allPartNames = {"head", "body", "rightArm", "leftArm", "rightLeg", "leftLeg"};
String[] allPartNames = {
"head",
"body",
"rightArm",
"leftArm",
"rightLeg",
"leftLeg",
};
for (String partName : allPartNames) {
KeyframeAnimation.StateCollection part = getPartByName(builder, partName);
KeyframeAnimation.StateCollection part = getPartByName(
builder,
partName
);
if (part != null) {
if (allOwnedParts.contains(partName)) {
part.fullyEnablePart(false);
@@ -305,9 +369,7 @@ public final class GltfAnimationApplier {
return true;
}
// ========================================
// CLEAR / QUERY
// ========================================
/**
* Clear all V2 animation layers from an entity and remove tracking.
@@ -342,9 +404,7 @@ public final class GltfAnimationApplier {
activeStateKeys.remove(entityId);
}
// ========================================
// CACHE MANAGEMENT
// ========================================
/**
* Invalidate all cached item animations and tracking state.
@@ -373,9 +433,7 @@ public final class GltfAnimationApplier {
ContextAnimationFactory.clearCache();
}
// ========================================
// LEGACY F9 DEBUG TOGGLE
// ========================================
private static boolean debugEnabled = false;
@@ -386,19 +444,29 @@ public final class GltfAnimationApplier {
*/
public static void toggle() {
debugEnabled = !debugEnabled;
LOGGER.info("[GltfPipeline] Debug toggle: {}", debugEnabled ? "ON" : "OFF");
LOGGER.info(
"[GltfPipeline] Debug toggle: {}",
debugEnabled ? "ON" : "OFF"
);
AbstractClientPlayer player = Minecraft.getInstance().player;
if (player == null) return;
if (debugEnabled) {
ResourceLocation modelLoc = ResourceLocation.fromNamespaceAndPath(
"tiedup", "models/gltf/v2/handcuffs/cuffs_prototype.glb"
"tiedup",
"models/gltf/v2/handcuffs/cuffs_prototype.glb"
);
Set<String> armParts = Set.of("rightArm", "leftArm");
RegionBoneMapper.BoneOwnership debugOwnership =
new RegionBoneMapper.BoneOwnership(armParts, Set.of());
applyV2Animation(player, modelLoc, null, AnimationContext.STAND_IDLE, debugOwnership);
applyV2Animation(
player,
modelLoc,
null,
AnimationContext.STAND_IDLE,
debugOwnership
);
} else {
clearV2Animation(player);
}
@@ -411,16 +479,17 @@ public final class GltfAnimationApplier {
return debugEnabled;
}
// ========================================
// INTERNAL
// ========================================
/**
* Build cache key for item-layer animations.
* Format: "animSource#contextName#sortedParts"
*/
private static String buildItemCacheKey(ResourceLocation animSource,
AnimationContext context, String partsKey) {
private static String buildItemCacheKey(
ResourceLocation animSource,
AnimationContext context,
String partsKey
) {
return animSource + "#" + context.name() + "#" + partsKey;
}
@@ -436,7 +505,9 @@ public final class GltfAnimationApplier {
* Look up an {@link KeyframeAnimation.StateCollection} by part name on a builder.
*/
private static KeyframeAnimation.StateCollection getPartByName(
KeyframeAnimation.AnimationBuilder builder, String name) {
KeyframeAnimation.AnimationBuilder builder,
String name
) {
return switch (name) {
case "head" -> builder.head;
case "body" -> builder.body;

View File

@@ -20,16 +20,22 @@ public final class GltfBoneMapper {
/** Lower bones that represent bend (elbow/knee) */
private static final Set<String> LOWER_BONES = Set.of(
"leftLowerArm", "rightLowerArm",
"leftLowerLeg", "rightLowerLeg"
"leftLowerArm",
"rightLowerArm",
"leftLowerLeg",
"rightLowerLeg"
);
/** Maps lower bone name -> corresponding upper bone name */
private static final Map<String, String> LOWER_TO_UPPER = Map.of(
"leftLowerArm", "leftUpperArm",
"rightLowerArm", "rightUpperArm",
"leftLowerLeg", "leftUpperLeg",
"rightLowerLeg", "rightUpperLeg"
"leftLowerArm",
"leftUpperArm",
"rightLowerArm",
"rightUpperArm",
"leftLowerLeg",
"leftUpperLeg",
"rightLowerLeg",
"rightUpperLeg"
);
static {
@@ -55,7 +61,10 @@ public final class GltfBoneMapper {
* @param boneName glTF bone name
* @return the ModelPart, or null if not mapped
*/
public static ModelPart getModelPart(HumanoidModel<?> model, String boneName) {
public static ModelPart getModelPart(
HumanoidModel<?> model,
String boneName
) {
String partName = BONE_TO_PART.get(boneName);
if (partName == null) return null;

View File

@@ -19,7 +19,8 @@ import org.apache.logging.log4j.Logger;
public final class GltfCache {
private static final Logger LOGGER = LogManager.getLogger("GltfPipeline");
private static final Map<ResourceLocation, GltfData> CACHE = new ConcurrentHashMap<>();
private static final Map<ResourceLocation, GltfData> CACHE =
new ConcurrentHashMap<>();
private GltfCache() {}

View File

@@ -8,6 +8,7 @@ import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemReloadListener;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.renderer.entity.player.PlayerRenderer;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.EntityRenderersEvent;
@@ -17,7 +18,6 @@ import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -59,7 +59,9 @@ public final class GltfClientSetup {
}
@SubscribeEvent
public static void onRegisterKeybindings(RegisterKeyMappingsEvent event) {
public static void onRegisterKeybindings(
RegisterKeyMappingsEvent event
) {
event.register(TOGGLE_KEY);
LOGGER.info("[GltfPipeline] Keybind registered: F9");
}
@@ -71,16 +73,24 @@ public final class GltfClientSetup {
var defaultRenderer = event.getSkin("default");
if (defaultRenderer instanceof PlayerRenderer playerRenderer) {
playerRenderer.addLayer(new GltfRenderLayer(playerRenderer));
playerRenderer.addLayer(new V2BondageRenderLayer<>(playerRenderer));
LOGGER.info("[GltfPipeline] Render layers added to 'default' player renderer");
playerRenderer.addLayer(
new V2BondageRenderLayer<>(playerRenderer)
);
LOGGER.info(
"[GltfPipeline] Render layers added to 'default' player renderer"
);
}
// Add both layers to slim player renderer (Alex)
var slimRenderer = event.getSkin("slim");
if (slimRenderer instanceof PlayerRenderer playerRenderer) {
playerRenderer.addLayer(new GltfRenderLayer(playerRenderer));
playerRenderer.addLayer(new V2BondageRenderLayer<>(playerRenderer));
LOGGER.info("[GltfPipeline] Render layers added to 'slim' player renderer");
playerRenderer.addLayer(
new V2BondageRenderLayer<>(playerRenderer)
);
LOGGER.info(
"[GltfPipeline] Render layers added to 'slim' player renderer"
);
}
}
@@ -89,32 +99,47 @@ public final class GltfClientSetup {
* This ensures re-exported GLB models are picked up without restarting the game.
*/
@SubscribeEvent
public static void onRegisterReloadListeners(RegisterClientReloadListenersEvent event) {
event.registerReloadListener(new SimplePreparableReloadListener<Void>() {
@Override
protected Void prepare(ResourceManager resourceManager, ProfilerFiller profiler) {
return null;
}
public static void onRegisterReloadListeners(
RegisterClientReloadListenersEvent event
) {
event.registerReloadListener(
new SimplePreparableReloadListener<Void>() {
@Override
protected Void prepare(
ResourceManager resourceManager,
ProfilerFiller profiler
) {
return null;
}
@Override
protected void apply(Void nothing, ResourceManager resourceManager, ProfilerFiller profiler) {
GltfCache.clearCache();
GltfAnimationApplier.invalidateCache();
GltfMeshRenderer.clearRenderTypeCache();
// Reload context GLB animations from resource packs FIRST,
// then clear the factory cache so it rebuilds against the
// new GLB registry (prevents stale JSON fallback caching).
ContextGlbRegistry.reload(resourceManager);
ContextAnimationFactory.clearCache();
com.tiedup.remake.v2.furniture.client.FurnitureGltfCache.clear();
LOGGER.info("[GltfPipeline] Caches cleared on resource reload");
@Override
protected void apply(
Void nothing,
ResourceManager resourceManager,
ProfilerFiller profiler
) {
GltfCache.clearCache();
GltfAnimationApplier.invalidateCache();
GltfMeshRenderer.clearRenderTypeCache();
// Reload context GLB animations from resource packs FIRST,
// then clear the factory cache so it rebuilds against the
// new GLB registry (prevents stale JSON fallback caching).
ContextGlbRegistry.reload(resourceManager);
ContextAnimationFactory.clearCache();
com.tiedup.remake.v2.furniture.client.FurnitureGltfCache.clear();
LOGGER.info(
"[GltfPipeline] Caches cleared on resource reload"
);
}
}
});
);
LOGGER.info("[GltfPipeline] Resource reload listener registered");
// Data-driven bondage item definitions (tiedup_items/*.json)
event.registerReloadListener(new DataDrivenItemReloadListener());
LOGGER.info("[GltfPipeline] Data-driven item reload listener registered");
LOGGER.info(
"[GltfPipeline] Data-driven item reload listener registered"
);
}
}

View File

@@ -21,14 +21,14 @@ import org.joml.Vector3f;
public final class GltfData {
// -- Mesh geometry (flattened arrays) --
private final float[] positions; // VEC3, length = vertexCount * 3
private final float[] normals; // VEC3, length = vertexCount * 3
private final float[] texCoords; // VEC2, length = vertexCount * 2
private final int[] indices; // triangle indices
private final float[] positions; // VEC3, length = vertexCount * 3
private final float[] normals; // VEC3, length = vertexCount * 3
private final float[] texCoords; // VEC2, length = vertexCount * 2
private final int[] indices; // triangle indices
// -- Skinning data (per-vertex, 4 influences) --
private final int[] joints; // 4 joint indices per vertex, length = vertexCount * 4
private final float[] weights; // 4 weights per vertex, length = vertexCount * 4
private final int[] joints; // 4 joint indices per vertex, length = vertexCount * 4
private final float[] weights; // 4 weights per vertex, length = vertexCount * 4
// -- Bone hierarchy (MC-converted for skinning) --
private final String[] jointNames;
@@ -39,6 +39,7 @@ public final class GltfData {
// -- Raw glTF rotations (unconverted, for pose conversion) --
private final Quaternionf[] rawGltfRestRotations;
@Nullable
private final AnimationClip rawGltfAnimation;
@@ -47,8 +48,8 @@ public final class GltfData {
private final AnimationClip animation;
// -- Multiple named animations --
private final Map<String, AnimationClip> namedAnimations; // MC-converted
private final Map<String, AnimationClip> rawNamedAnimations; // raw glTF space
private final Map<String, AnimationClip> namedAnimations; // MC-converted
private final Map<String, AnimationClip> rawNamedAnimations; // raw glTF space
// -- Per-primitive material/tint info --
private final List<Primitive> primitives;
@@ -61,18 +62,25 @@ public final class GltfData {
* Full constructor with multiple named animations and per-primitive data.
*/
public GltfData(
float[] positions, float[] normals, float[] texCoords,
int[] indices, int[] joints, float[] weights,
String[] jointNames, int[] parentJointIndices,
float[] positions,
float[] normals,
float[] texCoords,
int[] indices,
int[] joints,
float[] weights,
String[] jointNames,
int[] parentJointIndices,
Matrix4f[] inverseBindMatrices,
Quaternionf[] restRotations, Vector3f[] restTranslations,
Quaternionf[] restRotations,
Vector3f[] restTranslations,
Quaternionf[] rawGltfRestRotations,
@Nullable AnimationClip rawGltfAnimation,
@Nullable AnimationClip animation,
Map<String, AnimationClip> namedAnimations,
Map<String, AnimationClip> rawNamedAnimations,
List<Primitive> primitives,
int vertexCount, int jointCount
int vertexCount,
int jointCount
) {
this.positions = positions;
this.normals = normals;
@@ -88,8 +96,12 @@ public final class GltfData {
this.rawGltfRestRotations = rawGltfRestRotations;
this.rawGltfAnimation = rawGltfAnimation;
this.animation = animation;
this.namedAnimations = Collections.unmodifiableMap(new LinkedHashMap<>(namedAnimations));
this.rawNamedAnimations = Collections.unmodifiableMap(new LinkedHashMap<>(rawNamedAnimations));
this.namedAnimations = Collections.unmodifiableMap(
new LinkedHashMap<>(namedAnimations)
);
this.rawNamedAnimations = Collections.unmodifiableMap(
new LinkedHashMap<>(rawNamedAnimations)
);
this.primitives = List.copyOf(primitives);
this.vertexCount = vertexCount;
this.jointCount = jointCount;
@@ -99,81 +111,175 @@ public final class GltfData {
* Legacy constructor for backward compatibility (single animation only).
*/
public GltfData(
float[] positions, float[] normals, float[] texCoords,
int[] indices, int[] joints, float[] weights,
String[] jointNames, int[] parentJointIndices,
float[] positions,
float[] normals,
float[] texCoords,
int[] indices,
int[] joints,
float[] weights,
String[] jointNames,
int[] parentJointIndices,
Matrix4f[] inverseBindMatrices,
Quaternionf[] restRotations, Vector3f[] restTranslations,
Quaternionf[] restRotations,
Vector3f[] restTranslations,
Quaternionf[] rawGltfRestRotations,
@Nullable AnimationClip rawGltfAnimation,
@Nullable AnimationClip animation,
int vertexCount, int jointCount
int vertexCount,
int jointCount
) {
this(positions, normals, texCoords, indices, joints, weights,
jointNames, parentJointIndices, inverseBindMatrices,
restRotations, restTranslations, rawGltfRestRotations,
rawGltfAnimation, animation,
new LinkedHashMap<>(), new LinkedHashMap<>(),
this(
positions,
normals,
texCoords,
indices,
joints,
weights,
jointNames,
parentJointIndices,
inverseBindMatrices,
restRotations,
restTranslations,
rawGltfRestRotations,
rawGltfAnimation,
animation,
new LinkedHashMap<>(),
new LinkedHashMap<>(),
List.of(new Primitive(indices, null, false, null)),
vertexCount, jointCount);
vertexCount,
jointCount
);
}
public float[] positions() {
return positions;
}
public float[] normals() {
return normals;
}
public float[] texCoords() {
return texCoords;
}
public int[] indices() {
return indices;
}
public int[] joints() {
return joints;
}
public float[] weights() {
return weights;
}
public String[] jointNames() {
return jointNames;
}
public int[] parentJointIndices() {
return parentJointIndices;
}
public Matrix4f[] inverseBindMatrices() {
return inverseBindMatrices;
}
public Quaternionf[] restRotations() {
return restRotations;
}
public Vector3f[] restTranslations() {
return restTranslations;
}
public Quaternionf[] rawGltfRestRotations() {
return rawGltfRestRotations;
}
public float[] positions() { return positions; }
public float[] normals() { return normals; }
public float[] texCoords() { return texCoords; }
public int[] indices() { return indices; }
public int[] joints() { return joints; }
public float[] weights() { return weights; }
public String[] jointNames() { return jointNames; }
public int[] parentJointIndices() { return parentJointIndices; }
public Matrix4f[] inverseBindMatrices() { return inverseBindMatrices; }
public Quaternionf[] restRotations() { return restRotations; }
public Vector3f[] restTranslations() { return restTranslations; }
public Quaternionf[] rawGltfRestRotations() { return rawGltfRestRotations; }
@Nullable
public AnimationClip rawGltfAnimation() { return rawGltfAnimation; }
public AnimationClip rawGltfAnimation() {
return rawGltfAnimation;
}
@Nullable
public AnimationClip animation() { return animation; }
public int vertexCount() { return vertexCount; }
public int jointCount() { return jointCount; }
public AnimationClip animation() {
return animation;
}
public int vertexCount() {
return vertexCount;
}
public int jointCount() {
return jointCount;
}
/** Per-primitive material and tint metadata. One entry per glTF primitive in the mesh. */
public List<Primitive> primitives() { return primitives; }
public List<Primitive> primitives() {
return primitives;
}
/** All named animations in MC-converted space. Keys are animation names (e.g. "BasicPose", "Struggle"). */
public Map<String, AnimationClip> namedAnimations() { return namedAnimations; }
public Map<String, AnimationClip> namedAnimations() {
return namedAnimations;
}
/** Get a specific named animation in MC-converted space, or null if not found. */
@Nullable
public AnimationClip getAnimation(String name) { return namedAnimations.get(name); }
public AnimationClip getAnimation(String name) {
return namedAnimations.get(name);
}
/** Get a specific named animation in raw glTF space, or null if not found. */
@Nullable
public AnimationClip getRawAnimation(String name) { return rawNamedAnimations.get(name); }
public AnimationClip getRawAnimation(String name) {
return rawNamedAnimations.get(name);
}
/**
* Animation clip: per-bone timestamps, quaternion rotations, and optional translations.
*/
public static final class AnimationClip {
private final float[] timestamps; // shared timestamps
private final Quaternionf[][] rotations; // [jointIndex][frameIndex], null if no anim
private final float[] timestamps; // shared timestamps
private final Quaternionf[][] rotations; // [jointIndex][frameIndex], null if no anim
@Nullable
private final Vector3f[][] translations; // [jointIndex][frameIndex], null if no anim
private final Vector3f[][] translations; // [jointIndex][frameIndex], null if no anim
private final int frameCount;
public AnimationClip(float[] timestamps, Quaternionf[][] rotations,
@Nullable Vector3f[][] translations, int frameCount) {
public AnimationClip(
float[] timestamps,
Quaternionf[][] rotations,
@Nullable Vector3f[][] translations,
int frameCount
) {
this.timestamps = timestamps;
this.rotations = rotations;
this.translations = translations;
this.frameCount = frameCount;
}
public float[] timestamps() { return timestamps; }
public Quaternionf[][] rotations() { return rotations; }
public float[] timestamps() {
return timestamps;
}
public Quaternionf[][] rotations() {
return rotations;
}
@Nullable
public Vector3f[][] translations() { return translations; }
public int frameCount() { return frameCount; }
public Vector3f[][] translations() {
return translations;
}
public int frameCount() {
return frameCount;
}
}
/**

View File

@@ -60,7 +60,9 @@ public final class GltfLiveBoneReader {
* @return array of joint matrices ready for skinning, or null on failure
*/
public static Matrix4f[] computeJointMatricesFromModel(
HumanoidModel<?> model, GltfData data, LivingEntity entity
HumanoidModel<?> model,
GltfData data,
LivingEntity entity
) {
if (model == null || data == null || entity == null) return null;
@@ -83,14 +85,19 @@ public final class GltfLiveBoneReader {
if (GltfBoneMapper.isLowerBone(boneName)) {
// --- Lower bone: reconstruct from bend values ---
localRot = computeLowerBoneLocalRotation(
boneName, j, restRotations, emote
boneName,
j,
restRotations,
emote
);
} else if (hasUniqueModelPart(boneName)) {
// --- Upper bone with a unique ModelPart ---
ModelPart part = GltfBoneMapper.getModelPart(model, boneName);
if (part != null) {
localRot = computeUpperBoneLocalRotation(
part, j, restRotations
part,
j,
restRotations
);
} else {
// Fallback: use rest rotation
@@ -108,14 +115,17 @@ public final class GltfLiveBoneReader {
// Compose with parent to get world transform
if (parents[j] >= 0 && worldTransforms[parents[j]] != null) {
worldTransforms[j] = new Matrix4f(worldTransforms[parents[j]]).mul(local);
worldTransforms[j] = new Matrix4f(
worldTransforms[parents[j]]
).mul(local);
} else {
worldTransforms[j] = new Matrix4f(local);
}
// Final joint matrix = worldTransform * inverseBindMatrix
jointMatrices[j] = new Matrix4f(worldTransforms[j])
.mul(data.inverseBindMatrices()[j]);
jointMatrices[j] = new Matrix4f(worldTransforms[j]).mul(
data.inverseBindMatrices()[j]
);
}
return jointMatrices;
@@ -138,11 +148,16 @@ public final class GltfLiveBoneReader {
* the frame relationship.
*/
private static Quaternionf computeUpperBoneLocalRotation(
ModelPart part, int jointIndex,
ModelPart part,
int jointIndex,
Quaternionf[] restRotations
) {
// Reconstruct the MC-frame delta from ModelPart euler angles.
Quaternionf delta = new Quaternionf().rotationZYX(part.zRot, part.yRot, part.xRot);
Quaternionf delta = new Quaternionf().rotationZYX(
part.zRot,
part.yRot,
part.xRot
);
// Local rotation = delta applied on top of the local rest rotation.
return new Quaternionf(delta).mul(restRotations[jointIndex]);
}
@@ -160,7 +175,8 @@ public final class GltfLiveBoneReader {
* No de-parenting needed — same reasoning as upper bones.
*/
private static Quaternionf computeLowerBoneLocalRotation(
String boneName, int jointIndex,
String boneName,
int jointIndex,
Quaternionf[] restRotations,
AnimationApplier emote
) {
@@ -183,11 +199,16 @@ public final class GltfLiveBoneReader {
float halfAngle = bendValue * 0.5f;
float s = (float) Math.sin(halfAngle);
Quaternionf bendQuat = new Quaternionf(
ax * s, 0, az * s, (float) Math.cos(halfAngle)
ax * s,
0,
az * s,
(float) Math.cos(halfAngle)
);
// Local rotation = bend delta applied on top of local rest rotation
return new Quaternionf(bendQuat).mul(restRotations[jointIndex]);
return new Quaternionf(bendQuat).mul(
restRotations[jointIndex]
);
}
}
}
@@ -218,12 +239,12 @@ public final class GltfLiveBoneReader {
// LivingEntityRenderer's PoseStack transform, which applies to the entire
// mesh uniformly. No need to read body rotation into joint matrices.
return switch (boneName) {
case "head" -> true;
case "head" -> true;
case "leftUpperArm" -> true;
case "rightUpperArm"-> true;
case "rightUpperArm" -> true;
case "leftUpperLeg" -> true;
case "rightUpperLeg"-> true;
default -> false; // body, torso, lower bones, unknown
case "rightUpperLeg" -> true;
default -> false; // body, torso, lower bones, unknown
};
}
@@ -236,8 +257,11 @@ public final class GltfLiveBoneReader {
try {
return animated.playerAnimator_getAnimation();
} catch (Exception e) {
LOGGER.debug("[GltfPipeline] Could not get AnimationApplier for {}: {}",
entity.getClass().getSimpleName(), e.getMessage());
LOGGER.debug(
"[GltfPipeline] Could not get AnimationApplier for {}: {}",
entity.getClass().getSimpleName(),
e.getMessage()
);
}
}
return null;

View File

@@ -25,13 +25,17 @@ import org.joml.Vector4f;
public final class GltfMeshRenderer extends RenderStateShard {
private static final ResourceLocation WHITE_TEXTURE =
ResourceLocation.fromNamespaceAndPath("tiedup", "models/obj/shared/white.png");
ResourceLocation.fromNamespaceAndPath(
"tiedup",
"models/obj/shared/white.png"
);
/** Cached default RenderType (white texture). Created once, reused every frame. */
private static RenderType cachedDefaultRenderType;
/** Cache for texture-specific RenderTypes, keyed by ResourceLocation. */
private static final Map<ResourceLocation, RenderType> RENDER_TYPE_CACHE = new ConcurrentHashMap<>();
private static final Map<ResourceLocation, RenderType> RENDER_TYPE_CACHE =
new ConcurrentHashMap<>();
private GltfMeshRenderer() {
super("tiedup_gltf_renderer", () -> {}, () -> {});
@@ -61,15 +65,21 @@ public final class GltfMeshRenderer extends RenderStateShard {
* @param texture the texture ResourceLocation
* @return the cached or newly created RenderType
*/
private static RenderType getRenderTypeForTexture(ResourceLocation texture) {
return RENDER_TYPE_CACHE.computeIfAbsent(texture,
GltfMeshRenderer::createTriangleRenderType);
private static RenderType getRenderTypeForTexture(
ResourceLocation texture
) {
return RENDER_TYPE_CACHE.computeIfAbsent(
texture,
GltfMeshRenderer::createTriangleRenderType
);
}
/**
* Create a TRIANGLES-mode RenderType for glTF mesh rendering with the given texture.
*/
private static RenderType createTriangleRenderType(ResourceLocation texture) {
private static RenderType createTriangleRenderType(
ResourceLocation texture
) {
RenderType.CompositeState state = RenderType.CompositeState.builder()
.setShaderState(RENDERTYPE_ENTITY_CUTOUT_NO_CULL_SHADER)
.setTextureState(
@@ -112,12 +122,22 @@ public final class GltfMeshRenderer extends RenderStateShard {
* @param packedOverlay packed overlay value
*/
public static void renderSkinned(
GltfData data, Matrix4f[] jointMatrices,
PoseStack poseStack, MultiBufferSource buffer,
int packedLight, int packedOverlay
GltfData data,
Matrix4f[] jointMatrices,
PoseStack poseStack,
MultiBufferSource buffer,
int packedLight,
int packedOverlay
) {
renderSkinnedInternal(data, jointMatrices, poseStack, buffer,
packedLight, packedOverlay, getDefaultRenderType());
renderSkinnedInternal(
data,
jointMatrices,
poseStack,
buffer,
packedLight,
packedOverlay,
getDefaultRenderType()
);
}
/**
@@ -132,22 +152,35 @@ public final class GltfMeshRenderer extends RenderStateShard {
* @param texture the texture to use for rendering
*/
public static void renderSkinned(
GltfData data, Matrix4f[] jointMatrices,
PoseStack poseStack, MultiBufferSource buffer,
int packedLight, int packedOverlay,
GltfData data,
Matrix4f[] jointMatrices,
PoseStack poseStack,
MultiBufferSource buffer,
int packedLight,
int packedOverlay,
ResourceLocation texture
) {
renderSkinnedInternal(data, jointMatrices, poseStack, buffer,
packedLight, packedOverlay, getRenderTypeForTexture(texture));
renderSkinnedInternal(
data,
jointMatrices,
poseStack,
buffer,
packedLight,
packedOverlay,
getRenderTypeForTexture(texture)
);
}
/**
* Internal rendering implementation shared by both overloads.
*/
private static void renderSkinnedInternal(
GltfData data, Matrix4f[] jointMatrices,
PoseStack poseStack, MultiBufferSource buffer,
int packedLight, int packedOverlay,
GltfData data,
Matrix4f[] jointMatrices,
PoseStack poseStack,
MultiBufferSource buffer,
int packedLight,
int packedOverlay,
RenderType renderType
) {
Matrix4f pose = poseStack.last().pose();
@@ -167,13 +200,22 @@ public final class GltfMeshRenderer extends RenderStateShard {
for (int idx : indices) {
// Skin this vertex
GltfSkinningEngine.skinVertex(data, idx, jointMatrices, outPos, outNormal, tmpPos, tmpNorm);
GltfSkinningEngine.skinVertex(
data,
idx,
jointMatrices,
outPos,
outNormal,
tmpPos,
tmpNorm
);
// UV coordinates
float u = texCoords[idx * 2];
float v = texCoords[idx * 2 + 1];
vc.vertex(pose, outPos[0], outPos[1], outPos[2])
vc
.vertex(pose, outPos[0], outPos[1], outPos[2])
.color(255, 255, 255, 255)
.uv(u, 1.0f - v)
.overlayCoords(packedOverlay)
@@ -205,9 +247,12 @@ public final class GltfMeshRenderer extends RenderStateShard {
* @param tintColors channel name to RGB int (0xRRGGBB); empty map = white everywhere
*/
public static void renderSkinnedTinted(
GltfData data, Matrix4f[] jointMatrices,
PoseStack poseStack, MultiBufferSource buffer,
int packedLight, int packedOverlay,
GltfData data,
Matrix4f[] jointMatrices,
PoseStack poseStack,
MultiBufferSource buffer,
int packedLight,
int packedOverlay,
RenderType renderType,
Map<String, Integer> tintColors
) {
@@ -226,7 +271,9 @@ public final class GltfMeshRenderer extends RenderStateShard {
for (GltfData.Primitive prim : primitives) {
// Determine color for this primitive
int r = 255, g = 255, b = 255;
int r = 255,
g = 255,
b = 255;
if (prim.tintable() && prim.tintChannel() != null) {
Integer colorInt = tintColors.get(prim.tintChannel());
if (colorInt != null) {
@@ -237,12 +284,21 @@ public final class GltfMeshRenderer extends RenderStateShard {
}
for (int idx : prim.indices()) {
GltfSkinningEngine.skinVertex(data, idx, jointMatrices, outPos, outNormal, tmpPos, tmpNorm);
GltfSkinningEngine.skinVertex(
data,
idx,
jointMatrices,
outPos,
outNormal,
tmpPos,
tmpNorm
);
float u = texCoords[idx * 2];
float v = texCoords[idx * 2 + 1];
vc.vertex(pose, outPos[0], outPos[1], outPos[2])
vc
.vertex(pose, outPos[0], outPos[1], outPos[2])
.color(r, g, b, 255)
.uv(u, 1.0f - v)
.overlayCoords(packedOverlay)

View File

@@ -5,11 +5,11 @@ import dev.kosmx.playerAnim.core.data.KeyframeAnimation;
import dev.kosmx.playerAnim.core.util.Ease;
import java.util.HashSet;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import org.joml.Quaternionf;
import org.joml.Vector3f;
@@ -52,10 +52,16 @@ public final class GltfPoseConverter {
* @param animationName the name of the animation to convert (e.g. "Struggle", "Idle")
* @return a static looping KeyframeAnimation suitable for PlayerAnimator
*/
public static KeyframeAnimation convert(GltfData data, String animationName) {
public static KeyframeAnimation convert(
GltfData data,
String animationName
) {
GltfData.AnimationClip rawClip = data.getRawAnimation(animationName);
if (rawClip == null) {
LOGGER.warn("[GltfPipeline] Animation '{}' not found, falling back to default", animationName);
LOGGER.warn(
"[GltfPipeline] Animation '{}' not found, falling back to default",
animationName
);
return convert(data);
}
return convertClip(data, rawClip, "gltf_" + animationName);
@@ -76,8 +82,12 @@ public final class GltfPoseConverter {
* are only enabled if the GLB has keyframes for them
* @return KeyframeAnimation with selective parts active
*/
public static KeyframeAnimation convertSelective(GltfData data, @Nullable String animationName,
Set<String> ownedParts, Set<String> enabledParts) {
public static KeyframeAnimation convertSelective(
GltfData data,
@Nullable String animationName,
Set<String> ownedParts,
Set<String> enabledParts
) {
GltfData.AnimationClip rawClip;
String animName;
if (animationName != null) {
@@ -90,7 +100,13 @@ public final class GltfPoseConverter {
if (rawClip == null) {
rawClip = data.rawGltfAnimation();
}
return convertClipSelective(data, rawClip, animName, ownedParts, enabledParts);
return convertClipSelective(
data,
rawClip,
animName,
ownedParts,
enabledParts
);
}
/**
@@ -105,10 +121,17 @@ public final class GltfPoseConverter {
* @param ownedParts parts the item explicitly owns (always enabled)
* @param enabledParts parts the item may animate (owned + free)
*/
private static KeyframeAnimation convertClipSelective(GltfData data, GltfData.AnimationClip rawClip,
String animName, Set<String> ownedParts, Set<String> enabledParts) {
private static KeyframeAnimation convertClipSelective(
GltfData data,
GltfData.AnimationClip rawClip,
String animName,
Set<String> ownedParts,
Set<String> enabledParts
) {
KeyframeAnimation.AnimationBuilder builder =
new KeyframeAnimation.AnimationBuilder(AnimationFormat.JSON_EMOTECRAFT);
new KeyframeAnimation.AnimationBuilder(
AnimationFormat.JSON_EMOTECRAFT
);
builder.beginTick = 0;
builder.endTick = 1;
@@ -129,21 +152,27 @@ public final class GltfPoseConverter {
// Check if this joint has explicit animation data (not just rest pose fallback).
// A bone counts as explicitly animated if it has rotation OR translation keyframes.
boolean hasExplicitAnim = rawClip != null && (
(j < rawClip.rotations().length && rawClip.rotations()[j] != null)
|| (rawClip.translations() != null
&& j < rawClip.translations().length
&& rawClip.translations()[j] != null)
);
boolean hasExplicitAnim =
rawClip != null &&
((j < rawClip.rotations().length &&
rawClip.rotations()[j] != null) ||
(rawClip.translations() != null &&
j < rawClip.translations().length &&
rawClip.translations()[j] != null));
Quaternionf animQ = getRawAnimQuaternion(rawClip, rawRestRotations, j);
Quaternionf animQ = getRawAnimQuaternion(
rawClip,
rawRestRotations,
j
);
Quaternionf restQ = rawRestRotations[j];
// delta_local = inverse(rest_q) * anim_q (in bone-local frame)
Quaternionf deltaLocal = new Quaternionf(restQ).invert().mul(animQ);
// Convert to PARENT frame: delta_parent = rest * delta_local * inv(rest)
Quaternionf deltaParent = new Quaternionf(restQ).mul(deltaLocal)
Quaternionf deltaParent = new Quaternionf(restQ)
.mul(deltaLocal)
.mul(new Quaternionf(restQ).invert());
// Convert from glTF parent frame to MC model-def frame.
@@ -168,7 +197,9 @@ public final class GltfPoseConverter {
if (GltfBoneMapper.isLowerBone(boneName)) {
String upperBone = GltfBoneMapper.getUpperBoneFor(boneName);
if (upperBone != null) {
String upperPart = GltfBoneMapper.getAnimPartName(upperBone);
String upperPart = GltfBoneMapper.getAnimPartName(
upperBone
);
if (upperPart != null) {
partsWithKeyframes.add(upperPart);
}
@@ -178,11 +209,21 @@ public final class GltfPoseConverter {
}
// Selective: enable owned parts always, free parts only if they have keyframes
enableSelectiveParts(builder, ownedParts, enabledParts, partsWithKeyframes);
enableSelectiveParts(
builder,
ownedParts,
enabledParts,
partsWithKeyframes
);
KeyframeAnimation anim = builder.build();
LOGGER.debug("[GltfPipeline] Converted selective animation '{}' (owned: {}, enabled: {}, withKeyframes: {})",
animName, ownedParts, enabledParts, partsWithKeyframes);
LOGGER.debug(
"[GltfPipeline] Converted selective animation '{}' (owned: {}, enabled: {}, withKeyframes: {})",
animName,
ownedParts,
enabledParts,
partsWithKeyframes
);
return anim;
}
@@ -200,10 +241,11 @@ public final class GltfPoseConverter {
* @return set of part names that received actual keyframe data from the GLB
*/
public static Set<String> addBonesToBuilder(
KeyframeAnimation.AnimationBuilder builder,
GltfData data, @Nullable GltfData.AnimationClip rawClip,
Set<String> ownedParts) {
KeyframeAnimation.AnimationBuilder builder,
GltfData data,
@Nullable GltfData.AnimationClip rawClip,
Set<String> ownedParts
) {
String[] jointNames = data.jointNames();
Quaternionf[] rawRestRotations = data.rawGltfRestRotations();
Set<String> partsWithKeyframes = new HashSet<>();
@@ -221,23 +263,33 @@ public final class GltfPoseConverter {
if (GltfBoneMapper.isLowerBone(boneName)) {
String upperBone = GltfBoneMapper.getUpperBoneFor(boneName);
if (upperBone != null) {
String upperPart = GltfBoneMapper.getAnimPartName(upperBone);
if (upperPart == null || !ownedParts.contains(upperPart)) continue;
String upperPart = GltfBoneMapper.getAnimPartName(
upperBone
);
if (
upperPart == null || !ownedParts.contains(upperPart)
) continue;
}
}
boolean hasExplicitAnim = rawClip != null && (
(j < rawClip.rotations().length && rawClip.rotations()[j] != null)
|| (rawClip.translations() != null
&& j < rawClip.translations().length
&& rawClip.translations()[j] != null)
);
boolean hasExplicitAnim =
rawClip != null &&
((j < rawClip.rotations().length &&
rawClip.rotations()[j] != null) ||
(rawClip.translations() != null &&
j < rawClip.translations().length &&
rawClip.translations()[j] != null));
Quaternionf animQ = getRawAnimQuaternion(rawClip, rawRestRotations, j);
Quaternionf animQ = getRawAnimQuaternion(
rawClip,
rawRestRotations,
j
);
Quaternionf restQ = rawRestRotations[j];
Quaternionf deltaLocal = new Quaternionf(restQ).invert().mul(animQ);
Quaternionf deltaParent = new Quaternionf(restQ).mul(deltaLocal)
Quaternionf deltaParent = new Quaternionf(restQ)
.mul(deltaLocal)
.mul(new Quaternionf(restQ).invert());
Quaternionf deltaQ = new Quaternionf(deltaParent);
@@ -255,8 +307,12 @@ public final class GltfPoseConverter {
if (GltfBoneMapper.isLowerBone(boneName)) {
String upperBone = GltfBoneMapper.getUpperBoneFor(boneName);
if (upperBone != null) {
String upperPart = GltfBoneMapper.getAnimPartName(upperBone);
if (upperPart != null) partsWithKeyframes.add(upperPart);
String upperPart = GltfBoneMapper.getAnimPartName(
upperBone
);
if (upperPart != null) partsWithKeyframes.add(
upperPart
);
}
}
}
@@ -281,16 +337,25 @@ public final class GltfPoseConverter {
* @return a static looping KeyframeAnimation with all parts enabled
*/
public static KeyframeAnimation convertWithSkeleton(
GltfData skeleton, GltfData.AnimationClip clip, String animName) {
GltfData skeleton,
GltfData.AnimationClip clip,
String animName
) {
return convertClip(skeleton, clip, animName);
}
/**
* Internal: convert a specific raw animation clip to a KeyframeAnimation.
*/
private static KeyframeAnimation convertClip(GltfData data, GltfData.AnimationClip rawClip, String animName) {
private static KeyframeAnimation convertClip(
GltfData data,
GltfData.AnimationClip rawClip,
String animName
) {
KeyframeAnimation.AnimationBuilder builder =
new KeyframeAnimation.AnimationBuilder(AnimationFormat.JSON_EMOTECRAFT);
new KeyframeAnimation.AnimationBuilder(
AnimationFormat.JSON_EMOTECRAFT
);
builder.beginTick = 0;
builder.endTick = 1;
@@ -307,7 +372,11 @@ public final class GltfPoseConverter {
if (!GltfBoneMapper.isKnownBone(boneName)) continue;
Quaternionf animQ = getRawAnimQuaternion(rawClip, rawRestRotations, j);
Quaternionf animQ = getRawAnimQuaternion(
rawClip,
rawRestRotations,
j
);
Quaternionf restQ = rawRestRotations[j];
// delta_local = inverse(rest_q) * anim_q (in bone-local frame)
@@ -315,7 +384,8 @@ public final class GltfPoseConverter {
// Convert to PARENT frame: delta_parent = rest * delta_local * inv(rest)
// Simplifies algebraically to: animQ * inv(restQ)
Quaternionf deltaParent = new Quaternionf(restQ).mul(deltaLocal)
Quaternionf deltaParent = new Quaternionf(restQ)
.mul(deltaLocal)
.mul(new Quaternionf(restQ).invert());
// Convert from glTF parent frame to MC model-def frame.
@@ -324,12 +394,24 @@ public final class GltfPoseConverter {
deltaQ.x = -deltaQ.x;
deltaQ.y = -deltaQ.y;
LOGGER.debug(String.format(
"[GltfPipeline] Bone '%s': restQ=(%.3f,%.3f,%.3f,%.3f) animQ=(%.3f,%.3f,%.3f,%.3f) deltaQ=(%.3f,%.3f,%.3f,%.3f)",
boneName,
restQ.x, restQ.y, restQ.z, restQ.w,
animQ.x, animQ.y, animQ.z, animQ.w,
deltaQ.x, deltaQ.y, deltaQ.z, deltaQ.w));
LOGGER.debug(
String.format(
"[GltfPipeline] Bone '%s': restQ=(%.3f,%.3f,%.3f,%.3f) animQ=(%.3f,%.3f,%.3f,%.3f) deltaQ=(%.3f,%.3f,%.3f,%.3f)",
boneName,
restQ.x,
restQ.y,
restQ.z,
restQ.w,
animQ.x,
animQ.y,
animQ.z,
animQ.w,
deltaQ.x,
deltaQ.y,
deltaQ.z,
deltaQ.w
)
);
if (GltfBoneMapper.isLowerBone(boneName)) {
convertLowerBone(builder, boneName, deltaQ);
@@ -341,7 +423,10 @@ public final class GltfPoseConverter {
builder.fullyEnableParts();
KeyframeAnimation anim = builder.build();
LOGGER.debug("[GltfPipeline] Converted glTF animation '{}' to KeyframeAnimation", animName);
LOGGER.debug(
"[GltfPipeline] Converted glTF animation '{}' to KeyframeAnimation",
animName
);
return anim;
}
@@ -350,10 +435,15 @@ public final class GltfPoseConverter {
* Falls back to rest rotation if the clip is null or has no data for this joint.
*/
private static Quaternionf getRawAnimQuaternion(
GltfData.AnimationClip rawClip, Quaternionf[] rawRestRotations, int jointIndex
GltfData.AnimationClip rawClip,
Quaternionf[] rawRestRotations,
int jointIndex
) {
if (rawClip != null && jointIndex < rawClip.rotations().length
&& rawClip.rotations()[jointIndex] != null) {
if (
rawClip != null &&
jointIndex < rawClip.rotations().length &&
rawClip.rotations()[jointIndex] != null
) {
return rawClip.rotations()[jointIndex][0]; // first frame
}
return rawRestRotations[jointIndex]; // fallback to rest
@@ -361,29 +451,36 @@ public final class GltfPoseConverter {
private static void convertUpperBone(
KeyframeAnimation.AnimationBuilder builder,
String boneName, Quaternionf deltaQ
String boneName,
Quaternionf deltaQ
) {
// Decompose delta quaternion to Euler ZYX
// JOML's getEulerAnglesZYX stores: euler.x = X rotation, euler.y = Y rotation, euler.z = Z rotation
// (the "ZYX" refers to rotation ORDER, not storage order)
Vector3f euler = new Vector3f();
deltaQ.getEulerAnglesZYX(euler);
float pitch = euler.x; // X rotation (pitch)
float yaw = euler.y; // Y rotation (yaw)
float roll = euler.z; // Z rotation (roll)
float pitch = euler.x; // X rotation (pitch)
float yaw = euler.y; // Y rotation (yaw)
float roll = euler.z; // Z rotation (roll)
LOGGER.debug(String.format(
"[GltfPipeline] Upper bone '%s': pitch=%.1f° yaw=%.1f° roll=%.1f°",
boneName,
Math.toDegrees(pitch),
Math.toDegrees(yaw),
Math.toDegrees(roll)));
LOGGER.debug(
String.format(
"[GltfPipeline] Upper bone '%s': pitch=%.1f° yaw=%.1f° roll=%.1f°",
boneName,
Math.toDegrees(pitch),
Math.toDegrees(yaw),
Math.toDegrees(roll)
)
);
// Get the StateCollection for this body part
String animPart = GltfBoneMapper.getAnimPartName(boneName);
if (animPart == null) return;
KeyframeAnimation.StateCollection part = getPartByName(builder, animPart);
KeyframeAnimation.StateCollection part = getPartByName(
builder,
animPart
);
if (part == null) return;
part.pitch.addKeyFrame(0, pitch, Ease.CONSTANT);
@@ -393,12 +490,12 @@ public final class GltfPoseConverter {
private static void convertLowerBone(
KeyframeAnimation.AnimationBuilder builder,
String boneName, Quaternionf deltaQ
String boneName,
Quaternionf deltaQ
) {
// Extract bend angle and axis from the delta quaternion
float angle = 2.0f * (float) Math.acos(
Math.min(1.0, Math.abs(deltaQ.w))
);
float angle =
2.0f * (float) Math.acos(Math.min(1.0, Math.abs(deltaQ.w)));
// Determine bend direction from axis
float bendDirection = 0.0f;
@@ -411,11 +508,14 @@ public final class GltfPoseConverter {
angle = -angle;
}
LOGGER.debug(String.format(
"[GltfPipeline] Lower bone '%s': bendAngle=%.1f° bendDir=%.1f°",
boneName,
Math.toDegrees(angle),
Math.toDegrees(bendDirection)));
LOGGER.debug(
String.format(
"[GltfPipeline] Lower bone '%s': bendAngle=%.1f° bendDir=%.1f°",
boneName,
Math.toDegrees(angle),
Math.toDegrees(bendDirection)
)
);
// Apply bend to the upper bone's StateCollection
String upperBone = GltfBoneMapper.getUpperBoneFor(boneName);
@@ -424,7 +524,10 @@ public final class GltfPoseConverter {
String animPart = GltfBoneMapper.getAnimPartName(upperBone);
if (animPart == null) return;
KeyframeAnimation.StateCollection part = getPartByName(builder, animPart);
KeyframeAnimation.StateCollection part = getPartByName(
builder,
animPart
);
if (part == null || !part.isBendable) return;
part.bend.addKeyFrame(0, angle, Ease.CONSTANT);
@@ -432,7 +535,8 @@ public final class GltfPoseConverter {
}
private static KeyframeAnimation.StateCollection getPartByName(
KeyframeAnimation.AnimationBuilder builder, String name
KeyframeAnimation.AnimationBuilder builder,
String name
) {
return switch (name) {
case "head" -> builder.head;
@@ -461,17 +565,32 @@ public final class GltfPoseConverter {
* @param partsWithKeyframes parts that received actual animation data from the GLB
*/
private static void enableSelectiveParts(
KeyframeAnimation.AnimationBuilder builder,
Set<String> ownedParts, Set<String> enabledParts,
Set<String> partsWithKeyframes) {
String[] allParts = {"head", "body", "rightArm", "leftArm", "rightLeg", "leftLeg"};
KeyframeAnimation.AnimationBuilder builder,
Set<String> ownedParts,
Set<String> enabledParts,
Set<String> partsWithKeyframes
) {
String[] allParts = {
"head",
"body",
"rightArm",
"leftArm",
"rightLeg",
"leftLeg",
};
for (String partName : allParts) {
KeyframeAnimation.StateCollection part = getPartByName(builder, partName);
KeyframeAnimation.StateCollection part = getPartByName(
builder,
partName
);
if (part != null) {
if (ownedParts.contains(partName)) {
// Always enable owned parts — the item controls these bones
part.fullyEnablePart(false);
} else if (enabledParts.contains(partName) && partsWithKeyframes.contains(partName)) {
} else if (
enabledParts.contains(partName) &&
partsWithKeyframes.contains(partName)
) {
// Free part WITH keyframes: enable so the GLB animation drives it
part.fullyEnablePart(false);
} else {

View File

@@ -24,17 +24,22 @@ import org.joml.Matrix4f;
*/
@OnlyIn(Dist.CLIENT)
public class GltfRenderLayer
extends RenderLayer<AbstractClientPlayer, PlayerModel<AbstractClientPlayer>> {
extends RenderLayer<AbstractClientPlayer, PlayerModel<AbstractClientPlayer>>
{
private static final Logger LOGGER = LogManager.getLogger("GltfPipeline");
private static final ResourceLocation CUFFS_MODEL =
ResourceLocation.fromNamespaceAndPath(
"tiedup", "models/gltf/v2/handcuffs/cuffs_prototype.glb"
"tiedup",
"models/gltf/v2/handcuffs/cuffs_prototype.glb"
);
public GltfRenderLayer(
RenderLayerParent<AbstractClientPlayer, PlayerModel<AbstractClientPlayer>> renderer
RenderLayerParent<
AbstractClientPlayer,
PlayerModel<AbstractClientPlayer>
> renderer
) {
super(renderer);
}
@@ -71,7 +76,9 @@ public class GltfRenderLayer
// Live path: read skeleton from HumanoidModel (after PlayerAnimator)
PlayerModel<AbstractClientPlayer> parentModel = this.getParentModel();
Matrix4f[] joints = GltfLiveBoneReader.computeJointMatricesFromModel(
parentModel, data, entity
parentModel,
data,
entity
);
if (joints == null) {
// Fallback to GLB-internal path if live reading fails
@@ -84,10 +91,15 @@ public class GltfRenderLayer
poseStack.translate(0, ALIGNMENT_Y, 0);
GltfMeshRenderer.renderSkinned(
data, joints, poseStack, buffer,
data,
joints,
poseStack,
buffer,
packedLight,
net.minecraft.client.renderer.entity.LivingEntityRenderer
.getOverlayCoords(entity, 0.0f)
net.minecraft.client.renderer.entity.LivingEntityRenderer.getOverlayCoords(
entity,
0.0f
)
);
poseStack.popPose();
}

View File

@@ -43,7 +43,9 @@ public final class GltfSkinningEngine {
* @return interpolated joint matrices ready for skinning
*/
public static Matrix4f[] computeJointMatricesAnimated(
GltfData data, GltfData.AnimationClip clip, float time
GltfData data,
GltfData.AnimationClip clip,
float time
) {
int jointCount = data.jointCount();
Matrix4f[] jointMatrices = new Matrix4f[jointCount];
@@ -59,14 +61,17 @@ public final class GltfSkinningEngine {
// Compose with parent
if (parents[j] >= 0 && worldTransforms[parents[j]] != null) {
worldTransforms[j] = new Matrix4f(worldTransforms[parents[j]]).mul(local);
worldTransforms[j] = new Matrix4f(
worldTransforms[parents[j]]
).mul(local);
} else {
worldTransforms[j] = new Matrix4f(local);
}
// Final joint matrix = worldTransform * inverseBindMatrix
jointMatrices[j] = new Matrix4f(worldTransforms[j])
.mul(data.inverseBindMatrices()[j]);
jointMatrices[j] = new Matrix4f(worldTransforms[j]).mul(
data.inverseBindMatrices()[j]
);
}
return jointMatrices;
@@ -75,7 +80,10 @@ public final class GltfSkinningEngine {
/**
* Internal: compute joint matrices from a specific animation clip.
*/
private static Matrix4f[] computeJointMatricesFromClip(GltfData data, GltfData.AnimationClip clip) {
private static Matrix4f[] computeJointMatricesFromClip(
GltfData data,
GltfData.AnimationClip clip
) {
int jointCount = data.jointCount();
Matrix4f[] jointMatrices = new Matrix4f[jointCount];
Matrix4f[] worldTransforms = new Matrix4f[jointCount];
@@ -90,14 +98,17 @@ public final class GltfSkinningEngine {
// Compose with parent
if (parents[j] >= 0 && worldTransforms[parents[j]] != null) {
worldTransforms[j] = new Matrix4f(worldTransforms[parents[j]]).mul(local);
worldTransforms[j] = new Matrix4f(
worldTransforms[parents[j]]
).mul(local);
} else {
worldTransforms[j] = new Matrix4f(local);
}
// Final joint matrix = worldTransform * inverseBindMatrix
jointMatrices[j] = new Matrix4f(worldTransforms[j])
.mul(data.inverseBindMatrices()[j]);
jointMatrices[j] = new Matrix4f(worldTransforms[j]).mul(
data.inverseBindMatrices()[j]
);
}
return jointMatrices;
@@ -107,9 +118,16 @@ public final class GltfSkinningEngine {
* Get the animation rotation for a joint (MC-converted).
* Falls back to rest rotation if no animation.
*/
private static Quaternionf getAnimRotation(GltfData data, GltfData.AnimationClip clip, int jointIndex) {
if (clip != null && jointIndex < clip.rotations().length
&& clip.rotations()[jointIndex] != null) {
private static Quaternionf getAnimRotation(
GltfData data,
GltfData.AnimationClip clip,
int jointIndex
) {
if (
clip != null &&
jointIndex < clip.rotations().length &&
clip.rotations()[jointIndex] != null
) {
return clip.rotations()[jointIndex][0]; // first frame
}
return data.restRotations()[jointIndex];
@@ -119,10 +137,17 @@ public final class GltfSkinningEngine {
* Get the animation translation for a joint (MC-converted).
* Falls back to rest translation if no animation translation exists.
*/
private static Vector3f getAnimTranslation(GltfData data, GltfData.AnimationClip clip, int jointIndex) {
if (clip != null && clip.translations() != null
&& jointIndex < clip.translations().length
&& clip.translations()[jointIndex] != null) {
private static Vector3f getAnimTranslation(
GltfData data,
GltfData.AnimationClip clip,
int jointIndex
) {
if (
clip != null &&
clip.translations() != null &&
jointIndex < clip.translations().length &&
clip.translations()[jointIndex] != null
) {
return clip.translations()[jointIndex][0]; // first frame
}
return data.restTranslations()[jointIndex];
@@ -144,10 +169,16 @@ public final class GltfSkinningEngine {
* @return new Quaternionf with the interpolated rotation (never mutates source data)
*/
private static Quaternionf getInterpolatedRotation(
GltfData data, GltfData.AnimationClip clip, int jointIndex, float time
GltfData data,
GltfData.AnimationClip clip,
int jointIndex,
float time
) {
if (clip == null || jointIndex >= clip.rotations().length
|| clip.rotations()[jointIndex] == null) {
if (
clip == null ||
jointIndex >= clip.rotations().length ||
clip.rotations()[jointIndex] == null
) {
// No animation data for this joint -- use rest pose (copy to avoid mutation)
Quaternionf rest = data.restRotations()[jointIndex];
return new Quaternionf(rest);
@@ -187,11 +218,17 @@ public final class GltfSkinningEngine {
* @return new Vector3f with the interpolated translation (never mutates source data)
*/
private static Vector3f getInterpolatedTranslation(
GltfData data, GltfData.AnimationClip clip, int jointIndex, float time
GltfData data,
GltfData.AnimationClip clip,
int jointIndex,
float time
) {
if (clip == null || clip.translations() == null
|| jointIndex >= clip.translations().length
|| clip.translations()[jointIndex] == null) {
if (
clip == null ||
clip.translations() == null ||
jointIndex >= clip.translations().length ||
clip.translations()[jointIndex] == null
) {
// No animation data for this joint -- use rest pose (copy to avoid mutation)
Vector3f rest = data.restTranslations()[jointIndex];
return new Vector3f(rest);
@@ -232,9 +269,13 @@ public final class GltfSkinningEngine {
* @param tmpNorm pre-allocated scratch Vector4f for normal transforms
*/
public static void skinVertex(
GltfData data, int vertexIdx, Matrix4f[] jointMatrices,
float[] outPos, float[] outNormal,
Vector4f tmpPos, Vector4f tmpNorm
GltfData data,
int vertexIdx,
Matrix4f[] jointMatrices,
float[] outPos,
float[] outNormal,
Vector4f tmpPos,
Vector4f tmpNorm
) {
float[] positions = data.positions();
float[] normals = data.normals();
@@ -252,8 +293,12 @@ public final class GltfSkinningEngine {
float nz = normals[vertexIdx * 3 + 2];
// LBS: v_skinned = Σ(w[i] * jointMatrix[j[i]] * v_rest)
float sx = 0, sy = 0, sz = 0;
float snx = 0, sny = 0, snz = 0;
float sx = 0,
sy = 0,
sz = 0;
float snx = 0,
sny = 0,
snz = 0;
for (int i = 0; i < 4; i++) {
int ji = joints[vertexIdx * 4 + i];