started support for weighted models to support texture randomness. still W.I.P. (textures are normalized)

This commit is contained in:
Adrien1106 2024-02-09 18:42:16 +01:00
parent 7ae4858bb4
commit 4315314593
18 changed files with 433 additions and 242 deletions

View File

@ -108,9 +108,6 @@ public class TemplatesClient implements ClientModInitializer {
api.assignItemModel(Templates.id("wall_inventory_special") , Templates.WALL);
api.assignItemModel(Templates.id("slope_special") , Templates.SLOPE);
api.assignItemModel(Templates.id("tiny_slope_special") , Templates.TINY_SLOPE);
//TODO: i could stick some kind of entrypoint here for signalling other mods that it's ok to register now?
// Dont think it rly matters though, everything's all kept in nice hash maps
}
private void privateInit() {

View File

@ -1,7 +1,7 @@
package fr.adrien1106.reframedtemplates;
import fr.adrien1106.reframedtemplates.api.TemplatesClientApi;
import fr.adrien1106.reframedtemplates.model.TemplateAppearanceManager;
import fr.adrien1106.reframedtemplates.model.apperance.TemplateAppearanceManager;
import fr.adrien1106.reframedtemplates.model.UnbakedAutoRetexturedModel;
import fr.adrien1106.reframedtemplates.model.UnbakedJsonRetexturedModel;
import fr.adrien1106.reframedtemplates.model.UnbakedMeshRetexturedModel;

View File

@ -1,6 +1,6 @@
package fr.adrien1106.reframedtemplates;
import fr.adrien1106.reframedtemplates.model.TemplateAppearanceManager;
import fr.adrien1106.reframedtemplates.model.apperance.TemplateAppearanceManager;
import net.fabricmc.fabric.api.client.model.ModelProviderContext;
import net.fabricmc.fabric.api.client.model.ModelResourceProvider;
import net.fabricmc.fabric.api.client.model.ModelVariantProvider;

View File

@ -70,7 +70,7 @@ public class TemplateInteractionUtil {
ItemStack held = player.getStackInHand(hand);
TemplateInteractionUtilExt ext = state.getBlock() instanceof TemplateInteractionUtilExt e ? e : TemplateInteractionUtilExt.Default.INSTANCE;
//Glowstone
if(state.contains(LIGHT) && held.getItem() == Items.GLOWSTONE_DUST && !state.get(LIGHT) && !be.hasSpentGlowstoneDust()) {
world.setBlockState(pos, state.with(LIGHT, true));
@ -113,7 +113,7 @@ public class TemplateInteractionUtil {
if(held.getItem() instanceof BlockItem bi && be.getThemeState().getBlock() == Blocks.AIR) {
Block block = bi.getBlock();
ItemPlacementContext ctx = new ItemPlacementContext(new ItemUsageContext(player, hand, hit));
BlockState placementState = block.getPlacementState(ctx); // TODO Smart
BlockState placementState = block.getPlacementState(ctx);
if(placementState != null && Block.isShapeFullCube(placementState.getCollisionShape(world, pos)) && !(block instanceof BlockEntityProvider)) {
if(!world.isClient) be.setRenderedState(placementState);

View File

@ -1,6 +1,6 @@
package fr.adrien1106.reframedtemplates.api;
import fr.adrien1106.reframedtemplates.model.TemplateAppearanceManager;
import fr.adrien1106.reframedtemplates.model.apperance.TemplateAppearanceManager;
import fr.adrien1106.reframedtemplates.TemplatesClient;
import net.fabricmc.fabric.api.renderer.v1.Renderer;
import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh;

View File

@ -0,0 +1,16 @@
package fr.adrien1106.reframedtemplates.mixin.model;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.client.render.model.WeightedBakedModel;
import net.minecraft.util.collection.Weighted;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.List;
@Mixin(WeightedBakedModel.class)
public interface WeightedBakedModelAccessor {
@Accessor("models") List<Weighted.Present<BakedModel>> getModels();
}

View File

@ -21,11 +21,11 @@ record QuadUvBounds(float minU, float maxU, float minV, float maxV) {
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());
void normalizeUv(MutableQuadView quad, Sprite sprite) {
float remappedMinU = norm(minU, sprite.getMinU(), sprite.getMaxU());
float remappedMaxU = norm(maxU, sprite.getMinU(), sprite.getMaxU());
float remappedMinV = norm(minV, sprite.getMinV(), sprite.getMaxV());
float remappedMaxV = norm(maxV, sprite.getMinV(), sprite.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);

View File

@ -2,11 +2,14 @@ package fr.adrien1106.reframedtemplates.model;
import fr.adrien1106.reframedtemplates.block.TemplateEntity;
import fr.adrien1106.reframedtemplates.mixin.MinecraftAccessor;
import fr.adrien1106.reframedtemplates.model.apperance.TemplateAppearance;
import fr.adrien1106.reframedtemplates.model.apperance.TemplateAppearanceManager;
import fr.adrien1106.reframedtemplates.model.apperance.WeightedComputedAppearance;
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.mesh.QuadEmitter;
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.block.Blocks;
import net.minecraft.client.MinecraftClient;
@ -41,8 +44,10 @@ public abstract class RetexturingBakedModel extends ForwardingBakedModel {
protected final BlockState itemModelState;
protected final boolean ao;
protected record CacheKey(BlockState state, TemplateAppearance appearance) {}
protected final ConcurrentMap<CacheKey, Mesh> retexturedMeshes = new ConcurrentHashMap<>(); //mutable, append-only cache
protected record MeshCacheKey(BlockState state, TransformCacheKey transform) {}
protected final ConcurrentMap<MeshCacheKey, Mesh> retextured_meshes = new ConcurrentHashMap<>(); //mutable, append-only cache
protected record TransformCacheKey(TemplateAppearance appearance, int model_id) {}
protected final ConcurrentMap<TransformCacheKey, RetexturingTransformer> retextured_transforms = new ConcurrentHashMap<>();
protected static final Direction[] DIRECTIONS = Direction.values();
protected static final Direction[] DIRECTIONS_AND_NULL = new Direction[DIRECTIONS.length + 1];
@ -57,32 +62,34 @@ public abstract class RetexturingBakedModel extends ForwardingBakedModel {
@Override
public Sprite getParticleSprite() {
return tam.getDefaultAppearance().getSprite(Direction.UP);
return tam.getDefaultAppearance().getSprite(Direction.UP, 0);
}
@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;
BlockState theme = (blockView.getBlockEntityRenderData(pos) instanceof BlockState s) ? s : null;
QuadEmitter quad_emitter = context.getEmitter();
if(theme == null || theme.isAir()) {
context.meshConsumer().accept(getUntintedRetexturedMesh(new CacheKey(state, tam.getDefaultAppearance())));
return;
} else if(theme.getBlock() == Blocks.BARRIER) {
//TODO i don't love putting this rare specialcase smack in the middle of the hot code path
getUntintedRetexturedMesh(new MeshCacheKey(state, new TransformCacheKey(tam.getDefaultAppearance(), 0)), 0).outputTo(quad_emitter);
return;
}
if(theme.getBlock() == Blocks.BARRIER) return;
TemplateAppearance ta = tam.getAppearance(theme);
TemplateAppearance ta = tam.getTemplateAppearance(theme);
long seed = theme.getRenderingSeed(pos);
int model_id = 0;
if (ta instanceof WeightedComputedAppearance wca) model_id = wca.getAppearanceIndex(seed);
int tint = 0xFF000000 | MinecraftClient.getInstance().getBlockColors().getColor(theme, blockView, pos, 0);
Mesh untintedMesh = getUntintedRetexturedMesh(new CacheKey(state, ta));
Mesh untintedMesh = getUntintedRetexturedMesh(new MeshCacheKey(state, new TransformCacheKey(ta, 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 slighly different colors.
//the cached mesh will pollute it with a ton of single-use meshes with only slightly different colors.
if(tint == 0xFFFFFFFF) {
context.meshConsumer().accept(untintedMesh);
untintedMesh.outputTo(quad_emitter);
} else {
context.pushTransform(new TintingTransformer(ta, tint));
context.meshConsumer().accept(untintedMesh);
context.pushTransform(new TintingTransformer(ta, tint, seed));
untintedMesh.outputTo(quad_emitter);
context.popTransform();
}
}
@ -95,40 +102,39 @@ public abstract class RetexturingBakedModel extends ForwardingBakedModel {
int tint;
BlockState theme = TemplateEntity.readStateFromItem(stack);
if(!theme.isAir()) {
nbtAppearance = tam.getAppearance(theme);
nbtAppearance = tam.getTemplateAppearance(theme);
tint = 0xFF000000 | ((MinecraftAccessor) MinecraftClient.getInstance()).templates$getItemColors().getColor(new ItemStack(theme.getBlock()), 0);
} else {
nbtAppearance = tam.getDefaultAppearance();
tint = 0xFFFFFFFF;
}
Mesh untintedMesh = getUntintedRetexturedMesh(new CacheKey(itemModelState, nbtAppearance));
Mesh untintedMesh = getUntintedRetexturedMesh(new MeshCacheKey(itemModelState, new TransformCacheKey(nbtAppearance, 0)), 0);
QuadEmitter quad_emitter = context.getEmitter();
if(tint == 0xFFFFFFFF) {
context.meshConsumer().accept(untintedMesh);
untintedMesh.outputTo(quad_emitter);
} else {
context.pushTransform(new TintingTransformer(nbtAppearance, tint));
context.meshConsumer().accept(untintedMesh);
context.pushTransform(new TintingTransformer(nbtAppearance, tint, 0));
untintedMesh.outputTo(quad_emitter);
context.popTransform();
}
}
protected Mesh getUntintedRetexturedMesh(CacheKey key) {
return retexturedMeshes.computeIfAbsent(key, this::createUntintedRetexturedMesh);
protected Mesh getUntintedRetexturedMesh(MeshCacheKey key, long seed) {
return retextured_meshes.computeIfAbsent(key, (k) -> createUntintedRetexturedMesh(k, seed));
}
protected Mesh createUntintedRetexturedMesh(CacheKey key) {
return MeshTransformUtil.pretransformMesh(getBaseMesh(key.state), new RetexturingTransformer(key.appearance));
protected Mesh createUntintedRetexturedMesh(MeshCacheKey key, long seed) {
RetexturingTransformer transformer = retextured_transforms.computeIfAbsent(key.transform, (k) -> new RetexturingTransformer(k.appearance, seed));
return MeshTransformUtil.pretransformMesh(getBaseMesh(key.state), transformer);
}
protected class RetexturingTransformer implements RenderContext.QuadTransform {
protected RetexturingTransformer(TemplateAppearance ta) {
private final long seed;
protected RetexturingTransformer(TemplateAppearance ta, long seed) {
this.ta = ta;
}
@Deprecated(forRemoval = true) //TODO ABI: Deprecated in 2.2. Use TintingTransformer for retinting, it works better anyway
protected RetexturingTransformer(TemplateAppearance ta, int ignoredTint) {
this(ta);
this.seed = seed;
}
protected final TemplateAppearance ta;
@ -141,17 +147,18 @@ public abstract class RetexturingBakedModel extends ForwardingBakedModel {
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]);
quad.spriteBake(ta.getSprite(dir), MutableQuadView.BAKE_NORMALIZED | ta.getBakeFlags(dir) | (uvlock ? MutableQuadView.BAKE_LOCK_UV : 0));
Direction direction = facePermutation.get(DIRECTIONS[quad.tag() - 1]);
quad.spriteBake(ta.getSprite(direction, seed), MutableQuadView.BAKE_NORMALIZED | ta.getBakeFlags(direction, seed) | (uvlock ? MutableQuadView.BAKE_LOCK_UV : 0));
return true;
}
}
protected class TintingTransformer implements RenderContext.QuadTransform {
protected TintingTransformer(TemplateAppearance ta, int tint) {
private final long seed;
protected TintingTransformer(TemplateAppearance ta, int tint, long seed) {
this.ta = ta;
this.tint = tint;
this.seed = seed;
}
protected final TemplateAppearance ta;
@ -163,15 +170,9 @@ public abstract class RetexturingBakedModel extends ForwardingBakedModel {
if(tag == 0) return true;
Direction dir = facePermutation.get(DIRECTIONS[quad.tag() - 1]);
if(ta.hasColor(dir)) quad.color(tint, tint, tint, tint);
if(ta.hasColor(dir, seed)) quad.color(tint, tint, tint, tint);
return true;
}
}
//TODO ABI: From before there was an AO boolean, <2.1.1.
@Deprecated(forRemoval = true)
public RetexturingBakedModel(BakedModel baseModel, TemplateAppearanceManager tam, ModelBakeSettings settings, BlockState itemModelState) {
this(baseModel, tam, settings, itemModelState, true);
}
}

View File

@ -0,0 +1,69 @@
package fr.adrien1106.reframedtemplates.model;
import fr.adrien1106.reframedtemplates.api.TemplatesClientApi;
import fr.adrien1106.reframedtemplates.model.apperance.TemplateAppearanceManager;
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.client.render.model.BakedModel;
import net.minecraft.client.render.model.BakedQuad;
import net.minecraft.client.render.model.ModelBakeSettings;
import net.minecraft.util.collection.Weighted;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.random.Random;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class RetexturingWeightedBakedModel extends RetexturingBakedModel {
final ConcurrentMap<BlockState, List<Weighted.Present<Mesh>>> jsonToMesh = new ConcurrentHashMap<>();
public RetexturingWeightedBakedModel(BakedModel baseModel, TemplateAppearanceManager tam, ModelBakeSettings settings, BlockState itemModelState, boolean ao) {
super(baseModel, tam, settings, itemModelState, ao);
}
@Override
protected Mesh getBaseMesh(BlockState state) {
return null;
// random.setSeed(seed);
// List<Weighted.Present<Mesh>> models = jsonToMesh.computeIfAbsent(state, this::convertModel);
// return Weighting
// .getAt(models, Math.abs((int)random.nextLong()) % Weighting.getWeightSum(models))
// .map(Weighted.Present::getData).get();
}
private List<Weighted.Present<Mesh>> convertModel(BlockState state) {
Renderer r = TemplatesClientApi.getInstance().getFabricRenderer();
MeshBuilder builder = r.meshBuilder();
QuadEmitter emitter = builder.getEmitter();
RenderMaterial mat = tam.getCachedMaterial(state, false);
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();
}
}
// builder.build();
return null;
}
}

View File

@ -1,14 +0,0 @@
package fr.adrien1106.reframedtemplates.model;
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial;
import net.minecraft.client.texture.Sprite;
import net.minecraft.util.math.Direction;
import org.jetbrains.annotations.NotNull;
//TODO ABI: move to the api package
public interface TemplateAppearance {
@NotNull RenderMaterial getRenderMaterial(boolean ao);
@NotNull Sprite getSprite(Direction dir);
int getBakeFlags(Direction dir);
boolean hasColor(Direction dir);
}

View File

@ -11,6 +11,7 @@ import net.minecraft.client.render.model.UnbakedModel;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.util.SpriteIdentifier;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.random.Random;
import java.util.Collection;
import java.util.Collections;

View File

@ -0,0 +1,6 @@
package fr.adrien1106.reframedtemplates.model.apperance;
import net.minecraft.client.texture.Sprite;
record Appearance(Sprite[] sprites, int[] flags, byte color_mask) {
}

View File

@ -0,0 +1,62 @@
package fr.adrien1106.reframedtemplates.model.apperance;
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial;
import net.minecraft.client.texture.Sprite;
import net.minecraft.util.math.Direction;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
public class ComputedAppearance implements TemplateAppearance {
private final Appearance appearance;
private final int id;
private final RenderMaterial matWithAo;
private final RenderMaterial matWithoutAo;
public ComputedAppearance(@NotNull Appearance appearance, RenderMaterial withAo, RenderMaterial withoutAo, int id) {
this.appearance = appearance;
this.id = id;
this.matWithAo = withAo;
this.matWithoutAo = withoutAo;
}
@Override
public @NotNull RenderMaterial getRenderMaterial(boolean ao) {
return ao ? matWithAo : matWithoutAo;
}
@Override
public @NotNull Sprite getSprite(Direction dir, long seed) {
return appearance.sprites()[dir.ordinal()];
}
@Override
public int getBakeFlags(Direction dir, long seed) {
return appearance.flags()[dir.ordinal()];
}
@Override
public boolean hasColor(Direction dir, long seed) {
return (appearance.color_mask() & (1 << dir.ordinal())) != 0;
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
ComputedAppearance that = (ComputedAppearance) o;
return id == that.id;
}
@Override
public int hashCode() {
return id;
}
@Override
public String toString() {
return "ComputedApperance{sprites=%s, bakeFlags=%s, hasColorMask=%s, matWithoutAo=%s, matWithAo=%s, id=%d}"
.formatted(Arrays.toString(appearance.sprites()), Arrays.toString(appearance.flags()), appearance.color_mask(), matWithoutAo, matWithAo, id);
}
}

View File

@ -0,0 +1,56 @@
package fr.adrien1106.reframedtemplates.model.apperance;
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial;
import net.minecraft.client.texture.Sprite;
import net.minecraft.util.math.Direction;
import org.jetbrains.annotations.NotNull;
public class SingleSpriteAppearance implements TemplateAppearance {
private final @NotNull Sprite defaultSprite;
private final RenderMaterial mat;
private final int id;
public SingleSpriteAppearance(@NotNull Sprite defaultSprite, RenderMaterial mat, int id) {
this.defaultSprite = defaultSprite;
this.mat = mat;
this.id = id;
}
@Override
public @NotNull RenderMaterial getRenderMaterial(boolean ao) {
return mat;
}
@Override
public @NotNull Sprite getSprite(Direction dir, long seed) {
return defaultSprite;
}
@Override
public int getBakeFlags(Direction dir, long seed) {
return 0;
}
@Override
public boolean hasColor(Direction dir, long seed) {
return false;
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
SingleSpriteAppearance that = (SingleSpriteAppearance) o;
return id == that.id;
}
@Override
public int hashCode() {
return id;
}
@Override
public String toString() {
return "SingleSpriteAppearance[defaultSprite=%s, mat=%s, id=%d]".formatted(defaultSprite, mat, id);
}
}

View File

@ -0,0 +1,16 @@
package fr.adrien1106.reframedtemplates.model.apperance;
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial;
import net.minecraft.client.texture.Sprite;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.random.Random;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface TemplateAppearance {
@NotNull RenderMaterial getRenderMaterial(boolean ao);
@NotNull Sprite getSprite(Direction dir, @NotNull long seed);
int getBakeFlags(Direction dir, @NotNull long seed);
boolean hasColor(Direction dir, @NotNull long seed);
}

View File

@ -1,6 +1,7 @@
package fr.adrien1106.reframedtemplates.model;
package fr.adrien1106.reframedtemplates.model.apperance;
import fr.adrien1106.reframedtemplates.api.TemplatesClientApi;
import fr.adrien1106.reframedtemplates.mixin.model.WeightedBakedModelAccessor;
import net.fabricmc.fabric.api.renderer.v1.Renderer;
import net.fabricmc.fabric.api.renderer.v1.material.BlendMode;
import net.fabricmc.fabric.api.renderer.v1.material.MaterialFinder;
@ -14,25 +15,22 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.RenderLayers;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.client.render.model.BakedQuad;
import net.minecraft.client.render.model.json.ModelTransformation;
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.collection.Weighted;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.random.Random;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
//TODO: extract an API for the api package
public class TemplateAppearanceManager {
public TemplateAppearanceManager(Function<SpriteIdentifier, Sprite> spriteLookup) {
MaterialFinder finder = TemplatesClientApi.getInstance().getFabricRenderer().materialFinder();
for(BlendMode blend : BlendMode.values()) {
@ -50,10 +48,8 @@ public class TemplateAppearanceManager {
if(barrier == null) barrier = defaultSprite; //eh
this.barrierItemAppearance = new SingleSpriteAppearance(barrier, materialsWithoutAo.get(BlendMode.CUTOUT), serialNumber.getAndIncrement());
}
//TODO ABI: Shouldn't have been made public. Noticed this in 2.2.
@ApiStatus.Internal
public static final SpriteIdentifier DEFAULT_SPRITE_ID = new SpriteIdentifier(PlayerScreenHandler.BLOCK_ATLAS_TEXTURE, new Identifier("reframedtemplates:block/framed_block"));
protected static final SpriteIdentifier DEFAULT_SPRITE_ID = new SpriteIdentifier(PlayerScreenHandler.BLOCK_ATLAS_TEXTURE, new Identifier("reframedtemplates:block/framed_block"));
private static final SpriteIdentifier BARRIER_SPRITE_ID = new SpriteIdentifier(PlayerScreenHandler.BLOCK_ATLAS_TEXTURE, new Identifier("minecraft:item/barrier"));
private final TemplateAppearance defaultAppearance;
@ -69,7 +65,7 @@ public class TemplateAppearanceManager {
return defaultAppearance;
}
public TemplateAppearance getAppearance(BlockState state) {
public TemplateAppearance getTemplateAppearance(BlockState state) {
return appearanceCache.computeIfAbsent(state, this::computeAppearance);
}
@ -95,33 +91,54 @@ public class TemplateAppearanceManager {
//Tiny amount of wasted space in some caches if TemplateAppearances are used as a map key, then. IMO it's not a critical issue.
private TemplateAppearance computeAppearance(BlockState state) {
if(state.getBlock() == Blocks.BARRIER) return barrierItemAppearance;
Random rand = Random.create(42);
BakedModel model = MinecraftClient.getInstance().getBlockRenderManager().getModel(state);
if (!(model instanceof WeightedBakedModelAccessor weighted_model)) {
return new ComputedAppearance(
getAppearance(model),
getCachedMaterial(state, true),
getCachedMaterial(state, false),
serialNumber.getAndIncrement()
);
}
List<Weighted.Present<Appearance>> appearances = weighted_model.getModels().stream()
.map(baked_model -> Weighted.of(getAppearance(baked_model.getData()), baked_model.getWeight().getValue()))
.toList();
return new WeightedComputedAppearance(
appearances,
getCachedMaterial(state, true),
getCachedMaterial(state, false),
serialNumber.getAndIncrement()
);
}
private Appearance getAppearance(BakedModel model) {
//Only for parsing vanilla quads:
Renderer r = TemplatesClientApi.getInstance().getFabricRenderer();
QuadEmitter emitterAsParser = r.meshBuilder().getEmitter();
RenderMaterial defaultMat = r.materialFinder().clear().find();
QuadEmitter quad_emitter = r.meshBuilder().getEmitter();
RenderMaterial material = r.materialFinder().clear().find();
Random random = Random.create();
Sprite[] sprites = new Sprite[6];
int[] bakeFlags = new int[6];
byte hasColorMask = 0b000000;
int[] flags = new int[6];
byte[] color_mask = {0b000000};
//Read quads off the model by their `cullface`
for(Direction dir : Direction.values()) {
List<BakedQuad> sideQuads = model.getQuads(null, dir, rand);
if(sideQuads.isEmpty()) continue;
BakedQuad arbitraryQuad = sideQuads.get(0); //TODO: maybe pick a largest quad instead?
if(arbitraryQuad == null) continue;
if(arbitraryQuad.hasColor()) hasColorMask |= (byte) (1 << dir.ordinal());
Sprite sprite = arbitraryQuad.getSprite();
if(sprite == null) continue;
sprites[dir.ordinal()] = sprite;
Arrays.stream(Direction.values()).forEach(direction -> {
List<BakedQuad> quads = model.getQuads(null, direction, random);
if(quads.isEmpty() || quads.get(0) == null) {
sprites[direction.ordinal()] = defaultAppearance.getSprite(direction, 0); // make sure direction has a sprite
return;
}
BakedQuad quad = quads.get(0);
if(quad.hasColor()) color_mask[0] |= (byte) (1 << direction.ordinal());
Sprite sprite = quad.getSprite();
if(sprite == null) return;
sprites[direction.ordinal()] = sprite;
//Problem: Some models (eg. pistons, stone, glazed terracotta) have their UV coordinates permuted in
//non-standard ways. The actual png image appears sideways, but the original model's UVs rotate it right way up again.
//If I simply display the texture on my model, it will appear sideways, like it does in the png.
@ -133,139 +150,19 @@ public class TemplateAppearanceManager {
//of the other two (since UV coordinates are unique and shouldn't cross). There are 16 possibilities so this information
//is easily summarized in a bitfield, and the correct fabric rendering API "bake flags" to un-transform the sprite
//are looked up with a simple table.
emitterAsParser.fromVanilla(arbitraryQuad, defaultMat, dir);
quad_emitter.fromVanilla(quad, material, direction);
float spriteUAvg = (sprite.getMinU() + sprite.getMaxU()) / 2;
float spriteVAvg = (sprite.getMinV() + sprite.getMaxV()) / 2;
bakeFlags[dir.ordinal()] = MAGIC_BAKEFLAGS_SBOX[
(emitterAsParser.u(0) < spriteUAvg ? 8 : 0) |
(emitterAsParser.v(0) < spriteVAvg ? 4 : 0) |
(emitterAsParser.u(1) < spriteUAvg ? 2 : 0) |
(emitterAsParser.v(1) < spriteVAvg ? 1 : 0)
flags[direction.ordinal()] = MAGIC_BAKEFLAGS_SBOX[
(quad_emitter.u(0) < spriteUAvg ? 8 : 0) |
(quad_emitter.v(0) < spriteVAvg ? 4 : 0) |
(quad_emitter.u(1) < spriteUAvg ? 2 : 0) |
(quad_emitter.v(1) < spriteVAvg ? 1 : 0)
];
}
//Fill out any missing values in the sprites array, since failure to pick textures shouldn't lead to NPEs later on
for(int i = 0; i < sprites.length; i++) {
if(sprites[i] == null) sprites[i] = defaultAppearance.getSprite(Direction.byId(i));
}
return new ComputedApperance(
sprites,
bakeFlags,
hasColorMask,
getCachedMaterial(state, true),
getCachedMaterial(state, false),
serialNumber.getAndIncrement()
);
}
private static final class ComputedApperance implements TemplateAppearance {
private final Sprite @NotNull[] sprites;
private final int @NotNull[] bakeFlags;
private final byte hasColorMask;
private final int id;
private final RenderMaterial matWithAo;
private final RenderMaterial matWithoutAo;
private ComputedApperance(@NotNull Sprite @NotNull[] sprites, int @NotNull[] bakeFlags, byte hasColorMask, RenderMaterial withAo, RenderMaterial withoutAo, int id) {
this.sprites = sprites;
this.bakeFlags = bakeFlags;
this.hasColorMask = hasColorMask;
this.id = id;
this.matWithAo = withAo;
this.matWithoutAo = withoutAo;
}
@Override
public @NotNull RenderMaterial getRenderMaterial(boolean ao) {
return ao ? matWithAo : matWithoutAo;
}
@Override
public @NotNull Sprite getSprite(Direction dir) {
return sprites[dir.ordinal()];
}
@Override
public int getBakeFlags(Direction dir) {
return bakeFlags[dir.ordinal()];
}
@Override
public boolean hasColor(Direction dir) {
return (hasColorMask & (1 << dir.ordinal())) != 0;
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
ComputedApperance that = (ComputedApperance) o;
return id == that.id;
}
@Override
public int hashCode() {
return id;
}
@Override
public String toString() {
return "ComputedApperance{sprites=%s, bakeFlags=%s, hasColorMask=%s, matWithoutAo=%s, matWithAo=%s, id=%d}".formatted(Arrays.toString(sprites), Arrays.toString(bakeFlags), hasColorMask, matWithoutAo, matWithAo, id);
}
}
@SuppressWarnings("ClassCanBeRecord")
private static final class SingleSpriteAppearance implements TemplateAppearance {
private final @NotNull Sprite defaultSprite;
private final RenderMaterial mat;
private final int id;
private SingleSpriteAppearance(@NotNull Sprite defaultSprite, RenderMaterial mat, int id) {
this.defaultSprite = defaultSprite;
this.mat = mat;
this.id = id;
}
@Override
public @NotNull RenderMaterial getRenderMaterial(boolean ao) {
return mat;
}
@Override
public @NotNull Sprite getSprite(Direction dir) {
return defaultSprite;
}
@Override
public int getBakeFlags(Direction dir) {
return 0;
}
@Override
public boolean hasColor(Direction dir) {
return false;
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
SingleSpriteAppearance that = (SingleSpriteAppearance) o;
return id == that.id;
}
@Override
public int hashCode() {
return id;
}
@Override
public String toString() {
return "SingleSpriteAppearance[defaultSprite=%s, mat=%s, id=%d]".formatted(defaultSprite, mat, id);
}
});
return new Appearance(sprites, flags, color_mask[0]);
}
}

View File

@ -0,0 +1,82 @@
package fr.adrien1106.reframedtemplates.model.apperance;
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial;
import net.minecraft.client.texture.Sprite;
import net.minecraft.util.collection.Weighted;
import net.minecraft.util.collection.Weighting;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.random.Random;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
public class WeightedComputedAppearance implements TemplateAppearance {
private final List<Weighted.Present<Appearance>> appearances;
private final int total_weight;
private final int id;
private final RenderMaterial matWithAo;
private final RenderMaterial matWithoutAo;
public WeightedComputedAppearance(@NotNull List<Weighted.Present<Appearance>> appearances, RenderMaterial withAo, RenderMaterial withoutAo, int id) {
this.appearances = appearances;
this.total_weight = Weighting.getWeightSum(appearances);
this.id = id;
this.matWithAo = withAo;
this.matWithoutAo = withoutAo;
}
@Override
public @NotNull RenderMaterial getRenderMaterial(boolean ao) {
return ao ? matWithAo : matWithoutAo;
}
public int getAppearanceIndex(long seed) {
Random random = Random.create(seed);
return Weighting.getAt(appearances, Math.abs((int)random.nextLong()) % total_weight)
.map(appearances::indexOf).get();
}
private Appearance getAppearance(long seed) {
Random random = Random.create(seed);
return Weighting.getAt(appearances, Math.abs((int)random.nextLong()) % total_weight)
.map(Weighted.Present::getData).get();
}
@Override
public @NotNull Sprite getSprite(Direction dir, long seed) {
return getAppearance(seed).sprites()[dir.ordinal()];
}
@Override
public int getBakeFlags(Direction dir, long seed) {
return getAppearance(seed).flags()[dir.ordinal()];
}
@Override
public boolean hasColor(Direction dir, long seed) {
return (getAppearance(seed).color_mask() & (1 << dir.ordinal())) != 0;
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
WeightedComputedAppearance that = (WeightedComputedAppearance) o;
return id == that.id;
}
@Override
public int hashCode() {
return id;
}
@Override
public String toString() {
Appearance appearance = appearances.get(0).getData();
return "ComputedApperance{sprites=%s, bakeFlags=%s, hasColorMask=%s, matWithoutAo=%s, matWithAo=%s, id=%d}"
.formatted(Arrays.toString(appearance.sprites()), Arrays.toString(appearance.flags()), appearance.color_mask(), matWithoutAo, matWithAo, id);
}
}

View File

@ -3,15 +3,17 @@
"package": "fr.adrien1106.reframedtemplates.mixin",
"compatibilityLevel": "JAVA_17",
"mixins": [
"MinecraftAccessor",
"WallBlockAccessor",
"particles.MixinEntity",
"particles.MixinLivingEntity"
"MinecraftAccessor",
"WallBlockAccessor",
"particles.MixinEntity",
"particles.MixinLivingEntity"
],
"client": [
"particles.AccessorParticle",
"particles.AccessorSpriteBillboardParticle",
"particles.MixinBlockDustParticle"
"particles.AccessorParticle",
"particles.AccessorSpriteBillboardParticle",
"particles.MixinBlockDustParticle",
"particles.MixinBlockDustParticle",
"model.WeightedBakedModelAccessor"
],
"injectors": {
"defaultRequire": 1