We're much the same, you and I

This commit is contained in:
quat1024 2023-07-08 02:12:03 -04:00
parent ea476662d4
commit e373b8b933
10 changed files with 346 additions and 366 deletions

View File

@ -1,13 +1,11 @@
package io.github.cottonmc.templates.model; package io.github.cottonmc.templates.model;
import io.github.cottonmc.templates.TemplatesClient; 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.Mesh;
import net.fabricmc.fabric.api.renderer.v1.mesh.MeshBuilder; 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.mesh.QuadEmitter;
import net.fabricmc.fabric.api.renderer.v1.render.RenderContext; import net.fabricmc.fabric.api.renderer.v1.render.RenderContext;
import net.minecraft.client.render.model.ModelBakeSettings; import net.minecraft.client.render.model.ModelBakeSettings;
import net.minecraft.util.math.AffineTransformation;
import net.minecraft.util.math.Direction; import net.minecraft.util.math.Direction;
import org.joml.Matrix4f; import org.joml.Matrix4f;
import org.joml.Vector3f; import org.joml.Vector3f;
@ -17,51 +15,16 @@ import java.util.EnumMap;
import java.util.Map; import java.util.Map;
public class MeshTransformUtil { public class MeshTransformUtil {
public static Mesh aroundCenter(Mesh oldMesh, ModelBakeSettings settings) { public static Mesh pretransformMesh(Mesh mesh, RenderContext.QuadTransform transform) {
return aroundCenter(oldMesh, settings.getRotation().getMatrix()); MeshBuilder builder = TemplatesClient.getFabricRenderer().meshBuilder();
} QuadEmitter emitter = builder.getEmitter();
public static Mesh aroundCenter(Mesh oldMesh, Matrix4f mat) { mesh.forEach(quad -> {
Map<Direction, Direction> facePermutation = facePermutation(mat); emitter.copyFrom(quad);
if(transform.transform(emitter)) emitter.emit();
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();
}); });
return newMesh.build(); return builder.build();
} }
//Hard to explain what this is for... //Hard to explain what this is for...
@ -86,15 +49,39 @@ public class MeshTransformUtil {
return facePermutation; return facePermutation;
} }
public static Mesh pretransformMesh(Mesh mesh, RenderContext.QuadTransform transform) { public static RenderContext.QuadTransform applyAffine(ModelBakeSettings settings) {
MeshBuilder builder = TemplatesClient.getFabricRenderer().meshBuilder(); return applyMatrix(settings.getRotation().getMatrix());
QuadEmitter emitter = builder.getEmitter(); }
mesh.forEach(quad -> { public static RenderContext.QuadTransform applyMatrix(Matrix4f mat) {
emitter.copyFrom(quad); Map<Direction, Direction> facePermutation = facePermutation(mat);
if(transform.transform(emitter)) emitter.emit(); Vector3f pos3 = new Vector3f();
}); Vector4f pos4 = new Vector4f();
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;
};
} }
} }

View File

@ -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);
//}
}

View File

@ -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<SpriteIdentifier, Sprite> 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<Direction, Direction> facePermutation;
private final Sprite[] specialSprites = new Sprite[DIRECTIONS.length];
private final BlockState itemModelState;
private record CacheKey(BlockState state, TemplateAppearance appearance) {}
private final ConcurrentHashMap<CacheKey, Mesh> meshCache = new ConcurrentHashMap<>();
@Override
public boolean isVanillaAdapter() {
return false;
}
@Override
public void emitBlockQuads(BlockRenderView blockView, BlockState state, BlockPos pos, Supplier<Random> 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<Random> 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);
}
}

View File

@ -1,19 +1,32 @@
package io.github.cottonmc.templates.model; package io.github.cottonmc.templates.model;
import io.github.cottonmc.templates.Templates;
import io.github.cottonmc.templates.TemplatesClient; 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.BlockState;
import net.minecraft.block.Blocks; import net.minecraft.block.Blocks;
import net.minecraft.client.render.model.BakedModel; 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.Baker;
import net.minecraft.client.render.model.ModelBakeSettings; import net.minecraft.client.render.model.ModelBakeSettings;
import net.minecraft.client.render.model.UnbakedModel; import net.minecraft.client.render.model.UnbakedModel;
import net.minecraft.client.texture.Sprite; import net.minecraft.client.texture.Sprite;
import net.minecraft.client.util.SpriteIdentifier; import net.minecraft.client.util.SpriteIdentifier;
import net.minecraft.screen.PlayerScreenHandler;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.random.Random;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function; import java.util.function.Function;
public class RetexturedJsonModelUnbakedModel implements UnbakedModel { public class RetexturedJsonModelUnbakedModel implements UnbakedModel {
@ -42,12 +55,55 @@ public class RetexturedJsonModelUnbakedModel implements UnbakedModel {
@Nullable @Nullable
@Override @Override
public BakedModel bake(Baker baker, Function<SpriteIdentifier, Sprite> spriteLookup, ModelBakeSettings modelBakeSettings, Identifier identifier) { public BakedModel bake(Baker baker, Function<SpriteIdentifier, Sprite> 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<BlockState, Mesh> jsonToMesh = new ConcurrentHashMap<>();
return new RetexturingBakedModel(
baker.bake(parent, modelBakeSettings), baker.bake(parent, modelBakeSettings),
TemplatesClient.provider.getOrCreateTemplateApperanceManager(spriteLookup), TemplatesClient.provider.getOrCreateTemplateApperanceManager(spriteLookup),
modelBakeSettings, modelBakeSettings,
spriteLookup,
itemModelState 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();
}
};
} }
} }

View File

@ -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<Direction, Direction> facePermutation;
private final boolean uvLock;
private final ConcurrentHashMap<TemplateAppearance, Mesh> meshCache = new ConcurrentHashMap<>();
@Override
public boolean isVanillaAdapter() {
return false;
}
@Override
public void emitBlockQuads(BlockRenderView blockView, BlockState state, BlockPos pos, Supplier<Random> 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<Random> 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<Direction, Direction> 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;
}
}
}

View File

@ -2,6 +2,8 @@ package io.github.cottonmc.templates.model;
import io.github.cottonmc.templates.TemplatesClient; import io.github.cottonmc.templates.TemplatesClient;
import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh; 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.BakedModel;
import net.minecraft.client.render.model.Baker; import net.minecraft.client.render.model.Baker;
import net.minecraft.client.render.model.ModelBakeSettings; import net.minecraft.client.render.model.ModelBakeSettings;
@ -15,15 +17,18 @@ import java.util.Collections;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
@SuppressWarnings("ClassCanBeRecord")
public class RetexturedMeshUnbakedModel implements UnbakedModel { public class RetexturedMeshUnbakedModel implements UnbakedModel {
public RetexturedMeshUnbakedModel(Identifier parent, Supplier<Mesh> baseMeshFactory) { public RetexturedMeshUnbakedModel(Identifier parent, Supplier<Mesh> baseMeshFactory) {
this(parent, __ -> baseMeshFactory.get());
}
public RetexturedMeshUnbakedModel(Identifier parent, Function<Function<SpriteIdentifier, Sprite>, Mesh> baseMeshFactory) {
this.parent = parent; this.parent = parent;
this.baseMeshFactory = baseMeshFactory; this.baseMeshFactory = baseMeshFactory;
} }
protected final Identifier parent; protected final Identifier parent;
protected final Supplier<Mesh> baseMeshFactory; protected final Function<Function<SpriteIdentifier, Sprite>, Mesh> baseMeshFactory;
@Override @Override
public Collection<Identifier> getModelDependencies() { public Collection<Identifier> getModelDependencies() {
@ -32,16 +37,23 @@ public class RetexturedMeshUnbakedModel implements UnbakedModel {
@Override @Override
public void setParents(Function<Identifier, UnbakedModel> function) { public void setParents(Function<Identifier, UnbakedModel> function) {
function.apply(parent).setParents(function); //Still not sure what this function does lol function.apply(parent).setParents(function);
} }
@Override @Override
public BakedModel bake(Baker baker, Function<SpriteIdentifier, Sprite> spriteLookup, ModelBakeSettings modelBakeSettings, Identifier identifier) { public BakedModel bake(Baker baker, Function<SpriteIdentifier, Sprite> 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), baker.bake(parent, modelBakeSettings),
TemplatesClient.provider.getOrCreateTemplateApperanceManager(spriteLookup), TemplatesClient.provider.getOrCreateTemplateApperanceManager(spriteLookup),
modelBakeSettings, modelBakeSettings,
baseMeshFactory.get() Blocks.AIR.getDefaultState()
); ) {
@Override
protected Mesh getBaseMesh(BlockState state) {
return transformedBaseMesh;
}
};
} }
} }

View File

@ -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<Direction, Direction> facePermutation; //immutable
protected final boolean uvlock;
protected final BlockState itemModelState;
private static record CacheKey(BlockState state, TemplateAppearance appearance) {}
private final ConcurrentMap<CacheKey, Mesh> 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<Random> 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<Random> 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;
}
}
}

View File

@ -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.Mesh;
import net.fabricmc.fabric.api.renderer.v1.mesh.MeshBuilder; 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.mesh.QuadEmitter;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.util.SpriteIdentifier;
import net.minecraft.util.math.Direction; import net.minecraft.util.math.Direction;
import java.util.function.Function;
public class SlopeBaseMesh { 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_SLOPE = Direction.UP.ordinal() + 1;
public static final int TAG_LEFT = Direction.EAST.ordinal() + 1; public static final int TAG_LEFT = Direction.EAST.ordinal() + 1;

View File

@ -6,7 +6,7 @@ import net.minecraft.util.math.Direction;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public interface TemplateAppearance { 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 RenderMaterial getRenderMaterial();
@NotNull Sprite getSprite(Direction dir); @NotNull Sprite getSprite(Direction dir);

View File

@ -55,6 +55,10 @@ public class TemplateAppearanceManager {
return appearanceCache.computeIfAbsent(state, this::computeAppearance); 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. //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 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). //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, sprites,
bakeFlags, bakeFlags,
hasColorMask, hasColorMask,
blockMaterials.get(BlendMode.fromRenderLayer(RenderLayers.getBlockLayer(state))), getCachedMaterial(state),
serialNumber.getAndIncrement() serialNumber.getAndIncrement()
); );
} }