We're much the same, you and I
This commit is contained in:
parent
ea476662d4
commit
e373b8b933
@ -1,13 +1,11 @@
|
||||
package io.github.cottonmc.templates.model;
|
||||
|
||||
import io.github.cottonmc.templates.TemplatesClient;
|
||||
import net.fabricmc.fabric.api.renderer.v1.Renderer;
|
||||
import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh;
|
||||
import net.fabricmc.fabric.api.renderer.v1.mesh.MeshBuilder;
|
||||
import net.fabricmc.fabric.api.renderer.v1.mesh.QuadEmitter;
|
||||
import net.fabricmc.fabric.api.renderer.v1.render.RenderContext;
|
||||
import net.minecraft.client.render.model.ModelBakeSettings;
|
||||
import net.minecraft.util.math.AffineTransformation;
|
||||
import net.minecraft.util.math.Direction;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
@ -17,51 +15,16 @@ import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class MeshTransformUtil {
|
||||
public static Mesh aroundCenter(Mesh oldMesh, ModelBakeSettings settings) {
|
||||
return aroundCenter(oldMesh, settings.getRotation().getMatrix());
|
||||
}
|
||||
public static Mesh pretransformMesh(Mesh mesh, RenderContext.QuadTransform transform) {
|
||||
MeshBuilder builder = TemplatesClient.getFabricRenderer().meshBuilder();
|
||||
QuadEmitter emitter = builder.getEmitter();
|
||||
|
||||
public static Mesh aroundCenter(Mesh oldMesh, Matrix4f mat) {
|
||||
Map<Direction, Direction> facePermutation = facePermutation(mat);
|
||||
|
||||
Renderer r = TemplatesClient.getFabricRenderer();
|
||||
MeshBuilder newMesh = r.meshBuilder();
|
||||
QuadEmitter emitter = newMesh.getEmitter();
|
||||
|
||||
//re-used buffers
|
||||
Vector3f pos3 = new Vector3f();
|
||||
Vector4f pos4 = new Vector4f();
|
||||
|
||||
oldMesh.forEach(oldQuad -> {
|
||||
//Initialize the new quad
|
||||
emitter.copyFrom(oldQuad);
|
||||
|
||||
//For each vertex:
|
||||
for(int i = 0; i < 4; i++) {
|
||||
//Copy pos into a vec3, then a vec4. the w component is set to 0 since this is a point, not a normal
|
||||
emitter.copyPos(i, pos3);
|
||||
pos3.add(-0.5f, -0.5f, -0.5f);
|
||||
pos4.set(pos3, 0);
|
||||
|
||||
//Compute the matrix-vector product. This function mutates the vec4 in-place.
|
||||
//Note that `transformAffine` has the same purpose as `transform`; the difference is it
|
||||
//assumes (without checking) that the last row of the matrix is 0,0,0,1, as an optimization
|
||||
mat.transform(pos4);
|
||||
|
||||
//Manually copy the data back onto the vertex
|
||||
emitter.pos(i, pos4.x + 0.5f, pos4.y + 0.5f, pos4.z + 0.5f);
|
||||
}
|
||||
|
||||
emitter.nominalFace(facePermutation.get(emitter.lightFace()));
|
||||
|
||||
Direction cull = emitter.cullFace();
|
||||
if(cull != null) emitter.cullFace(facePermutation.get(cull));
|
||||
|
||||
//Output the quad
|
||||
emitter.emit();
|
||||
mesh.forEach(quad -> {
|
||||
emitter.copyFrom(quad);
|
||||
if(transform.transform(emitter)) emitter.emit();
|
||||
});
|
||||
|
||||
return newMesh.build();
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
//Hard to explain what this is for...
|
||||
@ -86,15 +49,39 @@ public class MeshTransformUtil {
|
||||
return facePermutation;
|
||||
}
|
||||
|
||||
public static Mesh pretransformMesh(Mesh mesh, RenderContext.QuadTransform transform) {
|
||||
MeshBuilder builder = TemplatesClient.getFabricRenderer().meshBuilder();
|
||||
QuadEmitter emitter = builder.getEmitter();
|
||||
public static RenderContext.QuadTransform applyAffine(ModelBakeSettings settings) {
|
||||
return applyMatrix(settings.getRotation().getMatrix());
|
||||
}
|
||||
|
||||
mesh.forEach(quad -> {
|
||||
emitter.copyFrom(quad);
|
||||
if(transform.transform(emitter)) emitter.emit();
|
||||
});
|
||||
public static RenderContext.QuadTransform applyMatrix(Matrix4f mat) {
|
||||
Map<Direction, Direction> facePermutation = facePermutation(mat);
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
//}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,19 +1,32 @@
|
||||
package io.github.cottonmc.templates.model;
|
||||
|
||||
import io.github.cottonmc.templates.Templates;
|
||||
import io.github.cottonmc.templates.TemplatesClient;
|
||||
import net.fabricmc.fabric.api.renderer.v1.Renderer;
|
||||
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial;
|
||||
import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh;
|
||||
import net.fabricmc.fabric.api.renderer.v1.mesh.MeshBuilder;
|
||||
import net.fabricmc.fabric.api.renderer.v1.mesh.QuadEmitter;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.block.Blocks;
|
||||
import net.minecraft.client.render.model.BakedModel;
|
||||
import net.minecraft.client.render.model.BakedQuad;
|
||||
import net.minecraft.client.render.model.Baker;
|
||||
import net.minecraft.client.render.model.ModelBakeSettings;
|
||||
import net.minecraft.client.render.model.UnbakedModel;
|
||||
import net.minecraft.client.texture.Sprite;
|
||||
import net.minecraft.client.util.SpriteIdentifier;
|
||||
import net.minecraft.screen.PlayerScreenHandler;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.math.Direction;
|
||||
import net.minecraft.util.math.random.Random;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class RetexturedJsonModelUnbakedModel implements UnbakedModel {
|
||||
@ -42,12 +55,55 @@ public class RetexturedJsonModelUnbakedModel implements UnbakedModel {
|
||||
@Nullable
|
||||
@Override
|
||||
public BakedModel bake(Baker baker, Function<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),
|
||||
TemplatesClient.provider.getOrCreateTemplateApperanceManager(spriteLookup),
|
||||
modelBakeSettings,
|
||||
spriteLookup,
|
||||
itemModelState
|
||||
);
|
||||
) {
|
||||
@Override
|
||||
protected Mesh getBaseMesh(BlockState state) {
|
||||
//Convert models to retexturable Meshes lazily, the first time we encounter each blockstate
|
||||
return jsonToMesh.computeIfAbsent(state, this::convertModel);
|
||||
}
|
||||
|
||||
private Mesh convertModel(BlockState state) {
|
||||
Renderer r = TemplatesClient.getFabricRenderer();
|
||||
MeshBuilder builder = r.meshBuilder();
|
||||
QuadEmitter emitter = builder.getEmitter();
|
||||
RenderMaterial mat = tam.getCachedMaterial(state);
|
||||
|
||||
Random rand = Random.create(42);
|
||||
|
||||
for(Direction cullFace : DIRECTIONS_AND_NULL) {
|
||||
for(BakedQuad quad : wrapped.getQuads(state, cullFace, rand)) {
|
||||
emitter.fromVanilla(quad, mat, cullFace);
|
||||
|
||||
QuadUvBounds bounds = QuadUvBounds.read(emitter);
|
||||
for(int i = 0; i < specialSprites.length; i++) {
|
||||
if(bounds.displaysSprite(specialSprites[i])) {
|
||||
bounds.normalizeUv(emitter, specialSprites[i]);
|
||||
emitter.tag(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emitter.emit();
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@ package io.github.cottonmc.templates.model;
|
||||
|
||||
import io.github.cottonmc.templates.TemplatesClient;
|
||||
import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.block.Blocks;
|
||||
import net.minecraft.client.render.model.BakedModel;
|
||||
import net.minecraft.client.render.model.Baker;
|
||||
import net.minecraft.client.render.model.ModelBakeSettings;
|
||||
@ -15,15 +17,18 @@ import java.util.Collections;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@SuppressWarnings("ClassCanBeRecord")
|
||||
public class RetexturedMeshUnbakedModel implements UnbakedModel {
|
||||
public RetexturedMeshUnbakedModel(Identifier parent, Supplier<Mesh> baseMeshFactory) {
|
||||
this(parent, __ -> baseMeshFactory.get());
|
||||
}
|
||||
|
||||
public RetexturedMeshUnbakedModel(Identifier parent, Function<Function<SpriteIdentifier, Sprite>, Mesh> baseMeshFactory) {
|
||||
this.parent = parent;
|
||||
this.baseMeshFactory = baseMeshFactory;
|
||||
}
|
||||
|
||||
protected final Identifier parent;
|
||||
protected final Supplier<Mesh> baseMeshFactory;
|
||||
protected final Function<Function<SpriteIdentifier, Sprite>, Mesh> baseMeshFactory;
|
||||
|
||||
@Override
|
||||
public Collection<Identifier> getModelDependencies() {
|
||||
@ -32,16 +37,23 @@ public class RetexturedMeshUnbakedModel implements UnbakedModel {
|
||||
|
||||
@Override
|
||||
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
|
||||
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),
|
||||
TemplatesClient.provider.getOrCreateTemplateApperanceManager(spriteLookup),
|
||||
modelBakeSettings,
|
||||
baseMeshFactory.get()
|
||||
);
|
||||
Blocks.AIR.getDefaultState()
|
||||
) {
|
||||
@Override
|
||||
protected Mesh getBaseMesh(BlockState state) {
|
||||
return transformedBaseMesh;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,11 +5,15 @@ import net.fabricmc.fabric.api.renderer.v1.Renderer;
|
||||
import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh;
|
||||
import net.fabricmc.fabric.api.renderer.v1.mesh.MeshBuilder;
|
||||
import net.fabricmc.fabric.api.renderer.v1.mesh.QuadEmitter;
|
||||
import net.minecraft.client.texture.Sprite;
|
||||
import net.minecraft.client.util.SpriteIdentifier;
|
||||
import net.minecraft.util.math.Direction;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
public class SlopeBaseMesh {
|
||||
/**
|
||||
* @see RetexturedMeshBakedModel.RetexturingTransformer for why these values were chosen
|
||||
* @see RetexturingBakedModel for why these values were chosen
|
||||
*/
|
||||
public static final int TAG_SLOPE = Direction.UP.ordinal() + 1;
|
||||
public static final int TAG_LEFT = Direction.EAST.ordinal() + 1;
|
||||
|
@ -6,7 +6,7 @@ import net.minecraft.util.math.Direction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface TemplateAppearance {
|
||||
@NotNull Sprite getParticleSprite(); //TODO: plug this in
|
||||
@NotNull Sprite getParticleSprite(); //TODO: plug this in (particle mixins don't use it atm)
|
||||
|
||||
@NotNull RenderMaterial getRenderMaterial();
|
||||
@NotNull Sprite getSprite(Direction dir);
|
||||
|
@ -55,6 +55,10 @@ public class TemplateAppearanceManager {
|
||||
return appearanceCache.computeIfAbsent(state, this::computeAppearance);
|
||||
}
|
||||
|
||||
public RenderMaterial getCachedMaterial(BlockState state) {
|
||||
return blockMaterials.get(BlendMode.fromRenderLayer(RenderLayers.getBlockLayer(state)));
|
||||
}
|
||||
|
||||
//I'm pretty sure ConcurrentHashMap semantics allow for this function to be called multiple times on the same key, on different threads.
|
||||
//The computeIfAbsent map update will work without corrupting the map, but there will be some "wasted effort" computing the value twice.
|
||||
//The results are going to be the same, apart from their serialNumbers differing (= their equals & hashCode differing).
|
||||
@ -136,7 +140,7 @@ public class TemplateAppearanceManager {
|
||||
sprites,
|
||||
bakeFlags,
|
||||
hasColorMask,
|
||||
blockMaterials.get(BlendMode.fromRenderLayer(RenderLayers.getBlockLayer(state))),
|
||||
getCachedMaterial(state),
|
||||
serialNumber.getAndIncrement()
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user