Strip all Phase references, TODO/FUTURE roadmap notes, and internal planning comments from the codebase. Run Prettier for consistent formatting across all Java files.
299 lines
10 KiB
Java
299 lines
10 KiB
Java
package com.tiedup.remake.client.gltf;
|
|
|
|
import com.google.gson.JsonArray;
|
|
import com.google.gson.JsonObject;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.List;
|
|
import org.joml.Quaternionf;
|
|
import org.joml.Vector3f;
|
|
|
|
/**
|
|
* Shared stateless utilities for parsing binary glTF (.glb) files.
|
|
*
|
|
* <p>These methods are used by both {@link GlbParser} (single-armature bondage meshes)
|
|
* and {@link com.tiedup.remake.v2.furniture.client.FurnitureGlbParser FurnitureGlbParser}
|
|
* (multi-armature furniture meshes). Extracted to eliminate ~160 lines of verbatim
|
|
* duplication between the two parsers.</p>
|
|
*
|
|
* <p>All methods are pure functions (no state, no side effects).</p>
|
|
*/
|
|
public final class GlbParserUtils {
|
|
|
|
// glTF component type constants
|
|
public static final int BYTE = 5120;
|
|
public static final int UNSIGNED_BYTE = 5121;
|
|
public static final int SHORT = 5122;
|
|
public static final int UNSIGNED_SHORT = 5123;
|
|
public static final int UNSIGNED_INT = 5125;
|
|
public static final int FLOAT = 5126;
|
|
|
|
private GlbParserUtils() {}
|
|
|
|
// ---- Material name parsing ----
|
|
|
|
/**
|
|
* Parse the root "materials" array and extract each material's "name" field.
|
|
* Returns an empty array if no materials are present.
|
|
*/
|
|
public static String[] parseMaterialNames(JsonObject root) {
|
|
if (!root.has("materials") || !root.get("materials").isJsonArray()) {
|
|
return new String[0];
|
|
}
|
|
JsonArray materials = root.getAsJsonArray("materials");
|
|
String[] names = new String[materials.size()];
|
|
for (int i = 0; i < materials.size(); i++) {
|
|
JsonObject mat = materials.get(i).getAsJsonObject();
|
|
names[i] = mat.has("name") ? mat.get("name").getAsString() : null;
|
|
}
|
|
return names;
|
|
}
|
|
|
|
// ---- Array flattening utilities ----
|
|
|
|
public static float[] flattenFloats(List<float[]> arrays) {
|
|
int total = 0;
|
|
for (float[] a : arrays) total += a.length;
|
|
float[] result = new float[total];
|
|
int offset = 0;
|
|
for (float[] a : arrays) {
|
|
System.arraycopy(a, 0, result, offset, a.length);
|
|
offset += a.length;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public static int[] flattenInts(List<int[]> arrays) {
|
|
int total = 0;
|
|
for (int[] a : arrays) total += a.length;
|
|
int[] result = new int[total];
|
|
int offset = 0;
|
|
for (int[] a : arrays) {
|
|
System.arraycopy(a, 0, result, offset, a.length);
|
|
offset += a.length;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ---- Accessor reading utilities ----
|
|
|
|
public static float[] readFloatAccessor(
|
|
JsonArray accessors,
|
|
JsonArray bufferViews,
|
|
ByteBuffer binData,
|
|
int accessorIdx
|
|
) {
|
|
JsonObject accessor = accessors.get(accessorIdx).getAsJsonObject();
|
|
int count = accessor.get("count").getAsInt();
|
|
int componentType = accessor.get("componentType").getAsInt();
|
|
String type = accessor.get("type").getAsString();
|
|
int components = typeComponents(type);
|
|
|
|
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 totalElements = count * components;
|
|
float[] result = new float[totalElements];
|
|
|
|
int componentSize = componentByteSize(componentType);
|
|
int stride = byteStride > 0 ? byteStride : components * componentSize;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
int pos = byteOffset + i * stride;
|
|
for (int c = 0; c < components; c++) {
|
|
binData.position(pos + c * componentSize);
|
|
result[i * components + c] = readComponentAsFloat(
|
|
binData,
|
|
componentType
|
|
);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public static int[] readIntAccessor(
|
|
JsonArray accessors,
|
|
JsonArray bufferViews,
|
|
ByteBuffer binData,
|
|
int accessorIdx
|
|
) {
|
|
JsonObject accessor = accessors.get(accessorIdx).getAsJsonObject();
|
|
int count = accessor.get("count").getAsInt();
|
|
int componentType = accessor.get("componentType").getAsInt();
|
|
String type = accessor.get("type").getAsString();
|
|
int components = typeComponents(type);
|
|
|
|
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 totalElements = count * components;
|
|
int[] result = new int[totalElements];
|
|
|
|
int componentSize = componentByteSize(componentType);
|
|
int stride = byteStride > 0 ? byteStride : components * componentSize;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
int pos = byteOffset + i * stride;
|
|
for (int c = 0; c < components; c++) {
|
|
binData.position(pos + c * componentSize);
|
|
result[i * components + c] = readComponentAsInt(
|
|
binData,
|
|
componentType
|
|
);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
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
|
|
);
|
|
};
|
|
}
|
|
|
|
public static int readComponentAsInt(ByteBuffer buf, int componentType) {
|
|
return switch (componentType) {
|
|
case BYTE -> buf.get();
|
|
case UNSIGNED_BYTE -> buf.get() & 0xFF;
|
|
case SHORT -> buf.getShort();
|
|
case UNSIGNED_SHORT -> buf.getShort() & 0xFFFF;
|
|
case UNSIGNED_INT -> buf.getInt();
|
|
case FLOAT -> (int) buf.getFloat();
|
|
default -> throw new IllegalArgumentException(
|
|
"Unknown component type: " + componentType
|
|
);
|
|
};
|
|
}
|
|
|
|
public static int typeComponents(String type) {
|
|
return switch (type) {
|
|
case "SCALAR" -> 1;
|
|
case "VEC2" -> 2;
|
|
case "VEC3" -> 3;
|
|
case "VEC4" -> 4;
|
|
case "MAT4" -> 16;
|
|
default -> throw new IllegalArgumentException(
|
|
"Unknown accessor type: " + type
|
|
);
|
|
};
|
|
}
|
|
|
|
public static int componentByteSize(int componentType) {
|
|
return switch (componentType) {
|
|
case BYTE, UNSIGNED_BYTE -> 1;
|
|
case SHORT, UNSIGNED_SHORT -> 2;
|
|
case UNSIGNED_INT, FLOAT -> 4;
|
|
default -> throw new IllegalArgumentException(
|
|
"Unknown component type: " + componentType
|
|
);
|
|
};
|
|
}
|
|
|
|
// ---- Deep-copy utility ----
|
|
|
|
/**
|
|
* 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][];
|
|
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]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Vector3f[][] rawTranslations = null;
|
|
if (clip.translations() != null) {
|
|
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];
|
|
for (int f = 0; f < clip.translations()[j].length; f++) {
|
|
rawTranslations[j][f] = new Vector3f(
|
|
clip.translations()[j][f]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return new GltfData.AnimationClip(
|
|
clip.timestamps().clone(),
|
|
rawRotations,
|
|
rawTranslations,
|
|
clip.frameCount()
|
|
);
|
|
}
|
|
|
|
// ---- Coordinate system conversion ----
|
|
|
|
/**
|
|
* 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
|
|
) {
|
|
if (clip == null) return;
|
|
|
|
Quaternionf[][] rotations = clip.rotations();
|
|
for (int j = 0; j < jointCount; j++) {
|
|
if (j < rotations.length && rotations[j] != null) {
|
|
for (Quaternionf q : rotations[j]) {
|
|
q.x = -q.x;
|
|
q.y = -q.y;
|
|
}
|
|
}
|
|
}
|
|
|
|
Vector3f[][] translations = clip.translations();
|
|
if (translations != null) {
|
|
for (int j = 0; j < jointCount; j++) {
|
|
if (j < translations.length && translations[j] != null) {
|
|
for (Vector3f t : translations[j]) {
|
|
t.x = -t.x;
|
|
t.y = -t.y;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|