Athena support + stair improvements + coloration #5

Merged
Adrien1106 merged 4 commits from dev into master 2024-02-26 17:05:24 +01:00
21 changed files with 510 additions and 126 deletions
Showing only changes of commit 58d9687ff8 - Show all commits

View File

@ -66,6 +66,12 @@ repositories {
name 'altarik-releases' name 'altarik-releases'
url 'https://repo.altarik.fr/releases/' url 'https://repo.altarik.fr/releases/'
} }
maven {
url "https://maven.teamresourceful.com/repository/maven-public/"
}
maven {
url "https://maven.resourcefulbees.com/repository/maven-public/"
}
mavenCentral() mavenCentral()
// Add repositories to retrieve artifacts from in here. // Add repositories to retrieve artifacts from in here.
@ -82,6 +88,14 @@ dependencies {
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
// Athena for connected texture
modCompileOnly "earth.terrarium.athena:athena-fabric-${project.minecraft_version}:${project.athena_version}"
modRuntimeOnly "earth.terrarium.athena:athena-fabric-${project.minecraft_version}:${project.athena_version}"
// Chipped to test athena implementation
modRuntimeOnly "com.teamresourceful.resourcefullib:resourcefullib-fabric-${project.minecraft_version}:2.4.7"
// modRuntimeOnly "earth.terrarium.chipped:chipped-fabric-${project.minecraft_version}:3.1.2" TODO not working for some reasons
// Fabric API. This is technically optional, but you probably want it anyway. // Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
} }
@ -91,6 +105,7 @@ processResources {
inputs.property "minecraft_version", project.minecraft_version inputs.property "minecraft_version", project.minecraft_version
inputs.property "loader_version", project.loader_version inputs.property "loader_version", project.loader_version
inputs.property "mod_id", project.mod_id inputs.property "mod_id", project.mod_id
inputs.property "athena_version", project.athena_version
filteringCharset "UTF-8" filteringCharset "UTF-8"
filesMatching("fabric.mod.json") { filesMatching("fabric.mod.json") {

View File

@ -8,7 +8,7 @@ yarn_mappings=1.20.4+build.3
loader_version=0.15.6 loader_version=0.15.6
# Mod Properties # Mod Properties
mod_version = 1.0-SNAPSHOT mod_version = 1.1
maven_group = fr.adrien1106 maven_group = fr.adrien1106
archives_base_name = ReFramed archives_base_name = ReFramed
mod_id = reframed mod_id = reframed
@ -19,3 +19,5 @@ fabric_version=0.95.4+1.20.4
git_owner=Altarik git_owner=Altarik
git_repo=ReFramed git_repo=ReFramed
athena_version=3.3.0

View File

@ -0,0 +1,10 @@
package fr.adrien1106.reframed.client.model;
import net.minecraft.block.BlockState;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.BlockRenderView;
public interface DynamicBakedModel {
BakedModel computeQuads(BlockRenderView level, BlockState state, BlockPos pos);
}

View File

@ -15,13 +15,16 @@ import java.util.EnumMap;
import java.util.Map; import java.util.Map;
public class MeshTransformUtil { public class MeshTransformUtil {
public static Mesh pretransformMesh(Mesh mesh, RenderContext.QuadTransform transform) { public static Mesh pretransformMesh(Mesh mesh, RetexturingBakedModel.RetexturingTransformer transform) {
MeshBuilder builder = ReFramedClient.HELPER.getFabricRenderer().meshBuilder(); MeshBuilder builder = ReFramedClient.HELPER.getFabricRenderer().meshBuilder();
QuadEmitter emitter = builder.getEmitter(); QuadEmitter emitter = builder.getEmitter();
mesh.forEach(quad -> { mesh.forEach(quad -> {
emitter.copyFrom(quad); int i = -1;
if(transform.transform(emitter)) emitter.emit(); do {
emitter.copyFrom(quad);
i = transform.transform(emitter, i);
} while (i > 0);
}); });
return builder.build(); return builder.build();

View File

@ -0,0 +1,79 @@
package fr.adrien1106.reframed.client.model;
import net.fabricmc.fabric.api.renderer.v1.mesh.MutableQuadView;
import net.fabricmc.fabric.api.renderer.v1.mesh.QuadView;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.MathHelper;
import org.joml.Vector3f;
public record QuadPosBounds(float min_x, float max_x, float min_y, float max_y, float min_z, float max_z) {
public static QuadPosBounds read(QuadView quad) {
return read(quad, true);
}
public static QuadPosBounds read(QuadView quad, boolean check_full) {
float x0 = quad.x(0), x1 = quad.x(1), x2 = quad.x(2), x3 = quad.x(3);
float y0 = quad.y(0), y1 = quad.y(1), y2 = quad.y(2), y3 = quad.y(3);
float z0 = quad.z(0), z1 = quad.z(1), z2 = quad.z(2), z3 = quad.z(3);
// Checks if the Dimensions are either 0 or 1 except for the Axis dimension
Direction.Axis axis = quad.nominalFace().getAxis();
if (check_full && (axis == Direction.Axis.X || (
(MathHelper.approximatelyEquals(x0, 0) || MathHelper.approximatelyEquals(x0, 1))
&& (MathHelper.approximatelyEquals(x1, 0) || MathHelper.approximatelyEquals(x1, 1))
&& (MathHelper.approximatelyEquals(x2, 0) || MathHelper.approximatelyEquals(x2, 1))
&& (MathHelper.approximatelyEquals(x3, 0) || MathHelper.approximatelyEquals(x3, 1))
)) && (axis == Direction.Axis.Y || (
(MathHelper.approximatelyEquals(y0, 0) || MathHelper.approximatelyEquals(y0, 1))
&& (MathHelper.approximatelyEquals(y1, 0) || MathHelper.approximatelyEquals(y1, 1))
&& (MathHelper.approximatelyEquals(y2, 0) || MathHelper.approximatelyEquals(y2, 1))
&& (MathHelper.approximatelyEquals(y3, 0) || MathHelper.approximatelyEquals(y3, 1))
)) & (axis == Direction.Axis.Z || (
(MathHelper.approximatelyEquals(z0, 0) || MathHelper.approximatelyEquals(z0, 1))
&& (MathHelper.approximatelyEquals(z1, 0) || MathHelper.approximatelyEquals(z1, 1))
&& (MathHelper.approximatelyEquals(z2, 0) || MathHelper.approximatelyEquals(z2, 1))
&& (MathHelper.approximatelyEquals(z3, 0) || MathHelper.approximatelyEquals(z3, 1))
))
) return null;
return new QuadPosBounds(
Math.min(Math.min(x0, x1), Math.min(x2, x3)),
Math.max(Math.max(x0, x1), Math.max(x2, x3)),
Math.min(Math.min(y0, y1), Math.min(y2, y3)),
Math.max(Math.max(y0, y1), Math.max(y2, y3)),
Math.min(Math.min(z0, z1), Math.min(z2, z3)),
Math.max(Math.max(z0, z1), Math.max(z2, z3))
);
}
public boolean matches(QuadPosBounds other_bounds) {
return !(
(min_x != max_x && (min_x >= other_bounds.max_x || max_x <= other_bounds.min_x))
|| (min_y != max_y && (min_y >= other_bounds.max_y || max_y <= other_bounds.min_y))
|| (min_z != max_z && (min_z >= other_bounds.max_z || max_z <= other_bounds.min_z))
);
}
public QuadPosBounds intersection(QuadPosBounds other_bounds, Direction.Axis axis) {
return new QuadPosBounds(
axis.equals(Direction.Axis.X) ? other_bounds.min_x: Math.max(min_x, other_bounds.min_x),
axis.equals(Direction.Axis.X) ? other_bounds.max_x: Math.min(max_x, other_bounds.max_x),
axis.equals(Direction.Axis.Y) ? other_bounds.min_y: Math.max(min_y, other_bounds.min_y),
axis.equals(Direction.Axis.Y) ? other_bounds.max_y: Math.min(max_y, other_bounds.max_y),
axis.equals(Direction.Axis.Z) ? other_bounds.min_z: Math.max(min_z, other_bounds.min_z),
axis.equals(Direction.Axis.Z) ? other_bounds.max_z: Math.min(max_z, other_bounds.max_z)
);
}
public void apply(MutableQuadView quad, QuadPosBounds origin_bounds) {
Vector3f pos = new Vector3f();
for (int i = 0; i < 4; i++) {
quad.copyPos(i, pos);
pos.x = MathHelper.approximatelyEquals(pos.x, origin_bounds.min_x)? min_x: max_x;
pos.y = MathHelper.approximatelyEquals(pos.y, origin_bounds.min_y)? min_y: max_y;
pos.z = MathHelper.approximatelyEquals(pos.z, origin_bounds.min_z)? min_z: max_z;
quad.pos(i, pos);
}
}
}

View File

@ -5,10 +5,10 @@ import net.fabricmc.fabric.api.renderer.v1.mesh.QuadView;
import net.minecraft.client.texture.Sprite; import net.minecraft.client.texture.Sprite;
import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.MathHelper;
record QuadUvBounds(float minU, float maxU, float minV, float maxV) { public record QuadUvBounds(float minU, float maxU, float minV, float maxV) {
static QuadUvBounds read(QuadView quad) { public 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 u0 = quad.u(0), u1 = quad.u(1), u2 = quad.u(2), u3 = quad.u(3);
float v0 = quad.v(0); float v1 = quad.v(1); float v2 = quad.v(2); float v3 = quad.v(3); float v0 = quad.v(0), v1 = quad.v(1), v2 = quad.v(2), v3 = quad.v(3);
return new QuadUvBounds( return new QuadUvBounds(
Math.min(Math.min(u0, u1), Math.min(u2, u3)), Math.min(Math.min(u0, u1), Math.min(u2, u3)),
Math.max(Math.max(u0, u1), Math.max(u2, u3)), Math.max(Math.max(u0, u1), Math.max(u2, u3)),
@ -36,9 +36,5 @@ record QuadUvBounds(float minU, float maxU, float minV, float maxV) {
float value2 = MathHelper.clamp(value, low, high); float value2 = MathHelper.clamp(value, low, high);
return (value2 - low) / (high - low); 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,6 +1,7 @@
package fr.adrien1106.reframed.client.model; package fr.adrien1106.reframed.client.model;
import fr.adrien1106.reframed.block.ReFramedEntity; import fr.adrien1106.reframed.block.ReFramedEntity;
import fr.adrien1106.reframed.client.model.apperance.SpriteProperties;
import fr.adrien1106.reframed.mixin.MinecraftAccessor; import fr.adrien1106.reframed.mixin.MinecraftAccessor;
import fr.adrien1106.reframed.client.model.apperance.CamoAppearance; import fr.adrien1106.reframed.client.model.apperance.CamoAppearance;
import fr.adrien1106.reframed.client.model.apperance.CamoAppearanceManager; import fr.adrien1106.reframed.client.model.apperance.CamoAppearanceManager;
@ -22,6 +23,7 @@ import net.minecraft.util.math.Direction;
import net.minecraft.util.math.random.Random; import net.minecraft.util.math.random.Random;
import net.minecraft.world.BlockRenderView; import net.minecraft.world.BlockRenderView;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -59,12 +61,12 @@ public abstract class RetexturingBakedModel extends ForwardingBakedModel {
@Override @Override
public Sprite getParticleSprite() { public Sprite getParticleSprite() {
return tam.getDefaultAppearance().getSprite(Direction.UP, 0); return tam.getDefaultAppearance().getSprites(Direction.UP, 0).get(0).sprite();
} }
@Override @Override
public void emitBlockQuads(BlockRenderView blockView, BlockState state, BlockPos pos, Supplier<Random> randomSupplier, RenderContext context) { public void emitBlockQuads(BlockRenderView world, BlockState state, BlockPos pos, Supplier<Random> randomSupplier, RenderContext context) {
BlockState theme = (blockView.getBlockEntityRenderData(pos) instanceof BlockState s) ? s : null; BlockState theme = (world.getBlockEntityRenderData(pos) instanceof BlockState s) ? s : null;
QuadEmitter quad_emitter = context.getEmitter(); QuadEmitter quad_emitter = context.getEmitter();
if(theme == null || theme.isAir()) { if(theme == null || theme.isAir()) {
getUntintedRetexturedMesh(new MeshCacheKey(state, new TransformCacheKey(tam.getDefaultAppearance(), 0)), 0).outputTo(quad_emitter); getUntintedRetexturedMesh(new MeshCacheKey(state, new TransformCacheKey(tam.getDefaultAppearance(), 0)), 0).outputTo(quad_emitter);
@ -72,12 +74,12 @@ public abstract class RetexturingBakedModel extends ForwardingBakedModel {
} }
if(theme.getBlock() == Blocks.BARRIER) return; if(theme.getBlock() == Blocks.BARRIER) return;
CamoAppearance camo = tam.getCamoAppearance(theme); CamoAppearance camo = tam.getCamoAppearance(world, theme, pos);
long seed = theme.getRenderingSeed(pos); long seed = theme.getRenderingSeed(pos);
int model_id = 0; int model_id = 0;
if (camo instanceof WeightedComputedAppearance wca) model_id = wca.getAppearanceIndex(seed); if (camo instanceof WeightedComputedAppearance wca) model_id = wca.getAppearanceIndex(seed);
int tint = 0xFF000000 | MinecraftClient.getInstance().getBlockColors().getColor(theme, blockView, pos, 0); int tint = 0xFF000000 | MinecraftClient.getInstance().getBlockColors().getColor(theme, world, pos, 0);
Mesh untintedMesh = getUntintedRetexturedMesh( Mesh untintedMesh = getUntintedRetexturedMesh(
new MeshCacheKey( new MeshCacheKey(
state, state,
@ -105,7 +107,7 @@ public abstract class RetexturingBakedModel extends ForwardingBakedModel {
int tint; int tint;
BlockState theme = ReFramedEntity.readStateFromItem(stack); BlockState theme = ReFramedEntity.readStateFromItem(stack);
if(!theme.isAir()) { if(!theme.isAir()) {
nbtAppearance = tam.getCamoAppearance(theme); nbtAppearance = tam.getCamoAppearance(null, theme, null);
tint = 0xFF000000 | ((MinecraftAccessor) MinecraftClient.getInstance()).getItemColors().getColor(new ItemStack(theme.getBlock()), 0); tint = 0xFF000000 | ((MinecraftAccessor) MinecraftClient.getInstance()).getItemColors().getColor(new ItemStack(theme.getBlock()), 0);
} else { } else {
nbtAppearance = tam.getDefaultAppearance(); nbtAppearance = tam.getDefaultAppearance();
@ -133,7 +135,7 @@ public abstract class RetexturingBakedModel extends ForwardingBakedModel {
return MeshTransformUtil.pretransformMesh(getBaseMesh(key.state), transformer); return MeshTransformUtil.pretransformMesh(getBaseMesh(key.state), transformer);
} }
protected class RetexturingTransformer implements RenderContext.QuadTransform { public class RetexturingTransformer {
private final long seed; private final long seed;
protected RetexturingTransformer(CamoAppearance ta, long seed) { protected RetexturingTransformer(CamoAppearance ta, long seed) {
this.ta = ta; this.ta = ta;
@ -141,22 +143,49 @@ public abstract class RetexturingBakedModel extends ForwardingBakedModel {
} }
protected final CamoAppearance ta; protected final CamoAppearance ta;
@Override public int transform(QuadEmitter quad, int i) {
public boolean transform(MutableQuadView quad) {
quad.material(ta.getRenderMaterial(ao));
int tag = quad.tag(); int tag = quad.tag();
if(tag == 0) return true; //Pass the quad through unmodified. if(tag == 0) return 0; //Pass the quad through unmodified.
//The quad tag numbers were selected so this magic trick works:
Direction direction = quad.nominalFace(); Direction direction = quad.nominalFace();
quad.spriteBake(ta.getSprite(direction, seed), MutableQuadView.BAKE_NORMALIZED | ta.getBakeFlags(direction, seed) | (uvlock ? MutableQuadView.BAKE_LOCK_UV : 0)); List<SpriteProperties> sprites = ta.getSprites(direction, seed);
return true; 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()
| (uvlock ? MutableQuadView.BAKE_LOCK_UV : 0)
);
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( // TODO check if the flags are usefull because it seems to be braking the functioning of it
properties.sprite(),
MutableQuadView.BAKE_NORMALIZED
| MutableQuadView.BAKE_LOCK_UV
);
quad.emit();
return i;
} }
} }
protected class TintingTransformer implements RenderContext.QuadTransform { protected static class TintingTransformer implements RenderContext.QuadTransform {
private final long seed; private final long seed;
protected TintingTransformer(CamoAppearance ta, int tint, long seed) { protected TintingTransformer(CamoAppearance ta, int tint, long seed) {
this.ta = ta; this.ta = ta;

View File

@ -1,5 +1,8 @@
package fr.adrien1106.reframed.client.model.apperance; package fr.adrien1106.reframed.client.model.apperance;
import net.minecraft.client.texture.Sprite; import net.minecraft.util.math.Direction;
public record Appearance(Sprite[] sprites, int[] flags, byte color_mask) {} import java.util.List;
import java.util.Map;
public record Appearance(Map<Direction, List<SpriteProperties>> sprites, byte color_mask) {}

View File

@ -1,13 +1,13 @@
package fr.adrien1106.reframed.client.model.apperance; package fr.adrien1106.reframed.client.model.apperance;
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial; import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial;
import net.minecraft.client.texture.Sprite;
import net.minecraft.util.math.Direction; import net.minecraft.util.math.Direction;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List;
public interface CamoAppearance { public interface CamoAppearance {
@NotNull RenderMaterial getRenderMaterial(boolean ao); @NotNull RenderMaterial getRenderMaterial(boolean ao);
@NotNull Sprite getSprite(Direction dir, long seed); @NotNull List<SpriteProperties> getSprites(Direction dir, long seed);
int getBakeFlags(Direction dir, long seed);
boolean hasColor(Direction dir, long seed); boolean hasColor(Direction dir, long seed);
} }

View File

@ -2,6 +2,8 @@ package fr.adrien1106.reframed.client.model.apperance;
import fr.adrien1106.reframed.ReFramed; import fr.adrien1106.reframed.ReFramed;
import fr.adrien1106.reframed.client.ReFramedClient; import fr.adrien1106.reframed.client.ReFramedClient;
import fr.adrien1106.reframed.client.model.DynamicBakedModel;
import fr.adrien1106.reframed.client.model.QuadPosBounds;
import fr.adrien1106.reframed.mixin.model.WeightedBakedModelAccessor; import fr.adrien1106.reframed.mixin.model.WeightedBakedModelAccessor;
import net.fabricmc.fabric.api.renderer.v1.Renderer; 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.BlendMode;
@ -21,13 +23,12 @@ import net.minecraft.client.util.SpriteIdentifier;
import net.minecraft.screen.PlayerScreenHandler; import net.minecraft.screen.PlayerScreenHandler;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.minecraft.util.collection.Weighted; import net.minecraft.util.collection.Weighted;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction; import net.minecraft.util.math.Direction;
import net.minecraft.util.math.random.Random; import net.minecraft.util.math.random.Random;
import net.minecraft.world.BlockRenderView;
import java.util.Arrays; import java.util.*;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function; import java.util.function.Function;
@ -68,8 +69,14 @@ public class CamoAppearanceManager {
return defaultAppearance; return defaultAppearance;
} }
public CamoAppearance getCamoAppearance(BlockState state) { public CamoAppearance getCamoAppearance(BlockRenderView world, BlockState state, BlockPos pos) {
return appearanceCache.computeIfAbsent(state, this::computeAppearance); BakedModel model = MinecraftClient.getInstance().getBlockRenderManager().getModel(state);
// add support for connected textures and more generally any compatible models injected so that they return baked quads
if (model instanceof DynamicBakedModel dynamic_model) {
return computeAppearance(dynamic_model.computeQuads(world, state, pos), state);
}
return appearanceCache.computeIfAbsent(state, block_state -> computeAppearance(model, block_state));
} }
public RenderMaterial getCachedMaterial(BlockState state, boolean ao) { public RenderMaterial getCachedMaterial(BlockState state, boolean ao) {
@ -81,10 +88,9 @@ public class CamoAppearanceManager {
// 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).
// Tiny amount of wasted space in some caches if CamoAppearances are used as a map key, then. IMO it's not a critical issue. // Tiny amount of wasted space in some caches if CamoAppearances are used as a map key, then. IMO it's not a critical issue.
private CamoAppearance computeAppearance(BlockState state) { private CamoAppearance computeAppearance(BakedModel model, BlockState state) {
if(state.getBlock() == Blocks.BARRIER) return barrierItemAppearance; if(state.getBlock() == Blocks.BARRIER) return barrierItemAppearance;
BakedModel model = MinecraftClient.getInstance().getBlockRenderManager().getModel(state);
if (!(model instanceof WeightedBakedModelAccessor weighted_model)) { if (!(model instanceof WeightedBakedModelAccessor weighted_model)) {
return new ComputedAppearance( return new ComputedAppearance(
getAppearance(model), getAppearance(model),
@ -112,42 +118,32 @@ public class CamoAppearanceManager {
RenderMaterial material = r.materialFinder().clear().find(); RenderMaterial material = r.materialFinder().clear().find();
Random random = Random.create(); Random random = Random.create();
Sprite[] sprites = new Sprite[6]; Map<Direction, List<SpriteProperties>> sprites = new EnumMap<>(Direction.class);
int[] flags = new int[6];
byte[] color_mask = {0b000000}; byte[] color_mask = {0b000000};
//Read quads off the model by their `cullface` //Read quads off the model by their `cullface`
Arrays.stream(Direction.values()).forEach(direction -> { Arrays.stream(Direction.values()).forEach(direction -> {
List<BakedQuad> quads = model.getQuads(null, direction, random); List<BakedQuad> quads = model.getQuads(null, direction, random);
if(quads.isEmpty() || quads.get(0) == null) { if(quads.isEmpty()) { // add default appearance if none present
sprites[direction.ordinal()] = defaultAppearance.getSprite(direction, 0); // make sure direction has a sprite sprites.put(direction, defaultAppearance.getSprites(direction, 0));
return; return;
} }
BakedQuad quad = quads.get(0); sprites.put(direction, new ArrayList<>());
if(quad.hasColor()) color_mask[0] |= (byte) (1 << direction.ordinal()); quads.forEach(quad -> {
if(quad.hasColor()) color_mask[0] |= (byte) (1 << direction.ordinal());
Sprite sprite = quad.getSprite(); Sprite sprite = quad.getSprite();
if(sprite == null) return; if(sprite == null) return;
sprites[direction.ordinal()] = sprite; sprites.compute(direction, (dir, pairs) -> {
quad_emitter.fromVanilla(quad, material, direction);
//Problem: Some models (eg. pistons, stone, glazed terracotta) have their UV coordinates permuted in pairs.add(new SpriteProperties(sprite, getBakeFlags(quad_emitter, sprite), QuadPosBounds.read(quad_emitter)));
//non-standard ways. The actual png image appears sideways, but the original model's UVs rotate it right way up again. return pairs;
//If I simply display the texture on my model, it will appear sideways, like it does in the png. });
//If I can discover the pattern of rotations and flips on the original model, I can "un-rotate" the texture back });
//into a standard orientation.
//
//Solution: Look at the first and second vertices of the orignial quad, and decide whether their UV coordinates
//are "low" or "high" compared to the middle of the sprite. The first two vertices uniquely determine the pattern
//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.
quad_emitter.fromVanilla(quad, material, direction);
flags[direction.ordinal()] = getBakeFlags(quad_emitter, sprite);
}); });
return new Appearance(sprites, flags, color_mask[0]); return new Appearance(sprites, color_mask[0]);
} }
private static int getBakeFlags(QuadEmitter emitter, Sprite sprite) { private static int getBakeFlags(QuadEmitter emitter, Sprite sprite) {

View File

@ -1,11 +1,10 @@
package fr.adrien1106.reframed.client.model.apperance; package fr.adrien1106.reframed.client.model.apperance;
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial; import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial;
import net.minecraft.client.texture.Sprite;
import net.minecraft.util.math.Direction; import net.minecraft.util.math.Direction;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Arrays; import java.util.List;
public class ComputedAppearance implements CamoAppearance { public class ComputedAppearance implements CamoAppearance {
private final Appearance appearance; private final Appearance appearance;
@ -27,13 +26,8 @@ public class ComputedAppearance implements CamoAppearance {
} }
@Override @Override
public @NotNull Sprite getSprite(Direction dir, long seed) { public @NotNull List<SpriteProperties> getSprites(Direction dir, long seed) {
return appearance.sprites()[dir.ordinal()]; return appearance.sprites().get(dir);
}
@Override
public int getBakeFlags(Direction dir, long seed) {
return appearance.flags()[dir.ordinal()];
} }
@Override @Override
@ -53,10 +47,4 @@ public class ComputedAppearance implements CamoAppearance {
public int hashCode() { public int hashCode() {
return id; 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

@ -5,6 +5,8 @@ import net.minecraft.client.texture.Sprite;
import net.minecraft.util.math.Direction; import net.minecraft.util.math.Direction;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List;
public class SingleSpriteAppearance implements CamoAppearance { public class SingleSpriteAppearance implements CamoAppearance {
private final @NotNull Sprite defaultSprite; private final @NotNull Sprite defaultSprite;
private final RenderMaterial mat; private final RenderMaterial mat;
@ -22,13 +24,8 @@ public class SingleSpriteAppearance implements CamoAppearance {
} }
@Override @Override
public @NotNull Sprite getSprite(Direction dir, long seed) { public @NotNull List<SpriteProperties> getSprites(Direction dir, long seed) {
return defaultSprite; return List.of(new SpriteProperties(defaultSprite, 0, null));
}
@Override
public int getBakeFlags(Direction dir, long seed) {
return 0;
} }
@Override @Override

View File

@ -0,0 +1,7 @@
package fr.adrien1106.reframed.client.model.apperance;
import fr.adrien1106.reframed.client.model.QuadPosBounds;
import net.minecraft.client.texture.Sprite;
public record SpriteProperties(Sprite sprite, int flags, QuadPosBounds bounds) {
}

View File

@ -1,14 +1,12 @@
package fr.adrien1106.reframed.client.model.apperance; package fr.adrien1106.reframed.client.model.apperance;
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial; 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.Weighted;
import net.minecraft.util.collection.Weighting; import net.minecraft.util.collection.Weighting;
import net.minecraft.util.math.Direction; import net.minecraft.util.math.Direction;
import net.minecraft.util.math.random.Random; import net.minecraft.util.math.random.Random;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List; import java.util.List;
public class WeightedComputedAppearance implements CamoAppearance { public class WeightedComputedAppearance implements CamoAppearance {
@ -46,14 +44,10 @@ public class WeightedComputedAppearance implements CamoAppearance {
} }
@Override @Override
public @NotNull Sprite getSprite(Direction dir, long seed) { public @NotNull List<SpriteProperties> getSprites(Direction dir, long seed) {
return getAppearance(seed).sprites()[dir.ordinal()]; return getAppearance(seed).sprites().get(dir);
} }
@Override
public int getBakeFlags(Direction dir, long seed) {
return getAppearance(seed).flags()[dir.ordinal()];
}
@Override @Override
public boolean hasColor(Direction dir, long seed) { public boolean hasColor(Direction dir, long seed) {
@ -72,11 +66,4 @@ public class WeightedComputedAppearance implements CamoAppearance {
public int hashCode() { public int hashCode() {
return id; 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

@ -0,0 +1,61 @@
package fr.adrien1106.reframed.compat;
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.json.ModelOverrideList;
import net.minecraft.client.render.model.json.ModelTransformation;
import net.minecraft.client.texture.Sprite;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.random.Random;
import java.util.List;
import java.util.Map;
public class RebakedAthenaModel implements BakedModel {
protected final Map<Direction, List<BakedQuad>> face_quads;
public RebakedAthenaModel(Map<Direction, List<BakedQuad>> face_quads) {
this.face_quads = face_quads;
}
@Override
public List<BakedQuad> getQuads(BlockState state, Direction direction, Random random) {
return face_quads.getOrDefault(direction, List.of());
}
@Override
public boolean useAmbientOcclusion() {
return false;
}
@Override
public boolean hasDepth() {
return false;
}
@Override
public boolean isSideLit() {
return false;
}
@Override
public boolean isBuiltin() {
return false;
}
@Override
public Sprite getParticleSprite() {
return null;
}
@Override
public ModelTransformation getTransformation() {
return null;
}
@Override
public ModelOverrideList getOverrides() {
return null;
}
}

View File

@ -0,0 +1,55 @@
package fr.adrien1106.reframed.mixin;
import net.fabricmc.loader.api.FabricLoader;
import org.objectweb.asm.tree.ClassNode;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
public class CompatMixinPlugin implements IMixinConfigPlugin {
private static final Map<String, Supplier<Boolean>> CONDITIONS = Map.of(
"fr.adrien1106.reframed.mixin.compat.AthenaBakedModelMixin", () -> FabricLoader.getInstance().isModLoaded("athena"),
"fr.adrien1106.reframed.mixin.compat.AthenaWrappedGetterMixin", () -> FabricLoader.getInstance().isModLoaded("athena")
);
@Override
public void onLoad(String mixin_package) {
}
@Override
public String getRefMapperConfig() {
return null;
}
@Override
public boolean shouldApplyMixin(String target_class, String mixin_class) {
return CONDITIONS.getOrDefault(mixin_class, () -> true).get();
}
@Override
public void acceptTargets(Set<String> mine, Set<String> others) {
}
@Override
public List<String> getMixins() {
return null;
}
@Override
public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}
@Override
public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}
}

View File

@ -0,0 +1,75 @@
package fr.adrien1106.reframed.mixin.compat;
import earth.terrarium.athena.api.client.fabric.AthenaBakedModel;
import earth.terrarium.athena.api.client.fabric.WrappedGetter;
import earth.terrarium.athena.api.client.models.AthenaBlockModel;
import fr.adrien1106.reframed.client.ReFramedClient;
import fr.adrien1106.reframed.client.model.DynamicBakedModel;
import fr.adrien1106.reframed.compat.RebakedAthenaModel;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import net.fabricmc.fabric.api.renderer.v1.Renderer;
import net.fabricmc.fabric.api.renderer.v1.mesh.MutableQuadView;
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.texture.Sprite;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.BlockRenderView;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import java.util.*;
@Mixin(AthenaBakedModel.class)
public abstract class AthenaBakedModelMixin implements DynamicBakedModel, BakedModel {
@Shadow @Final private AthenaBlockModel model;
@Shadow @Final private Int2ObjectMap<Sprite> textures;
/**
* Reuses the emitQuad method to compute the quads to be used by the frame
*
* @param level - the world
* @param state - the current block camo
* @param pos - the block position
* @return - the rebakedmodel containing the computed quads
*/
@Override
public BakedModel computeQuads(BlockRenderView level, BlockState state, BlockPos pos) {
if (level == null || pos == null) return this;
Map<Direction, List<BakedQuad>> face_quads = new HashMap<>();
Renderer r = ReFramedClient.HELPER.getFabricRenderer();
QuadEmitter emitter = r.meshBuilder().getEmitter();
WrappedGetter getter = new WrappedGetter(level);
Arrays.stream(Direction.values()).forEach(direction -> {
face_quads.put(direction, new ArrayList<>());
model.getQuads(getter, state, pos, direction).forEach(sprite -> face_quads.computeIfPresent(direction, (d, quads) -> {
Sprite texture = textures.get(sprite.sprite());
if (texture == null) return quads;
emitter.square(direction, sprite.left(), sprite.bottom(), sprite.right(), sprite.top(), sprite.depth());
int flag = MutableQuadView.BAKE_LOCK_UV;
switch (sprite.rotation()) {
case CLOCKWISE_90 -> flag |= MutableQuadView.BAKE_ROTATE_90;
case CLOCKWISE_180 -> flag |= MutableQuadView.BAKE_ROTATE_180;
case COUNTERCLOCKWISE_90 -> flag |= MutableQuadView.BAKE_ROTATE_270;
}
emitter.spriteBake(texture, flag);
emitter.color(-1, -1, -1, -1);
quads.add(emitter.toBakedQuad(texture));
return quads;
}));
});
return new RebakedAthenaModel(face_quads);
}
}

View File

@ -0,0 +1,30 @@
package fr.adrien1106.reframed.mixin.compat;
import earth.terrarium.athena.api.client.utils.AppearanceAndTintGetter;
import earth.terrarium.athena.api.client.utils.CtmUtils;
import earth.terrarium.athena.impl.client.models.ConnectedBlockModel;
import fr.adrien1106.reframed.block.ReFramedEntity;
import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ConnectedBlockModel.class)
public class AthenaConnectedBlockModelMixin {
@Redirect(method = "getQuads",
at = @At(value = "INVOKE", target = "Learth/terrarium/athena/api/client/utils/CtmUtils;" +
"checkRelative(Learth/terrarium/athena/api/client/utils/AppearanceAndTintGetter;" +
"Lnet/minecraft/block/BlockState;" +
"Lnet/minecraft/util/math/BlockPos;" +
"Lnet/minecraft/util/math/Direction;)Z"))
private boolean checkForCull(AppearanceAndTintGetter level, BlockState state, BlockPos pos, Direction direction) {
// Always get all the textures unless its another block then use default behaviour
if (level.getBlockEntity(pos) instanceof ReFramedEntity
|| level.getBlockEntity(pos.offset(direction)) instanceof ReFramedEntity)
return false;
return CtmUtils.checkRelative(level, state, pos, direction);
}
}

View File

@ -0,0 +1,45 @@
package fr.adrien1106.reframed.mixin.compat;
import earth.terrarium.athena.api.client.fabric.WrappedGetter;
import fr.adrien1106.reframed.block.ReFramedEntity;
import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.BlockRenderView;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(WrappedGetter.class)
public class AthenaWrappedGetterMixin {
@Shadow @Final private BlockRenderView getter;
@Inject(method = "getBlockState", at = @At(value = "HEAD"), cancellable = true)
private void getCamoState(BlockPos pos, CallbackInfoReturnable<BlockState> cir) {
if (!(getter.getBlockEntity(pos) instanceof ReFramedEntity framed_entity)) return;
cir.setReturnValue(framed_entity.getThemeState());
}
@Redirect(method = "getAppearance(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/math/Direction;)" +
"Lnet/minecraft/block/BlockState;",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/BlockRenderView;" +
"getBlockState(Lnet/minecraft/util/math/BlockPos;)" +
"Lnet/minecraft/block/BlockState;"))
private BlockState appearanceCamoState(BlockRenderView world, BlockPos pos) {
if (world.getBlockEntity(pos) instanceof ReFramedEntity framed_entity) return framed_entity.getThemeState();
return world.getBlockState(pos);
}
@Redirect(method = "query",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/BlockRenderView;" +
"getBlockState(Lnet/minecraft/util/math/BlockPos;)" +
"Lnet/minecraft/block/BlockState;"))
private BlockState queryCamoState(BlockRenderView world, BlockPos pos) {
if (world.getBlockEntity(pos) instanceof ReFramedEntity framed_entity) return framed_entity.getThemeState();
return world.getBlockState(pos);
}
}

View File

@ -29,5 +29,8 @@
"minecraft": "${minecraft_version}", "minecraft": "${minecraft_version}",
"fabricloader": "^${loader_version}", "fabricloader": "^${loader_version}",
"fabric-api": "*" "fabric-api": "*"
},
"suggest": {
"athena": "^${athena_version}"
} }
} }

View File

@ -1,23 +1,26 @@
{ {
"required": true, "required": true,
"package": "fr.adrien1106.reframed.mixin", "package": "fr.adrien1106.reframed.mixin",
"compatibilityLevel": "JAVA_17", "compatibilityLevel": "JAVA_17",
"mixins": [ "plugin": "fr.adrien1106.reframed.mixin.CompatMixinPlugin",
"WallBlockAccessor", "mixins": [
"particles.MixinEntity", "WallBlockAccessor",
"particles.MixinLivingEntity" "particles.MixinEntity",
], "particles.MixinLivingEntity"
"client": [ ],
"MinecraftAccessor", "client": [
"BlockRenderInfoMixin", "BlockMixin",
"BlockMixin", "BlockRenderInfoMixin",
"particles.AccessorParticle", "MinecraftAccessor",
"particles.AccessorSpriteBillboardParticle", "compat.AthenaBakedModelMixin",
"particles.MixinBlockDustParticle", "compat.AthenaWrappedGetterMixin",
"particles.MixinBlockDustParticle", "compat.AthenaConnectedBlockModelMixin",
"model.WeightedBakedModelAccessor" "model.WeightedBakedModelAccessor",
], "particles.AccessorParticle",
"injectors": { "particles.AccessorSpriteBillboardParticle",
"defaultRequire": 1 "particles.MixinBlockDustParticle"
],
"injectors": {
"defaultRequire": 1
} }
} }