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. * *

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.

* *

All methods are pure functions (no state, no side effects).

*/ 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 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 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; } } } } } }