diff --git a/src/main/java/io/github/cottonmc/templates/model/MeshTransformUtil.java b/src/main/java/io/github/cottonmc/templates/model/MeshTransformUtil.java index 50cfa5b..dd146cd 100644 --- a/src/main/java/io/github/cottonmc/templates/model/MeshTransformUtil.java +++ b/src/main/java/io/github/cottonmc/templates/model/MeshTransformUtil.java @@ -1,13 +1,11 @@ package io.github.cottonmc.templates.model; import io.github.cottonmc.templates.TemplatesClient; -import net.fabricmc.fabric.api.renderer.v1.Renderer; import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh; import net.fabricmc.fabric.api.renderer.v1.mesh.MeshBuilder; import net.fabricmc.fabric.api.renderer.v1.mesh.QuadEmitter; import net.fabricmc.fabric.api.renderer.v1.render.RenderContext; import net.minecraft.client.render.model.ModelBakeSettings; -import net.minecraft.util.math.AffineTransformation; import net.minecraft.util.math.Direction; import org.joml.Matrix4f; import org.joml.Vector3f; @@ -17,51 +15,16 @@ import java.util.EnumMap; import java.util.Map; public class MeshTransformUtil { - public static Mesh aroundCenter(Mesh oldMesh, ModelBakeSettings settings) { - return aroundCenter(oldMesh, settings.getRotation().getMatrix()); - } - - public static Mesh aroundCenter(Mesh oldMesh, Matrix4f mat) { - Map facePermutation = facePermutation(mat); + public static Mesh pretransformMesh(Mesh mesh, RenderContext.QuadTransform transform) { + MeshBuilder builder = TemplatesClient.getFabricRenderer().meshBuilder(); + QuadEmitter emitter = builder.getEmitter(); - Renderer r = TemplatesClient.getFabricRenderer(); - MeshBuilder newMesh = r.meshBuilder(); - QuadEmitter emitter = newMesh.getEmitter(); - - //re-used buffers - Vector3f pos3 = new Vector3f(); - Vector4f pos4 = new Vector4f(); - - oldMesh.forEach(oldQuad -> { - //Initialize the new quad - emitter.copyFrom(oldQuad); - - //For each vertex: - for(int i = 0; i < 4; i++) { - //Copy pos into a vec3, then a vec4. the w component is set to 0 since this is a point, not a normal - emitter.copyPos(i, pos3); - pos3.add(-0.5f, -0.5f, -0.5f); - pos4.set(pos3, 0); - - //Compute the matrix-vector product. This function mutates the vec4 in-place. - //Note that `transformAffine` has the same purpose as `transform`; the difference is it - //assumes (without checking) that the last row of the matrix is 0,0,0,1, as an optimization - mat.transform(pos4); - - //Manually copy the data back onto the vertex - emitter.pos(i, pos4.x + 0.5f, pos4.y + 0.5f, pos4.z + 0.5f); - } - - emitter.nominalFace(facePermutation.get(emitter.lightFace())); - - Direction cull = emitter.cullFace(); - if(cull != null) emitter.cullFace(facePermutation.get(cull)); - - //Output the quad - emitter.emit(); + mesh.forEach(quad -> { + emitter.copyFrom(quad); + if(transform.transform(emitter)) emitter.emit(); }); - return newMesh.build(); + return builder.build(); } //Hard to explain what this is for... @@ -86,15 +49,39 @@ public class MeshTransformUtil { return facePermutation; } - public static Mesh pretransformMesh(Mesh mesh, RenderContext.QuadTransform transform) { - MeshBuilder builder = TemplatesClient.getFabricRenderer().meshBuilder(); - QuadEmitter emitter = builder.getEmitter(); + public static RenderContext.QuadTransform applyAffine(ModelBakeSettings settings) { + return applyMatrix(settings.getRotation().getMatrix()); + } + + public static RenderContext.QuadTransform applyMatrix(Matrix4f mat) { + Map facePermutation = facePermutation(mat); + Vector3f pos3 = new Vector3f(); + Vector4f pos4 = new Vector4f(); - mesh.forEach(quad -> { - emitter.copyFrom(quad); - if(transform.transform(emitter)) emitter.emit(); - }); - - return builder.build(); + return quad -> { + //For each vertex: + for(int i = 0; i < 4; i++) { + //Copy pos into a vec3, then a vec4. the w component is set to 0 since this is a point, not a normal + quad.copyPos(i, pos3); + pos3.add(-0.5f, -0.5f, -0.5f); + pos4.set(pos3, 0); + + //Compute the matrix-vector product. This function mutates the vec4 in-place. + //Note that `transformAffine` has the same purpose as `transform`; the difference is it + //assumes (without checking) that the last row of the matrix is 0,0,0,1, as an optimization + mat.transform(pos4); + + //Manually copy the data back onto the vertex + quad.pos(i, pos4.x + 0.5f, pos4.y + 0.5f, pos4.z + 0.5f); + } + + quad.nominalFace(facePermutation.get(quad.lightFace())); + + Direction cull = quad.cullFace(); + if(cull != null) quad.cullFace(facePermutation.get(cull)); + + //Output the quad + return true; + }; } } diff --git a/src/main/java/io/github/cottonmc/templates/model/QuadUvBounds.java b/src/main/java/io/github/cottonmc/templates/model/QuadUvBounds.java new file mode 100644 index 0000000..d6aaa40 --- /dev/null +++ b/src/main/java/io/github/cottonmc/templates/model/QuadUvBounds.java @@ -0,0 +1,44 @@ +package io.github.cottonmc.templates.model; + +import net.fabricmc.fabric.api.renderer.v1.mesh.MutableQuadView; +import net.fabricmc.fabric.api.renderer.v1.mesh.QuadView; +import net.minecraft.client.texture.Sprite; +import net.minecraft.util.math.MathHelper; + +record QuadUvBounds(float minU, float maxU, float minV, float maxV) { + static QuadUvBounds read(QuadView quad) { + float u0 = quad.u(0); float u1 = quad.u(1); float u2 = quad.u(2); float u3 = quad.u(3); + float v0 = quad.v(0); float v1 = quad.v(1); float v2 = quad.v(2); float v3 = quad.v(3); + return new QuadUvBounds( + Math.min(Math.min(u0, u1), Math.min(u2, u3)), + Math.max(Math.max(u0, u1), Math.max(u2, u3)), + Math.min(Math.min(v0, v1), Math.min(v2, v3)), + Math.max(Math.max(v0, v1), Math.max(v2, v3)) + ); + } + + boolean displaysSprite(Sprite sprite) { + return sprite.getMinU() <= minU && sprite.getMaxU() >= maxU && sprite.getMinV() <= minV && sprite.getMaxV() >= maxV; + } + + void normalizeUv(MutableQuadView quad, Sprite specialSprite) { + float remappedMinU = norm(minU, specialSprite.getMinU(), specialSprite.getMaxU()); + float remappedMaxU = norm(maxU, specialSprite.getMinU(), specialSprite.getMaxU()); + float remappedMinV = norm(minV, specialSprite.getMinV(), specialSprite.getMaxV()); + float remappedMaxV = norm(maxV, specialSprite.getMinV(), specialSprite.getMaxV()); + quad.uv(0, MathHelper.approximatelyEquals(quad.u(0), minU) ? remappedMinU : remappedMaxU, MathHelper.approximatelyEquals(quad.v(0), minV) ? remappedMinV : remappedMaxV); + quad.uv(1, MathHelper.approximatelyEquals(quad.u(1), minU) ? remappedMinU : remappedMaxU, MathHelper.approximatelyEquals(quad.v(1), minV) ? remappedMinV : remappedMaxV); + quad.uv(2, MathHelper.approximatelyEquals(quad.u(2), minU) ? remappedMinU : remappedMaxU, MathHelper.approximatelyEquals(quad.v(2), minV) ? remappedMinV : remappedMaxV); + quad.uv(3, MathHelper.approximatelyEquals(quad.u(3), minU) ? remappedMinU : remappedMaxU, MathHelper.approximatelyEquals(quad.v(3), minV) ? remappedMinV : remappedMaxV); + } + + static float norm(float value, float low, float high) { + float value2 = MathHelper.clamp(value, low, high); + return (value2 - low) / (high - low); + } + + //static float rangeRemap(float value, float low1, float high1, float low2, float high2) { + // float value2 = MathHelper.clamp(value, low1, high1); + // return low2 + (value2 - low1) * (high2 - low2) / (high1 - low1); + //} +} diff --git a/src/main/java/io/github/cottonmc/templates/model/RetexturedJsonModelBakedModel.java b/src/main/java/io/github/cottonmc/templates/model/RetexturedJsonModelBakedModel.java deleted file mode 100644 index a6b21e1..0000000 --- a/src/main/java/io/github/cottonmc/templates/model/RetexturedJsonModelBakedModel.java +++ /dev/null @@ -1,174 +0,0 @@ -package io.github.cottonmc.templates.model; - -import io.github.cottonmc.templates.Templates; -import io.github.cottonmc.templates.TemplatesClient; -import net.fabricmc.fabric.api.renderer.v1.Renderer; -import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial; -import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh; -import net.fabricmc.fabric.api.renderer.v1.mesh.MeshBuilder; -import net.fabricmc.fabric.api.renderer.v1.mesh.MutableQuadView; -import net.fabricmc.fabric.api.renderer.v1.mesh.QuadEmitter; -import net.fabricmc.fabric.api.renderer.v1.mesh.QuadView; -import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel; -import net.fabricmc.fabric.api.renderer.v1.render.RenderContext; -import net.fabricmc.fabric.api.rendering.data.v1.RenderAttachedBlockView; -import net.minecraft.block.BlockState; -import net.minecraft.client.render.model.BakedModel; -import net.minecraft.client.render.model.BakedQuad; -import net.minecraft.client.render.model.ModelBakeSettings; -import net.minecraft.client.texture.Sprite; -import net.minecraft.client.util.SpriteIdentifier; -import net.minecraft.item.BlockItem; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NbtCompound; -import net.minecraft.nbt.NbtHelper; -import net.minecraft.registry.Registries; -import net.minecraft.screen.PlayerScreenHandler; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.Direction; -import net.minecraft.util.math.MathHelper; -import net.minecraft.util.math.random.Random; -import net.minecraft.world.BlockRenderView; - -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.function.Supplier; - -public class RetexturedJsonModelBakedModel extends ForwardingBakedModel { - public RetexturedJsonModelBakedModel(BakedModel baseModel, TemplateAppearanceManager tam, ModelBakeSettings settings, Function spriteLookup, BlockState itemModelState) { - this.wrapped = baseModel; - this.tam = tam; - this.facePermutation = MeshTransformUtil.facePermutation(settings); - this.itemModelState = itemModelState; - - for(int i = 0; i < DIRECTIONS.length; i++) { - SpriteIdentifier id = new SpriteIdentifier(PlayerScreenHandler.BLOCK_ATLAS_TEXTURE, Templates.id("templates_special/" + DIRECTIONS[i].getName())); - this.specialSprites[i] = Objects.requireNonNull(spriteLookup.apply(id), () -> "Couldn't find sprite " + id + " !"); - } - } - - private final TemplateAppearanceManager tam; - private final Map facePermutation; - private final Sprite[] specialSprites = new Sprite[DIRECTIONS.length]; - private final BlockState itemModelState; - - private record CacheKey(BlockState state, TemplateAppearance appearance) {} - private final ConcurrentHashMap meshCache = new ConcurrentHashMap<>(); - - @Override - public boolean isVanillaAdapter() { - return false; - } - - @Override - public void emitBlockQuads(BlockRenderView blockView, BlockState state, BlockPos pos, Supplier randomSupplier, RenderContext context) { - BlockState theme = (((RenderAttachedBlockView) blockView).getBlockEntityRenderAttachment(pos) instanceof BlockState s) ? s : null; - TemplateAppearance ta = theme == null || theme.isAir() ? tam.getDefaultAppearance() : tam.getAppearance(theme); - - CacheKey key = new CacheKey(state, ta); - context.meshConsumer().accept(meshCache.computeIfAbsent(key, this::makeMesh)); - } - - @Override - public void emitItemQuads(ItemStack stack, Supplier randomSupplier, RenderContext context) { - TemplateAppearance nbtAppearance = null; - - //cheeky: if the item has NBT data, pluck out the blockstate from it - NbtCompound tag = BlockItem.getBlockEntityNbt(stack); - if(tag != null && tag.contains("BlockState")) { - BlockState theme = NbtHelper.toBlockState(Registries.BLOCK.getReadOnlyWrapper(), tag.getCompound("BlockState")); - if(!theme.isAir()) nbtAppearance = tam.getAppearance(theme); - } - - CacheKey key = new CacheKey(itemModelState, nbtAppearance == null ? tam.getDefaultAppearance() : nbtAppearance); - context.meshConsumer().accept(meshCache.computeIfAbsent(key, this::makeMesh)); - } - - @Override - public Sprite getParticleSprite() { - return tam.getDefaultAppearance().getParticleSprite(); - } - - protected Mesh makeMesh(CacheKey key) { - Renderer r = TemplatesClient.getFabricRenderer(); - MeshBuilder builder = r.meshBuilder(); - QuadEmitter emitter = builder.getEmitter(); - RenderMaterial mat = key.appearance().getRenderMaterial(); - - Random rand = Random.create(42); - - for(Direction cullFace : DIRECTIONS_AND_NULL) { - for(BakedQuad quad : wrapped.getQuads(key.state, cullFace, rand)) { - emitter.fromVanilla(quad, mat, cullFace); - - QuadUvBounds bounds = QuadUvBounds.read(emitter); - for(int i = 0; i < specialSprites.length; i++) { - if(bounds.displaysSprite(specialSprites[i])) { - bounds.remap( - emitter, - specialSprites[i], - key.appearance().getSprite(facePermutation.get(DIRECTIONS[i])), - key.appearance().getBakeFlags(facePermutation.get(DIRECTIONS[i])) - ); - break; - } - } - - emitter.emit(); - } - } - - return builder.build(); - } - - record QuadUvBounds(float minU, float maxU, float minV, float maxV) { - static QuadUvBounds read(QuadView quad) { - float u0 = quad.u(0); float u1 = quad.u(1); float u2 = quad.u(2); float u3 = quad.u(3); - float v0 = quad.v(0); float v1 = quad.v(1); float v2 = quad.v(2); float v3 = quad.v(3); - return new QuadUvBounds( - Math.min(Math.min(u0, u1), Math.min(u2, u3)), - Math.max(Math.max(u0, u1), Math.max(u2, u3)), - Math.min(Math.min(v0, v1), Math.min(v2, v3)), - Math.max(Math.max(v0, v1), Math.max(v2, v3)) - ); - } - - boolean displaysSprite(Sprite sprite) { - return sprite.getMinU() <= minU && sprite.getMaxU() >= maxU && sprite.getMinV() <= minV && sprite.getMaxV() >= maxV; - } - - void remap(MutableQuadView quad, Sprite specialSprite, Sprite newSprite, int bakeFlags) { - //move the UVs into 0..1 range - float remappedMinU = norm(minU, specialSprite.getMinU(), specialSprite.getMaxU()); - float remappedMaxU = norm(maxU, specialSprite.getMinU(), specialSprite.getMaxU()); - float remappedMinV = norm(minV, specialSprite.getMinV(), specialSprite.getMaxV()); - float remappedMaxV = norm(maxV, specialSprite.getMinV(), specialSprite.getMaxV()); - quad.uv(0, MathHelper.approximatelyEquals(quad.u(0), minU) ? remappedMinU : remappedMaxU, MathHelper.approximatelyEquals(quad.v(0), minV) ? remappedMinV : remappedMaxV); - quad.uv(1, MathHelper.approximatelyEquals(quad.u(1), minU) ? remappedMinU : remappedMaxU, MathHelper.approximatelyEquals(quad.v(1), minV) ? remappedMinV : remappedMaxV); - quad.uv(2, MathHelper.approximatelyEquals(quad.u(2), minU) ? remappedMinU : remappedMaxU, MathHelper.approximatelyEquals(quad.v(2), minV) ? remappedMinV : remappedMaxV); - quad.uv(3, MathHelper.approximatelyEquals(quad.u(3), minU) ? remappedMinU : remappedMaxU, MathHelper.approximatelyEquals(quad.v(3), minV) ? remappedMinV : remappedMaxV); - - //call spriteBake - //done this way (instead of directly setting UV coordinates to their final values) so that I can use the convenient bakeFlags option - quad.spriteBake(newSprite, MutableQuadView.BAKE_NORMALIZED | bakeFlags); - } - - static float norm(float value, float low, float high) { - float value2 = MathHelper.clamp(value, low, high); - return (value2 - low) / (high - low); - } - - //static float rangeRemap(float value, float low1, float high1, float low2, float high2) { - // float value2 = MathHelper.clamp(value, low1, high1); - // return low2 + (value2 - low1) * (high2 - low2) / (high1 - low1); - //} - } - - private static final Direction[] DIRECTIONS = Direction.values(); - private static final Direction[] DIRECTIONS_AND_NULL = new Direction[DIRECTIONS.length + 1]; - static { - System.arraycopy(DIRECTIONS, 0, DIRECTIONS_AND_NULL, 0, DIRECTIONS.length); - } -} diff --git a/src/main/java/io/github/cottonmc/templates/model/RetexturedJsonModelUnbakedModel.java b/src/main/java/io/github/cottonmc/templates/model/RetexturedJsonModelUnbakedModel.java index 4bb1538..8b995ac 100644 --- a/src/main/java/io/github/cottonmc/templates/model/RetexturedJsonModelUnbakedModel.java +++ b/src/main/java/io/github/cottonmc/templates/model/RetexturedJsonModelUnbakedModel.java @@ -1,19 +1,32 @@ package io.github.cottonmc.templates.model; +import io.github.cottonmc.templates.Templates; import io.github.cottonmc.templates.TemplatesClient; +import net.fabricmc.fabric.api.renderer.v1.Renderer; +import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial; +import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh; +import net.fabricmc.fabric.api.renderer.v1.mesh.MeshBuilder; +import net.fabricmc.fabric.api.renderer.v1.mesh.QuadEmitter; import net.minecraft.block.BlockState; import net.minecraft.block.Blocks; import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.BakedQuad; import net.minecraft.client.render.model.Baker; import net.minecraft.client.render.model.ModelBakeSettings; import net.minecraft.client.render.model.UnbakedModel; import net.minecraft.client.texture.Sprite; import net.minecraft.client.util.SpriteIdentifier; +import net.minecraft.screen.PlayerScreenHandler; import net.minecraft.util.Identifier; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.random.Random; import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.function.Function; public class RetexturedJsonModelUnbakedModel implements UnbakedModel { @@ -42,12 +55,55 @@ public class RetexturedJsonModelUnbakedModel implements UnbakedModel { @Nullable @Override public BakedModel bake(Baker baker, Function spriteLookup, ModelBakeSettings modelBakeSettings, Identifier identifier) { - return new RetexturedJsonModelBakedModel( + Direction[] DIRECTIONS = RetexturingBakedModel.DIRECTIONS; + + Sprite[] specialSprites = new Sprite[DIRECTIONS.length]; + for(int i = 0; i < DIRECTIONS.length; i++) { + SpriteIdentifier id = new SpriteIdentifier(PlayerScreenHandler.BLOCK_ATLAS_TEXTURE, Templates.id("templates_special/" + DIRECTIONS[i].getName())); + specialSprites[i] = Objects.requireNonNull(spriteLookup.apply(id), () -> "Couldn't find sprite " + id + " !"); + } + + ConcurrentMap jsonToMesh = new ConcurrentHashMap<>(); + + return new RetexturingBakedModel( baker.bake(parent, modelBakeSettings), TemplatesClient.provider.getOrCreateTemplateApperanceManager(spriteLookup), modelBakeSettings, - spriteLookup, itemModelState - ); + ) { + @Override + protected Mesh getBaseMesh(BlockState state) { + //Convert models to retexturable Meshes lazily, the first time we encounter each blockstate + return jsonToMesh.computeIfAbsent(state, this::convertModel); + } + + private Mesh convertModel(BlockState state) { + Renderer r = TemplatesClient.getFabricRenderer(); + MeshBuilder builder = r.meshBuilder(); + QuadEmitter emitter = builder.getEmitter(); + RenderMaterial mat = tam.getCachedMaterial(state); + + Random rand = Random.create(42); + + for(Direction cullFace : DIRECTIONS_AND_NULL) { + for(BakedQuad quad : wrapped.getQuads(state, cullFace, rand)) { + emitter.fromVanilla(quad, mat, cullFace); + + QuadUvBounds bounds = QuadUvBounds.read(emitter); + for(int i = 0; i < specialSprites.length; i++) { + if(bounds.displaysSprite(specialSprites[i])) { + bounds.normalizeUv(emitter, specialSprites[i]); + emitter.tag(i + 1); + break; + } + } + + emitter.emit(); + } + } + + return builder.build(); + } + }; } } diff --git a/src/main/java/io/github/cottonmc/templates/model/RetexturedMeshBakedModel.java b/src/main/java/io/github/cottonmc/templates/model/RetexturedMeshBakedModel.java deleted file mode 100644 index 00582d2..0000000 --- a/src/main/java/io/github/cottonmc/templates/model/RetexturedMeshBakedModel.java +++ /dev/null @@ -1,127 +0,0 @@ -package io.github.cottonmc.templates.model; - -import net.fabricmc.fabric.api.client.rendering.v1.ColorProviderRegistry; -import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh; -import net.fabricmc.fabric.api.renderer.v1.mesh.MutableQuadView; -import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel; -import net.fabricmc.fabric.api.renderer.v1.render.RenderContext; -import net.fabricmc.fabric.api.rendering.data.v1.RenderAttachedBlockView; -import net.minecraft.block.BlockState; -import net.minecraft.client.color.block.BlockColorProvider; -import net.minecraft.client.render.model.BakedModel; -import net.minecraft.client.render.model.ModelBakeSettings; -import net.minecraft.client.texture.Sprite; -import net.minecraft.item.BlockItem; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NbtCompound; -import net.minecraft.nbt.NbtHelper; -import net.minecraft.registry.Registries; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.Direction; -import net.minecraft.util.math.random.Random; -import net.minecraft.world.BlockRenderView; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; - -public class RetexturedMeshBakedModel extends ForwardingBakedModel { - public RetexturedMeshBakedModel(BakedModel baseModel, TemplateAppearanceManager tam, ModelBakeSettings settings, Mesh baseMesh) { - this.wrapped = baseModel; - this.tam = tam; - this.baseMesh = MeshTransformUtil.aroundCenter(baseMesh, settings); - this.facePermutation = MeshTransformUtil.facePermutation(settings); - this.uvLock = settings.isUvLocked(); - } - - private final TemplateAppearanceManager tam; - private final Mesh baseMesh; - private final Map facePermutation; - private final boolean uvLock; - - private final ConcurrentHashMap meshCache = new ConcurrentHashMap<>(); - - @Override - public boolean isVanillaAdapter() { - return false; - } - - @Override - public void emitBlockQuads(BlockRenderView blockView, BlockState state, BlockPos pos, Supplier randomSupplier, RenderContext context) { - BlockState theme = (((RenderAttachedBlockView) blockView).getBlockEntityRenderAttachment(pos) instanceof BlockState s) ? s : null; - if(theme == null || theme.isAir()) { - context.meshConsumer().accept(getUntintedMesh(tam.getDefaultAppearance())); - return; - } - - TemplateAppearance ta = tam.getAppearance(theme); - - BlockColorProvider prov = ColorProviderRegistry.BLOCK.get(theme.getBlock()); - int tint = prov == null ? 0xFFFFFFFF : (0xFF000000 | prov.getColor(theme, blockView, pos, 1)); - - if(tint == 0xFFFFFFFF) { - //Cache this mesh indefinitely. - context.meshConsumer().accept(getUntintedMesh(ta)); - } else { - //The specific tint might vary a lot; imagine grass color smoothly changing. Baking the tint into the cached mesh - //is likely unnecessary and will fill the cache with a ton of single-use meshes with only slighly different colors. - //We'd also have to percolate that tint color into the cache key, which is an allocation, blah blah blah. - //Let's fall back to a quad transform. In practice this is still nice and quick. - context.pushTransform(new RetexturingTransformer(ta, tint, facePermutation, uvLock)); - context.meshConsumer().accept(baseMesh); - context.popTransform(); - } - } - - @Override - public void emitItemQuads(ItemStack stack, Supplier randomSupplier, RenderContext context) { - TemplateAppearance ta = tam.getDefaultAppearance(); - - //cheeky: if the item has NBT data, pluck out the blockstate from it - NbtCompound tag = BlockItem.getBlockEntityNbt(stack); - if(tag != null && tag.contains("BlockState")) { - BlockState state = NbtHelper.toBlockState(Registries.BLOCK.getReadOnlyWrapper(), tag.getCompound("BlockState")); - if(state != null && !state.isAir()) ta = tam.getAppearance(state); - } - - context.meshConsumer().accept(getUntintedMesh(ta)); - } - - @Override - public Sprite getParticleSprite() { - return tam.getDefaultAppearance().getParticleSprite(); - } - - protected Mesh getUntintedMesh(TemplateAppearance ta) { - return meshCache.computeIfAbsent(ta, this::makeUntintedMesh); - } - - protected Mesh makeUntintedMesh(TemplateAppearance appearance) { - return MeshTransformUtil.pretransformMesh(baseMesh, new RetexturingTransformer(appearance, 0xFFFFFFFF, facePermutation, uvLock)); - } - - public static record RetexturingTransformer(TemplateAppearance appearance, int color, Map facePermutation, boolean uvLock) implements RenderContext.QuadTransform { - private static final Direction[] DIRECTIONS = Direction.values(); - - @Override - public boolean transform(MutableQuadView quad) { - quad.material(appearance.getRenderMaterial()); - - //The quad tag numbers were selected so this magic trick works: - Direction dir = facePermutation.get(DIRECTIONS[quad.tag() - 1]); - //TODO: this newly-simplified direction passing to hasColor is almost certainly incorrect - // I think hasColor was kinda incorrect in the first place tho - if(appearance.hasColor(dir)) quad.color(color, color, color, color); - - Sprite sprite = appearance.getSprite(dir); - - int flags = MutableQuadView.BAKE_NORMALIZED; - flags |= appearance.getBakeFlags(dir); - if(uvLock) flags |= MutableQuadView.BAKE_LOCK_UV; - - quad.spriteBake(sprite, flags); - - return true; - } - } -} diff --git a/src/main/java/io/github/cottonmc/templates/model/RetexturedMeshUnbakedModel.java b/src/main/java/io/github/cottonmc/templates/model/RetexturedMeshUnbakedModel.java index 2611fff..e8108b0 100644 --- a/src/main/java/io/github/cottonmc/templates/model/RetexturedMeshUnbakedModel.java +++ b/src/main/java/io/github/cottonmc/templates/model/RetexturedMeshUnbakedModel.java @@ -2,6 +2,8 @@ package io.github.cottonmc.templates.model; import io.github.cottonmc.templates.TemplatesClient; import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; import net.minecraft.client.render.model.BakedModel; import net.minecraft.client.render.model.Baker; import net.minecraft.client.render.model.ModelBakeSettings; @@ -15,15 +17,18 @@ import java.util.Collections; import java.util.function.Function; import java.util.function.Supplier; -@SuppressWarnings("ClassCanBeRecord") public class RetexturedMeshUnbakedModel implements UnbakedModel { public RetexturedMeshUnbakedModel(Identifier parent, Supplier baseMeshFactory) { + this(parent, __ -> baseMeshFactory.get()); + } + + public RetexturedMeshUnbakedModel(Identifier parent, Function, Mesh> baseMeshFactory) { this.parent = parent; this.baseMeshFactory = baseMeshFactory; } protected final Identifier parent; - protected final Supplier baseMeshFactory; + protected final Function, Mesh> baseMeshFactory; @Override public Collection getModelDependencies() { @@ -32,16 +37,23 @@ public class RetexturedMeshUnbakedModel implements UnbakedModel { @Override public void setParents(Function function) { - function.apply(parent).setParents(function); //Still not sure what this function does lol + function.apply(parent).setParents(function); } @Override public BakedModel bake(Baker baker, Function spriteLookup, ModelBakeSettings modelBakeSettings, Identifier identifier) { - return new RetexturedMeshBakedModel( + Mesh transformedBaseMesh = MeshTransformUtil.pretransformMesh(baseMeshFactory.apply(spriteLookup), MeshTransformUtil.applyAffine(modelBakeSettings)); + + return new RetexturingBakedModel( baker.bake(parent, modelBakeSettings), TemplatesClient.provider.getOrCreateTemplateApperanceManager(spriteLookup), modelBakeSettings, - baseMeshFactory.get() - ); + Blocks.AIR.getDefaultState() + ) { + @Override + protected Mesh getBaseMesh(BlockState state) { + return transformedBaseMesh; + } + }; } } diff --git a/src/main/java/io/github/cottonmc/templates/model/RetexturingBakedModel.java b/src/main/java/io/github/cottonmc/templates/model/RetexturingBakedModel.java new file mode 100644 index 0000000..5fcee26 --- /dev/null +++ b/src/main/java/io/github/cottonmc/templates/model/RetexturingBakedModel.java @@ -0,0 +1,174 @@ +package io.github.cottonmc.templates.model; + +import net.fabricmc.fabric.api.client.rendering.v1.ColorProviderRegistry; +import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh; +import net.fabricmc.fabric.api.renderer.v1.mesh.MutableQuadView; +import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel; +import net.fabricmc.fabric.api.renderer.v1.render.RenderContext; +import net.fabricmc.fabric.api.rendering.data.v1.RenderAttachedBlockView; +import net.minecraft.block.BlockState; +import net.minecraft.client.color.block.BlockColorProvider; +import net.minecraft.client.color.item.ItemColorProvider; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.ModelBakeSettings; +import net.minecraft.client.texture.Sprite; +import net.minecraft.item.BlockItem; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtHelper; +import net.minecraft.registry.Registries; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.random.Random; +import net.minecraft.world.BlockRenderView; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Supplier; + +public abstract class RetexturingBakedModel extends ForwardingBakedModel { + public RetexturingBakedModel(BakedModel baseModel, TemplateAppearanceManager tam, ModelBakeSettings settings, BlockState itemModelState) { + this.wrapped = baseModel; + + this.tam = tam; + this.facePermutation = MeshTransformUtil.facePermutation(settings); + this.uvlock = settings.isUvLocked(); + this.itemModelState = itemModelState; + } + + protected final TemplateAppearanceManager tam; + protected final Map facePermutation; //immutable + protected final boolean uvlock; + protected final BlockState itemModelState; + + private static record CacheKey(BlockState state, TemplateAppearance appearance) {} + private final ConcurrentMap retexturedMeshes = new ConcurrentHashMap<>(); + + protected static final Direction[] DIRECTIONS = Direction.values(); + protected static final Direction[] DIRECTIONS_AND_NULL = new Direction[DIRECTIONS.length + 1]; + static { + System.arraycopy(DIRECTIONS, 0, DIRECTIONS_AND_NULL, 0, DIRECTIONS.length); + } + + protected abstract Mesh getBaseMesh(BlockState state); + + @Override + public boolean isVanillaAdapter() { + return false; + } + + @Override + public Sprite getParticleSprite() { + return tam.getDefaultAppearance().getParticleSprite(); + } + + @Override + public void emitBlockQuads(BlockRenderView blockView, BlockState state, BlockPos pos, Supplier randomSupplier, RenderContext context) { + BlockState theme = (((RenderAttachedBlockView) blockView).getBlockEntityRenderAttachment(pos) instanceof BlockState s) ? s : null; + if(theme == null || theme.isAir()) { + context.meshConsumer().accept(getUntintedRetexturedMesh(new CacheKey(state, tam.getDefaultAppearance()))); + return; + } + + TemplateAppearance ta = tam.getAppearance(theme); + + BlockColorProvider prov = ColorProviderRegistry.BLOCK.get(theme.getBlock()); + int tint = prov == null ? 0xFFFFFFFF : (0xFF000000 | prov.getColor(theme, blockView, pos, 1)); + Mesh untintedMesh = getUntintedRetexturedMesh(new CacheKey(state, ta)); + + //The specific tint might vary a lot; imagine grass color smoothly changing. Trying to bake the tint into + //the cached mesh will pollute it with a ton of single-use meshes with only slighly different colors. + if(tint == 0xFFFFFFFF) { + context.meshConsumer().accept(untintedMesh); + } else { + context.pushTransform(new TintingTransformer(ta, tint)); + context.meshConsumer().accept(untintedMesh); + context.popTransform(); + } + } + + @Override + public void emitItemQuads(ItemStack stack, Supplier randomSupplier, RenderContext context) { + TemplateAppearance nbtAppearance = tam.getDefaultAppearance(); + int tint = 0xFFFFFFFF; + + //cheeky: if the item has NBT data, pluck out the blockstate from it & look up the item color provider + //none of this is accessible unless you're in creative mode doing ctrl-pick btw + NbtCompound tag = BlockItem.getBlockEntityNbt(stack); + if(tag != null && tag.contains("BlockState")) { + BlockState theme = NbtHelper.toBlockState(Registries.BLOCK.getReadOnlyWrapper(), tag.getCompound("BlockState")); + if(!theme.isAir()) { + nbtAppearance = tam.getAppearance(theme); + + ItemColorProvider prov = ColorProviderRegistry.ITEM.get(theme.getBlock()); + if(prov != null) tint = prov.getColor(new ItemStack(theme.getBlock()), 1); + } + } + + Mesh untintedMesh = getUntintedRetexturedMesh(new CacheKey(itemModelState, nbtAppearance)); + + if(tint == 0xFFFFFFFF) { + context.meshConsumer().accept(untintedMesh); + } else { + context.pushTransform(new TintingTransformer(nbtAppearance, tint)); + context.meshConsumer().accept(untintedMesh); + context.popTransform(); + } + } + + protected Mesh getUntintedRetexturedMesh(CacheKey key) { + return retexturedMeshes.computeIfAbsent(key, this::createUntintedRetexturedMesh); + } + + protected Mesh createUntintedRetexturedMesh(CacheKey key) { + return MeshTransformUtil.pretransformMesh(getBaseMesh(key.state), new RetexturingTransformer(key.appearance, 0xFFFFFFFF)); + } + + protected class RetexturingTransformer implements RenderContext.QuadTransform { + protected RetexturingTransformer(TemplateAppearance ta, int tint) { + this.ta = ta; + this.tint = tint; + } + + protected final TemplateAppearance ta; + protected final int tint; + + @Override + public boolean transform(MutableQuadView quad) { + quad.material(ta.getRenderMaterial()); + + int tag = quad.tag(); + if(tag == 0) return true; //Pass the quad through unmodified. + + //The quad tag numbers were selected so this magic trick works: + Direction dir = facePermutation.get(DIRECTIONS[quad.tag() - 1]); + if(ta.hasColor(dir)) quad.color(tint, tint, tint, tint); //TODO: still doesn't cover stuff like grass blocks, leaf blocks, etc + + quad.spriteBake(ta.getSprite(dir), MutableQuadView.BAKE_NORMALIZED | ta.getBakeFlags(dir) | (uvlock ? MutableQuadView.BAKE_LOCK_UV : 0)); + + return true; + } + } + + protected class TintingTransformer implements RenderContext.QuadTransform { + protected TintingTransformer(TemplateAppearance ta, int tint) { + this.ta = ta; + this.tint = tint; + } + + protected final TemplateAppearance ta; + protected final int tint; + + @Override + public boolean transform(MutableQuadView quad) { + int tag = quad.tag(); + if(tag == 0) return true; + + Direction dir = facePermutation.get(DIRECTIONS[quad.tag() - 1]); + if(ta.hasColor(dir)) quad.color(tint, tint, tint, tint); //TODO: still doesn't cover stuff like grass blocks, leaf blocks, etc + + return true; + } + } +} diff --git a/src/main/java/io/github/cottonmc/templates/model/SlopeBaseMesh.java b/src/main/java/io/github/cottonmc/templates/model/SlopeBaseMesh.java index ec88220..9ec2fc2 100644 --- a/src/main/java/io/github/cottonmc/templates/model/SlopeBaseMesh.java +++ b/src/main/java/io/github/cottonmc/templates/model/SlopeBaseMesh.java @@ -5,11 +5,15 @@ import net.fabricmc.fabric.api.renderer.v1.Renderer; import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh; import net.fabricmc.fabric.api.renderer.v1.mesh.MeshBuilder; import net.fabricmc.fabric.api.renderer.v1.mesh.QuadEmitter; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.util.SpriteIdentifier; import net.minecraft.util.math.Direction; +import java.util.function.Function; + public class SlopeBaseMesh { /** - * @see RetexturedMeshBakedModel.RetexturingTransformer for why these values were chosen + * @see RetexturingBakedModel for why these values were chosen */ public static final int TAG_SLOPE = Direction.UP.ordinal() + 1; public static final int TAG_LEFT = Direction.EAST.ordinal() + 1; diff --git a/src/main/java/io/github/cottonmc/templates/model/TemplateAppearance.java b/src/main/java/io/github/cottonmc/templates/model/TemplateAppearance.java index 5efa23a..526b452 100644 --- a/src/main/java/io/github/cottonmc/templates/model/TemplateAppearance.java +++ b/src/main/java/io/github/cottonmc/templates/model/TemplateAppearance.java @@ -6,7 +6,7 @@ import net.minecraft.util.math.Direction; import org.jetbrains.annotations.NotNull; public interface TemplateAppearance { - @NotNull Sprite getParticleSprite(); //TODO: plug this in + @NotNull Sprite getParticleSprite(); //TODO: plug this in (particle mixins don't use it atm) @NotNull RenderMaterial getRenderMaterial(); @NotNull Sprite getSprite(Direction dir); diff --git a/src/main/java/io/github/cottonmc/templates/model/TemplateAppearanceManager.java b/src/main/java/io/github/cottonmc/templates/model/TemplateAppearanceManager.java index d16ac1b..f45d6a3 100644 --- a/src/main/java/io/github/cottonmc/templates/model/TemplateAppearanceManager.java +++ b/src/main/java/io/github/cottonmc/templates/model/TemplateAppearanceManager.java @@ -55,6 +55,10 @@ public class TemplateAppearanceManager { return appearanceCache.computeIfAbsent(state, this::computeAppearance); } + public RenderMaterial getCachedMaterial(BlockState state) { + return blockMaterials.get(BlendMode.fromRenderLayer(RenderLayers.getBlockLayer(state))); + } + //I'm pretty sure ConcurrentHashMap semantics allow for this function to be called multiple times on the same key, on different threads. //The computeIfAbsent map update will work without corrupting the map, but there will be some "wasted effort" computing the value twice. //The results are going to be the same, apart from their serialNumbers differing (= their equals & hashCode differing). @@ -136,7 +140,7 @@ public class TemplateAppearanceManager { sprites, bakeFlags, hasColorMask, - blockMaterials.get(BlendMode.fromRenderLayer(RenderLayers.getBlockLayer(state))), + getCachedMaterial(state), serialNumber.getAndIncrement() ); }