Finished the modelfactory stuff, is now renders block entities like shulkers

This commit is contained in:
mcrcortex
2025-05-06 22:52:00 +10:00
parent d072f474a4
commit fb52dea6fa
8 changed files with 438 additions and 862 deletions

View File

@@ -1,151 +0,0 @@
package me.cortex.voxy.client.core.model;
import com.mojang.blaze3d.textures.GpuTexture;
import com.mojang.blaze3d.vertex.VertexFormat;
import me.cortex.voxy.common.Logger;
import net.minecraft.block.BlockEntityProvider;
import net.minecraft.block.BlockState;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.*;
import net.minecraft.client.texture.GlTexture;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import org.joml.Matrix4f;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.glBindTexture;
public class BakedBlockEntityModel {
private static final class BakedVertices implements VertexConsumer {
private boolean makingVertex = false;
public final RenderLayer layer;
private float cX, cY, cZ;
private int cR, cG, cB, cA;
private float cU, cV;
private final List<int[]> vertices = new ArrayList<>();
private BakedVertices(RenderLayer layer) {
this.layer = layer;
}
@Override
public VertexConsumer vertex(float x, float y, float z) {
this.next();
this.cX = x;
this.cY = y;
this.cZ = z;
this.makingVertex = true;
return this;
}
@Override
public VertexConsumer color(int red, int green, int blue, int alpha) {
this.cR = 0;//red;
this.cG = 0;//green;
this.cB = 0;//blue;
this.cA = alpha;
return this;
}
@Override
public VertexConsumer texture(float u, float v) {
this.cU = u;
this.cV = v;
return this;
}
@Override
public VertexConsumer overlay(int u, int v) {
return this;
}
@Override
public VertexConsumer light(int u, int v) {
return this;
}
@Override
public VertexConsumer normal(float x, float y, float z) {
return this;
}
private void next() {
if (this.makingVertex) {
this.makingVertex = false;
this.vertices.add(new int[]{
Float.floatToIntBits(this.cX), Float.floatToIntBits(this.cY), Float.floatToIntBits(this.cZ),
this.cR, this.cG, this.cB, this.cA,
Float.floatToIntBits(this.cU), Float.floatToIntBits(this.cV)});
}
}
public void putInto(VertexConsumer vc) {
this.next();
for (var vert : this.vertices) {
vc.vertex(Float.intBitsToFloat(vert[0]), Float.intBitsToFloat(vert[1]), Float.intBitsToFloat(vert[2]))
.color(vert[3], vert[4], vert[5], vert[6])
.texture(Float.intBitsToFloat(vert[7]), Float.intBitsToFloat(vert[8]));
}
}
public boolean isEmpty() {
return this.vertices.isEmpty();
}
}
private final List<BakedVertices> layers;
private BakedBlockEntityModel(List<BakedVertices> layers) {
this.layers = layers;
}
public void renderOut(Matrix4f matrix, GpuTexture texture) {
var vc = Tessellator.getInstance();
for (var layer : this.layers) {
if (layer.isEmpty()) continue;
if (layer.layer instanceof RenderLayer.MultiPhase mp) {
Identifier textureId = mp.phases.texture.getId().orElse(null);
if (textureId == null) {
System.err.println("ERROR: Empty texture id for layer: " + layer);
} else {
texture = MinecraftClient.getInstance().getTextureManager().getTexture(textureId).getGlTexture();
}
}
var bb = vc.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_TEXTURE_COLOR);
layer.putInto(bb);
var mesh = bb.endNullable();
if (mesh!=null)
BudgetBufferRenderer.drawFast(mesh, texture, matrix);
}
}
public static BakedBlockEntityModel bake(BlockState state) {
Map<RenderLayer, BakedVertices> map = new HashMap<>();
var entity = ((BlockEntityProvider)state.getBlock()).createBlockEntity(BlockPos.ORIGIN, state);
if (entity == null) {
return null;
}
var renderer = MinecraftClient.getInstance().getBlockEntityRenderDispatcher().get(entity);
entity.setWorld(MinecraftClient.getInstance().world);
if (renderer != null) {
try {
renderer.render(entity, 0.0f, new MatrixStack(), layer->map.computeIfAbsent(layer, BakedVertices::new), 0, 0, new Vec3d(0,0,0));
} catch (Exception e) {
Logger.error("Unable to bake block entity: " + entity, e);
}
}
entity.markRemoved();
if (map.isEmpty()) {
return null;
}
return new BakedBlockEntityModel(map.values().stream().toList());
}
}

View File

@@ -5,7 +5,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import it.unimi.dsi.fastutil.objects.ObjectSet;
import me.cortex.voxy.client.core.gl.Capabilities;
import me.cortex.voxy.client.core.model.bakery.ModelTextureBakery2;
import me.cortex.voxy.client.core.model.bakery.ModelTextureBakery;
import me.cortex.voxy.client.core.rendering.util.RawDownloadStream;
import me.cortex.voxy.client.core.rendering.util.UploadStream;
import me.cortex.voxy.common.world.other.Mapper;
@@ -33,7 +33,6 @@ import org.jetbrains.annotations.Nullable;
import org.lwjgl.system.MemoryUtil;
import java.util.*;
import java.util.stream.Stream;
import static me.cortex.voxy.client.core.model.ModelStore.MODEL_SIZE;
import static org.lwjgl.opengl.GL11.*;
@@ -66,7 +65,7 @@ public class ModelFactory {
private final Biome DEFAULT_BIOME = MinecraftClient.getInstance().world.getRegistryManager().getOrThrow(RegistryKeys.BIOME).get(BiomeKeys.PLAINS);
public final ModelTextureBakery2 bakery;
public final ModelTextureBakery bakery;
//Model data might also contain a constant colour if the colour resolver produces a constant colour, this saves space in the
@@ -124,7 +123,7 @@ public class ModelFactory {
public ModelFactory(Mapper mapper, ModelStore storage) {
this.mapper = mapper;
this.storage = storage;
this.bakery = new ModelTextureBakery2(MODEL_TEXTURE_SIZE, MODEL_TEXTURE_SIZE);
this.bakery = new ModelTextureBakery(MODEL_TEXTURE_SIZE, MODEL_TEXTURE_SIZE);
this.metadataCache = new long[1<<16];
this.fluidStateLUT = new int[1<<16];

View File

@@ -0,0 +1,79 @@
package me.cortex.voxy.client.core.model.bakery;
import com.mojang.blaze3d.textures.GpuTexture;
import me.cortex.voxy.common.Logger;
import net.minecraft.block.BlockEntityProvider;
import net.minecraft.block.BlockState;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gl.GlGpuBuffer;
import net.minecraft.client.render.*;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import org.joml.Matrix4f;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class BakedBlockEntityModel {
private record LayerConsumer(RenderLayer layer, ReuseVertexConsumer consumer) {}
private final List<LayerConsumer> layers;
private BakedBlockEntityModel(List<LayerConsumer> layers) {
this.layers = layers;
}
public void render(Matrix4f matrix, int texId) {
for (var layer : this.layers) {
if (layer.consumer.isEmpty()) continue;
if (layer.layer instanceof RenderLayer.MultiPhase mp) {
Identifier textureId = mp.phases.texture.getId().orElse(null);
if (textureId == null) {
Logger.error("ERROR: Empty texture id for layer: " + layer);
} else {
texId = ((net.minecraft.client.texture.GlTexture)MinecraftClient.getInstance().getTextureManager().getTexture(textureId).getGlTexture()).getGlId();
}
}
if (texId == 0) continue;
BudgetBufferRenderer.setup(layer.consumer.getAddress(), layer.consumer.quadCount(), texId);
BudgetBufferRenderer.render(matrix);
}
}
public void release() {
this.layers.forEach(layer->layer.consumer.free());
}
public static BakedBlockEntityModel bake(BlockState state) {
Map<RenderLayer, LayerConsumer> map = new HashMap<>();
var entity = ((BlockEntityProvider)state.getBlock()).createBlockEntity(BlockPos.ORIGIN, state);
if (entity == null) {
return null;
}
var renderer = MinecraftClient.getInstance().getBlockEntityRenderDispatcher().get(entity);
entity.setWorld(MinecraftClient.getInstance().world);
if (renderer != null) {
try {
renderer.render(entity, 0.0f, new MatrixStack(), layer->map.computeIfAbsent(layer, rl -> new LayerConsumer(rl, new ReuseVertexConsumer())).consumer, 0, 0, new Vec3d(0,0,0));
} catch (Exception e) {
Logger.error("Unable to bake block entity: " + entity, e);
}
}
entity.markRemoved();
if (map.isEmpty()) {
return null;
}
for (var i : new ArrayList<>(map.values())) {
if (i.consumer.isEmpty()) {
map.remove(i.layer);
i.consumer.free();
}
}
if (map.isEmpty()) {
return null;
}
return new BakedBlockEntityModel(new ArrayList<>(map.values()));
}
}

View File

@@ -1,4 +1,4 @@
package me.cortex.voxy.client.core.model;
package me.cortex.voxy.client.core.model.bakery;
import com.mojang.blaze3d.buffers.GpuBuffer;
import com.mojang.blaze3d.opengl.GlConst;
import com.mojang.blaze3d.opengl.GlStateManager;
@@ -31,6 +31,8 @@ import static org.lwjgl.opengl.GL43.glBindVertexBuffer;
import static org.lwjgl.opengl.GL45.*;
public class BudgetBufferRenderer {
public static final int VERTEX_FORMAT_SIZE = 24;
private static final Shader bakeryShader = Shader.make()
.add(ShaderType.VERTEX, "voxy:bakery/position_tex.vsh")
.add(ShaderType.FRAGMENT, "voxy:bakery/position_tex.fsh")

View File

@@ -1,21 +1,16 @@
package me.cortex.voxy.client.core.model.bakery;
import com.mojang.blaze3d.textures.GpuTexture;
import com.mojang.blaze3d.vertex.VertexFormat;
import me.cortex.voxy.client.core.model.BakedBlockEntityModel;
import me.cortex.voxy.client.core.model.BudgetBufferRenderer;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.block.FluidBlock;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.*;
import net.minecraft.client.render.model.BlockStateModel;
import net.minecraft.client.util.BufferAllocator;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.RenderLayers;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.fluid.FluidState;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.ColorHelper;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.RotationAxis;
import net.minecraft.util.math.random.LocalRandom;
@@ -26,203 +21,53 @@ import net.minecraft.world.chunk.light.LightingProvider;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
import java.util.ArrayList;
import java.util.List;
import static org.lwjgl.opengl.ARBShaderImageLoadStore.GL_FRAMEBUFFER_BARRIER_BIT;
import static org.lwjgl.opengl.ARBShaderImageLoadStore.glMemoryBarrier;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL14C.glBlendFuncSeparate;
import static org.lwjgl.opengl.GL20C.glUniformMatrix4fv;
import static org.lwjgl.opengl.GL30.*;
import static org.lwjgl.opengl.GL45.glBlitNamedFramebuffer;
import static org.lwjgl.opengl.GL45.glClearNamedFramebufferfv;
//Builds a texture for each face of a model
public class ModelTextureBakery {
private static final List<MatrixStack> FACE_VIEWS = new ArrayList<>();
//Note: the first bit of metadata is if alpha discard is enabled
private static final Matrix4f[] VIEWS = new Matrix4f[6];
private final GlViewCapture capture;
private final ReuseVertexConsumer vc = new ReuseVertexConsumer();
private final int width;
private final int height;
private final GlViewCapture capture;
public ModelTextureBakery(int width, int height) {
this.capture = new GlViewCapture(width, height);
this.width = width;
this.height = height;
this.capture = new GlViewCapture(width, height);
//This is done to help make debugging easier
FACE_VIEWS.clear();
AddViews();
}
private static void AddViews() {
//TODO: FIXME: need to bake in the correct orientation, HOWEVER some orientations require a flipped winding order!!!!
addView(-90,0, 0, false);//Direction.DOWN
addView(90,0, 0, false);//Direction.UP
addView(0,180, 0, true);//Direction.NORTH
addView(0,0, 0, false);//Direction.SOUTH
//TODO: check these arnt the wrong way round
addView(0,90, 270, false);//Direction.EAST
addView(0,270, 270, false);//Direction.WEST
}
private static void addView(float pitch, float yaw, float rotation, boolean flipX) {
var stack = new MatrixStack();
stack.translate(0.5f,0.5f,0.5f);
stack.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(rotation));
stack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(pitch));
stack.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(yaw));
stack.translate(-0.5f,-0.5f,-0.5f);
FACE_VIEWS.add(stack);
}
//TODO: For block entities, also somehow attempt to render the default block entity, e.g. chests and stuff
// cause that will result in ok looking micro details in the terrain
public void renderFacesToStream(BlockState state, long randomValue, boolean renderFluid, int streamBuffer, int streamBaseOffset) {
private void bakeBlockModel(BlockState state, RenderLayer layer) {
var model = MinecraftClient.getInstance()
.getBakedModelManager()
.getBlockModels()
.getModel(state);
BakedBlockEntityModel entityModel = state.hasBlockEntity()?BakedBlockEntityModel.bake(state):null;
boolean hasDiscard = layer == RenderLayer.getCutout() ||
layer == RenderLayer.getCutoutMipped() ||
layer == RenderLayer.getTripwire();
var projection = new Matrix4f().identity().set(new float[]{
2,0,0,0,
0, 2,0,0,
0,0, -1f,0,
-1,-1,0,1,
});
for (Direction direction : new Direction[]{Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST, null}) {
for (var part : model.getParts(new LocalRandom(42L))) {
var quads = part.getQuads(direction);
for (var quad : quads) {
//TODO: add meta specifiying quad has a tint
int originalFramebuffer = glGetInteger(GL_FRAMEBUFFER_BINDING);
RenderLayer renderLayer = null;
if (!renderFluid) {
renderLayer = RenderLayers.getBlockLayer(state);
} else {
renderLayer = RenderLayers.getFluidLayer(state.getFluidState());
int meta = hasDiscard?1:0;
this.vc.quad(quad, meta);
}
}
}
}
//TODO: figure out why calling this makes minecraft render black
//renderLayer.startDrawing();
glBindFramebuffer(GL_FRAMEBUFFER, this.capture.framebuffer.id);
//TODO: make use glClearNamedFramebuffer* to reduce/remove state change
glClearColor(0,0,0,0);
glClearDepth(1);
glClearStencil(0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glEnable(GL_STENCIL_TEST);
//glDepthRange(0, 1);
glDepthMask(true);
glEnable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
//glDepthFunc(GL_LESS);
//TODO: Find a better solution
if (renderLayer == RenderLayer.getTranslucent()) {
//Very hacky blend function to retain the effect of the applied alpha since we dont really want to apply alpha
// this is because we apply the alpha again when rendering the terrain meaning the alpha is being double applied
glBlendFuncSeparate(GL_ONE_MINUS_DST_ALPHA, GL_DST_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
} else {
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_BLEND);
}
boolean hasDiscard = renderLayer == RenderLayer.getCutout() ||
renderLayer == RenderLayer.getCutoutMipped();
//glBlendFunc(GL_ONE, GL_ONE);
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
int[] viewdat = new int[4];
glGetIntegerv(GL_VIEWPORT, viewdat);
var tex = MinecraftClient.getInstance().getTextureManager().getTexture(Identifier.of("minecraft", "textures/atlas/blocks.png")).getGlTexture();
for (int i = 0; i < FACE_VIEWS.size(); i++) {
glViewport((i%3)*this.width, (i/3)*this.height, this.width, this.height);
glBindFramebuffer(GL_FRAMEBUFFER, this.capture.framebuffer.id);
var transform = new Matrix4f(projection).mul(FACE_VIEWS.get(i).peek().getPositionMatrix());
if (entityModel!=null&&!renderFluid) {
entityModel.renderOut(transform, tex);
}
this.rasterView(state, model, transform, randomValue, i, renderFluid, tex, hasDiscard);
}
glViewport(viewdat[0], viewdat[1], viewdat[2], viewdat[3]);
//renderLayer.endDrawing();
glDisable(GL_STENCIL_TEST);
glDisable(GL_BLEND);
glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT);
this.capture.emitToStream(streamBuffer, streamBaseOffset);
//var target = DefaultTerrainRenderPasses.CUTOUT.getTarget();
//int boundFB = ((net.minecraft.client.texture.GlTexture) target.getColorAttachment()).getOrCreateFramebuffer(((GlBackend) RenderSystem.getDevice()).getFramebufferManager(), target.getDepthAttachment());
//glBlitNamedFramebuffer(this.capture.framebuffer.id, boundFB, 0,0,16*3, 16*2, 0,0, 16*3*4,16*2*4, GL_COLOR_BUFFER_BIT, GL_NEAREST);
//SOMEBODY PLEASE FUCKING EXPLAIN TO ME WHY MUST CLEAR THE FRAMEBUFFER HERE WHEN IT IS LITERALLY CLEARED AT THE START OF THE FRAME
// WITHOUT THIS, WATER DOESNT RENDER
//TODO: FIXME, WHAT THE ACTUAL FUCK
glBindFramebuffer(GL_FRAMEBUFFER, this.capture.framebuffer.id);
glClearDepth(1);
glClear(GL_DEPTH_BUFFER_BIT);
glBindFramebuffer(GL_FRAMEBUFFER, originalFramebuffer);
}
private static boolean shouldReturnAirForFluid(BlockPos pos, int face) {
var fv = Direction.byIndex(face).getVector();
int dot = fv.getX()*pos.getX() + fv.getY()*pos.getY() + fv.getZ()*pos.getZ();
return dot >= 1;
}
private final BufferAllocator allocator = new BufferAllocator(786432);
private void rasterView(BlockState state, BlockStateModel model, Matrix4f transform, long randomValue, int face, boolean renderFluid, GpuTexture texture, boolean hasDiscard) {
var bb = new BufferBuilder(this.allocator, VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_TEXTURE_COLOR) {
@Override
public void vertex(float x, float y, float z, int color, float u, float v, int overlay, int light, float normalX, float normalY, float normalZ) {
int colour = color;
colour |= hasDiscard?2:0;
if (renderFluid) {
colour = ColorHelper.getArgb(0,0,1);
}
super.vertex(x, y, z, colour, u, v, overlay, light, normalX, normalY, normalZ);
}
@Override
public VertexConsumer color(int argb) {
return super.color(ColorHelper.getArgb(0,0,1));
}
@Override
public VertexConsumer color(int red, int green, int blue, int alpha) {
return super.color(0, 0, 1, 255);
}
};
if (!renderFluid) {
//TODO: need to do 2 variants for quads, one which have coloured, ones that dont, might be able to pull a spare bit
// at the end whether or not a pixel should be mixed with texture
//TODO: CACHE THE BUILT MODEL AND REUSE AND JUST RENDER FROM DIFFERENT VIEWS
renderQuads(bb, model, new MatrixStack(), randomValue);
} else {
private void bakeFluidState(BlockState state, int face) {
MinecraftClient.getInstance().getBlockRenderManager().renderFluid(BlockPos.ORIGIN, new BlockRenderView() {
@Override
public float getBrightness(Direction direction, boolean shaded) {
@@ -273,9 +118,7 @@ public class ModelTextureBakery {
if (shouldReturnAirForFluid(pos, face)) {
return Blocks.AIR.getDefaultState().getFluidState();
}
//if (pos.getY() == 1) {
// return Blocks.AIR.getDefaultState().getFluidState();
//}
return state.getFluidState();
}
@@ -288,29 +131,173 @@ public class ModelTextureBakery {
public int getBottomY() {
return 0;
}
}, bb, state, state.getFluidState());
}, this.vc, state, state.getFluidState());
}
var mesh = bb.endNullable();
if (mesh != null)
BudgetBufferRenderer.drawFast(mesh, texture, transform);
}
private static void renderQuads(BufferBuilder builder, BlockStateModel model, MatrixStack stack, long randomValue) {
for (Direction direction : new Direction[]{Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST, null}) {
for (var part : model.getParts(new LocalRandom(randomValue))) {
var quads = part.getQuads(direction);
for (var quad : quads) {
//TODO: mark pixels that have
int meta = 1;
builder.quad(stack.peek(), quad, ((meta >> 16) & 0xff) / 255f, ((meta >> 8) & 0xff) / 255f, (meta & 0xff) / 255f, 1.0f, 0, 0);
}
}
}
private static boolean shouldReturnAirForFluid(BlockPos pos, int face) {
var fv = Direction.byIndex(face).getVector();
int dot = fv.getX()*pos.getX() + fv.getY()*pos.getY() + fv.getZ()*pos.getZ();
return dot >= 1;
}
public void free() {
this.capture.free();
this.allocator.close();
this.vc.free();
}
public void renderToStream(BlockState state, int streamBuffer, int streamOffset) {
this.capture.clear();
boolean isBlock = true;
RenderLayer layer;
if (state.getBlock() instanceof FluidBlock) {
layer = RenderLayers.getFluidLayer(state.getFluidState());
isBlock = false;
} else {
layer = RenderLayers.getBlockLayer(state);
}
//TODO: support block model entities
BakedBlockEntityModel bbem = null;
if (state.hasBlockEntity()) {
bbem = BakedBlockEntityModel.bake(state);
}
//Setup GL state
int[] viewdat = new int[4];
int blockTextureId;
{
glEnable(GL_STENCIL_TEST);
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
if (layer == RenderLayer.getTranslucent()) {
glEnable(GL_BLEND);
glBlendFuncSeparate(GL_ONE_MINUS_DST_ALPHA, GL_DST_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
} else {
glDisable(GL_BLEND);
}
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
glGetIntegerv(GL_VIEWPORT, viewdat);//TODO: faster way todo this, or just use main framebuffer resolution
//Bind the capture framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, this.capture.framebuffer.id);
var tex = MinecraftClient.getInstance().getTextureManager().getTexture(Identifier.of("minecraft", "textures/atlas/blocks.png")).getGlTexture();
blockTextureId = ((net.minecraft.client.texture.GlTexture)tex).getGlId();
}
//TODO: fastpath for blocks
if (isBlock) {
this.vc.reset();
this.bakeBlockModel(state, layer);
if (!this.vc.isEmpty()) {//only render if there... is shit to render
//Setup for continual emission
BudgetBufferRenderer.setup(this.vc.getAddress(), this.vc.quadCount(), blockTextureId);//note: this.vc.buffer.address NOT this.vc.ptr
var mat = new Matrix4f();
for (int i = 0; i < VIEWS.length; i++) {
glViewport((i % 3) * this.width, (i / 3) * this.height, this.width, this.height);
//The projection matrix
mat.set(2, 0, 0, 0,
0, 2, 0, 0,
0, 0, -1f, 0,
-1, -1, 0, 1)
.mul(VIEWS[i]);
BudgetBufferRenderer.render(mat);
}
}
glBindVertexArray(0);
} else {//Is fluid, slow path :(
if (!(state.getBlock() instanceof FluidBlock)) throw new IllegalStateException();
var mat = new Matrix4f();
for (int i = 0; i < VIEWS.length; i++) {
this.vc.reset();
this.bakeFluidState(state, i);
if (this.vc.isEmpty()) continue;
BudgetBufferRenderer.setup(this.vc.getAddress(), this.vc.quadCount(), blockTextureId);
glViewport((i % 3) * this.width, (i / 3) * this.height, this.width, this.height);
//The projection matrix
mat.set(2, 0, 0, 0,
0, 2, 0, 0,
0, 0, -1f, 0,
-1, -1, 0, 1)
.mul(VIEWS[i]);
BudgetBufferRenderer.render(mat);
}
glBindVertexArray(0);
}
//Render block model entity data if it exists
if (bbem != null) {
//Rerender everything again ;-; but is ok (is not)
var mat = new Matrix4f();
for (int i = 0; i < VIEWS.length; i++) {
glViewport((i % 3) * this.width, (i / 3) * this.height, this.width, this.height);
//The projection matrix
mat.set(2, 0, 0, 0,
0, 2, 0, 0,
0, 0, -1f, 0,
-1, -1, 0, 1)
.mul(VIEWS[i]);
bbem.render(mat, blockTextureId);
}
glBindVertexArray(0);
bbem.release();
}
//"Restore" gl state
glViewport(viewdat[0], viewdat[1], viewdat[2], viewdat[3]);
glDisable(GL_STENCIL_TEST);
glDisable(GL_BLEND);
//Finish and download
glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT);
this.capture.emitToStream(streamBuffer, streamOffset);
glBindFramebuffer(GL_FRAMEBUFFER, this.capture.framebuffer.id);
glClearDepth(1);
glClear(GL_DEPTH_BUFFER_BIT);
}
static {
//TODO: FIXME: need to bake in the correct orientation, HOWEVER some orientations require a flipped winding order!!!!
addView(0, -90,0, 0, false);//Direction.DOWN
addView(1, 90,0, 0, false);//Direction.UP
addView(2, 0,180, 0, true);//Direction.NORTH
addView(3, 0,0, 0, false);//Direction.SOUTH
//TODO: check these arnt the wrong way round
addView(4, 0,90, 270, false);//Direction.EAST
addView(5, 0,270, 270, false);//Direction.WEST
}
private static void addView(int i, float pitch, float yaw, float rotation, boolean flipX) {
var stack = new MatrixStack();
stack.translate(0.5f,0.5f,0.5f);
stack.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(rotation));
stack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(pitch));
stack.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(yaw));
stack.translate(-0.5f,-0.5f,-0.5f);
VIEWS[i] = new Matrix4f(stack.peek().getPositionMatrix());
}
}

View File

@@ -1,375 +0,0 @@
package me.cortex.voxy.client.core.model.bakery;
import me.cortex.voxy.client.core.model.BudgetBufferRenderer;
import me.cortex.voxy.common.util.MemoryBuffer;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.block.FluidBlock;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.RenderLayers;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.model.BakedQuad;
import net.minecraft.client.render.model.BlockStateModel;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.fluid.FluidState;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.RotationAxis;
import net.minecraft.util.math.random.LocalRandom;
import net.minecraft.world.BlockRenderView;
import net.minecraft.world.LightType;
import net.minecraft.world.biome.ColorResolver;
import net.minecraft.world.chunk.light.LightingProvider;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
import org.lwjgl.system.MemoryUtil;
import static org.lwjgl.opengl.ARBShaderImageLoadStore.GL_FRAMEBUFFER_BARRIER_BIT;
import static org.lwjgl.opengl.ARBShaderImageLoadStore.glMemoryBarrier;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL11C.GL_DEPTH;
import static org.lwjgl.opengl.GL14C.glBlendFuncSeparate;
import static org.lwjgl.opengl.GL30.*;
import static org.lwjgl.opengl.GL45.glClearNamedFramebufferfv;
public class ModelTextureBakery2 {
//Note: the first bit of metadata is if alpha discard is enabled
private static final Matrix4f[] VIEWS = new Matrix4f[6];
private final GlViewCapture capture;
private final ReuseVertexConsumer vc = new ReuseVertexConsumer();
private static final int FORMAT_STRIDE = 24;
private static final class ReuseVertexConsumer implements VertexConsumer {
private MemoryBuffer buffer = new MemoryBuffer(8192);
private long ptr;
private int count;
public ReuseVertexConsumer() {
this.reset();
}
@Override
public ReuseVertexConsumer vertex(float x, float y, float z) {
this.ensureCanPut();
this.ptr += FORMAT_STRIDE; this.count++; //Goto next vertex
MemoryUtil.memPutFloat(this.ptr, x);
MemoryUtil.memPutFloat(this.ptr + 4, y);
MemoryUtil.memPutFloat(this.ptr + 8, z);
return this;
}
public ReuseVertexConsumer meta(int metadata) {
MemoryUtil.memPutInt(this.ptr + 12, metadata);
return this;
}
@Override
public ReuseVertexConsumer color(int red, int green, int blue, int alpha) {
return this;
}
@Override
public ReuseVertexConsumer texture(float u, float v) {
MemoryUtil.memPutFloat(this.ptr + 16, u);
MemoryUtil.memPutFloat(this.ptr + 20, v);
return this;
}
@Override
public ReuseVertexConsumer overlay(int u, int v) {
return this;
}
@Override
public ReuseVertexConsumer light(int u, int v) {
return this;
}
@Override
public ReuseVertexConsumer normal(float x, float y, float z) {
return this;
}
public ReuseVertexConsumer quad(BakedQuad quad, int metadata) {
int[] data = quad.vertexData();
for (int i = 0; i < 4; i++) {
float x = Float.intBitsToFloat(data[i * 8]);
float y = Float.intBitsToFloat(data[i * 8 + 1]);
float z = Float.intBitsToFloat(data[i * 8 + 2]);
this.vertex(x,y,z);
float u = Float.intBitsToFloat(data[i * 8 + 4]);
float v = Float.intBitsToFloat(data[i * 8 + 5]);
this.texture(u,v);
this.meta(metadata);
}
return this;
}
private void ensureCanPut() {
if ((long) (this.count + 1) * FORMAT_STRIDE < this.buffer.size) {
return;
}
long offset = this.buffer.address-this.ptr;
//1.5x the size
var newBuffer = new MemoryBuffer((((int)(this.buffer.size*1.5)+FORMAT_STRIDE-1)/FORMAT_STRIDE)*FORMAT_STRIDE);
this.buffer.cpyTo(newBuffer.address);
this.buffer.free();
this.buffer = newBuffer;
this.ptr = offset + newBuffer.address;
}
public ReuseVertexConsumer reset() {
this.count = 0;
this.ptr = this.buffer.address - FORMAT_STRIDE;//the thing is first time this gets incremented by FORMAT_STRIDE
return this;
}
public void free() {
this.buffer.free();
this.buffer = null;
}
}
private final int width;
private final int height;
public ModelTextureBakery2(int width, int height) {
this.capture = new GlViewCapture(width, height);
this.width = width;
this.height = height;
}
private void bakeBlockModel(BlockState state, RenderLayer layer) {
var model = MinecraftClient.getInstance()
.getBakedModelManager()
.getBlockModels()
.getModel(state);
boolean hasDiscard = layer == RenderLayer.getCutout() ||
layer == RenderLayer.getCutoutMipped() ||
layer == RenderLayer.getTripwire();
for (Direction direction : new Direction[]{Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST, null}) {
for (var part : model.getParts(new LocalRandom(42L))) {
var quads = part.getQuads(direction);
for (var quad : quads) {
//TODO: add meta specifiying quad has a tint
int meta = hasDiscard?1:0;
this.vc.quad(quad, meta);
}
}
}
}
private void bakeFluidState(BlockState state, int face) {
MinecraftClient.getInstance().getBlockRenderManager().renderFluid(BlockPos.ORIGIN, new BlockRenderView() {
@Override
public float getBrightness(Direction direction, boolean shaded) {
return 0;
}
@Override
public LightingProvider getLightingProvider() {
return null;
}
@Override
public int getLightLevel(LightType type, BlockPos pos) {
return 0;
}
@Override
public int getColor(BlockPos pos, ColorResolver colorResolver) {
return 0;
}
@Nullable
@Override
public BlockEntity getBlockEntity(BlockPos pos) {
return null;
}
@Override
public BlockState getBlockState(BlockPos pos) {
if (shouldReturnAirForFluid(pos, face)) {
return Blocks.AIR.getDefaultState();
}
//Fixme:
// This makes it so that the top face of water is always air, if this is commented out
// the up block will be a liquid state which makes the sides full
// if this is uncommented, that issue is fixed but e.g. stacking water layers ontop of eachother
// doesnt fill the side of the block
//if (pos.getY() == 1) {
// return Blocks.AIR.getDefaultState();
//}
return state;
}
@Override
public FluidState getFluidState(BlockPos pos) {
if (shouldReturnAirForFluid(pos, face)) {
return Blocks.AIR.getDefaultState().getFluidState();
}
return state.getFluidState();
}
@Override
public int getHeight() {
return 0;
}
@Override
public int getBottomY() {
return 0;
}
}, this.vc, state, state.getFluidState());
}
private static boolean shouldReturnAirForFluid(BlockPos pos, int face) {
var fv = Direction.byIndex(face).getVector();
int dot = fv.getX()*pos.getX() + fv.getY()*pos.getY() + fv.getZ()*pos.getZ();
return dot >= 1;
}
public void free() {
this.capture.free();
this.vc.free();
}
public void renderToStream(BlockState state, int streamBuffer, int streamOffset) {
this.capture.clear();
boolean isBlock = true;
RenderLayer layer;
if (state.getBlock() instanceof FluidBlock) {
layer = RenderLayers.getFluidLayer(state.getFluidState());
isBlock = false;
} else {
layer = RenderLayers.getBlockLayer(state);
}
//TODO: support block model entities
//state.hasBlockEntity()
//Setup GL state
int[] viewdat = new int[4];
int blockTextureId;
{
glEnable(GL_STENCIL_TEST);
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
if (layer == RenderLayer.getTranslucent()) {
glEnable(GL_BLEND);
glBlendFuncSeparate(GL_ONE_MINUS_DST_ALPHA, GL_DST_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
} else {
glDisable(GL_BLEND);
}
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
glGetIntegerv(GL_VIEWPORT, viewdat);//TODO: faster way todo this, or just use main framebuffer resolution
//Bind the capture framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, this.capture.framebuffer.id);
var tex = MinecraftClient.getInstance().getTextureManager().getTexture(Identifier.of("minecraft", "textures/atlas/blocks.png")).getGlTexture();
blockTextureId = ((net.minecraft.client.texture.GlTexture)tex).getGlId();
}
//TODO: fastpath for blocks
if (isBlock) {
this.vc.reset();
this.bakeBlockModel(state, layer);
if (this.vc.count != 0) {//only render if there... is shit to render
if (this.vc.count % 4 != 0) throw new IllegalStateException();
//Setup for continual emission
BudgetBufferRenderer.setup(this.vc.buffer.address, this.vc.count / 4, blockTextureId);//note: this.vc.buffer.address NOT this.vc.ptr
var mat = new Matrix4f();
for (int i = 0; i < VIEWS.length; i++) {
glViewport((i % 3) * this.width, (i / 3) * this.height, this.width, this.height);
//The projection matrix
mat.set(2, 0, 0, 0,
0, 2, 0, 0,
0, 0, -1f, 0,
-1, -1, 0, 1);
BudgetBufferRenderer.render(mat.mul(VIEWS[i]));
}
}
glBindVertexArray(0);
} else {//Is fluid, slow path :(
if (!(state.getBlock() instanceof FluidBlock)) throw new IllegalStateException();
var mat = new Matrix4f();
for (int i = 0; i < VIEWS.length; i++) {
this.vc.reset();
this.bakeFluidState(state, i);
if (this.vc.count == 0) continue;
BudgetBufferRenderer.setup(this.vc.buffer.address, this.vc.count / 4, blockTextureId);
glViewport((i % 3) * this.width, (i / 3) * this.height, this.width, this.height);
//The projection matrix
mat.set(2, 0, 0, 0,
0, 2, 0, 0,
0, 0, -1f, 0,
-1, -1, 0, 1);
BudgetBufferRenderer.render(mat.mul(VIEWS[i]));
}
glBindVertexArray(0);
}
//"Restore" gl state
glViewport(viewdat[0], viewdat[1], viewdat[2], viewdat[3]);
glDisable(GL_STENCIL_TEST);
glDisable(GL_BLEND);
//Finish and download
glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT);
this.capture.emitToStream(streamBuffer, streamOffset);
glBindFramebuffer(GL_FRAMEBUFFER, this.capture.framebuffer.id);
glClearDepth(1);
glClear(GL_DEPTH_BUFFER_BIT);
}
static {
//TODO: FIXME: need to bake in the correct orientation, HOWEVER some orientations require a flipped winding order!!!!
addView(0, -90,0, 0, false);//Direction.DOWN
addView(1, 90,0, 0, false);//Direction.UP
addView(2, 0,180, 0, true);//Direction.NORTH
addView(3, 0,0, 0, false);//Direction.SOUTH
//TODO: check these arnt the wrong way round
addView(4, 0,90, 270, false);//Direction.EAST
addView(5, 0,270, 270, false);//Direction.WEST
}
private static void addView(int i, float pitch, float yaw, float rotation, boolean flipX) {
var stack = new MatrixStack();
stack.translate(0.5f,0.5f,0.5f);
stack.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(rotation));
stack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(pitch));
stack.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(yaw));
stack.translate(-0.5f,-0.5f,-0.5f);
VIEWS[i] = new Matrix4f(stack.peek().getPositionMatrix());
}
}

View File

@@ -0,0 +1,114 @@
package me.cortex.voxy.client.core.model.bakery;
import me.cortex.voxy.common.util.MemoryBuffer;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.model.BakedQuad;
import org.lwjgl.system.MemoryUtil;
import static me.cortex.voxy.client.core.model.bakery.BudgetBufferRenderer.VERTEX_FORMAT_SIZE;
public final class ReuseVertexConsumer implements VertexConsumer {
private MemoryBuffer buffer = new MemoryBuffer(8192);
private long ptr;
private int count;
public ReuseVertexConsumer() {
this.reset();
}
@Override
public ReuseVertexConsumer vertex(float x, float y, float z) {
this.ensureCanPut();
this.ptr += VERTEX_FORMAT_SIZE; this.count++; //Goto next vertex
MemoryUtil.memPutFloat(this.ptr, x);
MemoryUtil.memPutFloat(this.ptr + 4, y);
MemoryUtil.memPutFloat(this.ptr + 8, z);
return this;
}
public ReuseVertexConsumer meta(int metadata) {
MemoryUtil.memPutInt(this.ptr + 12, metadata);
return this;
}
@Override
public ReuseVertexConsumer color(int red, int green, int blue, int alpha) {
return this;
}
@Override
public ReuseVertexConsumer texture(float u, float v) {
MemoryUtil.memPutFloat(this.ptr + 16, u);
MemoryUtil.memPutFloat(this.ptr + 20, v);
return this;
}
@Override
public ReuseVertexConsumer overlay(int u, int v) {
return this;
}
@Override
public ReuseVertexConsumer light(int u, int v) {
return this;
}
@Override
public ReuseVertexConsumer normal(float x, float y, float z) {
return this;
}
public ReuseVertexConsumer quad(BakedQuad quad, int metadata) {
int[] data = quad.vertexData();
for (int i = 0; i < 4; i++) {
float x = Float.intBitsToFloat(data[i * 8]);
float y = Float.intBitsToFloat(data[i * 8 + 1]);
float z = Float.intBitsToFloat(data[i * 8 + 2]);
this.vertex(x,y,z);
float u = Float.intBitsToFloat(data[i * 8 + 4]);
float v = Float.intBitsToFloat(data[i * 8 + 5]);
this.texture(u,v);
this.meta(metadata);
}
return this;
}
private void ensureCanPut() {
if ((long) (this.count + 1) * VERTEX_FORMAT_SIZE < this.buffer.size) {
return;
}
long offset = this.buffer.address-this.ptr;
//1.5x the size
var newBuffer = new MemoryBuffer((((int)(this.buffer.size*1.5)+VERTEX_FORMAT_SIZE-1)/VERTEX_FORMAT_SIZE)*VERTEX_FORMAT_SIZE);
this.buffer.cpyTo(newBuffer.address);
this.buffer.free();
this.buffer = newBuffer;
this.ptr = offset + newBuffer.address;
}
public ReuseVertexConsumer reset() {
this.count = 0;
this.ptr = this.buffer.address - VERTEX_FORMAT_SIZE;//the thing is first time this gets incremented by FORMAT_STRIDE
return this;
}
public void free() {
this.buffer.free();
this.buffer = null;
}
public boolean isEmpty() {
return this.count == 0;
}
public int quadCount() {
if (this.count%4 != 0) throw new IllegalStateException();
return this.count/4;
}
public long getAddress() {
return this.buffer.address;
}
}

View File

@@ -1627,83 +1627,4 @@ public class RenderDataFactory45 {
public void free() {
this.quadBuffer.free();
}
//Returns true if a face was placed
private boolean putFluidFaceIfCan(Mesher2D mesher, int face, int opposingFace, long self, long metadata, int selfClientModelId, int selfBlockId, long facingState, long facingMetadata, int facingClientModelId, int a, int b) {
int selfFluidClientId = this.modelMan.getFluidClientStateId(selfClientModelId);
long selfFluidMetadata = this.modelMan.getModelMetadataFromClientId(selfFluidClientId);
int facingFluidClientId = -1;
if (ModelQueries.containsFluid(facingMetadata)) {
facingFluidClientId = this.modelMan.getFluidClientStateId(facingClientModelId);
}
//If both of the states are the same, then dont render the fluid face
if (selfFluidClientId == facingFluidClientId) {
return false;
}
if (facingFluidClientId != -1) {
//TODO: OPTIMIZE
if (this.world.getMapper().getBlockStateFromBlockId(selfBlockId).getBlock() == this.world.getMapper().getBlockStateFromBlockId(Mapper.getBlockId(facingState)).getBlock()) {
return false;
}
}
if (ModelQueries.faceOccludes(facingMetadata, opposingFace)) {
return false;
}
//if the model has a fluid state but is not a liquid need to see if the solid state had a face rendered and that face is occluding, if so, dont render the fluid state face
if ((!ModelQueries.isFluid(metadata)) && ModelQueries.faceOccludes(metadata, face)) {
return false;
}
//TODO:FIXME SOMEHOW THIS IS CRITICAL!!!!!!!!!!!!!!!!!!
// so there is one more issue need to be fixed, if water is layered ontop of eachother, the side faces depend on the water state ontop
// this has been hackfixed in the model texture bakery but a proper solution that doesnt explode the sides of the water textures needs to be done
// the issue is that the fluid rendering depends on the up state aswell not just the face state which is really really painful to account for
// e.g the sides of a full water is 8 high or something, not the full block height, this results in a gap between water layers
long otherFlags = 0;
otherFlags |= ModelQueries.isTranslucent(selfFluidMetadata)?1L<<33:0;
otherFlags |= ModelQueries.isDoubleSided(selfFluidMetadata)?1L<<34:0;
mesher.put(a, b, ((long)selfFluidClientId) | (((long) Mapper.getLightId(ModelQueries.faceUsesSelfLighting(selfFluidMetadata, face)?self:facingState))<<16) | ((((long) Mapper.getBiomeId(self))<<24) * (ModelQueries.isBiomeColoured(selfFluidMetadata)?1:0)) | otherFlags);
return true;
}
//Returns true if a face was placed
private boolean putFaceIfCan(Mesher2D mesher, int face, int opposingFace, long metadata, int clientModelId, int selfBiome, long facingModelId, long facingMetadata, int selfLight, int facingLight, int a, int b) {
if (ModelQueries.cullsSame(metadata) && clientModelId == facingModelId) {
//If we are facing a block, and we are both the same state, dont render that face
return false;
}
//If face can be occluded and is occluded from the facing block, then dont render the face
if (ModelQueries.faceCanBeOccluded(metadata, face) && ModelQueries.faceOccludes(facingMetadata, opposingFace)) {
return false;
}
long otherFlags = 0;
otherFlags |= ModelQueries.isTranslucent(metadata)?1L<<33:0;
otherFlags |= ModelQueries.isDoubleSided(metadata)?1L<<34:0;
mesher.put(a, b, ((long)clientModelId) | (((long) Mapper.getLightId(ModelQueries.faceUsesSelfLighting(metadata, face)?selfLight:facingLight))<<16) | (ModelQueries.isBiomeColoured(metadata)?(((long) Mapper.getBiomeId(selfBiome))<<24):0) | otherFlags);
return true;
}
private static int getMeshletHoldingCount(int quads, int quadsPerMeshlet, int meshletSize) {
return ((quads+(quadsPerMeshlet-1))/quadsPerMeshlet)*meshletSize;
}
public static int alignUp(int n, int alignment) {
return (n + alignment - 1) & -alignment;
}
}