diff --git a/src/main/java/com/tiedup/remake/client/gltf/GltfClientSetup.java b/src/main/java/com/tiedup/remake/client/gltf/GltfClientSetup.java index 7c2b204..8868887 100644 --- a/src/main/java/com/tiedup/remake/client/gltf/GltfClientSetup.java +++ b/src/main/java/com/tiedup/remake/client/gltf/GltfClientSetup.java @@ -96,6 +96,7 @@ public final class GltfClientSetup { ProfilerFiller profiler ) { GltfCache.clearCache(); + GltfSkinCache.clearAll(); GltfAnimationApplier.invalidateCache(); GltfMeshRenderer.clearRenderTypeCache(); // Reload context GLB animations from resource packs FIRST, @@ -120,4 +121,24 @@ public final class GltfClientSetup { } } + /** + * FORGE bus event subscribers for entity lifecycle cleanup. + * Removes skin cache entries when entities leave the level, preventing memory leaks. + */ + @Mod.EventBusSubscriber( + modid = "tiedup", + bus = Mod.EventBusSubscriber.Bus.FORGE, + value = Dist.CLIENT + ) + public static class ForgeBusEvents { + + @SubscribeEvent + public static void onEntityLeaveLevel( + net.minecraftforge.event.entity.EntityLeaveLevelEvent event + ) { + if (event.getLevel().isClientSide()) { + GltfSkinCache.removeEntity(event.getEntity().getId()); + } + } + } } diff --git a/src/main/java/com/tiedup/remake/client/gltf/GltfMeshRenderer.java b/src/main/java/com/tiedup/remake/client/gltf/GltfMeshRenderer.java index e7a9c5b..d25e706 100644 --- a/src/main/java/com/tiedup/remake/client/gltf/GltfMeshRenderer.java +++ b/src/main/java/com/tiedup/remake/client/gltf/GltfMeshRenderer.java @@ -225,6 +225,101 @@ public final class GltfMeshRenderer extends RenderStateShard { } } + /** + * Two-pass skinned renderer with cache support. + * + *
Pass 1 (skippable): if {@code cachedPositions} is null, skin every + * unique vertex into flat {@code float[]} arrays (positions and normals). + * If cached arrays are provided, Pass 1 is skipped entirely.
+ * + *Pass 2 (always): iterate the index buffer, read skinned data from + * the arrays, and emit to the {@link VertexConsumer}.
+ * + * @param data parsed glTF data + * @param jointMatrices computed joint matrices from skinning engine + * @param poseStack current pose stack + * @param buffer multi-buffer source + * @param packedLight packed light value + * @param packedOverlay packed overlay value + * @param renderType the RenderType to use for rendering + * @param cachedPositions previously cached skinned positions, or null to compute fresh + * @param cachedNormals previously cached skinned normals, or null to compute fresh + * @return {@code new float[][] { positions, normals }} for the caller to cache + */ + public static float[][] renderSkinnedWithCache( + GltfData data, + Matrix4f[] jointMatrices, + PoseStack poseStack, + MultiBufferSource buffer, + int packedLight, + int packedOverlay, + RenderType renderType, + float[] cachedPositions, + float[] cachedNormals + ) { + int vertexCount = data.vertexCount(); + float[] positions; + float[] normals; + + // -- Pass 1: Skin all unique vertices (skipped when cache hit) -- + if (cachedPositions != null && cachedNormals != null) { + positions = cachedPositions; + normals = cachedNormals; + } else { + positions = new float[vertexCount * 3]; + normals = new float[vertexCount * 3]; + + float[] outPos = new float[3]; + float[] outNormal = new float[3]; + Vector4f tmpPos = new Vector4f(); + Vector4f tmpNorm = new Vector4f(); + + for (int v = 0; v < vertexCount; v++) { + GltfSkinningEngine.skinVertex( + data, v, jointMatrices, outPos, outNormal, tmpPos, tmpNorm + ); + positions[v * 3] = outPos[0]; + positions[v * 3 + 1] = outPos[1]; + positions[v * 3 + 2] = outPos[2]; + normals[v * 3] = outNormal[0]; + normals[v * 3 + 1] = outNormal[1]; + normals[v * 3 + 2] = outNormal[2]; + } + } + + // -- Pass 2: Emit vertices from arrays to VertexConsumer -- + Matrix4f pose = poseStack.last().pose(); + Matrix3f normalMat = poseStack.last().normal(); + VertexConsumer vc = buffer.getBuffer(renderType); + + int[] indices = data.indices(); + float[] texCoords = data.texCoords(); + + for (int idx : indices) { + float px = positions[idx * 3]; + float py = positions[idx * 3 + 1]; + float pz = positions[idx * 3 + 2]; + + float nx = normals[idx * 3]; + float ny = normals[idx * 3 + 1]; + float nz = normals[idx * 3 + 2]; + + float u = texCoords[idx * 2]; + float v = texCoords[idx * 2 + 1]; + + vc + .vertex(pose, px, py, pz) + .color(255, 255, 255, 255) + .uv(u, 1.0f - v) + .overlayCoords(packedOverlay) + .uv2(packedLight) + .normal(normalMat, nx, ny, nz) + .endVertex(); + } + + return new float[][] { positions, normals }; + } + /** * Render a skinned glTF mesh with per-primitive tint colors. * diff --git a/src/main/java/com/tiedup/remake/client/gltf/GltfSkinCache.java b/src/main/java/com/tiedup/remake/client/gltf/GltfSkinCache.java new file mode 100644 index 0000000..5360600 --- /dev/null +++ b/src/main/java/com/tiedup/remake/client/gltf/GltfSkinCache.java @@ -0,0 +1,121 @@ +package com.tiedup.remake.client.gltf; + +import java.util.HashMap; +import java.util.Map; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +/** + * Skinning result cache that avoids re-skinning when a player's pose hasn't changed. + * + *Uses a dirty-flag approach: each cache entry stores the raw int bits of + * every float input (joint euler angles) that drove the last skinning pass. + * On the next frame, if all bits match exactly, the cached skinned positions + * and normals are reused (skipping the expensive LBS loop). + * + *
Bit comparison via {@link Float#floatToRawIntBits(float)} avoids epsilon
+ * drift: a pose is "unchanged" only when the inputs are identical down to the
+ * bit (idle, AFK, paused animation frame).
+ */
+@OnlyIn(Dist.CLIENT)
+public final class GltfSkinCache {
+
+ private record CacheKey(int entityId, ResourceLocation modelLoc) {}
+
+ private static final class Entry {
+ int[] lastInputBits;
+ float[] skinnedPositions;
+ float[] skinnedNormals;
+ }
+
+ private static final Map