Files
ReFramed/src/main/java/fr/adrien1106/reframed/client/model/RetexturingBakedModel.java

242 lines
9.0 KiB
Java

package fr.adrien1106.reframed.client.model;
import fr.adrien1106.reframed.block.ReFramedEntity;
import fr.adrien1106.reframed.client.ReFramedClient;
import fr.adrien1106.reframed.client.model.apperance.SpriteProperties;
import fr.adrien1106.reframed.mixin.MinecraftAccessor;
import fr.adrien1106.reframed.client.model.apperance.CamoAppearance;
import fr.adrien1106.reframed.client.model.apperance.CamoAppearanceManager;
import fr.adrien1106.reframed.client.model.apperance.WeightedComputedAppearance;
import fr.adrien1106.reframed.util.ThemeableBlockEntity;
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.model.ForwardingBakedModel;
import net.fabricmc.fabric.api.renderer.v1.render.RenderContext;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.client.render.model.ModelBakeSettings;
import net.minecraft.client.texture.Sprite;
import net.minecraft.item.ItemStack;
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.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;
public abstract class RetexturingBakedModel extends ForwardingBakedModel {
public RetexturingBakedModel(BakedModel base_model, CamoAppearanceManager tam, int theme_index, ModelBakeSettings settings, BlockState item_state, boolean ao) {
this.wrapped = base_model; //field from the superclass; vanilla getQuads etc. will delegate through to this
this.tam = tam;
this.theme_index = theme_index;
this.uv_lock = settings.isUvLocked();
this.item_state = item_state;
this.ao = ao;
}
protected final CamoAppearanceManager tam;
protected final int theme_index;
protected final boolean uv_lock;
protected final BlockState item_state;
protected final boolean ao;
/* ----------------------------------------------- CACHE ELEMENT ------------------------------------------------ */
// TODO make static ? for connected textures ?
protected record MeshCacheKey(BlockState state, TransformCacheKey transform) {}
protected record TransformCacheKey(CamoAppearance appearance, int model_id) {}
protected final ConcurrentMap<TransformCacheKey, RetexturingTransformer> retextured_transforms = new ConcurrentHashMap<>();
protected final ConcurrentMap<MeshCacheKey, Mesh> retextured_meshes = new ConcurrentHashMap<>(); //mutable, append-only cache
protected static final Direction[] DIRECTIONS_AND_NULL;
static {
Direction[] values = Direction.values();
DIRECTIONS_AND_NULL = new Direction[values.length + 1];
System.arraycopy(values, 0, DIRECTIONS_AND_NULL, 0, values.length);
}
protected final ConcurrentMap<BlockState, Mesh> jsonToMesh = new ConcurrentHashMap<>();
protected Mesh getBaseMesh(BlockState state) {
//Convert models to re-texturable Meshes lazily, the first time we encounter each blockstate
return jsonToMesh.computeIfAbsent(state, this::convertModel);
}
protected abstract Mesh convertModel(BlockState state);
@Override
public boolean isVanillaAdapter() {
return false;
}
@Override
public Sprite getParticleSprite() {
return tam.getDefaultAppearance(theme_index).getSprites(Direction.UP, 0).get(0).sprite();
}
@Override
public void emitBlockQuads(BlockRenderView world, BlockState state, BlockPos pos, Supplier<Random> randomSupplier, RenderContext context) {
BlockState theme = (world.getBlockEntity(pos) instanceof ThemeableBlockEntity s) ? s.getTheme(theme_index) : null;
QuadEmitter quad_emitter = context.getEmitter();
if(theme == null || theme.isAir()) {
getUntintedRetexturedMesh(new MeshCacheKey(state, new TransformCacheKey(tam.getDefaultAppearance(theme_index), 0)), 0).outputTo(quad_emitter);
return;
}
if(theme.getBlock() == Blocks.BARRIER) return;
CamoAppearance camo = tam.getCamoAppearance(world, theme, pos);
long seed = theme.getRenderingSeed(pos);
int model_id = 0;
if (camo instanceof WeightedComputedAppearance wca) model_id = wca.getAppearanceIndex(seed);
int tint = 0xFF000000 | MinecraftClient.getInstance().getBlockColors().getColor(theme, world, pos, 0);
Mesh untintedMesh = getUntintedRetexturedMesh(
new MeshCacheKey(
state,
new TransformCacheKey(camo, model_id)
),
seed
);
//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 slightly different colors.
if(tint == 0xFFFFFFFF) {
untintedMesh.outputTo(quad_emitter);
} else {
context.pushTransform(new TintingTransformer(camo, tint, seed));
untintedMesh.outputTo(quad_emitter);
context.popTransform();
}
}
@Override
public void emitItemQuads(ItemStack stack, Supplier<Random> randomSupplier, RenderContext context) {
//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
CamoAppearance nbtAppearance;
int tint;
BlockState theme = ReFramedEntity.readStateFromItem(stack, theme_index);
if(!theme.isAir()) {
nbtAppearance = tam.getCamoAppearance(null, theme, null);
tint = 0xFF000000 | ((MinecraftAccessor) MinecraftClient.getInstance()).getItemColors().getColor(new ItemStack(theme.getBlock()), 0);
} else {
nbtAppearance = tam.getDefaultAppearance(theme_index);
tint = 0xFFFFFFFF;
}
Mesh untintedMesh = getUntintedRetexturedMesh(new MeshCacheKey(item_state, new TransformCacheKey(nbtAppearance, 0)), 0);
QuadEmitter quad_emitter = context.getEmitter();
if(tint == 0xFFFFFFFF) {
untintedMesh.outputTo(quad_emitter);
} else {
context.pushTransform(new TintingTransformer(nbtAppearance, tint, 0));
untintedMesh.outputTo(quad_emitter);
context.popTransform();
}
}
protected Mesh getUntintedRetexturedMesh(MeshCacheKey key, long seed) {
return retextured_meshes.computeIfAbsent(key, (k) -> createUntintedRetexturedMesh(k, seed));
}
protected Mesh createUntintedRetexturedMesh(MeshCacheKey key, long seed) {
RetexturingTransformer transformer = retextured_transforms.computeIfAbsent(key.transform, (k) -> new RetexturingTransformer(k.appearance, seed));
return pretransformMesh(getBaseMesh(key.state), transformer);
}
private static Mesh pretransformMesh(Mesh mesh, RetexturingTransformer transform) {
MeshBuilder builder = ReFramedClient.HELPER.getFabricRenderer().meshBuilder();
QuadEmitter emitter = builder.getEmitter();
mesh.forEach(quad -> {
int i = -1;
do {
emitter.copyFrom(quad);
i = transform.transform(emitter, i);
} while (i > 0);
});
return builder.build();
}
public class RetexturingTransformer {
private final long seed;
protected RetexturingTransformer(CamoAppearance ta, long seed) {
this.ta = ta;
this.seed = seed;
}
protected final CamoAppearance ta;
public int transform(QuadEmitter quad, int i) {
if(quad.tag() == 0) return 0; //Pass the quad through unmodified.
Direction direction = quad.nominalFace();
List<SpriteProperties> sprites = ta.getSprites(direction, seed);
if (i == -1) i = sprites.size();
SpriteProperties properties = sprites.get(sprites.size() - i);
i--;
QuadPosBounds bounds = properties.bounds();
if (bounds == null) { // sprite applies anywhere e.g. default behaviour
quad.material(ta.getRenderMaterial(ao));
quad.spriteBake(
properties.sprite(),
MutableQuadView.BAKE_NORMALIZED
| properties.flags()
| (uv_lock ? MutableQuadView.BAKE_LOCK_UV : 0)
);
quad.tag(i+1);
quad.emit();
return i;
}
// verify if sprite covers the current quad and apply the new size
QuadPosBounds origin_bounds = QuadPosBounds.read(quad, false);
if (!bounds.matches(origin_bounds)) return i;
// apply new quad shape
quad.material(ta.getRenderMaterial(ao));
bounds.intersection(origin_bounds, direction.getAxis()).apply(quad, origin_bounds);
quad.spriteBake( // seems to work without the flags and break with it
properties.sprite(),
MutableQuadView.BAKE_NORMALIZED
| MutableQuadView.BAKE_LOCK_UV
);
quad.tag(i+1);
quad.emit();
return i;
}
}
protected static class TintingTransformer implements RenderContext.QuadTransform {
private final long seed;
protected TintingTransformer(CamoAppearance ta, int tint, long seed) {
this.ta = ta;
this.tint = tint;
this.seed = seed;
}
protected final CamoAppearance ta;
protected final int tint;
@Override
public boolean transform(MutableQuadView quad) {
if(quad.tag() == 0) return true;
if(ta.hasColor(quad.nominalFace(), seed, quad.tag())) quad.color(tint, tint, tint, tint);
return true;
}
}
}