From 20a75321e2554f10c967e3723bc401c413f8a2c6 Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Mon, 14 Feb 2022 20:36:35 -0800 Subject: [PATCH 1/3] Fix occasional crash when flywheel objects are loaded in --- .../backend/instancing/instancing/InstancedMaterialGroup.java | 2 +- .../java/com/jozufozu/flywheel/backend/model/ModelPool.java | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/instancing/InstancedMaterialGroup.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/instancing/InstancedMaterialGroup.java index 73ea3e1c5..bd85a26b8 100644 --- a/src/main/java/com/jozufozu/flywheel/backend/instancing/instancing/InstancedMaterialGroup.java +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/instancing/InstancedMaterialGroup.java @@ -44,7 +44,7 @@ public class InstancedMaterialGroup

implements MaterialG .onAMDWindows()) { this.allocator = FallbackAllocator.INSTANCE; } else { - this.allocator = new ModelPool(Formats.POS_TEX_NORMAL, 2048); + this.allocator = new ModelPool(Formats.POS_TEX_NORMAL); } } diff --git a/src/main/java/com/jozufozu/flywheel/backend/model/ModelPool.java b/src/main/java/com/jozufozu/flywheel/backend/model/ModelPool.java index 7e50a5ddc..5c1d426c7 100644 --- a/src/main/java/com/jozufozu/flywheel/backend/model/ModelPool.java +++ b/src/main/java/com/jozufozu/flywheel/backend/model/ModelPool.java @@ -35,16 +35,14 @@ public class ModelPool implements ModelAllocator { * Create a new model pool. * * @param vertexType The vertex type of the models that will be stored in the pool. - * @param initialSize The initial size of the pool, in vertices. */ - public ModelPool(VertexType vertexType, int initialSize) { + public ModelPool(VertexType vertexType) { this.vertexType = vertexType; int stride = vertexType.getStride(); vbo = new MappedGlBuffer(GlBufferType.ARRAY_BUFFER); vbo.bind(); - vbo.ensureCapacity((long) stride * initialSize); vbo.setGrowthMargin(stride * 64); } From 52f66dec9d4dd10a55470e42ebb44904a3dc4330 Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Mon, 14 Feb 2022 20:45:13 -0800 Subject: [PATCH 2/3] Move diffuse declaration to vertex compiler --- .../com/jozufozu/flywheel/core/Templates.java | 5 +++++ .../flywheel/core/compile/VertexCompiler.java | 2 ++ .../com/jozufozu/flywheel/util/TextureBinder.java | 15 --------------- .../flywheel/shaders/context/crumbling.glsl | 1 - .../flywheel/flywheel/shaders/context/world.glsl | 1 - 5 files changed, 7 insertions(+), 17 deletions(-) delete mode 100644 src/main/java/com/jozufozu/flywheel/util/TextureBinder.java diff --git a/src/main/java/com/jozufozu/flywheel/core/Templates.java b/src/main/java/com/jozufozu/flywheel/core/Templates.java index 59b538a2f..30987940b 100644 --- a/src/main/java/com/jozufozu/flywheel/core/Templates.java +++ b/src/main/java/com/jozufozu/flywheel/core/Templates.java @@ -1,13 +1,18 @@ package com.jozufozu.flywheel.core; +import com.jozufozu.flywheel.Flywheel; import com.jozufozu.flywheel.backend.gl.GLSLVersion; import com.jozufozu.flywheel.core.compile.FragmentTemplateData; import com.jozufozu.flywheel.core.compile.InstancingTemplateData; import com.jozufozu.flywheel.core.compile.OneShotTemplateData; import com.jozufozu.flywheel.core.compile.Template; +import com.jozufozu.flywheel.core.source.FileResolution; +import com.jozufozu.flywheel.core.source.Resolver; public class Templates { + public static final FileResolution DIFFUSE_FILE = Resolver.INSTANCE.get(Flywheel.rl("core/diffuse.glsl")); + public static final Template INSTANCING = new Template<>(GLSLVersion.V330, InstancingTemplateData::new); public static final Template ONE_SHOT = new Template<>(GLSLVersion.V150, OneShotTemplateData::new); public static final Template FRAGMENT = new Template<>(GLSLVersion.V150, FragmentTemplateData::new); diff --git a/src/main/java/com/jozufozu/flywheel/core/compile/VertexCompiler.java b/src/main/java/com/jozufozu/flywheel/core/compile/VertexCompiler.java index 6487c45c4..08e63a93b 100644 --- a/src/main/java/com/jozufozu/flywheel/core/compile/VertexCompiler.java +++ b/src/main/java/com/jozufozu/flywheel/core/compile/VertexCompiler.java @@ -5,6 +5,7 @@ import java.util.Objects; import com.jozufozu.flywheel.api.vertex.VertexType; import com.jozufozu.flywheel.backend.gl.shader.GlShader; import com.jozufozu.flywheel.backend.gl.shader.ShaderType; +import com.jozufozu.flywheel.core.Templates; import com.jozufozu.flywheel.core.shader.StateSnapshot; import com.jozufozu.flywheel.core.source.FileIndexImpl; import com.jozufozu.flywheel.core.source.FileResolution; @@ -40,6 +41,7 @@ public class VertexCompiler extends Memoizer { FileIndexImpl index = new FileIndexImpl(); + Templates.DIFFUSE_FILE.getFile().generateFinalSource(index, finalSource); header.getFile().generateFinalSource(index, finalSource); key.file.generateFinalSource(index, finalSource); diff --git a/src/main/java/com/jozufozu/flywheel/util/TextureBinder.java b/src/main/java/com/jozufozu/flywheel/util/TextureBinder.java deleted file mode 100644 index e90435078..000000000 --- a/src/main/java/com/jozufozu/flywheel/util/TextureBinder.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.jozufozu.flywheel.util; - -import net.minecraft.client.renderer.RenderType; - -/** - * This is a silly hack that's needed because flywheel does things too different from vanilla. - * - *

- * When a {@link RenderType} is setup, the associated textures are "bound" within RenderSystem, but not actually - * bound via opengl. This class provides a helper function to forward the bindings to opengl. - *

- */ -public class TextureBinder { - -} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/context/crumbling.glsl b/src/main/resources/assets/flywheel/flywheel/shaders/context/crumbling.glsl index 958941126..423977c8a 100644 --- a/src/main/resources/assets/flywheel/flywheel/shaders/context/crumbling.glsl +++ b/src/main/resources/assets/flywheel/flywheel/shaders/context/crumbling.glsl @@ -1,5 +1,4 @@ #use "flywheel:context/fog.glsl" -#use "flywheel:core/diffuse.glsl" uniform float uTime; uniform mat4 uViewProjection; diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/context/world.glsl b/src/main/resources/assets/flywheel/flywheel/shaders/context/world.glsl index 28e36ab3d..b6386e92d 100644 --- a/src/main/resources/assets/flywheel/flywheel/shaders/context/world.glsl +++ b/src/main/resources/assets/flywheel/flywheel/shaders/context/world.glsl @@ -1,5 +1,4 @@ #use "flywheel:context/fog.glsl" -#use "flywheel:core/diffuse.glsl" uniform float uTime; uniform mat4 uViewProjection; From 0c74be53f6feef64a41c73ece5332e85e1967924 Mon Sep 17 00:00:00 2001 From: PepperCode1 <44146161+PepperCode1@users.noreply.github.com> Date: Fri, 18 Feb 2022 20:39:19 -0800 Subject: [PATCH 3/3] Add partial support for non-shaded quads - Add ShadeSeparatedBufferBuilder and other code to allow separating all non-shaded vertices to the end of the buffer - Add ShadedVertexList to allow defining if a certain vertex is shaded or not - Add new methods to ModelUtil with arguments for more flexibility - Refactor VirtualEmptyBlockGetter to allow defining arbitrary light values - Add shaded argument to DiffuseLightCalculator --- .../flywheel/api/vertex/ShadedVertexList.java | 5 + .../backend/model/BufferBuilderExtension.java | 10 ++ .../jozufozu/flywheel/core/model/Model.java | 2 +- .../flywheel/core/model/ModelTransformer.java | 11 +- .../flywheel/core/model/ModelUtil.java | 62 +++++-- .../model/ShadeSeparatedBufferBuilder.java | 25 +++ .../model/ShadeSeparatingVertexConsumer.java | 84 +++++++++ .../flywheel/core/vertex/BlockVertex.java | 11 +- .../flywheel/core/vertex/BlockVertexList.java | 18 ++ .../core/vertex/BlockVertexListUnsafe.java | 18 ++ .../core/virtual/VirtualEmptyBlockGetter.java | 162 +++++++++--------- .../flywheel/mixin/BufferBuilderMixin.java | 52 +++++- .../flywheel/util/DiffuseLightCalculator.java | 5 +- .../jozufozu/flywheel/util/RenderMath.java | 14 +- 14 files changed, 371 insertions(+), 108 deletions(-) create mode 100644 src/main/java/com/jozufozu/flywheel/api/vertex/ShadedVertexList.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/model/ShadeSeparatedBufferBuilder.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/model/ShadeSeparatingVertexConsumer.java diff --git a/src/main/java/com/jozufozu/flywheel/api/vertex/ShadedVertexList.java b/src/main/java/com/jozufozu/flywheel/api/vertex/ShadedVertexList.java new file mode 100644 index 000000000..227879b3a --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/api/vertex/ShadedVertexList.java @@ -0,0 +1,5 @@ +package com.jozufozu.flywheel.api.vertex; + +public interface ShadedVertexList extends VertexList { + boolean isShaded(int index); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/model/BufferBuilderExtension.java b/src/main/java/com/jozufozu/flywheel/backend/model/BufferBuilderExtension.java index 87d840792..4aec62268 100644 --- a/src/main/java/com/jozufozu/flywheel/backend/model/BufferBuilderExtension.java +++ b/src/main/java/com/jozufozu/flywheel/backend/model/BufferBuilderExtension.java @@ -12,6 +12,8 @@ import com.mojang.blaze3d.vertex.VertexFormat; */ public interface BufferBuilderExtension { + int flywheel$getVertices(); + /** * Frees the internal ByteBuffer, if it exists. */ @@ -24,4 +26,12 @@ public interface BufferBuilderExtension { * @param vertexCount The number of vertices in the buffer. */ void flywheel$injectForRender(ByteBuffer buffer, VertexFormat format, int vertexCount); + + /** + * Appends the remaining bytes from the given buffer to this BufferBuilder. + * @param buffer The buffer from which to copy bytes. + * @throws IllegalStateException If this BufferBuilder is not started or is the process of writing a vertex + * @throws IllegalArgumentException If the given buffer does not contain a whole number of vertices + */ + void flywheel$appendBufferUnsafe(ByteBuffer buffer); } diff --git a/src/main/java/com/jozufozu/flywheel/core/model/Model.java b/src/main/java/com/jozufozu/flywheel/core/model/Model.java index 6d1c2c0c5..eacfd3394 100644 --- a/src/main/java/com/jozufozu/flywheel/core/model/Model.java +++ b/src/main/java/com/jozufozu/flywheel/core/model/Model.java @@ -16,7 +16,7 @@ import com.jozufozu.flywheel.core.QuadConverter; *

* *
{@code
- * IModel model = ...;
+ * Model model = ...;
  * VecBuffer into = ...;
  *
  * int initial = VecBuffer.unwrap().position();
diff --git a/src/main/java/com/jozufozu/flywheel/core/model/ModelTransformer.java b/src/main/java/com/jozufozu/flywheel/core/model/ModelTransformer.java
index 10eafd1c4..52e542ec7 100644
--- a/src/main/java/com/jozufozu/flywheel/core/model/ModelTransformer.java
+++ b/src/main/java/com/jozufozu/flywheel/core/model/ModelTransformer.java
@@ -1,5 +1,8 @@
 package com.jozufozu.flywheel.core.model;
 
+import java.util.function.IntPredicate;
+
+import com.jozufozu.flywheel.api.vertex.ShadedVertexList;
 import com.jozufozu.flywheel.api.vertex.VertexList;
 import com.jozufozu.flywheel.util.DiffuseLightCalculator;
 import com.jozufozu.flywheel.util.transform.Transform;
@@ -19,12 +22,18 @@ public class ModelTransformer {
 
 	private final Model model;
 	private final VertexList reader;
+	private final IntPredicate shadedPredicate;
 
 	public final Context context = new Context();
 
 	public ModelTransformer(Model model) {
 		this.model = model;
 		reader = model.getReader();
+		if (reader instanceof ShadedVertexList shaded) {
+			shadedPredicate = shaded::isShaded;
+		} else {
+			shadedPredicate = index -> true;
+		}
 	}
 
 	public void renderInto(Params params, PoseStack input, VertexConsumer builder) {
@@ -82,7 +91,7 @@ public class ModelTransformer {
 				a = reader.getA(i);
 			}
 			if (context.outputColorDiffuse) {
-				float instanceDiffuse = diffuseCalculator.getDiffuse(nx, ny, nz);
+				float instanceDiffuse = diffuseCalculator.getDiffuse(nx, ny, nz, shadedPredicate.test(i));
 				int colorR = transformColor(r, instanceDiffuse);
 				int colorG = transformColor(g, instanceDiffuse);
 				int colorB = transformColor(b, instanceDiffuse);
diff --git a/src/main/java/com/jozufozu/flywheel/core/model/ModelUtil.java b/src/main/java/com/jozufozu/flywheel/core/model/ModelUtil.java
index c839e4093..0815adf28 100644
--- a/src/main/java/com/jozufozu/flywheel/core/model/ModelUtil.java
+++ b/src/main/java/com/jozufozu/flywheel/core/model/ModelUtil.java
@@ -38,6 +38,8 @@ public class ModelUtil {
 	 */
 	public static final BlockRenderDispatcher VANILLA_RENDERER = createVanillaRenderer();
 
+	private static final ThreadLocal THREAD_LOCAL_OBJECTS = ThreadLocal.withInitial(ThreadLocalObjects::new);
+
 	private static BlockRenderDispatcher createVanillaRenderer() {
 		BlockRenderDispatcher defaultDispatcher = Minecraft.getInstance().getBlockRenderer();
 		BlockRenderDispatcher dispatcher = new BlockRenderDispatcher(null, null, null);
@@ -54,23 +56,47 @@ public class ModelUtil {
 		return dispatcher;
 	}
 
-	public static BufferBuilder getBufferBuilder(BakedModel model, BlockState referenceState, PoseStack ms) {
+	public static ShadeSeparatedBufferBuilder getBufferBuilder(BakedModel model, BlockState referenceState, PoseStack poseStack) {
+		return getBufferBuilder(VirtualEmptyBlockGetter.INSTANCE, model, referenceState, poseStack);
+	}
+
+	public static ShadeSeparatedBufferBuilder getBufferBuilder(BlockAndTintGetter renderWorld, BakedModel model, BlockState referenceState, PoseStack poseStack) {
 		ModelBlockRenderer blockRenderer = VANILLA_RENDERER.getModelRenderer();
-		BufferBuilder builder = new BufferBuilder(512);
+		ThreadLocalObjects objects = THREAD_LOCAL_OBJECTS.get();
+
+		ShadeSeparatingVertexConsumer shadeSeparatingWrapper = objects.shadeSeparatingWrapper;
+		ShadeSeparatedBufferBuilder builder = new ShadeSeparatedBufferBuilder(512);
+		BufferBuilder unshadedBuilder = objects.unshadedBuilder;
+
 		builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.BLOCK);
-		blockRenderer.tesselateBlock(VirtualEmptyBlockGetter.INSTANCE, model, referenceState, BlockPos.ZERO, ms, builder,
-				false, new Random(), 42, OverlayTexture.NO_OVERLAY, VirtualEmptyModelData.INSTANCE);
+		unshadedBuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.BLOCK);
+		shadeSeparatingWrapper.prepare(builder, unshadedBuilder);
+		blockRenderer.tesselateBlock(renderWorld, model, referenceState, BlockPos.ZERO, poseStack, shadeSeparatingWrapper,
+				false, objects.random, 42, OverlayTexture.NO_OVERLAY, VirtualEmptyModelData.INSTANCE);
+		shadeSeparatingWrapper.clear();
+		unshadedBuilder.end();
+		builder.appendUnshadedVertices(unshadedBuilder);
 		builder.end();
+
 		return builder;
 	}
 
-	public static BufferBuilder getBufferBuilderFromTemplate(BlockAndTintGetter renderWorld, RenderType layer, Collection blocks) {
-		ModelBlockRenderer modelRenderer = VANILLA_RENDERER.getModelRenderer();
+	public static ShadeSeparatedBufferBuilder getBufferBuilderFromTemplate(BlockAndTintGetter renderWorld, RenderType layer, Collection blocks) {
+		return getBufferBuilderFromTemplate(renderWorld, layer, blocks, new PoseStack());
+	}
+
+	public static ShadeSeparatedBufferBuilder getBufferBuilderFromTemplate(BlockAndTintGetter renderWorld, RenderType layer, Collection blocks, PoseStack poseStack) {
+		ModelBlockRenderer modelRenderer = VANILLA_RENDERER.getModelRenderer();
+		ThreadLocalObjects objects = THREAD_LOCAL_OBJECTS.get();
+
+		Random random = objects.random;
+		ShadeSeparatingVertexConsumer shadeSeparatingWrapper = objects.shadeSeparatingWrapper;
+		ShadeSeparatedBufferBuilder builder = new ShadeSeparatedBufferBuilder(512);
+		BufferBuilder unshadedBuilder = objects.unshadedBuilder;
 
-		PoseStack ms = new PoseStack();
-		Random random = new Random();
-		BufferBuilder builder = new BufferBuilder(512);
 		builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.BLOCK);
+		unshadedBuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.BLOCK);
+		shadeSeparatingWrapper.prepare(builder, unshadedBuilder);
 
 		ForgeHooksClient.setRenderType(layer);
 		ModelBlockRenderer.enableCaching();
@@ -84,16 +110,20 @@ public class ModelUtil {
 
 			BlockPos pos = info.pos;
 
-			ms.pushPose();
-			ms.translate(pos.getX(), pos.getY(), pos.getZ());
-			modelRenderer.tesselateBlock(renderWorld, VANILLA_RENDERER.getBlockModel(state), state, pos, ms, builder,
+			poseStack.pushPose();
+			poseStack.translate(pos.getX(), pos.getY(), pos.getZ());
+			modelRenderer.tesselateBlock(renderWorld, VANILLA_RENDERER.getBlockModel(state), state, pos, poseStack, shadeSeparatingWrapper,
 					true, random, 42, OverlayTexture.NO_OVERLAY, EmptyModelData.INSTANCE);
-			ms.popPose();
+			poseStack.popPose();
 		}
 		ModelBlockRenderer.clearCache();
 		ForgeHooksClient.setRenderType(null);
 
+		shadeSeparatingWrapper.clear();
+		unshadedBuilder.end();
+		builder.appendUnshadedVertices(unshadedBuilder);
 		builder.end();
+
 		return builder;
 	}
 
@@ -107,4 +137,10 @@ public class ModelUtil {
 			return stack;
 		};
 	}
+
+	private static class ThreadLocalObjects {
+		public final Random random = new Random();
+		public final ShadeSeparatingVertexConsumer shadeSeparatingWrapper = new ShadeSeparatingVertexConsumer();
+		public final BufferBuilder unshadedBuilder = new BufferBuilder(512);
+	}
 }
diff --git a/src/main/java/com/jozufozu/flywheel/core/model/ShadeSeparatedBufferBuilder.java b/src/main/java/com/jozufozu/flywheel/core/model/ShadeSeparatedBufferBuilder.java
new file mode 100644
index 000000000..38043702b
--- /dev/null
+++ b/src/main/java/com/jozufozu/flywheel/core/model/ShadeSeparatedBufferBuilder.java
@@ -0,0 +1,25 @@
+package com.jozufozu.flywheel.core.model;
+
+import java.nio.ByteBuffer;
+
+import com.jozufozu.flywheel.backend.model.BufferBuilderExtension;
+import com.mojang.blaze3d.vertex.BufferBuilder;
+import com.mojang.datafixers.util.Pair;
+
+public class ShadeSeparatedBufferBuilder extends BufferBuilder {
+	protected int unshadedStartVertex;
+
+	public ShadeSeparatedBufferBuilder(int capacity) {
+		super(capacity);
+	}
+
+	public void appendUnshadedVertices(BufferBuilder unshadedBuilder) {
+		Pair data = unshadedBuilder.popNextBuffer();
+		unshadedStartVertex = ((BufferBuilderExtension) this).flywheel$getVertices();
+		((BufferBuilderExtension) this).flywheel$appendBufferUnsafe(data.getSecond());
+	}
+
+	public int getUnshadedStartVertex() {
+		return unshadedStartVertex;
+	}
+}
diff --git a/src/main/java/com/jozufozu/flywheel/core/model/ShadeSeparatingVertexConsumer.java b/src/main/java/com/jozufozu/flywheel/core/model/ShadeSeparatingVertexConsumer.java
new file mode 100644
index 000000000..ea50184a7
--- /dev/null
+++ b/src/main/java/com/jozufozu/flywheel/core/model/ShadeSeparatingVertexConsumer.java
@@ -0,0 +1,84 @@
+package com.jozufozu.flywheel.core.model;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.blaze3d.vertex.VertexConsumer;
+
+import net.minecraft.client.renderer.block.model.BakedQuad;
+
+public class ShadeSeparatingVertexConsumer implements VertexConsumer {
+	protected VertexConsumer shadedConsumer;
+	protected VertexConsumer unshadedConsumer;
+
+	public void prepare(VertexConsumer shadedConsumer, VertexConsumer unshadedConsumer) {
+		this.shadedConsumer = shadedConsumer;
+		this.unshadedConsumer = unshadedConsumer;
+	}
+
+	public void clear() {
+		shadedConsumer = null;
+		unshadedConsumer = null;
+	}
+
+	@Override
+	public void putBulkData(PoseStack.Pose poseEntry, BakedQuad quad, float[] colorMuls, float red, float green, float blue, int[] combinedLights, int combinedOverlay, boolean mulColor) {
+		if (quad.isShade()) {
+			shadedConsumer.putBulkData(poseEntry, quad, colorMuls, red, green, blue, combinedLights, combinedOverlay, mulColor);
+		} else {
+			unshadedConsumer.putBulkData(poseEntry, quad, colorMuls, red, green, blue, combinedLights, combinedOverlay, mulColor);
+		}
+	}
+
+	@Override
+	public void putBulkData(PoseStack.Pose matrixEntry, BakedQuad bakedQuad, float[] baseBrightness, float red, float green, float blue, float alpha, int[] lightmapCoords, int overlayCoords, boolean readExistingColor) {
+		if (bakedQuad.isShade()) {
+			shadedConsumer.putBulkData(matrixEntry, bakedQuad, baseBrightness, red, green, blue, alpha, lightmapCoords, overlayCoords, readExistingColor);
+		} else {
+			unshadedConsumer.putBulkData(matrixEntry, bakedQuad, baseBrightness, red, green, blue, alpha, lightmapCoords, overlayCoords, readExistingColor);
+		}
+	}
+
+	@Override
+	public VertexConsumer vertex(double x, double y, double z) {
+		throw new UnsupportedOperationException("ShadeSeparatingVertexConsumer only supports putBulkData!");
+	}
+
+	@Override
+	public VertexConsumer color(int red, int green, int blue, int alpha) {
+		throw new UnsupportedOperationException("ShadeSeparatingVertexConsumer only supports putBulkData!");
+	}
+
+	@Override
+	public VertexConsumer uv(float u, float v) {
+		throw new UnsupportedOperationException("ShadeSeparatingVertexConsumer only supports putBulkData!");
+	}
+
+	@Override
+	public VertexConsumer overlayCoords(int u, int v) {
+		throw new UnsupportedOperationException("ShadeSeparatingVertexConsumer only supports putBulkData!");
+	}
+
+	@Override
+	public VertexConsumer uv2(int u, int v) {
+		throw new UnsupportedOperationException("ShadeSeparatingVertexConsumer only supports putBulkData!");
+	}
+
+	@Override
+	public VertexConsumer normal(float x, float y, float z) {
+		throw new UnsupportedOperationException("ShadeSeparatingVertexConsumer only supports putBulkData!");
+	}
+
+	@Override
+	public void endVertex() {
+		throw new UnsupportedOperationException("ShadeSeparatingVertexConsumer only supports putBulkData!");
+	}
+
+	@Override
+	public void defaultColor(int red, int green, int blue, int alpha) {
+		throw new UnsupportedOperationException("ShadeSeparatingVertexConsumer only supports putBulkData!");
+	}
+
+	@Override
+	public void unsetDefaultColor() {
+		throw new UnsupportedOperationException("ShadeSeparatingVertexConsumer only supports putBulkData!");
+	}
+}
diff --git a/src/main/java/com/jozufozu/flywheel/core/vertex/BlockVertex.java b/src/main/java/com/jozufozu/flywheel/core/vertex/BlockVertex.java
index e382d0ab7..d74dde894 100644
--- a/src/main/java/com/jozufozu/flywheel/core/vertex/BlockVertex.java
+++ b/src/main/java/com/jozufozu/flywheel/core/vertex/BlockVertex.java
@@ -6,6 +6,7 @@ import com.jozufozu.flywheel.api.vertex.VertexList;
 import com.jozufozu.flywheel.api.vertex.VertexType;
 import com.jozufozu.flywheel.core.layout.BufferLayout;
 import com.jozufozu.flywheel.core.layout.CommonItems;
+import com.jozufozu.flywheel.core.model.ShadeSeparatedBufferBuilder;
 import com.mojang.blaze3d.vertex.BufferBuilder;
 import com.mojang.blaze3d.vertex.DefaultVertexFormat;
 import com.mojang.datafixers.util.Pair;
@@ -57,6 +58,10 @@ Vertex FLWCreateVertex() {
 				""";
 	}
 
+	public BlockVertexListUnsafe.Shaded createReader(ByteBuffer buffer, int vertexCount, int unshadedStartVertex) {
+		return new BlockVertexListUnsafe.Shaded(buffer, vertexCount, unshadedStartVertex);
+	}
+
 	public VertexList createReader(BufferBuilder bufferBuilder) {
 		// TODO: try to avoid virtual model rendering
 		Pair pair = bufferBuilder.popNextBuffer();
@@ -66,6 +71,10 @@ Vertex FLWCreateVertex() {
 			throw new RuntimeException("Cannot use BufferBuilder with " + drawState.format());
 		}
 
-		return new BlockVertexListUnsafe(pair.getSecond(), drawState.vertexCount());
+		if (bufferBuilder instanceof ShadeSeparatedBufferBuilder separated) {
+			return createReader(pair.getSecond(), drawState.vertexCount(), separated.getUnshadedStartVertex());
+		} else {
+			return createReader(pair.getSecond(), drawState.vertexCount());
+		}
 	}
 }
diff --git a/src/main/java/com/jozufozu/flywheel/core/vertex/BlockVertexList.java b/src/main/java/com/jozufozu/flywheel/core/vertex/BlockVertexList.java
index ff4c98126..eaac02cee 100644
--- a/src/main/java/com/jozufozu/flywheel/core/vertex/BlockVertexList.java
+++ b/src/main/java/com/jozufozu/flywheel/core/vertex/BlockVertexList.java
@@ -2,7 +2,9 @@ package com.jozufozu.flywheel.core.vertex;
 
 import java.nio.ByteBuffer;
 
+import com.jozufozu.flywheel.api.vertex.ShadedVertexList;
 import com.jozufozu.flywheel.api.vertex.VertexList;
+import com.jozufozu.flywheel.core.model.ShadeSeparatedBufferBuilder;
 import com.jozufozu.flywheel.util.RenderMath;
 import com.mojang.blaze3d.vertex.BufferBuilder;
 import com.mojang.datafixers.util.Pair;
@@ -104,4 +106,20 @@ public class BlockVertexList implements VertexList {
 		return vertexCount;
 	}
 
+	public static class Shaded extends BlockVertexList implements ShadedVertexList {
+
+		private final int unshadedStartVertex;
+
+		public Shaded(ShadeSeparatedBufferBuilder builder) {
+			super(builder);
+			unshadedStartVertex = builder.getUnshadedStartVertex();
+		}
+
+		@Override
+		public boolean isShaded(int index) {
+			return index < unshadedStartVertex;
+		}
+
+	}
+
 }
diff --git a/src/main/java/com/jozufozu/flywheel/core/vertex/BlockVertexListUnsafe.java b/src/main/java/com/jozufozu/flywheel/core/vertex/BlockVertexListUnsafe.java
index 7c93b29d6..0993f29e5 100644
--- a/src/main/java/com/jozufozu/flywheel/core/vertex/BlockVertexListUnsafe.java
+++ b/src/main/java/com/jozufozu/flywheel/core/vertex/BlockVertexListUnsafe.java
@@ -4,6 +4,7 @@ import java.nio.ByteBuffer;
 
 import org.lwjgl.system.MemoryUtil;
 
+import com.jozufozu.flywheel.api.vertex.ShadedVertexList;
 import com.jozufozu.flywheel.api.vertex.VertexList;
 import com.jozufozu.flywheel.util.RenderMath;
 
@@ -97,4 +98,21 @@ public class BlockVertexListUnsafe implements VertexList {
 	public int getVertexCount() {
 		return vertexCount;
 	}
+
+	public static class Shaded extends BlockVertexListUnsafe implements ShadedVertexList {
+
+		private final int unshadedStartVertex;
+
+		public Shaded(ByteBuffer buffer, int vertexCount, int unshadedStartVertex) {
+			super(buffer, vertexCount);
+			this.unshadedStartVertex = unshadedStartVertex;
+		}
+
+		@Override
+		public boolean isShaded(int index) {
+			return index < unshadedStartVertex;
+		}
+
+	}
+
 }
diff --git a/src/main/java/com/jozufozu/flywheel/core/virtual/VirtualEmptyBlockGetter.java b/src/main/java/com/jozufozu/flywheel/core/virtual/VirtualEmptyBlockGetter.java
index 8058bcf68..ebfbe9389 100644
--- a/src/main/java/com/jozufozu/flywheel/core/virtual/VirtualEmptyBlockGetter.java
+++ b/src/main/java/com/jozufozu/flywheel/core/virtual/VirtualEmptyBlockGetter.java
@@ -22,115 +22,119 @@ import net.minecraft.world.level.lighting.LevelLightEngine;
 import net.minecraft.world.level.material.FluidState;
 import net.minecraft.world.level.material.Fluids;
 
-public enum VirtualEmptyBlockGetter implements BlockAndTintGetter {
-	INSTANCE;
-
-	private final LevelLightEngine lightEngine = new LevelLightEngine(new LightChunkGetter() {
-		@Override
-		public BlockGetter getChunkForLighting(int p_63023_, int p_63024_) {
-			return VirtualEmptyBlockGetter.this;
-		}
-
-		@Override
-		public BlockGetter getLevel() {
-			return VirtualEmptyBlockGetter.this;
-		}
-	}, false, false) {
-		private static final LayerLightEventListener SKY_DUMMY_LISTENER = new LayerLightEventListener() {
-			@Override
-			public void checkBlock(BlockPos pos) {
-			}
-
-			@Override
-			public void onBlockEmissionIncrease(BlockPos pos, int p_164456_) {
-			}
-
-			@Override
-			public boolean hasLightWork() {
-				return false;
-			}
-
-			@Override
-			public int runUpdates(int p_164449_, boolean p_164450_, boolean p_164451_) {
-				return p_164449_;
-			}
-
-			@Override
-			public void updateSectionStatus(SectionPos pos, boolean p_75838_) {
-			}
-
-			@Override
-			public void enableLightSources(ChunkPos pos, boolean p_164453_) {
-			}
-
-			@Override
-			public DataLayer getDataLayerData(SectionPos pos) {
-				return null;
-			}
-
-			@Override
-			public int getLightValue(BlockPos pos) {
-				return 15;
-			}
-		};
-
-		@Override
-		public LayerLightEventListener getLayerListener(LightLayer layer) {
-			if (layer == LightLayer.BLOCK) {
-				return LayerLightEventListener.DummyLightLayerEventListener.INSTANCE;
-			} else {
-				return SKY_DUMMY_LISTENER;
-			}
-		}
-
-		@Override
-		public int getRawBrightness(BlockPos pos, int skyDarken) {
-			return 15 - skyDarken;
-		}
-	};
+public interface VirtualEmptyBlockGetter extends BlockAndTintGetter {
+	public static final VirtualEmptyBlockGetter INSTANCE = new StaticLightImpl(0, 15);
+	public static final VirtualEmptyBlockGetter FULL_BRIGHT = new StaticLightImpl(15, 15);
+	public static final VirtualEmptyBlockGetter FULL_DARK = new StaticLightImpl(0, 0);
 
 	public static boolean is(BlockAndTintGetter blockGetter) {
-		return blockGetter == INSTANCE;
+		return blockGetter instanceof VirtualEmptyBlockGetter;
 	}
 
 	@Override
-	public BlockEntity getBlockEntity(BlockPos pos) {
+	default BlockEntity getBlockEntity(BlockPos pos) {
 		return null;
 	}
 
 	@Override
-	public BlockState getBlockState(BlockPos pos) {
+	default BlockState getBlockState(BlockPos pos) {
 		return Blocks.AIR.defaultBlockState();
 	}
 
 	@Override
-	public FluidState getFluidState(BlockPos pos) {
+	default FluidState getFluidState(BlockPos pos) {
 		return Fluids.EMPTY.defaultFluidState();
 	}
 
 	@Override
-	public int getHeight() {
+	default int getHeight() {
 		return 1;
 	}
 
 	@Override
-	public int getMinBuildHeight() {
+	default int getMinBuildHeight() {
 		return 0;
 	}
 
 	@Override
-	public float getShade(Direction direction, boolean bool) {
+	default float getShade(Direction direction, boolean shaded) {
 		return 1f;
 	}
 
 	@Override
-	public LevelLightEngine getLightEngine() {
-		return lightEngine;
-	}
-
-	@Override
-	public int getBlockTint(BlockPos pos, ColorResolver resolver) {
+	default int getBlockTint(BlockPos pos, ColorResolver resolver) {
 		Biome plainsBiome = Minecraft.getInstance().getConnection().registryAccess().registryOrThrow(Registry.BIOME_REGISTRY).getOrThrow(Biomes.PLAINS);
 		return resolver.getColor(plainsBiome, pos.getX(), pos.getZ());
 	}
+
+	public static class StaticLightImpl implements VirtualEmptyBlockGetter {
+		private final LevelLightEngine lightEngine;
+
+		public StaticLightImpl(int blockLight, int skyLight) {
+			lightEngine = new LevelLightEngine(new LightChunkGetter() {
+				@Override
+				public BlockGetter getChunkForLighting(int p_63023_, int p_63024_) {
+					return StaticLightImpl.this;
+				}
+
+				@Override
+				public BlockGetter getLevel() {
+					return StaticLightImpl.this;
+				}
+			}, false, false) {
+				private final LayerLightEventListener blockListener = createStaticListener(blockLight);
+				private final LayerLightEventListener skyListener = createStaticListener(skyLight);
+
+				@Override
+				public LayerLightEventListener getLayerListener(LightLayer layer) {
+					return layer == LightLayer.BLOCK ? blockListener : skyListener;
+				}
+			};
+		}
+
+		private static LayerLightEventListener createStaticListener(int light) {
+			return new LayerLightEventListener() {
+				@Override
+				public void checkBlock(BlockPos pos) {
+				}
+
+				@Override
+				public void onBlockEmissionIncrease(BlockPos pos, int p_164456_) {
+				}
+
+				@Override
+				public boolean hasLightWork() {
+					return false;
+				}
+
+				@Override
+				public int runUpdates(int p_164449_, boolean p_164450_, boolean p_164451_) {
+					return p_164449_;
+				}
+
+				@Override
+				public void updateSectionStatus(SectionPos pos, boolean p_75838_) {
+				}
+
+				@Override
+				public void enableLightSources(ChunkPos pos, boolean p_164453_) {
+				}
+
+				@Override
+				public DataLayer getDataLayerData(SectionPos pos) {
+					return null;
+				}
+
+				@Override
+				public int getLightValue(BlockPos pos) {
+					return light;
+				}
+			};
+		}
+
+		@Override
+		public LevelLightEngine getLightEngine() {
+			return lightEngine;
+		}
+	}
 }
diff --git a/src/main/java/com/jozufozu/flywheel/mixin/BufferBuilderMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/BufferBuilderMixin.java
index 6b13b9a92..bc82deaf8 100644
--- a/src/main/java/com/jozufozu/flywheel/mixin/BufferBuilderMixin.java
+++ b/src/main/java/com/jozufozu/flywheel/mixin/BufferBuilderMixin.java
@@ -20,16 +20,10 @@ public abstract class BufferBuilderMixin implements BufferBuilderExtension {
 	private ByteBuffer buffer;
 
 	@Shadow
-	private boolean building;
+	private int nextElementByte;
 
 	@Shadow
-	public abstract void begin(VertexFormat.Mode p_166780_, VertexFormat p_166781_);
-
-	@Shadow
-	private VertexFormat.Mode mode;
-
-	@Shadow
-	private VertexFormat format;
+	private int vertices;
 
 	@Shadow
 	@Nullable
@@ -39,7 +33,22 @@ public abstract class BufferBuilderMixin implements BufferBuilderExtension {
 	private int elementIndex;
 
 	@Shadow
-	private int vertices;
+	private VertexFormat format;
+
+	@Shadow
+	private VertexFormat.Mode mode;
+
+	@Shadow
+	private boolean building;
+
+	@Shadow
+	private void ensureCapacity(int increaseAmount) {
+	}
+
+	@Override
+	public int flywheel$getVertices() {
+		return vertices;
+	}
 
 	@Override
 	public void flywheel$freeBuffer() {
@@ -61,4 +70,29 @@ public abstract class BufferBuilderMixin implements BufferBuilderExtension {
 		this.currentElement = this.format.getElements().get(0);
 		this.elementIndex = 0;
 	}
+
+	@Override
+	public void flywheel$appendBufferUnsafe(ByteBuffer buffer) {
+		if (!building) {
+			throw new IllegalStateException("BufferBuilder not started");
+		}
+		if (elementIndex != 0) {
+			throw new IllegalStateException("Cannot append buffer while building vertex");
+		}
+
+		int numBytes = buffer.remaining();
+		if (numBytes % format.getVertexSize() != 0) {
+			throw new IllegalArgumentException("Cannot append buffer with non-whole number of vertices");
+		}
+		int numVertices = numBytes / format.getVertexSize();
+
+		ensureCapacity(numBytes + format.getVertexSize());
+		int originalPosition = this.buffer.position();
+		this.buffer.position(nextElementByte);
+		MemoryUtil.memCopy(buffer, this.buffer);
+		this.buffer.position(originalPosition);
+
+		nextElementByte += numBytes;
+		vertices += numVertices;
+	}
 }
diff --git a/src/main/java/com/jozufozu/flywheel/util/DiffuseLightCalculator.java b/src/main/java/com/jozufozu/flywheel/util/DiffuseLightCalculator.java
index d7f712582..4f1ac0557 100644
--- a/src/main/java/com/jozufozu/flywheel/util/DiffuseLightCalculator.java
+++ b/src/main/java/com/jozufozu/flywheel/util/DiffuseLightCalculator.java
@@ -2,10 +2,9 @@ package com.jozufozu.flywheel.util;
 
 import net.minecraft.client.Minecraft;
 import net.minecraft.client.multiplayer.ClientLevel;
-import net.minecraftforge.client.model.pipeline.LightUtil;
 
 public interface DiffuseLightCalculator {
-	DiffuseLightCalculator DEFAULT = LightUtil::diffuseLight;
+	DiffuseLightCalculator DEFAULT = RenderMath::diffuseLight;
 	DiffuseLightCalculator NETHER = RenderMath::diffuseLightNether;
 
 	static DiffuseLightCalculator forCurrentLevel() {
@@ -16,5 +15,5 @@ public interface DiffuseLightCalculator {
 		return level.effects().constantAmbientLight() ? NETHER : DEFAULT;
 	}
 
-	float getDiffuse(float normalX, float normalY, float normalZ);
+	float getDiffuse(float normalX, float normalY, float normalZ, boolean shaded);
 }
diff --git a/src/main/java/com/jozufozu/flywheel/util/RenderMath.java b/src/main/java/com/jozufozu/flywheel/util/RenderMath.java
index f280c2064..9ee9ecddc 100644
--- a/src/main/java/com/jozufozu/flywheel/util/RenderMath.java
+++ b/src/main/java/com/jozufozu/flywheel/util/RenderMath.java
@@ -1,5 +1,7 @@
 package com.jozufozu.flywheel.util;
 
+import net.minecraftforge.client.model.pipeline.LightUtil;
+
 public class RenderMath {
 
 	/**
@@ -68,7 +70,17 @@ public class RenderMath {
 		return (float) (((((target - current) % 360) + 540) % 360) - 180);
 	}
 
-	public static float diffuseLightNether(float x, float y, float z) {
+	public static float diffuseLight(float x, float y, float z, boolean shaded) {
+		if (!shaded) {
+			return 1f;
+		}
+		return LightUtil.diffuseLight(x, y, z);
+	}
+
+	public static float diffuseLightNether(float x, float y, float z, boolean shaded) {
+		if (!shaded) {
+			return 0.9f;
+		}
 		return Math.min(x * x * 0.6f + y * y * 0.9f + z * z * 0.8f, 1f);
 	}
 }