From 1422f25ad93354c20698f6e4a3386e2810e01d26 Mon Sep 17 00:00:00 2001 From: FoundationGames <43485105+FoundationGames@users.noreply.github.com> Date: Mon, 7 Aug 2023 11:35:49 -0700 Subject: [PATCH] Make changes to VBOs and add a config option for them - Rendering with VBOs can be enabled and disabled - BlockEntityClientState was generalized into CableVBOContainer - Most reliance on an available BlockEntity for rendering with VBOs has been removed - Major refactoring of VBO rendering away from CableOutputBlockEntityRenderer - Abstracted BlockEntityOutputs --- .../entity/AbstractOutputBlockEntity.java | 32 ++- .../entity/ElectronicJukeboxBlockEntity.java | 33 ++- .../client/render/BlockEntityClientState.java | 41 ---- .../phonos/client/render/CableRenderer.java | 212 +++++++++--------- .../client/render/CableVBOContainer.java | 102 +++++++++ .../block/CableOutputBlockEntityRenderer.java | 64 ++---- .../phonos/config/PhonosClientConfig.java | 1 + .../config/PhonosClientConfigScreen.java | 1 + .../phonos/world/sound/CableConnection.java | 4 + .../phonos/world/sound/CablePlugPoint.java | 6 + .../world/sound/ConnectionCollection.java | 7 + .../sound/block/BlockConnectionLayout.java | 10 + .../world/sound/block/BlockEntityOutputs.java | 4 +- .../world/sound/block/OutputBlockEntity.java | 6 +- .../resources/assets/phonos/lang/en_us.json | 3 +- 15 files changed, 305 insertions(+), 221 deletions(-) delete mode 100644 src/main/java/io/github/foundationgames/phonos/client/render/BlockEntityClientState.java create mode 100644 src/main/java/io/github/foundationgames/phonos/client/render/CableVBOContainer.java create mode 100644 src/main/java/io/github/foundationgames/phonos/world/sound/ConnectionCollection.java diff --git a/src/main/java/io/github/foundationgames/phonos/block/entity/AbstractOutputBlockEntity.java b/src/main/java/io/github/foundationgames/phonos/block/entity/AbstractOutputBlockEntity.java index bb7bfc9..142a1b8 100644 --- a/src/main/java/io/github/foundationgames/phonos/block/entity/AbstractOutputBlockEntity.java +++ b/src/main/java/io/github/foundationgames/phonos/block/entity/AbstractOutputBlockEntity.java @@ -1,6 +1,6 @@ package io.github.foundationgames.phonos.block.entity; -import io.github.foundationgames.phonos.client.render.BlockEntityClientState; +import io.github.foundationgames.phonos.client.render.CableVBOContainer; import io.github.foundationgames.phonos.sound.emitter.SoundSource; import io.github.foundationgames.phonos.util.UniqueId; import io.github.foundationgames.phonos.world.sound.block.BlockConnectionLayout; @@ -23,14 +23,15 @@ public abstract class AbstractOutputBlockEntity extends BlockEntity implements S public final BlockEntityOutputs outputs; protected @Nullable NbtCompound pendingNbt = null; protected final long emitterId; - private BlockEntityClientState clientState; + + private CableVBOContainer vboContainer; public AbstractOutputBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state, BlockConnectionLayout outputLayout) { super(type, pos, state); this.emitterId = UniqueId.ofBlock(pos); this.outputs = new BlockEntityOutputs(outputLayout, this); - this.clientState = null; + this.vboContainer = null; } @Override @@ -120,20 +121,29 @@ public BlockEntityOutputs getOutputs() { } @Override - public BlockEntityClientState getClientState() { - if (this.clientState == null) { - this.clientState = new BlockEntityClientState(); + public void enforceVBOState(boolean enabled) { + if (this.vboContainer != null && !enabled) { + this.vboContainer.close(); + + this.vboContainer = null; + } + } + + @Override + public CableVBOContainer getOrCreateVBOContainer() { + if (this.vboContainer == null) { + this.vboContainer = new CableVBOContainer(); } - this.clientState.genState(this.outputs); - return this.clientState; + this.vboContainer.refresh(this.outputs); + return this.vboContainer; } @Override public void markRemoved() { - if (this.hasWorld() && this.world.isClient && this.clientState != null) { - this.clientState.dirty = true; - this.clientState.close(); + if (this.hasWorld() && this.world.isClient() && this.vboContainer != null) { + this.vboContainer.rebuild = true; + this.vboContainer.close(); } super.markRemoved(); } diff --git a/src/main/java/io/github/foundationgames/phonos/block/entity/ElectronicJukeboxBlockEntity.java b/src/main/java/io/github/foundationgames/phonos/block/entity/ElectronicJukeboxBlockEntity.java index d0f43c5..4c8698f 100644 --- a/src/main/java/io/github/foundationgames/phonos/block/entity/ElectronicJukeboxBlockEntity.java +++ b/src/main/java/io/github/foundationgames/phonos/block/entity/ElectronicJukeboxBlockEntity.java @@ -1,7 +1,7 @@ package io.github.foundationgames.phonos.block.entity; import io.github.foundationgames.phonos.block.PhonosBlocks; -import io.github.foundationgames.phonos.client.render.BlockEntityClientState; +import io.github.foundationgames.phonos.client.render.CableVBOContainer; import io.github.foundationgames.phonos.network.PayloadPackets; import io.github.foundationgames.phonos.sound.SoundStorage; import io.github.foundationgames.phonos.sound.emitter.SoundEmitterTree; @@ -47,17 +47,17 @@ public class ElectronicJukeboxBlockEntity extends JukeboxBlockEntity implements private final BlockEntityType<?> type; private @Nullable NbtCompound pendingNbt = null; private final long emitterId; - private BlockEntityClientState clientState; - private @Nullable SoundEmitterTree playingSound = null; + private CableVBOContainer vboContainer; + public ElectronicJukeboxBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) { super(pos, state); this.type = type; this.emitterId = UniqueId.ofBlock(pos); this.outputs = new BlockEntityOutputs(OUTPUT_LAYOUT, this); - this.clientState = null; + this.vboContainer = null; } public ElectronicJukeboxBlockEntity(BlockPos pos, BlockState state) { @@ -201,20 +201,29 @@ public BlockEntityOutputs getOutputs() { } @Override - public BlockEntityClientState getClientState() { - if (this.clientState == null) { - this.clientState = new BlockEntityClientState(); + public void enforceVBOState(boolean enabled) { + if (this.vboContainer != null && !enabled) { + this.vboContainer.close(); + + this.vboContainer = null; + } + } + + @Override + public CableVBOContainer getOrCreateVBOContainer() { + if (this.vboContainer == null) { + this.vboContainer = new CableVBOContainer(); } - this.clientState.genState(this.outputs); - return this.clientState; + this.vboContainer.refresh(this.outputs); + return this.vboContainer; } @Override public void markRemoved() { - if (this.hasWorld() && this.world.isClient && this.clientState != null) { - this.clientState.dirty = true; - this.clientState.close(); + if (this.hasWorld() && this.world.isClient() && this.vboContainer != null) { + this.vboContainer.rebuild = true; + this.vboContainer.close(); } super.markRemoved(); } diff --git a/src/main/java/io/github/foundationgames/phonos/client/render/BlockEntityClientState.java b/src/main/java/io/github/foundationgames/phonos/client/render/BlockEntityClientState.java deleted file mode 100644 index 71cdaa4..0000000 --- a/src/main/java/io/github/foundationgames/phonos/client/render/BlockEntityClientState.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.github.foundationgames.phonos.client.render; - -import io.github.foundationgames.phonos.world.sound.CableConnection; -import io.github.foundationgames.phonos.world.sound.block.BlockEntityOutputs; -import net.minecraft.client.gl.VertexBuffer; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; - -public class BlockEntityClientState { - public boolean dirty; - public @Nullable VertexBuffer buffer = null; - private List<CableConnection> cachedCons = new ArrayList<>(); - - public void genState(BlockEntityOutputs outs) { - List<CableConnection> connections = new ArrayList<>(); - outs.forEach((i, o) -> connections.add(o)); - - if (!cachedCons.equals(connections) || this.buffer == null) { - // Mark ourselves as needing re-rendering - dirty = true; - - // Close the existing buffer - if (buffer != null) { - buffer.close(); - } - - buffer = null; - cachedCons = connections; - } - } - - public void close() { - if (buffer != null) { - buffer.close(); - } - - buffer = null; - } -} diff --git a/src/main/java/io/github/foundationgames/phonos/client/render/CableRenderer.java b/src/main/java/io/github/foundationgames/phonos/client/render/CableRenderer.java index 0b4fc50..94f3d36 100644 --- a/src/main/java/io/github/foundationgames/phonos/client/render/CableRenderer.java +++ b/src/main/java/io/github/foundationgames/phonos/client/render/CableRenderer.java @@ -1,20 +1,21 @@ package io.github.foundationgames.phonos.client.render; -import com.mojang.blaze3d.systems.RenderSystem; import io.github.foundationgames.phonos.config.PhonosClientConfig; import io.github.foundationgames.phonos.util.PhonosUtil; import io.github.foundationgames.phonos.util.Pose3f; import io.github.foundationgames.phonos.world.sound.CableConnection; import io.github.foundationgames.phonos.world.sound.CablePlugPoint; -import net.minecraft.block.entity.BlockEntity; import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gl.VertexBuffer; import net.minecraft.client.model.Model; -import net.minecraft.client.render.*; +import net.minecraft.client.render.BufferBuilder; +import net.minecraft.client.render.Tessellator; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.WorldRenderer; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.MathHelper; import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; import org.joml.Quaternionf; import org.joml.Vector3f; import org.joml.Vector4f; @@ -66,127 +67,134 @@ private static void lerpCableEnd(Vector4f[] out, Vector4f[] begin, Vector4f[] en } } - public static void renderConnection(BlockEntityClientState clientState, BlockEntity e, PhonosClientConfig config, World world, CableConnection conn, MatrixStack matrices, VertexConsumer buffer, Model cableEndModel, int overlay, float tickDelta) { + public static void renderConnection(@Nullable CableVBOContainer vboContainer, PhonosClientConfig config, World world, CableConnection conn, + MatrixStack matrices, VertexConsumer immediate, Model cableEndModel, int overlay, float tickDelta) { int startLight, endLight; - // Connection points are always rendered immediate - + // Connection plug points are always rendered immediate matrices.push(); transformConnPoint(world, conn.start, matrices, cableStPt, tickDelta); lightPos.set(cableStPt.x, cableStPt.y, cableStPt.z); startLight = WorldRenderer.getLightmapCoordinates(world, lightPos); - cableEndModel.render(matrices, buffer, startLight, overlay, 1, 1, 1, 1); + cableEndModel.render(matrices, immediate, startLight, overlay, 1, 1, 1, 1); matrices.pop(); - matrices.push(); transformConnPoint(world, conn.end, matrices, cableEnPt, tickDelta); lightPos.set(cableEnPt.x, cableEnPt.y, cableEnPt.z); endLight = WorldRenderer.getLightmapCoordinates(world, lightPos); - cableEndModel.render(matrices, buffer, endLight, overlay, 1, 1, 1, 1); + cableEndModel.render(matrices, immediate, endLight, overlay, 1, 1, 1, 1); matrices.pop(); + // --- + + float length = cableStPt.distance(cableEnPt); + double detail = config.cableLODNearDetail; + int segments = Math.max((int) Math.ceil(4 * length * detail), 1); + + if (conn.isStatic() && vboContainer != null) { // Vbo can be used for this connection + if (vboContainer.rebuild) { + BufferBuilder buffer = Tessellator.getInstance().getBuffer(); + matrices = new MatrixStack(); - // Rendering for the first time? Generate VBOs now. - if (clientState.dirty) { - BufferBuilder builder = Tessellator.getInstance().getBuffer(); - - matrices = new MatrixStack(); - matrices.push(); - - float vOffset = conn.color != null ? 0.125f : 0; - float r = conn.color != null ? conn.color.getColorComponents()[0] : 1; - float g = conn.color != null ? conn.color.getColorComponents()[1] : 1; - float b = conn.color != null ? conn.color.getColorComponents()[2] : 1; - float length = cableStPt.distance(cableEnPt); - - double detail = config.cableLODNearDetail; - int segments = Math.max((int) Math.ceil(4 * length * detail), 1); - - // TODO: baking does not support LODs -// if (config.cableLODs) { -// float cx = (cableStPt.x + cableEnPt.x) * 0.5f; -// float cy = (cableStPt.y + cableEnPt.y) * 0.5f; -// float cz = (cableStPt.z + cableEnPt.z) * 0.5f; -// -// double sqDist = MinecraftClient.getInstance().gameRenderer.getCamera().getPos() -// .squaredDistanceTo(cx, cy, cz); -// double delta = MathHelper.clamp(sqDist / (length * length * 4), 0, 1); -// detail = MathHelper.lerp(delta, config.cableLODNearDetail, config.cableLODFarDetail); -// -// segments = Math.max((int) Math.ceil(4 * length * detail), Math.min(3, segments)); -// } - - final float texUWid = (float) (0.25 / detail); - - cableRotAxis.set(cableEnPt.z - cableStPt.z, 0, cableEnPt.x - cableStPt.x); - - float cablePitch = (float) Math.atan2(cableEnPt.y - cableStPt.y, cableRotAxis.length()); - float cableYaw = (float) Math.atan2(cableRotAxis.z, cableRotAxis.x); - cableRotAxis.normalize(); - - loadCableEnd(cableStart); - loadCableEnd(cableEnd); - cableNormal[0].set(-PhonosUtil.SQRT2DIV2, -PhonosUtil.SQRT2DIV2, 0); - cableNormal[1].set(PhonosUtil.SQRT2DIV2, -PhonosUtil.SQRT2DIV2, 0); - cableNormal[2].set(PhonosUtil.SQRT2DIV2, PhonosUtil.SQRT2DIV2, 0); - cableNormal[3].set(-PhonosUtil.SQRT2DIV2, PhonosUtil.SQRT2DIV2, 0); - - rotationCache.setAngleAxis(cablePitch, 1, 0, 0); - transformCableEnd(cableStart, vec -> vec.rotate(rotationCache)); - transformCableEnd(cableEnd, vec -> vec.rotate(rotationCache)); - transformCableNormal(cableNormal, vec -> vec.rotate(rotationCache)); - - rotationCache.setAngleAxis(Math.PI + cableYaw, 0, 1, 0); - transformCableEnd(cableStart, vec -> vec.rotate(rotationCache)); - transformCableEnd(cableEnd, vec -> vec.rotate(rotationCache)); - transformCableNormal(cableNormal, vec -> vec.rotate(rotationCache)); - - transformCableEnd(cableStart, vec -> vec.set(vec.x + cableStPt.x, vec.y + cableStPt.y, vec.z + cableStPt.z, 1)); - transformCableEnd(cableEnd, vec -> vec.set(vec.x + cableEnPt.x, vec.y + cableEnPt.y, vec.z + cableEnPt.z, 1)); - - transformCableNormal(cableNormal, matrices.peek().getNormalMatrix()::transform); - - for (int s = 0; s < segments; s++) { - float startDelta = (float) s / segments; - float endDelta = (float) (s + 1) / segments; - float startYOffset = length * 0.15f * (0.25f - (float) Math.pow(startDelta - 0.5, 2)); - float endYOffset = length * 0.15f * (0.25f - (float) Math.pow(endDelta - 0.5, 2)); - int segStartLight = PhonosUtil.lerpLight(startDelta, startLight, endLight); - int segEndLight = PhonosUtil.lerpLight(endDelta, startLight, endLight); - - lerpCableEnd(currCableStart, cableStart, cableEnd, startDelta); - lerpCableEnd(currCableEnd, cableStart, cableEnd, endDelta); - - transformCableEnd(currCableStart, vec -> vec.add(0, -startYOffset, 0, 0)); - transformCableEnd(currCableEnd, vec -> vec.add(0, -endYOffset, 0, 0)); - - transformCableEnd(currCableStart, matrices.peek().getPositionMatrix()::transform); - transformCableEnd(currCableEnd, matrices.peek().getPositionMatrix()::transform); - - for (int i = 0; i < 4; i++) { - float vOffset2 = i % 2 == 0 ? vOffset + 0.0625f : vOffset; - int next = (i + 1) % 4; - var nml = cableNormal[i]; - - builder.vertex(currCableStart[i].x, currCableStart[i].y, currCableStart[i].z).color(r, g, b, 1) - .texture(texUWid, 0.3125f + vOffset2).overlay(overlay).light(segStartLight).normal(nml.x, nml.y, nml.z).next(); - builder.vertex(currCableEnd[i].x, currCableEnd[i].y, currCableEnd[i].z).color(r, g, b, 1) - .texture(0, 0.3125f + vOffset2).overlay(overlay).light(segEndLight).normal(nml.x, nml.y, nml.z).next(); - builder.vertex(currCableEnd[next].x, currCableEnd[next].y, currCableEnd[next].z).color(r, g, b, 1) - .texture(0, 0.375f + vOffset2).overlay(overlay).light(segEndLight).normal(nml.x, nml.y, nml.z).next(); - builder.vertex(currCableStart[next].x, currCableStart[next].y, currCableStart[next].z).color(r, g, b, 1) - .texture(texUWid, 0.375f + vOffset2).overlay(overlay).light(segStartLight).normal(nml.x, nml.y, nml.z).next(); - } + buildCableGeometry(conn, matrices, buffer, segments, length, detail, startLight, endLight, overlay); + } + } else { // Connection must be rendered immediate + if (config.cableLODs) { + float cx = (cableStPt.x + cableEnPt.x) * 0.5f; + float cy = (cableStPt.y + cableEnPt.y) * 0.5f; + float cz = (cableStPt.z + cableEnPt.z) * 0.5f; + + double sqDist = MinecraftClient.getInstance().gameRenderer.getCamera().getPos() + .squaredDistanceTo(cx, cy, cz); + double delta = MathHelper.clamp(sqDist / (length * length * 4), 0, 1); + detail = MathHelper.lerp(delta, config.cableLODNearDetail, config.cableLODFarDetail); + + segments = Math.max((int) Math.ceil(4 * length * detail), Math.min(3, segments)); } - matrices.pop(); + buildCableGeometry(conn, matrices, immediate, segments, length, detail, startLight, endLight, overlay); } } + private static void buildCableGeometry(CableConnection conn, MatrixStack matrices, VertexConsumer buffer, int segments, + float length, double detail, int startLight, int endLight, int overlay) { + matrices.push(); + + float vOffset = conn.color != null ? 0.125f : 0; + float r = conn.color != null ? conn.color.getColorComponents()[0] : 1; + float g = conn.color != null ? conn.color.getColorComponents()[1] : 1; + float b = conn.color != null ? conn.color.getColorComponents()[2] : 1; + + final float texUWid = (float) (0.25 / detail); + + cableRotAxis.set(cableEnPt.z - cableStPt.z, 0, cableEnPt.x - cableStPt.x); + + float cablePitch = (float) Math.atan2(cableEnPt.y - cableStPt.y, cableRotAxis.length()); + float cableYaw = (float) Math.atan2(cableRotAxis.z, cableRotAxis.x); + cableRotAxis.normalize(); + + loadCableEnd(cableStart); + loadCableEnd(cableEnd); + cableNormal[0].set(-PhonosUtil.SQRT2DIV2, -PhonosUtil.SQRT2DIV2, 0); + cableNormal[1].set(PhonosUtil.SQRT2DIV2, -PhonosUtil.SQRT2DIV2, 0); + cableNormal[2].set(PhonosUtil.SQRT2DIV2, PhonosUtil.SQRT2DIV2, 0); + cableNormal[3].set(-PhonosUtil.SQRT2DIV2, PhonosUtil.SQRT2DIV2, 0); + + rotationCache.setAngleAxis(cablePitch, 1, 0, 0); + transformCableEnd(cableStart, vec -> vec.rotate(rotationCache)); + transformCableEnd(cableEnd, vec -> vec.rotate(rotationCache)); + transformCableNormal(cableNormal, vec -> vec.rotate(rotationCache)); + + rotationCache.setAngleAxis(Math.PI + cableYaw, 0, 1, 0); + transformCableEnd(cableStart, vec -> vec.rotate(rotationCache)); + transformCableEnd(cableEnd, vec -> vec.rotate(rotationCache)); + transformCableNormal(cableNormal, vec -> vec.rotate(rotationCache)); + + transformCableEnd(cableStart, vec -> vec.set(vec.x + cableStPt.x, vec.y + cableStPt.y, vec.z + cableStPt.z, 1)); + transformCableEnd(cableEnd, vec -> vec.set(vec.x + cableEnPt.x, vec.y + cableEnPt.y, vec.z + cableEnPt.z, 1)); + + transformCableNormal(cableNormal, matrices.peek().getNormalMatrix()::transform); + + for (int s = 0; s < segments; s++) { + float startDelta = (float) s / segments; + float endDelta = (float) (s + 1) / segments; + float startYOffset = length * 0.15f * (0.25f - (float) Math.pow(startDelta - 0.5, 2)); + float endYOffset = length * 0.15f * (0.25f - (float) Math.pow(endDelta - 0.5, 2)); + int segStartLight = PhonosUtil.lerpLight(startDelta, startLight, endLight); + int segEndLight = PhonosUtil.lerpLight(endDelta, startLight, endLight); + + lerpCableEnd(currCableStart, cableStart, cableEnd, startDelta); + lerpCableEnd(currCableEnd, cableStart, cableEnd, endDelta); + + transformCableEnd(currCableStart, vec -> vec.add(0, -startYOffset, 0, 0)); + transformCableEnd(currCableEnd, vec -> vec.add(0, -endYOffset, 0, 0)); + + transformCableEnd(currCableStart, matrices.peek().getPositionMatrix()::transform); + transformCableEnd(currCableEnd, matrices.peek().getPositionMatrix()::transform); + + for (int i = 0; i < 4; i++) { + float vOffset2 = i % 2 == 0 ? vOffset + 0.0625f : vOffset; + int next = (i + 1) % 4; + var nml = cableNormal[i]; + + buffer.vertex(currCableStart[i].x, currCableStart[i].y, currCableStart[i].z).color(r, g, b, 1) + .texture(texUWid, 0.3125f + vOffset2).overlay(overlay).light(segStartLight).normal(nml.x, nml.y, nml.z).next(); + buffer.vertex(currCableEnd[i].x, currCableEnd[i].y, currCableEnd[i].z).color(r, g, b, 1) + .texture(0, 0.3125f + vOffset2).overlay(overlay).light(segEndLight).normal(nml.x, nml.y, nml.z).next(); + buffer.vertex(currCableEnd[next].x, currCableEnd[next].y, currCableEnd[next].z).color(r, g, b, 1) + .texture(0, 0.375f + vOffset2).overlay(overlay).light(segEndLight).normal(nml.x, nml.y, nml.z).next(); + buffer.vertex(currCableStart[next].x, currCableStart[next].y, currCableStart[next].z).color(r, g, b, 1) + .texture(texUWid, 0.375f + vOffset2).overlay(overlay).light(segStartLight).normal(nml.x, nml.y, nml.z).next(); + } + } + + matrices.pop(); + } + private static final Pose3f tcp_originPose = new Pose3f(new Vector3f(), new Quaternionf()); private static final Pose3f tcp_endPose = new Pose3f(new Vector3f(), new Quaternionf()); diff --git a/src/main/java/io/github/foundationgames/phonos/client/render/CableVBOContainer.java b/src/main/java/io/github/foundationgames/phonos/client/render/CableVBOContainer.java new file mode 100644 index 0000000..bd22460 --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/client/render/CableVBOContainer.java @@ -0,0 +1,102 @@ +package io.github.foundationgames.phonos.client.render; + +import com.mojang.blaze3d.systems.RenderSystem; +import io.github.foundationgames.phonos.client.model.BasicModel; +import io.github.foundationgames.phonos.config.PhonosClientConfig; +import io.github.foundationgames.phonos.world.sound.CableConnection; +import io.github.foundationgames.phonos.world.sound.ConnectionCollection; +import net.minecraft.client.gl.VertexBuffer; +import net.minecraft.client.render.*; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class CableVBOContainer { + public boolean rebuild; + public @Nullable VertexBuffer buffer = null; + private List<CableConnection> cachedCons = new ArrayList<>(); + + public void refresh(ConnectionCollection conns) { + List<CableConnection> connections = new ArrayList<>(); + conns.forEach((i, conn) -> { + if (conn.isStatic()) { + connections.add(conn); + } + }); + + if (!cachedCons.equals(connections) || this.buffer == null) { + // Mark ourselves as needing re-rendering + rebuild = true; + + // Close the existing buffer + if (buffer != null) { + buffer.close(); + } + + buffer = null; + cachedCons = connections; + } + } + + public void render(MatrixStack matrices, VertexConsumer immediate, RenderLayer layer, BasicModel cableEndModel, + ConnectionCollection conns, PhonosClientConfig config, World world, int overlay, float tickDelta) { + boolean rebuild = this.buffer == null || this.rebuild; + + if (rebuild) { + // If we're re-rendering into the vertexbuffer, create a new VBO, + // grab the tessellator and start tessellating with our vertex format + var vbo = new VertexBuffer(VertexBuffer.Usage.STATIC); + BufferBuilder builder = Tessellator.getInstance().getBuffer(); + builder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE_OVERLAY_LIGHT_NORMAL); + + this.buffer = vbo; + } + + // Render each connection point in immediate mode, and render cables into the given vertex buffer + conns.forEach((i, conn) -> + CableRenderer.renderConnection(this, config, world, conn, matrices, immediate, cableEndModel, overlay, tickDelta)); + + var vbo = this.buffer; + + if (rebuild) { + // If we rerendered, upload the buffer to the GPU and mark ourselves as not dirty + vbo.bind(); + vbo.upload(Tessellator.getInstance().getBuffer().end()); + VertexBuffer.unbind(); + this.rebuild = false; + } + + + + // Set up the render state for this render phase (and texture) + layer.startDrawing(); + + // Grab fog and set to an extravagant value + // TODO: this is a total hack, but it's needed to convince the shader to not apply fog everywhere + float realEnd = RenderSystem.getShaderFogEnd(); + RenderSystem.setShaderFogEnd(9999999); + + matrices.push(); + // Render the buffer, which contains all the cables connected to this + vbo.bind(); + vbo.draw(matrices.peek().getPositionMatrix(), RenderSystem.getProjectionMatrix(), GameRenderer.getRenderTypeEntitySolidProgram()); + VertexBuffer.unbind(); + + matrices.pop(); + + // Reset the render state + RenderSystem.setShaderFogEnd(realEnd); + layer.endDrawing(); + } + + public void close() { + if (buffer != null) { + buffer.close(); + } + + buffer = null; + } +} diff --git a/src/main/java/io/github/foundationgames/phonos/client/render/block/CableOutputBlockEntityRenderer.java b/src/main/java/io/github/foundationgames/phonos/client/render/block/CableOutputBlockEntityRenderer.java index ddbc6b4..aac0c7c 100644 --- a/src/main/java/io/github/foundationgames/phonos/client/render/block/CableOutputBlockEntityRenderer.java +++ b/src/main/java/io/github/foundationgames/phonos/client/render/block/CableOutputBlockEntityRenderer.java @@ -1,22 +1,18 @@ package io.github.foundationgames.phonos.client.render.block; -import com.mojang.blaze3d.systems.RenderSystem; import io.github.foundationgames.phonos.Phonos; import io.github.foundationgames.phonos.PhonosClient; import io.github.foundationgames.phonos.client.model.BasicModel; -import io.github.foundationgames.phonos.client.render.BlockEntityClientState; import io.github.foundationgames.phonos.client.render.CableRenderer; +import io.github.foundationgames.phonos.client.render.CableVBOContainer; import io.github.foundationgames.phonos.config.PhonosClientConfig; import io.github.foundationgames.phonos.world.sound.block.OutputBlockEntity; import net.minecraft.block.entity.BlockEntity; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gl.VertexBuffer; -import net.minecraft.client.render.*; +import net.minecraft.client.render.VertexConsumerProvider; import net.minecraft.client.render.block.entity.BlockEntityRenderer; import net.minecraft.client.render.block.entity.BlockEntityRendererFactory; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.util.Identifier; -import net.minecraft.util.math.Vec3d; public class CableOutputBlockEntityRenderer<E extends BlockEntity & OutputBlockEntity> implements BlockEntityRenderer<E> { public static final Identifier TEXTURE = Phonos.id("textures/entity/audio_cable.png"); @@ -28,59 +24,25 @@ public CableOutputBlockEntityRenderer(BlockEntityRendererFactory.Context ctx) { @Override public void render(E entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) { - RenderLayer layer = cableEndModel.getLayer(TEXTURE); - var buffer = vertexConsumers.getBuffer(layer); + var renderLayer = cableEndModel.getLayer(TEXTURE); + var immediate = vertexConsumers.getBuffer(renderLayer); var config = PhonosClientConfig.get(); matrices.push(); - matrices.translate(-entity.getPos().getX(), -entity.getPos().getY(), -entity.getPos().getZ()); - - BlockEntityClientState clientState = entity.getClientState(); - boolean rerender = clientState.buffer == null || clientState.dirty; - - if (rerender) { - // If we're re-rendering into the vertexbuffer, create a new VBO, grab the tessellator and start tessellating with our vertex format - VertexBuffer vbo = new VertexBuffer(VertexBuffer.Usage.STATIC); - BufferBuilder builder = Tessellator.getInstance().getBuffer(); - builder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE_OVERLAY_LIGHT_NORMAL); - clientState.buffer = vbo; - } - - // Render each connection point in immediate mode, and render cables into the given vertex buffer - entity.getOutputs().forEach((i, conn) -> - CableRenderer.renderConnection(clientState, entity, config, entity.getWorld(), conn, matrices, buffer, cableEndModel, overlay, tickDelta)); + matrices.translate(-entity.getPos().getX(), -entity.getPos().getY(), -entity.getPos().getZ()); - VertexBuffer vbo = clientState.buffer; + boolean vbos = config.cableVBOs; + entity.enforceVBOState(vbos); - if (rerender) { - // If we rerendered, upload the buffer to the GPU and mark ourselves as not dirty - vbo.bind(); - vbo.upload(Tessellator.getInstance().getBuffer().end()); - VertexBuffer.unbind(); - clientState.dirty = false; + if (vbos) { + CableVBOContainer vboContainer = entity.getOrCreateVBOContainer(); + vboContainer.render(matrices, immediate, renderLayer, cableEndModel, entity.getOutputs(), config, entity.getWorld(), overlay, tickDelta); + } else { + entity.getOutputs().forEach((i, conn) -> + CableRenderer.renderConnection(null, config, entity.getWorld(), conn, matrices, immediate, cableEndModel, overlay, tickDelta)); } - // Setup the render state for this render phase (and texture) - layer.startDrawing(); - - // Grab fog and set to an extravagant value - // TODO: this is a total hack, but it's needed to convince the shader to not apply fog everywhere - float realEnd = RenderSystem.getShaderFogEnd(); - RenderSystem.setShaderFogEnd(9999999); - - matrices.push(); - // Render the buffer, which contains all the cables connected to this - vbo.bind(); - vbo.draw(matrices.peek().getPositionMatrix(), RenderSystem.getProjectionMatrix(), GameRenderer.getRenderTypeEntitySolidProgram()); - VertexBuffer.unbind(); - - matrices.pop(); - - // Reset the render state - RenderSystem.setShaderFogEnd(realEnd); - layer.endDrawing(); - matrices.pop(); } diff --git a/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfig.java b/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfig.java index c24d812..2a0f78b 100644 --- a/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfig.java +++ b/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfig.java @@ -16,6 +16,7 @@ public class PhonosClientConfig { public double streamVolume = 1; public boolean cableCulling = true; + public boolean cableVBOs = false; public boolean cableLODs = true; public double cableLODNearDetail = 1; public double cableLODFarDetail = 0.25; diff --git a/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfigScreen.java b/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfigScreen.java index 39ce7aa..77432f8 100644 --- a/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfigScreen.java +++ b/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfigScreen.java @@ -46,6 +46,7 @@ public static PhonosClientConfigScreen create(PhonosClientConfig config, Screen screen.addPercentage("phonosMasterVolume", val -> copy.phonosMasterVolume = val, copy.phonosMasterVolume); screen.addPercentage("streamVolume", val -> copy.streamVolume = val, copy.streamVolume); screen.addBoolean("cableCulling", val -> copy.cableCulling = val, copy.cableCulling); + screen.addBoolean("cableVBOs", val -> copy.cableVBOs = val, copy.cableVBOs); screen.addBoolean("cableLODs", val -> copy.cableLODs = val, copy.cableLODs); screen.addPercentage("cableLODNearDetail", val -> copy.cableLODNearDetail = val, copy.cableLODNearDetail); screen.addPercentage("cableLODFarDetail", val -> copy.cableLODFarDetail = val, copy.cableLODFarDetail); diff --git a/src/main/java/io/github/foundationgames/phonos/world/sound/CableConnection.java b/src/main/java/io/github/foundationgames/phonos/world/sound/CableConnection.java index f3b69b3..5d69b7c 100644 --- a/src/main/java/io/github/foundationgames/phonos/world/sound/CableConnection.java +++ b/src/main/java/io/github/foundationgames/phonos/world/sound/CableConnection.java @@ -29,6 +29,10 @@ public boolean shouldRemove(World world) { return false; //start.getPos(world).squaredDistanceTo(end.getPos(world)) > PhonosUtil.maxSquaredConnectionDistance(world); } + public boolean isStatic() { + return start.isStatic() && end.isStatic(); + } + public void writeNbt(NbtCompound nbt) { if (color != null) { nbt.putString("color", color.getName()); diff --git a/src/main/java/io/github/foundationgames/phonos/world/sound/CablePlugPoint.java b/src/main/java/io/github/foundationgames/phonos/world/sound/CablePlugPoint.java index 0846149..c0546d7 100644 --- a/src/main/java/io/github/foundationgames/phonos/world/sound/CablePlugPoint.java +++ b/src/main/java/io/github/foundationgames/phonos/world/sound/CablePlugPoint.java @@ -29,4 +29,10 @@ default Vec3d calculatePos(World world, double extend) { void writePlugPose(World world, float delta, Pose3f out); void writeOriginPose(World world, float delta, Pose3f out); + + boolean isStatic(); + + boolean equals(Object other); + + int hashCode(); } diff --git a/src/main/java/io/github/foundationgames/phonos/world/sound/ConnectionCollection.java b/src/main/java/io/github/foundationgames/phonos/world/sound/ConnectionCollection.java new file mode 100644 index 0000000..eb38602 --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/world/sound/ConnectionCollection.java @@ -0,0 +1,7 @@ +package io.github.foundationgames.phonos.world.sound; + +import java.util.function.BiConsumer; + +public interface ConnectionCollection { + void forEach(BiConsumer<Integer, CableConnection> action); +} diff --git a/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockConnectionLayout.java b/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockConnectionLayout.java index 88f894d..50c5057 100644 --- a/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockConnectionLayout.java +++ b/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockConnectionLayout.java @@ -120,6 +120,11 @@ public void writeOriginPose(World world, float delta, Pose3f out) { out.rotation().set(RotationAxis.POSITIVE_Y.rotation(0)); } + @Override + public boolean isStatic() { + return true; + } + @Override public boolean canPlugExist(World world) { if (world.isPosLoaded(this.blockPos.getX(), this.blockPos.getZ())) { @@ -239,6 +244,11 @@ public void writeOriginPose(World world, float delta, Pose3f out) { out.rotation().set(RotationAxis.POSITIVE_Y.rotation(0)); } + @Override + public boolean isStatic() { + return true; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockEntityOutputs.java b/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockEntityOutputs.java index 297b9c0..712292f 100644 --- a/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockEntityOutputs.java +++ b/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockEntityOutputs.java @@ -1,6 +1,7 @@ package io.github.foundationgames.phonos.world.sound.block; import io.github.foundationgames.phonos.world.sound.CableConnection; +import io.github.foundationgames.phonos.world.sound.ConnectionCollection; import io.github.foundationgames.phonos.world.sound.InputPlugPoint; import net.minecraft.block.entity.BlockEntity; import net.minecraft.entity.ItemEntity; @@ -16,7 +17,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; -public class BlockEntityOutputs { +public class BlockEntityOutputs implements ConnectionCollection { private final boolean[] skip; // Will not be purged on first tick of existing protected final CableConnection[] connections; protected final BlockConnectionLayout connectionLayout; @@ -86,6 +87,7 @@ public int getOutputCount() { return count; } + @Override public void forEach(BiConsumer<Integer, CableConnection> action) { for (int i = 0; i < connections.length; i++) { if (connections[i] != null) { diff --git a/src/main/java/io/github/foundationgames/phonos/world/sound/block/OutputBlockEntity.java b/src/main/java/io/github/foundationgames/phonos/world/sound/block/OutputBlockEntity.java index 630b0fd..4245d92 100644 --- a/src/main/java/io/github/foundationgames/phonos/world/sound/block/OutputBlockEntity.java +++ b/src/main/java/io/github/foundationgames/phonos/world/sound/block/OutputBlockEntity.java @@ -1,6 +1,6 @@ package io.github.foundationgames.phonos.world.sound.block; -import io.github.foundationgames.phonos.client.render.BlockEntityClientState; +import io.github.foundationgames.phonos.client.render.CableVBOContainer; import io.github.foundationgames.phonos.sound.emitter.SoundEmitter; import io.github.foundationgames.phonos.world.sound.InputPlugPoint; import net.minecraft.item.ItemStack; @@ -19,7 +19,9 @@ public interface OutputBlockEntity extends SoundEmitter { BlockEntityOutputs getOutputs(); - BlockEntityClientState getClientState(); + void enforceVBOState(boolean enabled); + + CableVBOContainer getOrCreateVBOContainer(); default Direction getRotation() { return Direction.NORTH; diff --git a/src/main/resources/assets/phonos/lang/en_us.json b/src/main/resources/assets/phonos/lang/en_us.json index efecc77..e369ad7 100644 --- a/src/main/resources/assets/phonos/lang/en_us.json +++ b/src/main/resources/assets/phonos/lang/en_us.json @@ -66,7 +66,8 @@ "text.config.phonos.option.phonosMasterVolume": "Loudspeaker Master Volume", "text.config.phonos.option.streamVolume": "Satellite Audio Volume", "text.config.phonos.option.cableCulling": "Cull Audio Cables not on-screen", - "text.config.phonos.option.cableLODs": "Use Detail Levels for Audio Cables", + "text.config.phonos.option.cableVBOs": "Render Audio Cables with VBOs", + "text.config.phonos.option.cableLODs": "Render Audio Cables with Detail Levels", "text.config.phonos.option.cableLODNearDetail": "Highest Audio Cable Detail Level", "text.config.phonos.option.cableLODFarDetail": "Lowest Audio Cable Detail Level",