perf(gltf): add skinning cache — skip re-skinning when pose is unchanged
This commit is contained in:
@@ -96,6 +96,7 @@ public final class GltfClientSetup {
|
|||||||
ProfilerFiller profiler
|
ProfilerFiller profiler
|
||||||
) {
|
) {
|
||||||
GltfCache.clearCache();
|
GltfCache.clearCache();
|
||||||
|
GltfSkinCache.clearAll();
|
||||||
GltfAnimationApplier.invalidateCache();
|
GltfAnimationApplier.invalidateCache();
|
||||||
GltfMeshRenderer.clearRenderTypeCache();
|
GltfMeshRenderer.clearRenderTypeCache();
|
||||||
// Reload context GLB animations from resource packs FIRST,
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,6 +225,101 @@ public final class GltfMeshRenderer extends RenderStateShard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two-pass skinned renderer with cache support.
|
||||||
|
*
|
||||||
|
* <p><b>Pass 1</b> (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.</p>
|
||||||
|
*
|
||||||
|
* <p><b>Pass 2</b> (always): iterate the index buffer, read skinned data from
|
||||||
|
* the arrays, and emit to the {@link VertexConsumer}.</p>
|
||||||
|
*
|
||||||
|
* @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.
|
* Render a skinned glTF mesh with per-primitive tint colors.
|
||||||
*
|
*
|
||||||
|
|||||||
121
src/main/java/com/tiedup/remake/client/gltf/GltfSkinCache.java
Normal file
121
src/main/java/com/tiedup/remake/client/gltf/GltfSkinCache.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>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).
|
||||||
|
*
|
||||||
|
* <p>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<CacheKey, Entry> cache = new HashMap<>();
|
||||||
|
|
||||||
|
private GltfSkinCache() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the pose inputs are bit-identical to the last cached skinning pass.
|
||||||
|
*
|
||||||
|
* @param entityId the entity's numeric ID
|
||||||
|
* @param modelLoc the model ResourceLocation (distinguishes multiple items on one entity)
|
||||||
|
* @param currentInputs flat array of float inputs that drove joint matrix computation
|
||||||
|
* @return true if every input bit matches the cached entry (safe to reuse cached data)
|
||||||
|
*/
|
||||||
|
public static boolean isPoseUnchanged(
|
||||||
|
int entityId,
|
||||||
|
ResourceLocation modelLoc,
|
||||||
|
float[] currentInputs
|
||||||
|
) {
|
||||||
|
CacheKey key = new CacheKey(entityId, modelLoc);
|
||||||
|
Entry entry = cache.get(key);
|
||||||
|
if (entry == null || entry.lastInputBits == null) return false;
|
||||||
|
if (entry.lastInputBits.length != currentInputs.length) return false;
|
||||||
|
for (int i = 0; i < currentInputs.length; i++) {
|
||||||
|
if (
|
||||||
|
entry.lastInputBits[i]
|
||||||
|
!= Float.floatToRawIntBits(currentInputs[i])
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a skinning result in the cache, replacing any previous entry for the same key.
|
||||||
|
*
|
||||||
|
* @param entityId the entity's numeric ID
|
||||||
|
* @param modelLoc the model ResourceLocation
|
||||||
|
* @param poseInputs the float inputs that produced these results (will be bit-snapshotted)
|
||||||
|
* @param positions skinned vertex positions (will be cloned)
|
||||||
|
* @param normals skinned vertex normals (will be cloned)
|
||||||
|
*/
|
||||||
|
public static void store(
|
||||||
|
int entityId,
|
||||||
|
ResourceLocation modelLoc,
|
||||||
|
float[] poseInputs,
|
||||||
|
float[] positions,
|
||||||
|
float[] normals
|
||||||
|
) {
|
||||||
|
CacheKey key = new CacheKey(entityId, modelLoc);
|
||||||
|
Entry entry = cache.computeIfAbsent(key, k -> new Entry());
|
||||||
|
entry.lastInputBits = new int[poseInputs.length];
|
||||||
|
for (int i = 0; i < poseInputs.length; i++) {
|
||||||
|
entry.lastInputBits[i] = Float.floatToRawIntBits(poseInputs[i]);
|
||||||
|
}
|
||||||
|
entry.skinnedPositions = positions.clone();
|
||||||
|
entry.skinnedNormals = normals.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve cached skinned positions, or null if no cache entry exists.
|
||||||
|
*/
|
||||||
|
public static float[] getCachedPositions(
|
||||||
|
int entityId,
|
||||||
|
ResourceLocation modelLoc
|
||||||
|
) {
|
||||||
|
Entry entry = cache.get(new CacheKey(entityId, modelLoc));
|
||||||
|
return entry != null ? entry.skinnedPositions : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve cached skinned normals, or null if no cache entry exists.
|
||||||
|
*/
|
||||||
|
public static float[] getCachedNormals(
|
||||||
|
int entityId,
|
||||||
|
ResourceLocation modelLoc
|
||||||
|
) {
|
||||||
|
Entry entry = cache.get(new CacheKey(entityId, modelLoc));
|
||||||
|
return entry != null ? entry.skinnedNormals : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear the entire cache (called on resource reload). */
|
||||||
|
public static void clearAll() {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove all cache entries for a specific entity (called on entity leave). */
|
||||||
|
public static void removeEntity(int entityId) {
|
||||||
|
cache.entrySet().removeIf(e -> e.getKey().entityId == entityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user