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
|
||||
) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
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