Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
package com.tiedup.remake.client.gltf;
|
||||
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Quaternionf;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/**
|
||||
* CPU-based Linear Blend Skinning (LBS) engine.
|
||||
* Computes joint matrices purely from glTF data (rest translations + animation rotations).
|
||||
* All data is in MC-converted space (consistent with IBMs and vertex positions).
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public final class GltfSkinningEngine {
|
||||
|
||||
private GltfSkinningEngine() {}
|
||||
|
||||
/**
|
||||
* Compute joint matrices from glTF animation/rest data (default animation).
|
||||
* Each joint matrix = worldTransform * inverseBindMatrix.
|
||||
* Uses MC-converted glTF data throughout for consistency.
|
||||
*
|
||||
* @param data parsed glTF data (MC-converted)
|
||||
* @return array of joint matrices ready for skinning
|
||||
*/
|
||||
public static Matrix4f[] computeJointMatrices(GltfData data) {
|
||||
return computeJointMatricesFromClip(data, data.animation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute joint matrices with frame interpolation for animated entities.
|
||||
* Uses SLERP for rotations and LERP for translations between adjacent keyframes.
|
||||
*
|
||||
* <p>The {@code time} parameter is in frame-space: 0.0 corresponds to the first
|
||||
* keyframe and {@code frameCount - 1} to the last. Values between integer frames
|
||||
* are interpolated. Out-of-range values are clamped.</p>
|
||||
*
|
||||
* @param data the parsed glTF data (MC-converted)
|
||||
* @param clip the animation clip to sample (null = rest pose for all joints)
|
||||
* @param time time in frame-space (0.0 = first frame, N-1 = last frame)
|
||||
* @return interpolated joint matrices ready for skinning
|
||||
*/
|
||||
public static Matrix4f[] computeJointMatricesAnimated(
|
||||
GltfData data, GltfData.AnimationClip clip, float time
|
||||
) {
|
||||
int jointCount = data.jointCount();
|
||||
Matrix4f[] jointMatrices = new Matrix4f[jointCount];
|
||||
Matrix4f[] worldTransforms = new Matrix4f[jointCount];
|
||||
|
||||
int[] parents = data.parentJointIndices();
|
||||
|
||||
for (int j = 0; j < jointCount; j++) {
|
||||
// Build local transform: translate(interpT) * rotate(interpQ)
|
||||
Matrix4f local = new Matrix4f();
|
||||
local.translate(getInterpolatedTranslation(data, clip, j, time));
|
||||
local.rotate(getInterpolatedRotation(data, clip, j, time));
|
||||
|
||||
// Compose with parent
|
||||
if (parents[j] >= 0 && worldTransforms[parents[j]] != null) {
|
||||
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]);
|
||||
}
|
||||
|
||||
return jointMatrices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: compute joint matrices from a specific animation clip.
|
||||
*/
|
||||
private static Matrix4f[] computeJointMatricesFromClip(GltfData data, GltfData.AnimationClip clip) {
|
||||
int jointCount = data.jointCount();
|
||||
Matrix4f[] jointMatrices = new Matrix4f[jointCount];
|
||||
Matrix4f[] worldTransforms = new Matrix4f[jointCount];
|
||||
|
||||
int[] parents = data.parentJointIndices();
|
||||
|
||||
for (int j = 0; j < jointCount; j++) {
|
||||
// Build local transform: translate(animT or restT) * rotate(animQ or restQ)
|
||||
Matrix4f local = new Matrix4f();
|
||||
local.translate(getAnimTranslation(data, clip, j));
|
||||
local.rotate(getAnimRotation(data, clip, j));
|
||||
|
||||
// Compose with parent
|
||||
if (parents[j] >= 0 && worldTransforms[parents[j]] != null) {
|
||||
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]);
|
||||
}
|
||||
|
||||
return jointMatrices;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return clip.rotations()[jointIndex][0]; // first frame
|
||||
}
|
||||
return data.restRotations()[jointIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return clip.translations()[jointIndex][0]; // first frame
|
||||
}
|
||||
return data.restTranslations()[jointIndex];
|
||||
}
|
||||
|
||||
// ---- Interpolated accessors (for computeJointMatricesAnimated) ----
|
||||
|
||||
/**
|
||||
* Get an interpolated rotation for a joint at a fractional frame time.
|
||||
* Uses SLERP between the two bounding keyframes.
|
||||
*
|
||||
* <p>Falls back to rest rotation when the clip is null or has no rotation
|
||||
* data for the given joint. A single-frame channel returns that frame directly.</p>
|
||||
*
|
||||
* @param data parsed glTF data
|
||||
* @param clip animation clip (may be null)
|
||||
* @param jointIndex joint to query
|
||||
* @param time frame-space time (clamped internally)
|
||||
* @return new Quaternionf with the interpolated rotation (never mutates source data)
|
||||
*/
|
||||
private static Quaternionf getInterpolatedRotation(
|
||||
GltfData data, GltfData.AnimationClip clip, int jointIndex, float time
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
Quaternionf[] frames = clip.rotations()[jointIndex];
|
||||
if (frames.length == 1) {
|
||||
return new Quaternionf(frames[0]);
|
||||
}
|
||||
|
||||
// Clamp time to valid range [0, frameCount-1]
|
||||
float clamped = Math.max(0.0f, Math.min(time, frames.length - 1));
|
||||
int f0 = (int) Math.floor(clamped);
|
||||
int f1 = Math.min(f0 + 1, frames.length - 1);
|
||||
float alpha = clamped - f0;
|
||||
|
||||
if (alpha < 1e-6f || f0 == f1) {
|
||||
return new Quaternionf(frames[f0]);
|
||||
}
|
||||
|
||||
// SLERP: create a copy of frame0 and slerp toward frame1
|
||||
return new Quaternionf(frames[f0]).slerp(frames[f1], alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an interpolated translation for a joint at a fractional frame time.
|
||||
* Uses LERP between the two bounding keyframes.
|
||||
*
|
||||
* <p>Falls back to rest translation when the clip is null, the clip has no
|
||||
* translation data at all, or has no translation data for the given joint.
|
||||
* A single-frame channel returns that frame directly.</p>
|
||||
*
|
||||
* @param data parsed glTF data
|
||||
* @param clip animation clip (may be null)
|
||||
* @param jointIndex joint to query
|
||||
* @param time frame-space time (clamped internally)
|
||||
* @return new Vector3f with the interpolated translation (never mutates source data)
|
||||
*/
|
||||
private static Vector3f getInterpolatedTranslation(
|
||||
GltfData data, GltfData.AnimationClip clip, int jointIndex, float time
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
Vector3f[] frames = clip.translations()[jointIndex];
|
||||
if (frames.length == 1) {
|
||||
return new Vector3f(frames[0]);
|
||||
}
|
||||
|
||||
// Clamp time to valid range [0, frameCount-1]
|
||||
float clamped = Math.max(0.0f, Math.min(time, frames.length - 1));
|
||||
int f0 = (int) Math.floor(clamped);
|
||||
int f1 = Math.min(f0 + 1, frames.length - 1);
|
||||
float alpha = clamped - f0;
|
||||
|
||||
if (alpha < 1e-6f || f0 == f1) {
|
||||
return new Vector3f(frames[f0]);
|
||||
}
|
||||
|
||||
// LERP: create a copy of frame0 and lerp toward frame1
|
||||
return new Vector3f(frames[f0]).lerp(frames[f1], alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skin a single vertex using Linear Blend Skinning.
|
||||
*
|
||||
* <p>Callers should pre-allocate {@code tmpPos} and {@code tmpNorm} and reuse
|
||||
* them across all vertices in a mesh to avoid per-vertex allocations (12k+
|
||||
* allocations per frame for a typical mesh).</p>
|
||||
*
|
||||
* @param data parsed glTF data
|
||||
* @param vertexIdx index into the vertex arrays
|
||||
* @param jointMatrices joint matrices from computeJointMatrices
|
||||
* @param outPos output skinned position (3 floats)
|
||||
* @param outNormal output skinned normal (3 floats)
|
||||
* @param tmpPos pre-allocated scratch Vector4f for position transforms
|
||||
* @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
|
||||
) {
|
||||
float[] positions = data.positions();
|
||||
float[] normals = data.normals();
|
||||
int[] joints = data.joints();
|
||||
float[] weights = data.weights();
|
||||
|
||||
// Rest position
|
||||
float vx = positions[vertexIdx * 3];
|
||||
float vy = positions[vertexIdx * 3 + 1];
|
||||
float vz = positions[vertexIdx * 3 + 2];
|
||||
|
||||
// Rest normal
|
||||
float nx = normals[vertexIdx * 3];
|
||||
float ny = normals[vertexIdx * 3 + 1];
|
||||
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;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int ji = joints[vertexIdx * 4 + i];
|
||||
float w = weights[vertexIdx * 4 + i];
|
||||
if (w <= 0.0f || ji >= jointMatrices.length) continue;
|
||||
|
||||
Matrix4f jm = jointMatrices[ji];
|
||||
|
||||
// Transform position
|
||||
tmpPos.set(vx, vy, vz, 1.0f);
|
||||
jm.transform(tmpPos);
|
||||
sx += w * tmpPos.x;
|
||||
sy += w * tmpPos.y;
|
||||
sz += w * tmpPos.z;
|
||||
|
||||
// Transform normal (ignore translation)
|
||||
tmpNorm.set(nx, ny, nz, 0.0f);
|
||||
jm.transform(tmpNorm);
|
||||
snx += w * tmpNorm.x;
|
||||
sny += w * tmpNorm.y;
|
||||
snz += w * tmpNorm.z;
|
||||
}
|
||||
|
||||
outPos[0] = sx;
|
||||
outPos[1] = sy;
|
||||
outPos[2] = sz;
|
||||
|
||||
// Normalize the normal
|
||||
float len = (float) Math.sqrt(snx * snx + sny * sny + snz * snz);
|
||||
if (len > 0.0001f) {
|
||||
outNormal[0] = snx / len;
|
||||
outNormal[1] = sny / len;
|
||||
outNormal[2] = snz / len;
|
||||
} else {
|
||||
outNormal[0] = 0;
|
||||
outNormal[1] = 1;
|
||||
outNormal[2] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user