From cbffbcaf48609340f9795f936071386b80e6319a Mon Sep 17 00:00:00 2001
From: Ruffled <105522716+RuffledPlume@users.noreply.github.com>
Date: Fri, 5 Jun 2026 21:10:27 +0100
Subject: [PATCH] Introduced RenderPass & Created ScenePass
Moved Directional Pass out of Zone & into RenderPass
Moved TiledLighting into a RenderPass
Added Zone Visibility Flags with Camera Culling IDs
SceneCullingManager WIP
DebugDrawPass Implemention
Revert TileInfoOverlay
Moved DebugDraw static functions into a DebugDraw Class
Debug Draw Text Support
WIP
Sync
Sync
Culling Improvements
Give MultiLoc Models the CullingSphere
Cleanup of RenderPasses by introducing RenderPipeline
Support Projection properly
Tweaks
Move Debug Drawing of culling behind a dev command
Fix Flixering Zone due to invalidation being swapped in
Only perform multi loc if zone is visible for the scene camera
Improvements hmmm, need to remove the execute base class
---
build.gradle | 1 -
src/main/java/rs117/hd/HdPlugin.java | 1 +
.../java/rs117/hd/opengl/GLPrimitives.java | 117 +++++
.../opengl/shader/DebugDrawShaderProgram.java | 49 ++
.../hd/opengl/uniforms/UniformBuffer.java | 10 +
.../rs117/hd/overlays/TileInfoOverlay.java | 6 +-
src/main/java/rs117/hd/overlays/Timer.java | 7 +
.../renderer/zone/ModelStreamingManager.java | 26 +-
.../rs117/hd/renderer/zone/SceneUploader.java | 47 ++
.../hd/renderer/zone/WorldViewContext.java | 75 ++-
.../java/rs117/hd/renderer/zone/Zone.java | 116 +++-
.../rs117/hd/renderer/zone/ZoneRenderer.java | 479 +++--------------
.../renderer/zone/passes/DebugDrawPass.java | 494 ++++++++++++++++++
.../zone/passes/DirectionalShadowPass.java | 312 +++++++++++
.../hd/renderer/zone/passes/RenderPass.java | 52 ++
.../renderer/zone/passes/RenderPassType.java | 20 +
.../renderer/zone/passes/RenderPipeline.java | 390 ++++++++++++++
.../hd/renderer/zone/passes/ScenePass.java | 234 +++++++++
.../zone/passes/TiledLightingPass.java | 63 +++
.../rs117/hd/scene/SceneCullingManager.java | 332 ++++++++++++
src/main/java/rs117/hd/utils/Camera.java | 28 +-
src/main/java/rs117/hd/utils/DebugDraw.java | 221 ++++++++
.../java/rs117/hd/utils/DeveloperTools.java | 11 +-
.../java/rs117/hd/utils/buffer/GLBuffer.java | 14 +
.../utils/collections/PrimitiveIntArray.java | 5 +
.../resources/rs117/hd/debug_draw_frag.glsl | 27 +
.../resources/rs117/hd/debug_draw_vert.glsl | 133 +++++
src/main/resources/rs117/hd/utils/font.glsl | 208 ++++++++
28 files changed, 3035 insertions(+), 443 deletions(-)
create mode 100644 src/main/java/rs117/hd/opengl/GLPrimitives.java
create mode 100644 src/main/java/rs117/hd/opengl/shader/DebugDrawShaderProgram.java
create mode 100644 src/main/java/rs117/hd/renderer/zone/passes/DebugDrawPass.java
create mode 100644 src/main/java/rs117/hd/renderer/zone/passes/DirectionalShadowPass.java
create mode 100644 src/main/java/rs117/hd/renderer/zone/passes/RenderPass.java
create mode 100644 src/main/java/rs117/hd/renderer/zone/passes/RenderPassType.java
create mode 100644 src/main/java/rs117/hd/renderer/zone/passes/RenderPipeline.java
create mode 100644 src/main/java/rs117/hd/renderer/zone/passes/ScenePass.java
create mode 100644 src/main/java/rs117/hd/renderer/zone/passes/TiledLightingPass.java
create mode 100644 src/main/java/rs117/hd/scene/SceneCullingManager.java
create mode 100644 src/main/java/rs117/hd/utils/DebugDraw.java
create mode 100644 src/main/resources/rs117/hd/debug_draw_frag.glsl
create mode 100644 src/main/resources/rs117/hd/debug_draw_vert.glsl
create mode 100644 src/main/resources/rs117/hd/utils/font.glsl
diff --git a/build.gradle b/build.gradle
index 7ab7c4f2ac..518b8bd277 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,7 +3,6 @@ plugins {
}
repositories {
- mavenLocal()
maven {
url = 'https://repo.runelite.net'
content {
diff --git a/src/main/java/rs117/hd/HdPlugin.java b/src/main/java/rs117/hd/HdPlugin.java
index 6cdf8b6741..7bd830ae82 100644
--- a/src/main/java/rs117/hd/HdPlugin.java
+++ b/src/main/java/rs117/hd/HdPlugin.java
@@ -433,6 +433,7 @@ public class HdPlugin extends Plugin {
public boolean enableFreezeFrame;
public boolean orthographicProjection;
public boolean freezeCulling;
+ public boolean showCulling;
@Getter
private boolean isPluginStopPending;
diff --git a/src/main/java/rs117/hd/opengl/GLPrimitives.java b/src/main/java/rs117/hd/opengl/GLPrimitives.java
new file mode 100644
index 0000000000..b25c739237
--- /dev/null
+++ b/src/main/java/rs117/hd/opengl/GLPrimitives.java
@@ -0,0 +1,117 @@
+package rs117.hd.opengl;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import lombok.Value;
+import org.lwjgl.system.MemoryStack;
+import rs117.hd.utils.buffer.GLBuffer;
+
+import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
+import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;
+
+public class GLPrimitives {
+
+ @Value
+ public static class Mesh {
+ GLBuffer vbo;
+ GLBuffer ebo;
+ int indexCount;
+
+ public void destroy() {
+ vbo.destroy();
+ ebo.destroy();
+ }
+ }
+
+ public static Mesh buildCube(MemoryStack stack) {
+ FloatBuffer vertices = stack.mallocFloat(24).put(new float[]{
+ -1,-1,-1, 1,-1,-1, 1, 1,-1, -1, 1,-1,
+ -1,-1, 1, 1,-1, 1, 1, 1, 1, -1, 1, 1
+ }).flip();
+
+ IntBuffer indices = stack.mallocInt(36).put(new int[]{
+ 0, 1, 2, 0, 2, 3,
+ 4, 6, 5, 4, 7, 6,
+ 0, 3, 7, 0, 7, 4,
+ 1, 5, 6, 1, 6, 2,
+ 0, 4, 5, 0, 5, 1,
+ 3, 2, 6, 3, 6, 7
+ }).flip();
+
+ return new Mesh(
+ new GLBuffer("VBO::Cube", GL_ARRAY_BUFFER, GL_STATIC_DRAW).initialize(vertices),
+ new GLBuffer.EBO("EBO::Cube", GL_STATIC_DRAW).initialize(indices),
+ 36
+ );
+ }
+
+ public static Mesh buildSphere(MemoryStack stack, int stacks, int slices) {
+ FloatBuffer vertices = stack.mallocFloat((stacks + 1) * (slices + 1) * 3);
+ for (int s = 0; s <= stacks; s++) {
+ float phi = (float) (Math.PI * s / stacks);
+ for (int sl = 0; sl <= slices; sl++) {
+ float theta = (float) (2 * Math.PI * sl / slices);
+ vertices.put((float) (Math.sin(phi) * Math.cos(theta)));
+ vertices.put((float) Math.cos(phi));
+ vertices.put((float) (Math.sin(phi) * Math.sin(theta)));
+ }
+ }
+ vertices.flip();
+
+ int indexCount = stacks * slices * 6;
+ IntBuffer indices = stack.mallocInt(indexCount);
+ for (int s = 0; s < stacks; s++) {
+ for (int sl = 0; sl < slices; sl++) {
+ int cur = s * (slices + 1) + sl;
+ int next = cur + (slices + 1);
+ indices.put(cur ).put(next ).put(cur + 1);
+ indices.put(cur + 1).put(next ).put(next + 1);
+ }
+ }
+ indices.flip();
+
+ return new Mesh(
+ new GLBuffer("VBO::Sphere", GL_ARRAY_BUFFER, GL_STATIC_DRAW).initialize(vertices),
+ new GLBuffer.EBO("EBO::Sphere", GL_STATIC_DRAW).initialize(indices),
+ indexCount
+ );
+ }
+
+ public static Mesh buildLine(MemoryStack stack) {
+ FloatBuffer vertices = stack.mallocFloat(24).put(new float[]{
+ -1,-1, 0, 1,-1, 0, 1, 1, 0, -1, 1, 0,
+ 0,-1,-1, 0,-1, 1, 0, 1, 1, 0, 1,-1
+ }).flip();
+
+ IntBuffer indices = stack.mallocInt(12).put(new int[]{
+ 0, 1, 2, 0, 2, 3,
+ 4, 5, 6, 4, 6, 7
+ }).flip();
+
+ return new Mesh(
+ new GLBuffer("VBO::Line", GL_ARRAY_BUFFER, GL_STATIC_DRAW).initialize(vertices),
+ new GLBuffer.EBO("EBO::Line", GL_STATIC_DRAW).initialize(indices),
+ 12
+ );
+ }
+
+ public static Mesh buildQuad(MemoryStack stack) {
+ FloatBuffer vertices = stack.mallocFloat(12).put(new float[]{
+ 0, 0, 0,
+ 1, 0, 0,
+ 1, 1, 0,
+ 0, 1, 0
+ }).flip();
+
+ IntBuffer indices = stack.mallocInt(6).put(new int[]{
+ 0, 1, 2,
+ 0, 2, 3
+ }).flip();
+
+ return new Mesh(
+ new GLBuffer("VBO::Quad", GL_ARRAY_BUFFER, GL_STATIC_DRAW).initialize(vertices),
+ new GLBuffer.EBO("EBO::Quad", GL_STATIC_DRAW).initialize(indices),
+ 6
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/rs117/hd/opengl/shader/DebugDrawShaderProgram.java b/src/main/java/rs117/hd/opengl/shader/DebugDrawShaderProgram.java
new file mode 100644
index 0000000000..761f3ce581
--- /dev/null
+++ b/src/main/java/rs117/hd/opengl/shader/DebugDrawShaderProgram.java
@@ -0,0 +1,49 @@
+package rs117.hd.opengl.shader;
+
+import java.io.IOException;
+import rs117.hd.renderer.zone.passes.DebugDrawPass.PrimitiveDrawType;
+
+import static org.lwjgl.opengl.GL20C.GL_FRAGMENT_SHADER;
+import static org.lwjgl.opengl.GL20C.GL_VERTEX_SHADER;
+
+public abstract class DebugDrawShaderProgram extends ShaderProgram {
+ private final PrimitiveDrawType type;
+
+ public DebugDrawShaderProgram(PrimitiveDrawType type) {
+ super(t -> t
+ .add(GL_VERTEX_SHADER, "debug_draw_vert.glsl")
+ .add(GL_FRAGMENT_SHADER, "debug_draw_frag.glsl"));
+ this.type = type;
+ }
+
+ @Override
+ public void compile(ShaderIncludes includes) throws ShaderException, IOException {
+ super.compile(includes.copy().define("PRIMITIVE_TYPE", type.ordinal()));
+ }
+
+ public static class DebugDrawCubeShaderProgram extends DebugDrawShaderProgram {
+ public DebugDrawCubeShaderProgram() {
+ super(PrimitiveDrawType.AABB);
+ }
+ }
+
+ public static class DebugDrawSphereShaderProgram extends DebugDrawShaderProgram {
+ public DebugDrawSphereShaderProgram() {
+ super(PrimitiveDrawType.SPHERE);
+ }
+ }
+
+ public static class DebugDrawLineShaderProgram extends DebugDrawShaderProgram {
+ public DebugDrawLineShaderProgram() {
+ super(PrimitiveDrawType.LINE);
+ }
+ }
+
+ public static class DebugDrawTextShaderProgram extends DebugDrawShaderProgram {
+ public final Uniform1f uniCharScale = addUniform1f("charScale");
+
+ public DebugDrawTextShaderProgram() {
+ super(PrimitiveDrawType.TEXT);
+ }
+ }
+}
diff --git a/src/main/java/rs117/hd/opengl/uniforms/UniformBuffer.java b/src/main/java/rs117/hd/opengl/uniforms/UniformBuffer.java
index 0d1ca3104f..7bd14987ca 100644
--- a/src/main/java/rs117/hd/opengl/uniforms/UniformBuffer.java
+++ b/src/main/java/rs117/hd/opengl/uniforms/UniformBuffer.java
@@ -315,6 +315,16 @@ private void markWaterLine(int position, int size) {
dirtyHighTide = max(dirtyHighTide, position + size);
}
+ protected void setSize(int size) {
+ assert properties.isEmpty() : "Uniform buffer size can only be set, if your not using addStruct() or addProperty()!";
+ this.size = size;
+ }
+
+ public void write(int position, int x) {
+ dataInt.put(x);
+ markWaterLine(position, 4);
+ }
+
public void initialize() {
if (data != null)
destroy();
diff --git a/src/main/java/rs117/hd/overlays/TileInfoOverlay.java b/src/main/java/rs117/hd/overlays/TileInfoOverlay.java
index 64a2eeb51d..5436a4502f 100644
--- a/src/main/java/rs117/hd/overlays/TileInfoOverlay.java
+++ b/src/main/java/rs117/hd/overlays/TileInfoOverlay.java
@@ -948,7 +948,7 @@ private String getModelInfo(Renderable r) {
case MODE_TILE_INFO:
return isStatic ? "
static" :
isDynamic ? " dynamic" :
- " maybe dynamic";
+ " maybe dynamic";
case MODE_MODEL_INFO:
int[] faceColors = model.getFaceColors1();
byte[] faceTransparencies = model.getFaceTransparencies();
@@ -1046,9 +1046,9 @@ private static int getHeight(SceneContext ctx, int localX, int localY, int plane
int x = localX & (LOCAL_TILE_SIZE - 1);
int y = localY & (LOCAL_TILE_SIZE - 1);
int var8 = x * tileHeights[plane][sceneExX + 1][sceneExY] +
- (LOCAL_TILE_SIZE - x) * tileHeights[plane][sceneExX][sceneExY] >> LOCAL_COORD_BITS;
+ (LOCAL_TILE_SIZE - x) * tileHeights[plane][sceneExX][sceneExY] >> LOCAL_COORD_BITS;
int var9 = x * tileHeights[plane][sceneExX + 1][sceneExY + 1] +
- (LOCAL_TILE_SIZE - x) * tileHeights[plane][sceneExX][sceneExY + 1] >> LOCAL_COORD_BITS;
+ (LOCAL_TILE_SIZE - x) * tileHeights[plane][sceneExX][sceneExY + 1] >> LOCAL_COORD_BITS;
return y * var9 + (LOCAL_TILE_SIZE - y) * var8 >> 7;
}
diff --git a/src/main/java/rs117/hd/overlays/Timer.java b/src/main/java/rs117/hd/overlays/Timer.java
index 5f998a8651..fd4ace97c6 100644
--- a/src/main/java/rs117/hd/overlays/Timer.java
+++ b/src/main/java/rs117/hd/overlays/Timer.java
@@ -26,6 +26,12 @@ public enum Timer {
DRAW_TILED_LIGHTING,
DRAW_SUBMIT,
+ // RENDER_PASSES
+ TILED_LIGHTING_PASS,
+ DIRECTIONAL_PASS,
+ SCENE_PASS,
+ DEBUG_DRAW_PASS,
+
// Miscellaneous
SWAP_BUFFERS,
EXECUTE_COMMAND_BUFFER,
@@ -71,6 +77,7 @@ public enum Timer {
RENDER_SHADOWS(GPU_TIMER),
RENDER_SCENE(GPU_TIMER),
RENDER_UI(GPU_TIMER, "Render UI"),
+ RENDER_DEBUG_DRAW(GPU_TIMER),
;
public static final Timer[] TIMERS = values();
diff --git a/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java b/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java
index c85fd35e46..da11d51763 100644
--- a/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java
+++ b/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java
@@ -18,7 +18,10 @@
import rs117.hd.config.ShadowMode;
import rs117.hd.overlays.FrameTimer;
import rs117.hd.overlays.Timer;
+import rs117.hd.renderer.zone.passes.RenderPass;
+import rs117.hd.renderer.zone.passes.RenderPipeline;
import rs117.hd.scene.ModelOverrideManager;
+import rs117.hd.scene.SceneCullingManager;
import rs117.hd.scene.model_overrides.ModelOverride;
import rs117.hd.utils.HDUtils;
import rs117.hd.utils.ModelHash;
@@ -71,6 +74,9 @@ public class ModelStreamingManager {
@Inject
private ZoneRenderer renderer;
+ @Inject
+ private RenderPipeline renderPipeline;
+
private final ArrayList pending = new ArrayList<>();
private final StreamingContext[] streamingContexts = new StreamingContext[RL_RENDER_THREADS + 1];
private int numRenderThreads = -1;
@@ -213,23 +219,18 @@ public void drawTemp(
final int modelClassification = renderer.sceneCamera.classifySphere(
objectWorldPos[0], objectWorldPos[1], objectWorldPos[2], m.getRadius());
- boolean isOffScreen = modelClassification == -1;
+ boolean isOnScreen = modelClassification != -1;
// Additional Culling checks to help reduce dynamic object perf impact when off-screen
- if (isOffScreen && (
- !modelOverride.castShadows ||
- !renderer.directionalShadowCasterVolume.intersectsPoint(
- (int) objectWorldPos[0],
- (int) objectWorldPos[1],
- (int) objectWorldPos[2]
- )
- )) {
- return;
+ if (!isOnScreen) {
+ isOnScreen = renderPipeline.dynamicInFrustum.execute(ctx, r, m, modelOverride, x, y, z);
+ if(!isOnScreen)
+ return;
}
streamingContext.renderableCount++;
final boolean hasAlpha =
(m.getFaceTransparencies() != null || modelOverride.mightHaveTransparency) &&
- (!sceneManager.isRoot(ctx) || zone.inSceneFrustum);
+ (!sceneManager.isRoot(ctx) || zone.isVisible(renderer.sceneCamera));
final Zone.AlphaModel alphaModel = hasAlpha ?
zone.requestTempAlphaModel(
modelOverride,
@@ -372,8 +373,7 @@ public void uploadTempModel(
if (culledFaces.length > 0 &&
modelOverride.castShadows &&
- plugin.configShadowMode != ShadowMode.OFF &&
- (!sceneManager.isRoot(ctx) || zone != null && zone.inShadowFrustum)
+ plugin.configShadowMode != ShadowMode.OFF
) {
final DynamicModelVAO.View shadowView = ctx.beginDraw(VAO_SHADOW, culledFaces.length);
sceneUploader.uploadTempModel(
diff --git a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java
index 462b16c5b4..17f34bdbac 100644
--- a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java
+++ b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java
@@ -35,6 +35,7 @@
import rs117.hd.scene.MaterialManager;
import rs117.hd.scene.ModelOverrideManager;
import rs117.hd.scene.ProceduralGenerator;
+import rs117.hd.scene.SceneCullingManager;
import rs117.hd.scene.areas.Area;
import rs117.hd.scene.ground_materials.GroundMaterial;
import rs117.hd.scene.materials.Material;
@@ -111,6 +112,9 @@ public class SceneUploader implements AutoCloseable {
@Inject
private ProceduralGenerator proceduralGenerator;
+ @Inject
+ private SceneCullingManager sceneCullingManager;
+
@FunctionalInterface
public interface OnBeforeProcessTileFunc {
void invoke(Tile t, boolean isEstimate) throws InterruptedException;
@@ -134,6 +138,8 @@ public interface OnBeforeProcessTileFunc {
private final float[] modelUvs = new float[12];
private final int[] modelNormals = new int[9];
private final short[][] tileNormals = new short[4][3];
+ private final int[][] levelMinAABB = new int[Zone.LEVEL_COUNT][3];
+ private final int[][] levelMaxAABB = new int[Zone.LEVEL_COUNT][3];
private int[] modelVertices;
public int tempModelAlphaFaces = 0;
@@ -224,6 +230,11 @@ public void uploadZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) throws
}
}
+ for(int i = 0; i < Zone.LEVEL_COUNT; i++) {
+ Arrays.fill(levelMinAABB[i], Integer.MAX_VALUE);
+ Arrays.fill(levelMaxAABB[i], Integer.MIN_VALUE);
+ }
+
zone.rids = new int[4][roofIds.length];
zone.roofStart = new int[4][roofIds.length];
zone.roofEnd = new int[4][roofIds.length];
@@ -255,6 +266,13 @@ public void uploadZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) throws
uploadZoneGapFillers(ctx, zone, mzx, mzz, vb, fb);
zone.levelOffsets[Zone.LEVEL_GAP_FILLER] = vb.position();
}
+
+ for(int i = 0; i < Zone.LEVEL_COUNT; i++) {
+ final int[] minAABB = levelMinAABB[i];
+ final int[] maxAABB = levelMaxAABB[i];
+ if(minAABB[0] < maxAABB[0] && minAABB[1] < maxAABB[1] && minAABB[2] < maxAABB[2])
+ zone.levelCullingResults[i] = sceneCullingManager.obtainBox(minAABB[0], minAABB[1], minAABB[2], maxAABB[0], maxAABB[1], maxAABB[2]);
+ }
}
private void uploadZoneLevel(
@@ -354,6 +372,7 @@ private void uploadZoneWater(
this.basez = (mzz - (ctx.sceneOffset >> 3)) << 10;
for (int level = 0; level < MAX_Z; level++) {
+ this.level = level;
for (int xoff = 0; xoff < CHUNK_SIZE; ++xoff) {
for (int zoff = 0; zoff < CHUNK_SIZE; ++zoff) {
final int msx = (mzx << 3) + xoff;
@@ -806,6 +825,7 @@ private void uploadZoneRenderable(
try {
zone.addAlphaModel(
plugin,
+ sceneCullingManager,
materialManager,
zone.glVaoA,
zone.tboF.getTexId(),
@@ -833,6 +853,18 @@ private void uploadZoneRenderable(
}
}
+ private void encapsulatePoint(int x, int y, int z) {
+ final int[] minAABB = levelMinAABB[level];
+ final int[] maxAABB = levelMaxAABB[level];
+ minAABB[0] = min(minAABB[0], x);
+ minAABB[1] = min(minAABB[1], y);
+ minAABB[2] = min(minAABB[2], z);
+
+ maxAABB[0] = max(maxAABB[0], x);
+ maxAABB[1] = max(maxAABB[1], y);
+ maxAABB[2] = max(maxAABB[2], z);
+ }
+
@SuppressWarnings({ "UnnecessaryLocalVariable" })
private void uploadTilePaint(
ZoneSceneContext ctx,
@@ -1043,12 +1075,14 @@ private void uploadTilePaint(
final var vb = writeCache.getVertexBuffer();
final var tb = writeCache.getTextureBuffer();
+
int texturedFaceIdx = tb.putFace(
neColor, nwColor, seColor,
neMaterialData, nwMaterialData, seMaterialData,
neTerrainData, nwTerrainData, seTerrainData
);
+ encapsulatePoint(lx2, neHeight, lz2);
vb.putStaticVertex(
lx2, neHeight, lz2,
uvx, uvy, 0,
@@ -1056,6 +1090,7 @@ private void uploadTilePaint(
texturedFaceIdx
);
+ encapsulatePoint(lx3, nwHeight, lz3);
vb.putStaticVertex(
lx3, nwHeight, lz3,
uvx - uvcos, uvy - uvsin, 0,
@@ -1063,6 +1098,7 @@ private void uploadTilePaint(
texturedFaceIdx
);
+ encapsulatePoint(lx1, seHeight, lz1);
vb.putStaticVertex(
lx1, seHeight, lz1,
uvx + uvsin, uvy - uvcos, 0,
@@ -1076,6 +1112,7 @@ private void uploadTilePaint(
swTerrainData, seTerrainData, nwTerrainData
);
+ encapsulatePoint(lx0, swHeight, lz0);
vb.putStaticVertex(
lx0, swHeight, lz0,
uvx - uvcos + uvsin, uvy - uvsin - uvcos, 0,
@@ -1083,6 +1120,7 @@ private void uploadTilePaint(
texturedFaceIdx
);
+ encapsulatePoint(lx1, seHeight, lz1);
vb.putStaticVertex(
lx1, seHeight, lz1,
uvx + uvsin, uvy - uvcos, 0,
@@ -1090,6 +1128,7 @@ private void uploadTilePaint(
texturedFaceIdx
);
+ encapsulatePoint(lx3, nwHeight, lz3);
vb.putStaticVertex(
lx3, nwHeight, lz3,
uvx - uvcos, uvy - uvsin, 0,
@@ -1367,6 +1406,7 @@ private void uploadTileModel(
terrainDataA, terrainDataB, terrainDataC
);
+ encapsulatePoint(lx0, ly0, lz0);
vb.putStaticVertex(
lx0, ly0, lz0,
uvAx, uvAy, 0,
@@ -1374,6 +1414,7 @@ private void uploadTileModel(
texturedFaceIdx
);
+ encapsulatePoint(lx1, ly1, lz1);
vb.putStaticVertex(
lx1, ly1, lz1,
uvBx, uvBy, 0,
@@ -1381,6 +1422,7 @@ private void uploadTileModel(
texturedFaceIdx
);
+ encapsulatePoint(lx2, ly2, lz2);
vb.putStaticVertex(
lx2, ly2, lz2,
uvCx, uvCy, 0,
@@ -1721,6 +1763,7 @@ private int uploadStaticModel(
0, 0, 0
);
+ encapsulatePoint(vx1, vy1, vz1);
vb.putStaticVertex(
vx1, vy1, vz1,
faceUVs[0], faceUVs[1], faceUVs[2],
@@ -1728,6 +1771,7 @@ private int uploadStaticModel(
texturedFaceIdx
);
+ encapsulatePoint(vx2, vy2, vz2);
vb.putStaticVertex(
vx2, vy2, vz2,
faceUVs[4], faceUVs[5], faceUVs[6],
@@ -1735,6 +1779,7 @@ private int uploadStaticModel(
texturedFaceIdx
);
+ encapsulatePoint(vx3, vy3, vz3);
vb.putStaticVertex(
vx3, vy3, vz3,
faceUVs[8], faceUVs[9], faceUVs[10],
@@ -2227,6 +2272,8 @@ public void uploadZoneGapFillers(
int sceneMax = SCENE_SIZE + ctx.expandedMapLoadingChunks * CHUNK_SIZE;
Tile[][][] extendedTiles = ctx.scene.getExtendedTiles();
+ level = 0;
+
int posBefore = vb.position();
for (int xoff = 0; xoff < CHUNK_SIZE; ++xoff) {
for (int zoff = 0; zoff < CHUNK_SIZE; ++zoff) {
diff --git a/src/main/java/rs117/hd/renderer/zone/WorldViewContext.java b/src/main/java/rs117/hd/renderer/zone/WorldViewContext.java
index 20a8ffd81d..69db9caf1f 100644
--- a/src/main/java/rs117/hd/renderer/zone/WorldViewContext.java
+++ b/src/main/java/rs117/hd/renderer/zone/WorldViewContext.java
@@ -15,6 +15,7 @@
import rs117.hd.HdPlugin;
import rs117.hd.opengl.uniforms.UBOWorldViews;
import rs117.hd.opengl.uniforms.UBOWorldViews.WorldViewStruct;
+import rs117.hd.scene.SceneCullingManager;
import rs117.hd.utils.Camera;
import rs117.hd.utils.CommandBuffer;
import rs117.hd.utils.DestructibleHandler;
@@ -26,6 +27,7 @@
import static rs117.hd.renderer.zone.DynamicModelVAO.METADATA_SIZE;
import static rs117.hd.renderer.zone.SceneManager.NUM_ZONES;
import static rs117.hd.renderer.zone.ZoneRenderer.FRAMES_IN_FLIGHT;
+import static rs117.hd.utils.MathUtils.*;
import static rs117.hd.utils.collections.Util.quickSort;
@Slf4j
@@ -54,11 +56,14 @@ public class WorldViewContext {
@Inject
private SceneManager sceneManager;
+ @Inject
+ private SceneCullingManager sceneCullingManager;
+
final int worldViewId;
final int sizeX, sizeZ;
@Nullable
- WorldViewStruct uboWorldViewStruct;
- ZoneSceneContext sceneContext;
+ public WorldViewStruct uboWorldViewStruct;
+ public ZoneSceneContext sceneContext;
Zone[][] zones;
GLBuffer vboM;
boolean isLoading = true;
@@ -69,8 +74,8 @@ public class WorldViewContext {
private final Comparator alphaSortComparator = Comparator.comparingInt((Zone z) -> z.dist).reversed();
private final List alphaZones = new ArrayList<>();
- CommandBuffer vaoSceneCmd;
- CommandBuffer vaoDirectionalCmd;
+ public CommandBuffer vaoSceneCmd;
+ public CommandBuffer vaoDirectionalCmd;
final DynamicModelVAO[][] dynamicModelVaos = new DynamicModelVAO[FRAMES_IN_FLIGHT][VAO_COUNT];
public long loadTime;
@@ -134,11 +139,6 @@ void initBuffers() {
log.trace("WorldViewContext - WorldViewId: {} initBuffers took {}ms", worldViewId, (System.nanoTime() - start) / 1000000);
}
- void map() {
- for (int i = 0; i < VAO_COUNT; i++)
- dynamicModelVaos[plugin.frame % FRAMES_IN_FLIGHT][i].map();
- }
-
DynamicModelVAO.View beginDraw(int type, int faces) {
return dynamicModelVaos[plugin.frame % FRAMES_IN_FLIGHT][type].beginDraw(faces);
}
@@ -151,7 +151,7 @@ int obtainDrawIndex(int type) {
return dynamicModelVaos[plugin.frame % FRAMES_IN_FLIGHT][type].obtainDrawIndex();
}
- void drawAll(int type, CommandBuffer cmd) {
+ public void drawAll(int type, CommandBuffer cmd) {
dynamicModelVaos[plugin.frame % FRAMES_IN_FLIGHT][type].draw(cmd);
}
@@ -171,7 +171,7 @@ void sortStaticAlphaModels(Camera camera) {
for (int zx = 0; zx < sizeX; zx++) {
for (int zz = 0; zz < sizeZ; zz++) {
final Zone z = zones[zx][zz];
- if (z.alphaModels.isEmpty() || (worldViewId == WorldView.TOPLEVEL && !z.inSceneFrustum))
+ if (z.alphaModels.isEmpty() || (worldViewId == WorldView.TOPLEVEL && !z.isVisible(camera)))
continue;
final int dx = camPosX - ((zx - offset) << 10);
@@ -213,11 +213,8 @@ void handleZoneSwap(int zx, int zz, boolean queue) {
zones[zx][zz] = curZone = uploadTask.zone;
clientThread.invoke(curZone::unmap);
- if (prevZone != curZone) {
- curZone.inSceneFrustum = prevZone.inSceneFrustum;
- curZone.inShadowFrustum = prevZone.inShadowFrustum;
+ if (prevZone != curZone)
DestructibleHandler.queueDestruction(prevZone);
- }
sceneContext.animatedDynamicObjectIds.addAll(curZone.animatedDynamicObjectIds);
} else if (uploadTask.wasCancelled() && !curZone.cull) {
@@ -258,6 +255,54 @@ void processZoneRebuilds() {
}
}
+ void preSceneDraw(Camera camera) {
+ completeInvalidation();
+
+ for (int zx = 0; zx < sizeX; ++zx)
+ for (int zz = 0; zz < sizeZ; ++zz)
+ zones[zx][zz].queueVisibility(this, zx, zz);
+ sceneCullingManager.flush();
+
+ int offset = sceneContext.sceneOffset >> 3;
+ for (int zx = 0; zx < sizeX; ++zx) {
+ for (int zz = 0; zz < sizeZ; ++zz) {
+ final Zone z = zones[zx][zz];
+ z.resolveVisibility();
+
+ if(z.isVisible(camera))
+ z.multizoneLocs(sceneContext, zx - offset, zz - offset, camera, zones);
+ }
+ }
+
+ sortStaticAlphaModels(camera);
+
+ for (int i = 0; i < VAO_COUNT; i++)
+ dynamicModelVaos[plugin.frame % FRAMES_IN_FLIGHT][i].map();
+ }
+
+ void debugDraw(Camera camera) {
+ int offset = sceneContext.sceneOffset >> 3;
+ int startX = clamp(((int)camera.getPositionX() >> 10) + offset, 0, sizeX - 1);
+ int startZ = clamp(((int)camera.getPositionZ() >> 10) + offset, 0, sizeZ - 1);
+
+ int drawRange = 2;
+ for(int x = -drawRange; x < drawRange; x++) {
+ int zx = startX + x;
+ if(zx < 0 || zx >= sizeX)
+ continue;
+
+ for(int z = -drawRange; z < drawRange; z++) {
+ int zz = startZ + z;
+ if(zz < 0 || zz >= sizeZ)
+ continue;
+
+ Zone zone = zones[zx][zz];
+ if(zone != null)
+ zone.debugDrawVisibility(sceneCullingManager);
+ }
+ }
+ }
+
void completeInvalidation() {
if (invalidationGroup.getPendingCount() <= 0)
return;
diff --git a/src/main/java/rs117/hd/renderer/zone/Zone.java b/src/main/java/rs117/hd/renderer/zone/Zone.java
index 129b0f977a..e27330bbaf 100644
--- a/src/main/java/rs117/hd/renderer/zone/Zone.java
+++ b/src/main/java/rs117/hd/renderer/zone/Zone.java
@@ -15,6 +15,9 @@
import rs117.hd.HdPlugin;
import rs117.hd.scene.MaterialManager;
import rs117.hd.scene.SceneContext;
+import rs117.hd.scene.SceneCullingManager;
+import rs117.hd.scene.SceneCullingManager.CullingResult;
+import rs117.hd.scene.SceneCullingManager.CullingSphere;
import rs117.hd.scene.materials.Material;
import rs117.hd.scene.model_overrides.ModelOverride;
import rs117.hd.utils.Camera;
@@ -86,24 +89,42 @@ public class Zone implements Destructible {
public boolean hasWater; // whether the zone has any water tiles
public boolean onlyWater; // whether the zone only contains water tiles
public boolean hasGapFiller; // whether the zone has any gap filler geometry
- public boolean inSceneFrustum; // whether the zone is visible to the scene camera
- public boolean inShadowFrustum; // whether the zone casts shadows into the visible scene
public boolean isFirstLoadingAttempt = true;
+ public byte visibilityFlags = (byte) 0xFF;
+
public IntHashSet animatedDynamicObjectIds = new IntHashSet();
final StaticAlphaSortingJob alphaSortingJob = new StaticAlphaSortingJob();
ZoneUploadJob uploadJob;
int[] levelOffsets = new int[LEVEL_COUNT]; // buffer pos in ints for the end of the level
+ CullingResult[] levelCullingResults = new CullingResult[LEVEL_COUNT];
int[][] rids;
int[][] roofStart;
int[][] roofEnd;
- final List alphaModels = new ArrayList<>(0);
+ public final List alphaModels = new ArrayList<>(0);
final ConcurrentLinkedQueue pendingModelJobs = new ConcurrentLinkedQueue<>();
+ public boolean setVisibility(Camera camera, boolean visible) {
+ if (visible) {
+ visibilityFlags |= (byte) camera.getCullingMask();
+ } else {
+ visibilityFlags &= (byte) ~camera.getCullingMask();
+ }
+ return visible;
+ }
+
+ public boolean isVisible(Camera camera) {
+ return (visibilityFlags & camera.getCullingMask()) != 0;
+ }
+
+ public boolean isVisible(int cameraId) {
+ return (visibilityFlags & (1 << cameraId)) != 0;
+ }
+
public void initialize(GLBuffer o, GLBuffer a, GLTextureBuffer f) {
assert glVao == 0;
assert glVaoA == 0;
@@ -185,19 +206,23 @@ public void destroy() {
sortedAlphaFacesUpload.release();
+ for(int i = 0; i < LEVEL_COUNT; i++) {
+ if(levelCullingResults[i] != null)
+ levelCullingResults[i].release();
+ }
+
sizeO = 0;
sizeA = 0;
sizeF = 0;
bufLen = 0;
bufLenA = 0;
+ visibilityFlags = 0;
initialized = false;
cull = false;
hasWater = false;
onlyWater = false;
hasGapFiller = false;
- inSceneFrustum = false;
- inShadowFrustum = false;
Arrays.fill(levelOffsets, 0);
rids = null;
@@ -274,12 +299,12 @@ private void setupVao(int vao, int buffer, int metadata) {
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
- public void setMetadata(WorldViewContext viewContext, SceneContext sceneContext, int mx, int mz) {
+ public void setMetadata(WorldViewContext viewContext, SceneContext sceneContext, int zx, int zz) {
if (vboM == null)
return;
- int baseX = (mx - (sceneContext.sceneOffset >> 3)) << 10;
- int baseZ = (mz - (sceneContext.sceneOffset >> 3)) << 10;
+ final int baseX = (zx - (sceneContext.sceneOffset >> 3)) << 10;
+ final int baseZ = (zz - (sceneContext.sceneOffset >> 3)) << 10;
try (MemoryStack stack = MemoryStack.stackPush()) {
IntBuffer buf = stack.mallocInt(3)
@@ -325,18 +350,21 @@ private void convertForDraw(int vertSize) {
copyTo(glDrawLength, drawEnd, 0, drawIdx);
}
- void renderOpaque(CommandBuffer cmd, WorldViewContext ctx, boolean roofShadows) {
+ public void renderOpaque(CommandBuffer cmd, WorldViewContext ctx, Camera camera, boolean ignoreRoofRemoval) {
drawIdx = 0;
int currentLevel = ctx.level;
int maxLevel = ctx.maxLevel;
var hiddenRoofIds = ctx.hideRoofIds;
- if (roofShadows) {
+ if (ignoreRoofRemoval) {
maxLevel = 3;
hiddenRoofIds = Collections.emptySet();
}
for (int level = ctx.minLevel; level <= maxLevel; ++level) {
+ if(camera != null && (levelCullingResults[level] != null && !levelCullingResults[level].isVisible(camera)))
+ continue;
+
int[] rids = this.rids[level];
int[] roofStart = this.roofStart[level];
int[] roofEnd = this.roofEnd[level];
@@ -382,7 +410,7 @@ void renderOpaque(CommandBuffer cmd, WorldViewContext ctx, boolean roofShadows)
flush(cmd);
}
- void renderOpaqueLevel(CommandBuffer cmd, int level) {
+ public void renderOpaqueLevel(CommandBuffer cmd, int level) {
drawIdx = 0;
pushRange(this.levelOffsets[level - 1], this.levelOffsets[level]);
@@ -413,6 +441,7 @@ private static void pushRange(int start, int end) {
public static final class AlphaModel {
int id;
ModelOverride modelOverride;
+ CullingSphere cullingSphere;
int startpos, endpos;
short x, y, z; // local position
short rid;
@@ -462,8 +491,53 @@ void setView(DynamicModelVAO.View view) {
}
}
+ void queueVisibility(WorldViewContext ctx, int zx, int zz) {
+
+ final Projection projection = ctx.uboWorldViewStruct != null ? ctx.uboWorldViewStruct.worldView.getMainWorldProjection() : null;
+ final int baseX = (zx - (ctx.sceneContext.sceneOffset >> 3)) << 10;
+ final int baseZ = (zz - (ctx.sceneContext.sceneOffset >> 3)) << 10;
+
+ for(int i = 0; i < alphaModels.size(); i++) {
+ final AlphaModel m = alphaModels.get(i);
+ if(m.cullingSphere != null) {
+ m.cullingSphere.offsetX = baseX;
+ m.cullingSphere.offsetZ = baseZ;
+ m.cullingSphere.projection = projection;
+ m.cullingSphere.queue();
+ }
+ }
+
+ for(int i = 0; i < LEVEL_COUNT; i++) {
+ final CullingResult result = levelCullingResults[i];
+ if(result != null) {
+ result.projection = projection;
+ result.offsetX = baseX;
+ result.offsetZ = baseZ;
+ result.queue();
+ }
+ }
+ }
+
+ void debugDrawVisibility(SceneCullingManager sceneCullingManager) {
+ sceneCullingManager.debugDraw(levelCullingResults);
+
+ for(int i = 0; i < alphaModels.size(); i++)
+ sceneCullingManager.debugDraw(alphaModels.get(i).cullingSphere);
+ }
+
+ void resolveVisibility(){
+ visibilityFlags = 0;
+ for(int i = 0; i < LEVEL_COUNT; i++) {
+ if(levelCullingResults[i] != null)
+ visibilityFlags |= levelCullingResults[i].getVisibilityFlags();
+ }
+
+ // TODO: We build a "visible" set of alpha models
+ }
+
void addAlphaModel(
HdPlugin plugin,
+ SceneCullingManager sceneCullingManager,
MaterialManager materialManager,
int vao,
int tboF,
@@ -617,6 +691,7 @@ void addAlphaModel(
m.radius = 2 + (int) Math.sqrt(radius);
m.sortedFaces = new int[bufferIdx * 3];
+ m.cullingSphere = sceneCullingManager.obtainSphere(x + cx, y + cy, z + cz, m.radius * 2);
assert packedFaces.length > 0;
// Normally these will be equal, but transparency is used to hide faces in the TzHaar reskin
@@ -710,12 +785,13 @@ void alphaStaticModelSort(Camera camera) {
alphaSortingJob.queue(camera);
}
- void renderAlpha(
+ public void renderAlpha(
CommandBuffer cmd,
int zx,
int zz,
int level,
WorldViewContext ctx,
+ Camera camera,
boolean isShadowPass,
boolean includeRoof
) {
@@ -748,6 +824,16 @@ void renderAlpha(
level > currentLevel && !hiddenRoofIds.isEmpty() && hiddenRoofIds.contains((int) m.rid))
continue;
+ if(camera != null) {
+ if(m.cullingSphere != null) {
+ if(!m.cullingSphere.isVisible(camera))
+ continue;
+ } else {
+ if((levelCullingResults[m.level] != null && !levelCullingResults[m.level].isVisible(camera)))
+ continue;
+ }
+ }
+
int drawMode = STATIC;
if (m.isTemp()) {
// these are already sorted and so just requires a glMultiDrawArrays() from the active vao
@@ -847,6 +933,9 @@ synchronized void multizoneLocs(SceneContext ctx, int zx, int zz, Camera camera,
if (m.lx == -1)
continue;
+ if(m.cullingSphere != null && !m.cullingSphere.isVisible(camera))
+ continue;
+
// calculate which zone this model should be drawn from
// TODO fix for boats
int max = Integer.MAX_VALUE;
@@ -861,7 +950,7 @@ synchronized void multizoneLocs(SceneContext ctx, int zx, int zz, Camera camera,
int zx2 = (centerX >> 10) + offset;
int zz2 = (centerZ >> 10) + offset;
if (zx2 >= 0 && zx2 < zones.length && zz2 >= 0 && zz2 < zones[0].length) {
- if (zones[zx2][zz2].inSceneFrustum && zones[zx2][zz2].initialized) {
+ if (visibilityFlags != 0 && zones[zx2][zz2].initialized) {
max = distance;
closestZoneX = centerX >> 10;
closestZoneZ = centerZ >> 10;
@@ -883,6 +972,7 @@ synchronized void multizoneLocs(SceneContext ctx, int zx, int zz, Camera camera,
AlphaModel m2 = ALPHA_MODEL_POOL.acquire();
m2.id = m.id;
m2.modelOverride = m.modelOverride;
+ m2.cullingSphere = m.cullingSphere;
m2.startpos = m.startpos;
m2.endpos = m.endpos;
m2.x = m.x;
diff --git a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java
index 6a914772ec..fb5aee2d12 100644
--- a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java
+++ b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java
@@ -25,7 +25,6 @@
package rs117.hd.renderer.zone;
import com.google.inject.Injector;
-import java.io.IOException;
import java.util.Arrays;
import java.util.Set;
import javax.inject.Inject;
@@ -42,29 +41,24 @@
import rs117.hd.HdPluginConfig;
import rs117.hd.config.ColorFilter;
import rs117.hd.config.DynamicLights;
-import rs117.hd.config.ShadowMode;
-import rs117.hd.opengl.shader.SceneShaderProgram;
-import rs117.hd.opengl.shader.ShaderException;
import rs117.hd.opengl.shader.ShaderIncludes;
-import rs117.hd.opengl.shader.ShadowShaderProgram;
import rs117.hd.opengl.uniforms.UBOLights;
import rs117.hd.opengl.uniforms.UBOWorldViews;
import rs117.hd.overlays.FrameTimer;
import rs117.hd.overlays.Timer;
import rs117.hd.renderer.Renderer;
+import rs117.hd.renderer.zone.passes.RenderPipeline;
import rs117.hd.scene.EnvironmentManager;
import rs117.hd.scene.LightManager;
import rs117.hd.scene.ProceduralGenerator;
import rs117.hd.scene.SceneContext;
+import rs117.hd.scene.SceneCullingManager;
import rs117.hd.scene.lights.Light;
-import rs117.hd.scene.model_overrides.ModelOverride;
import rs117.hd.utils.Camera;
import rs117.hd.utils.ColorUtils;
-import rs117.hd.utils.CommandBuffer;
import rs117.hd.utils.HDUtils;
import rs117.hd.utils.Mat4;
import rs117.hd.utils.RenderState;
-import rs117.hd.utils.ShadowCasterVolume;
import rs117.hd.utils.buffer.GLBuffer;
import rs117.hd.utils.buffer.GLMappedBufferIntWriter;
import rs117.hd.utils.buffer.GpuIntBuffer;
@@ -82,7 +76,6 @@
import static rs117.hd.HdPluginConfig.*;
import static rs117.hd.renderer.zone.WorldViewContext.VAO_OPAQUE;
import static rs117.hd.renderer.zone.WorldViewContext.VAO_PLAYER;
-import static rs117.hd.renderer.zone.WorldViewContext.VAO_PRESCENE;
import static rs117.hd.renderer.zone.WorldViewContext.VAO_SHADOW;
import static rs117.hd.utils.MathUtils.*;
@@ -91,6 +84,9 @@
public class ZoneRenderer implements Renderer {
public static final int FRAMES_IN_FLIGHT = 3;
+ public static int CAMERA_COUNT;
+ public static final int SCENE_CAMERA_ID = CAMERA_COUNT++;
+
private static int TEXTURE_UNIT_COUNT = HdPlugin.TEXTURE_UNIT_COUNT;
public static final int TEXTURE_UNIT_TEXTURED_FACES = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++;
@@ -128,16 +124,10 @@ public class ZoneRenderer implements Renderer {
private ModelStreamingManager modelStreamingManager;
@Inject
- private FrameTimer frameTimer;
-
- @Inject
- private SceneShaderProgram sceneProgram;
-
- @Inject
- private ShadowShaderProgram.Fast fastShadowProgram;
+ private SceneCullingManager sceneCullingManager;
@Inject
- private ShadowShaderProgram.Detailed detailedShadowProgram;
+ private FrameTimer frameTimer;
@Inject
private JobSystem jobSystem;
@@ -145,16 +135,14 @@ public class ZoneRenderer implements Renderer {
@Inject
private UBOWorldViews uboWorldViews;
- public final Camera sceneCamera = new Camera().setReverseZ(true);
- public final Camera directionalCamera = new Camera().setOrthographic(true);
- public final ShadowCasterVolume directionalShadowCasterVolume = new ShadowCasterVolume(directionalCamera);
+ @Inject
+ private RenderPipeline renderPipeline;
+
+ public final Camera sceneCamera = new Camera().setReverseZ(true).setCullingId(SCENE_CAMERA_ID);
public final RenderState renderState = new RenderState();
- public final CommandBuffer sceneCmd = new CommandBuffer("Scene");
- public final CommandBuffer directionalCmd = new CommandBuffer("Directional");
- public final CommandBuffer gapFillerCmd = new CommandBuffer("GapFiller");
- private GLBuffer indirectDrawCmds;
+ public GLBuffer indirectDrawCmds;
public static GpuIntBuffer indirectDrawCmdsStaging;
public static GLBuffer.EBO eboAlpha;
@@ -163,8 +151,6 @@ public class ZoneRenderer implements Renderer {
private boolean sceneFboValid;
private boolean shouldRenderSkybox;
private boolean shouldRenderScene;
- private boolean shouldClearShadowFbo;
- private boolean shouldDrawRoofShadows;
@Override
public boolean supportsGpu(GLCapabilities glCaps) {
@@ -189,18 +175,14 @@ public void initialize() {
if (FacePrioritySorter.POOL == null)
FacePrioritySorter.POOL = new ConcurrentPool<>(() -> injector.getInstance(FacePrioritySorter.class));
- sceneCmd.setFrameTimer(frameTimer);
- directionalCmd.setFrameTimer(frameTimer);
- gapFillerCmd.setFrameTimer(frameTimer);
-
jobSystem.startUp(config.cpuUsageLimit());
uboWorldViews.initialize(UNIFORM_BLOCK_WORLD_VIEWS);
sceneManager.initialize(uboWorldViews);
modelStreamingManager.initialize();
+ renderPipeline.initialize();
// Force updates that only run when the cameras change
sceneCamera.setDirty();
- directionalCamera.setDirty();
}
@Override
@@ -211,6 +193,7 @@ public void destroy() {
modelStreamingManager.destroy();
sceneManager.destroy();
uboWorldViews.destroy();
+ renderPipeline.destroy();
if (SceneUploader.POOL != null)
SceneUploader.POOL.destroy();
@@ -231,20 +214,18 @@ public void addShaderIncludes(ShaderIncludes includes) {
.define("MAX_SIMULTANEOUS_WORLD_VIEWS", UBOWorldViews.MAX_SIMULTANEOUS_WORLD_VIEWS)
.addInclude("WORLD_VIEW_GETTER", () -> plugin.generateGetter("WorldView", UBOWorldViews.MAX_SIMULTANEOUS_WORLD_VIEWS))
.addUniformBuffer(uboWorldViews);
+
+ renderPipeline.addShaderIncludes.execute(includes);
}
@Override
- public void initializeShaders(ShaderIncludes includes) throws ShaderException, IOException {
- sceneProgram.compile(includes);
- fastShadowProgram.compile(includes);
- detailedShadowProgram.compile(includes);
+ public void initializeShaders(ShaderIncludes includes) {
+ renderPipeline.initializeShaders.execute(includes);
}
@Override
public void destroyShaders() {
- sceneProgram.destroy();
- fastShadowProgram.destroy();
- detailedShadowProgram.destroy();
+ renderPipeline.destroyShaders.execute();
}
private void initializeBuffers() {
@@ -278,6 +259,8 @@ private void destroyBuffers() {
public void processConfigChanges(Set keys) {
if (keys.contains(KEY_ASYNC_MODEL_PROCESSING))
modelStreamingManager.reinitialize();
+
+ renderPipeline.processConfigChanges.execute(keys);
}
@Override
@@ -309,45 +292,15 @@ public void preSceneDraw(
if (ctx.uboWorldViewStruct != null)
ctx.uboWorldViewStruct.update();
- if (scene.getWorldViewId() == WorldView.TOPLEVEL)
+ final boolean isTopLevel = scene.getWorldViewId() == WorldView.TOPLEVEL;
+ if (isTopLevel)
preSceneDrawTopLevel(scene, cameraX, cameraY, cameraZ, cameraPitch, cameraYaw);
- ctx.completeInvalidation();
-
- int offset = ctx.sceneContext.sceneOffset >> 3;
- for (int zx = 0; zx < ctx.sizeX; ++zx)
- for (int zz = 0; zz < ctx.sizeZ; ++zz)
- ctx.zones[zx][zz].multizoneLocs(ctx.sceneContext, zx - offset, zz - offset, sceneCamera, ctx.zones);
-
- ctx.sortStaticAlphaModels(sceneCamera);
-
- ctx.map();
-
- if (scene.getWorldViewId() == WorldView.TOPLEVEL) {
- Model skybox = scene.getSkybox();
- if (skybox != null) {
- skybox.calculateBoundsCylinder();
- modelStreamingManager.uploadTempModel(
- ctx,
- sceneCamera,
- null,
- skybox,
- ModelOverride.UNLIT,
- skybox,
- null,
- null,
- true,
- VAO_PRESCENE,
- -1,
- 0,
- cameraX, cameraY, cameraZ
- );
- }
+ renderPipeline.preSceneDraw.execute(ctx, isTopLevel);
+ ctx.preSceneDraw(sceneCamera);
- sceneCmd.DepthMask(false);
- ctx.drawAll(VAO_PRESCENE, sceneCmd);
- sceneCmd.DepthMask(true);
- }
+ if(plugin.showCulling)
+ ctx.debugDraw(sceneCamera);
frameTimer.end(Timer.DRAW_PRESCENE);
} catch (Throwable ex) {
@@ -390,7 +343,6 @@ private void preSceneDrawTopLevel(
Arrays.fill(plugin.cameraShift, 0);
float zoom = client.get3dZoom();
- float drawDistance = (float) plugin.getDrawDistance();
if (plugin.orthographicProjection)
zoom *= ORTHOGRAPHIC_ZOOM;
@@ -431,86 +383,6 @@ private void preSceneDrawTopLevel(
return;
}
- directionalCamera.setPitch(environmentManager.currentSunAngles[0]);
- directionalCamera.setYaw(PI - environmentManager.currentSunAngles[1]);
- boolean hasDirectionalCameraChanged = directionalCamera.isViewDirty() || directionalCamera.isProjDirty();
-
- if (plugin.configShadowsEnabled &&
- (hasSceneCameraChanged || hasDirectionalCameraChanged) &&
- !sceneCamera.isOrthographic()
- ) {
- int shadowDrawDistance = 90 * LOCAL_TILE_SIZE;
-
- final float[][] volumeCorners = directionalShadowCasterVolume
- .build(sceneCamera, drawDistance * LOCAL_TILE_SIZE, shadowDrawDistance);
-
- final float[] sceneCenter = new float[3];
- for (float[] corner : volumeCorners)
- add(sceneCenter, sceneCenter, corner);
- divide(sceneCenter, sceneCenter, (float) volumeCorners.length);
-
- // Reset position before transforming points
- directionalCamera.setPosition(0, 0, 0);
-
- float minX = Float.POSITIVE_INFINITY, maxX = Float.NEGATIVE_INFINITY;
- float minY = Float.POSITIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY;
- float minZ = Float.POSITIVE_INFINITY, maxZ = Float.NEGATIVE_INFINITY;
- float radius = 0f;
- for (float[] corner : volumeCorners) {
- radius = max(radius, distance(sceneCenter, corner));
-
- directionalCamera.transformPoint(corner, corner);
-
- minX = min(minX, corner[0]);
- maxX = max(maxX, corner[0]);
-
- minY = min(minY, corner[1]);
- maxY = max(maxY, corner[1]);
-
- minZ = min(minZ, corner[2]);
- maxZ = max(maxZ, corner[2]);
- }
-
- // Offset the Directional Camera by the radius of the scene
- float[] directionalFwd = directionalCamera.getForwardDirection();
- multiply(directionalFwd, directionalFwd, radius);
- add(sceneCenter, sceneCenter, directionalFwd);
-
- // Calculate directional size from the AABB of the scene frustum corners
- // Then snap to the nearest multiple of `LOCAL_HALF_TILE_SIZE` to prevent shimmering
- int directionalSize = (int) max(abs(maxY - minY), abs(maxX - minX), abs(maxZ - minZ));
- directionalSize = Math.round(directionalSize / (float) LOCAL_HALF_TILE_SIZE) * LOCAL_HALF_TILE_SIZE;
- directionalSize = max(8000, directionalSize); // Clamp the size to prevent going too small at reduced draw distances
-
- // Ignore directional size changes below the change threshold to avoid inducing shimmering
- int previousDirectionalSize = directionalCamera.getViewportWidth();
- float changeThreshold = previousDirectionalSize * 0.05f; // 10% of the previous directional size
- if (abs(directionalSize - previousDirectionalSize) < changeThreshold)
- directionalSize = previousDirectionalSize;
-
- // Snap Position to Shadow Texel Grid to prevent shimmering
- directionalCamera.transformPoint(sceneCenter, sceneCenter);
-
- float texelSize = (float) directionalSize / plugin.shadowMapResolution;
- sceneCenter[0] = (float) floor(sceneCenter[0] / texelSize + 0.5f) * texelSize;
- sceneCenter[1] = (float) floor(sceneCenter[1] / texelSize + 0.5f) * texelSize;
-
- directionalCamera.setPosition(directionalCamera.inverseTransformPoint(sceneCenter, sceneCenter));
- directionalCamera.setNearPlane(Math.max(0.1f, radius * 0.05f));
- directionalCamera.setFarPlane(radius * 2.0f);
- directionalCamera.setZoom(1.0f);
- directionalCamera.setViewportWidth(directionalSize);
- directionalCamera.setViewportHeight(directionalSize);
-
- plugin.uboGlobal.lightProjectionMatrix.set(directionalCamera.getViewProjMatrix());
- }
-
- shouldDrawRoofShadows =
- plugin.configShadowsEnabled &&
- plugin.configRoofShadows &&
- environmentManager.allowRoofShadows();
-
- plugin.uboGlobal.lightDir.set(directionalCamera.getForwardDirection());
plugin.uboGlobal.cameraPos.set(plugin.cameraPosition);
plugin.uboGlobal.viewMatrix.set(plugin.viewMatrix);
plugin.uboGlobal.projectionMatrix.set(plugin.viewProjMatrix);
@@ -647,9 +519,6 @@ private void preSceneDrawTopLevel(
// Reset buffers for the next frame
indirectDrawCmdsStaging.clear();
- sceneCmd.reset();
- directionalCmd.reset();
- gapFillerCmd.reset();
renderState.reset();
eboAlpha.orphan();
@@ -673,6 +542,9 @@ public void postSceneDraw(Scene scene) {
frameTimer.begin(Timer.DRAW_POSTSCENE);
if (scene.getWorldViewId() == WorldView.TOPLEVEL)
postDrawTopLevel();
+
+ renderPipeline.postSceneDraw.execute(ctx);
+
frameTimer.end(Timer.DRAW_POSTSCENE);
} catch (Throwable ex) {
log.error("Error in postSceneDraw({}):", scene != null ? scene.getWorldViewId() : null, ex);
@@ -709,135 +581,6 @@ private void postDrawTopLevel() {
checkGLErrors();
}
- private void tiledLightingPass() {
- if (!plugin.configTiledLighting || plugin.configDynamicLights == DynamicLights.NONE)
- return;
-
- plugin.updateTiledLightingFbo();
- assert plugin.fboTiledLighting != 0;
-
- frameTimer.begin(Timer.DRAW_TILED_LIGHTING);
- frameTimer.begin(Timer.RENDER_TILED_LIGHTING);
-
- renderState.framebuffer.set(GL_FRAMEBUFFER, plugin.fboTiledLighting);
- renderState.viewport.set(0, 0, plugin.tiledLightingResolution[0], plugin.tiledLightingResolution[1]);
- renderState.vao.setVao(plugin.vaoTri);
-
- if (plugin.tiledLightingImageStoreProgram.isValid()) {
- renderState.program.set(plugin.tiledLightingImageStoreProgram);
- renderState.drawBuffer.set(GL_NONE);
- renderState.apply();
- glDrawArrays(GL_TRIANGLES, 0, 3);
- } else {
- renderState.drawBuffer.set(GL_COLOR_ATTACHMENT0);
- int layerCount = plugin.configDynamicLights.getTiledLightingLayers();
- for (int layer = 0; layer < layerCount; layer++) {
- renderState.program.set(plugin.tiledLightingShaderPrograms.get(layer));
- renderState.framebufferTextureLayer.set(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, plugin.texTiledLighting, 0, layer);
- renderState.apply();
- glDrawArrays(GL_TRIANGLES, 0, 3);
- }
- }
-
- frameTimer.end(Timer.RENDER_TILED_LIGHTING);
- frameTimer.end(Timer.DRAW_TILED_LIGHTING);
- }
-
- private void directionalShadowPass() {
- final boolean shouldRenderShadows =
- plugin.configShadowsEnabled &&
- plugin.fboShadowMap != 0 &&
- environmentManager.currentDirectionalStrength > 0;
-
- if (shouldRenderShadows || shouldClearShadowFbo) {
- // Render to the shadow depth map
- renderState.framebuffer.set(GL_FRAMEBUFFER, plugin.fboShadowMap);
- renderState.viewport.set(0, 0, plugin.shadowMapResolution, plugin.shadowMapResolution);
- renderState.apply();
-
- glClearDepth(1);
- glClear(GL_DEPTH_BUFFER_BIT);
- shouldClearShadowFbo = false;
- }
-
- if (!shouldRenderShadows)
- return;
-
- frameTimer.begin(Timer.RENDER_SHADOWS);
-
- renderState.enable.set(GL_DEPTH_TEST);
- renderState.disable.set(GL_CULL_FACE);
- renderState.depthFunc.set(GL_LEQUAL);
- renderState.ido.set(indirectDrawCmds.id);
-
- CommandBuffer.SKIP_DEPTH_MASKING = true;
- directionalCmd.execute(renderState);
- CommandBuffer.SKIP_DEPTH_MASKING = false;
-
- glBindVertexArray(0);
-
- renderState.disable.set(GL_DEPTH_TEST);
-
- shouldClearShadowFbo = true;
- frameTimer.end(Timer.RENDER_SHADOWS);
- }
-
- private void scenePass() {
- sceneProgram.use();
-
- frameTimer.begin(Timer.DRAW_SCENE);
- renderState.framebuffer.set(GL_DRAW_FRAMEBUFFER, plugin.fboScene);
- if (plugin.msaaSamples > 1) {
- renderState.enable.set(GL_MULTISAMPLE);
- } else {
- renderState.disable.set(GL_MULTISAMPLE);
- }
- renderState.viewport.set(0, 0, plugin.sceneResolution[0], plugin.sceneResolution[1]);
- renderState.ido.set(indirectDrawCmds.id);
- renderState.apply();
-
- // Clear scene
- frameTimer.begin(Timer.CLEAR_SCENE);
-
- float[] clearColor = { 0, 0, 0 };
- if (!shouldRenderSkybox) {
- float[] fogColor = ColorUtils.linearToSrgb(environmentManager.currentFogColor);
- pow(clearColor, fogColor, plugin.getGammaCorrection());
- }
- glClearColor(clearColor[0], clearColor[1], clearColor[2], 1f);
- glClearDepth(0);
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
- frameTimer.end(Timer.CLEAR_SCENE);
-
- frameTimer.begin(Timer.RENDER_SCENE);
-
- renderState.enable.set(GL_BLEND);
- renderState.enable.set(GL_CULL_FACE);
- renderState.enable.set(GL_DEPTH_TEST);
- renderState.depthFunc.set(GL_GEQUAL);
- renderState.blendFunc.set(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE);
-
- if (!gapFillerCmd.isEmpty()) {
- renderState.depthMask.set(false);
- gapFillerCmd.execute(renderState);
- renderState.depthMask.set(true);
- }
-
- sceneCmd.execute(renderState);
-
- frameTimer.end(Timer.RENDER_SCENE);
-
- glBindVertexArray(0);
-
- // Done rendering the scene
- renderState.disable.set(GL_BLEND);
- renderState.disable.set(GL_CULL_FACE);
- renderState.disable.set(GL_DEPTH_TEST);
- renderState.apply();
-
- frameTimer.end(Timer.DRAW_SCENE);
- }
-
@Override
public boolean zoneInFrustum(int zx, int zz, int maxY, int minY) {
if (plugin.isPluginStopPending())
@@ -847,64 +590,42 @@ public boolean zoneInFrustum(int zx, int zz, int maxY, int minY) {
if (!sceneManager.isTopLevelValid())
return false;
+ if (plugin.enableDetailedTimers)
+ frameTimer.begin(Timer.VISIBILITY_CHECK);
+
WorldViewContext ctx = sceneManager.getRoot();
- if (plugin.enableDetailedTimers) frameTimer.begin(Timer.VISIBILITY_CHECK);
- int minX = zx * CHUNK_SIZE - ctx.sceneContext.sceneOffset;
- int minZ = zz * CHUNK_SIZE - ctx.sceneContext.sceneOffset;
+ int x = zx * CHUNK_SIZE - ctx.sceneContext.sceneOffset;
+ int z = zz * CHUNK_SIZE - ctx.sceneContext.sceneOffset;
if (ctx.sceneContext.currentArea != null) {
var base = ctx.sceneContext.sceneBase;
assert base != null;
boolean inArea = ctx.sceneContext.currentArea.intersects(
- true, base[0] + minX, base[1] + minZ, base[0] + minX + 7, base[1] + minZ + 7);
+ true, base[0] + x, base[1] + z, base[0] + x + 7, base[1] + z + 7);
if (!inArea) {
- if (plugin.enableDetailedTimers) frameTimer.end(Timer.VISIBILITY_CHECK);
return false;
}
}
Zone zone = ctx.zones[zx][zz];
if (plugin.freezeCulling)
- return zone.inSceneFrustum || zone.inShadowFrustum;
-
- minX *= LOCAL_TILE_SIZE;
- minZ *= LOCAL_TILE_SIZE;
- int maxX = minX + CHUNK_SIZE * LOCAL_TILE_SIZE;
- int maxZ = minZ + CHUNK_SIZE * LOCAL_TILE_SIZE;
- if (zone.hasWater) {
- maxY += ProceduralGenerator.MAX_DEPTH;
- minY -= ProceduralGenerator.MAX_DEPTH;
- }
+ return zone.visibilityFlags != 0;
- final int PADDING = 4 * LOCAL_TILE_SIZE;
- zone.inSceneFrustum = sceneCamera.intersectsAABB(
- minX - PADDING, minY, minZ - PADDING, maxX + PADDING, maxY, maxZ + PADDING);
+ final int zMinX = x * LOCAL_TILE_SIZE;
+ final int zMinZ = z * LOCAL_TILE_SIZE;
+ final int zMaxX = zMinX + CHUNK_SIZE * LOCAL_TILE_SIZE;
+ final int zMaxZ = zMinZ + CHUNK_SIZE * LOCAL_TILE_SIZE;
+ final int zMinY = minY - (zone.hasWater ? ProceduralGenerator.MAX_DEPTH : 0);
+ final int zMaxY = maxY + (zone.hasWater ? ProceduralGenerator.MAX_DEPTH : 0);
- if (zone.inSceneFrustum) {
- if (plugin.enableDetailedTimers)
- frameTimer.end(Timer.VISIBILITY_CHECK);
- return zone.inShadowFrustum = true;
- }
-
- if (plugin.configShadowsEnabled && plugin.configExpandShadowDraw) {
- zone.inShadowFrustum = directionalCamera.intersectsAABB(minX, minY, minZ, maxX, maxY, maxZ);
- if (zone.inShadowFrustum) {
- int centerX = minX + (maxX - minX) / 2;
- int centerY = minY + (maxY - minY) / 2;
- int centerZ = minZ + (maxZ - minZ) / 2;
- zone.inShadowFrustum = directionalShadowCasterVolume.intersectsPoint(centerX, centerY, centerZ);
- }
- if (plugin.enableDetailedTimers)
- frameTimer.end(Timer.VISIBILITY_CHECK);
- return zone.inShadowFrustum;
- }
+ renderPipeline.zoneInFrustum.execute(zone, zx, zz, zMinX, zMinY, zMinZ, zMaxX, zMaxY, zMaxZ);
- if (plugin.enableDetailedTimers)
- frameTimer.end(Timer.VISIBILITY_CHECK);
- if (plugin.orthographicProjection)
- return zone.inSceneFrustum = true;
+ return zone.visibilityFlags != 0;
} catch (Throwable ex) {
log.error("Error in zoneInFrustum({}, {}, {}, {}):", zx, zz, maxY, minY, ex);
plugin.requestPluginStop();
+ } finally {
+ if (plugin.enableDetailedTimers)
+ frameTimer.end(Timer.VISIBILITY_CHECK);
}
return false;
}
@@ -924,18 +645,7 @@ public void drawZoneOpaque(Projection entityProjection, Scene scene, int zx, int
return;
frameTimer.begin(Timer.DRAW_ZONE_OPAQUE);
- if (!sceneManager.isRoot(ctx) || z.inSceneFrustum) {
- z.renderOpaque(sceneCmd, ctx, false);
-
- if (z.hasGapFiller)
- z.renderOpaqueLevel(gapFillerCmd, Zone.LEVEL_GAP_FILLER);
- }
-
- final boolean isSquashed = ctx.uboWorldViewStruct != null && ctx.uboWorldViewStruct.isSquashed();
- if (!isSquashed && (!sceneManager.isRoot(ctx) || z.inShadowFrustum)) {
- directionalCmd.SetShader(fastShadowProgram);
- z.renderOpaque(directionalCmd, ctx, shouldDrawRoofShadows);
- }
+ renderPipeline.drawZoneOpaque.execute(ctx, z, zx, zz);
frameTimer.end(Timer.DRAW_ZONE_OPAQUE);
checkGLErrors();
@@ -960,28 +670,14 @@ public void drawZoneAlpha(Projection entityProjection, Scene scene, int level, i
return;
frameTimer.begin(Timer.DRAW_ZONE_ALPHA);
- final boolean renderWater = z.inSceneFrustum && level == 0 && z.hasWater;
- if (renderWater)
- z.renderOpaqueLevel(sceneCmd, Zone.LEVEL_WATER_SURFACE);
-
modelStreamingManager.ensureAsyncUploadsComplete(z);
- final boolean hasAlpha = z.sizeA != 0 || !z.alphaModels.isEmpty();
- if (hasAlpha) {
- final int offset = ctx.sceneContext.sceneOffset >> 3;
- // Only sort if the alpha will be directly visible, since shadows don't require sorting
- if (level == 0 && (!sceneManager.isRoot(ctx) || z.inSceneFrustum))
- z.alphaSort(zx - offset, zz - offset, sceneCamera);
-
- final boolean isSquashed = ctx.uboWorldViewStruct != null && ctx.uboWorldViewStruct.isSquashed();
- if (!isSquashed && (!sceneManager.isRoot(ctx) || z.inShadowFrustum)) {
- directionalCmd.SetShader(plugin.configShadowMode == ShadowMode.DETAILED ? detailedShadowProgram : fastShadowProgram);
- z.renderAlpha(directionalCmd, zx - offset, zz - offset, level, ctx, true, shouldDrawRoofShadows);
- }
+ final int offset = ctx.sceneContext.sceneOffset >> 3;
+ if (level == 0)
+ z.alphaSort(zx - offset, zz - offset, sceneCamera);
+
+ renderPipeline.drawZoneAlpha.execute(ctx, z, level, zx, zz);
- if (!sceneManager.isRoot(ctx) || z.inSceneFrustum)
- z.renderAlpha(sceneCmd, zx - offset, zz - offset, level, ctx, false, false);
- }
frameTimer.end(Timer.DRAW_ZONE_ALPHA);
checkGLErrors();
@@ -1003,48 +699,41 @@ public void drawPass(Projection projection, Scene scene, int pass) {
frameTimer.begin(Timer.DRAW_PASS);
- switch (pass) {
- case DrawCallbacks.PASS_OPAQUE:
- directionalCmd.SetShader(fastShadowProgram);
- directionalCmd.ExecuteSubCommandBuffer(ctx.vaoDirectionalCmd);
+ renderPipeline.drawPass.execute(ctx, pass);
- sceneCmd.ExecuteSubCommandBuffer(ctx.vaoSceneCmd);
- break;
- case DrawCallbacks.PASS_ALPHA:
- modelStreamingManager.ensureAsyncUploadsComplete(null);
+ if (pass == DrawCallbacks.PASS_ALPHA) {
+ modelStreamingManager.ensureAsyncUploadsComplete(null);
- if (sceneManager.isRoot(ctx))
- frameTimer.begin(Timer.UNMAP_ROOT_CTX);
+ if (sceneManager.isRoot(ctx))
+ frameTimer.begin(Timer.UNMAP_ROOT_CTX);
- ctx.unmap();
+ ctx.unmap();
- if (sceneManager.isRoot(ctx))
- frameTimer.end(Timer.UNMAP_ROOT_CTX);
+ if (sceneManager.isRoot(ctx))
+ frameTimer.end(Timer.UNMAP_ROOT_CTX);
- // Draw opaque
- ctx.drawAll(VAO_OPAQUE, ctx.vaoSceneCmd);
- ctx.drawAll(VAO_OPAQUE, ctx.vaoDirectionalCmd);
- ctx.drawAll(VAO_PLAYER, ctx.vaoDirectionalCmd);
+ // Draw opaque
+ ctx.drawAll(VAO_OPAQUE, ctx.vaoSceneCmd);
+ ctx.drawAll(VAO_OPAQUE, ctx.vaoDirectionalCmd);
+ ctx.drawAll(VAO_PLAYER, ctx.vaoDirectionalCmd);
- // Draw shadow-only models
- ctx.drawAll(VAO_SHADOW, ctx.vaoDirectionalCmd);
+ // Draw shadow-only models
+ ctx.drawAll(VAO_SHADOW, ctx.vaoDirectionalCmd);
- // Draw players with sorted alpha, without writing depth
- ctx.vaoSceneCmd.DepthMask(false);
- ctx.drawAll(VAO_PLAYER, ctx.vaoSceneCmd);
- ctx.vaoSceneCmd.DepthMask(true);
+ // Draw players with sorted alpha, without writing depth
+ ctx.vaoSceneCmd.DepthMask(false);
+ ctx.drawAll(VAO_PLAYER, ctx.vaoSceneCmd);
+ ctx.vaoSceneCmd.DepthMask(true);
- // Redraw players, this time only writing depth, for correct ordering with the background
- ctx.vaoSceneCmd.ColorMask(false, false, false, false);
- ctx.drawAll(VAO_PLAYER, ctx.vaoSceneCmd);
- ctx.vaoSceneCmd.ColorMask(true, true, true, true);
+ // Redraw players, this time only writing depth, for correct ordering with the background
+ ctx.vaoSceneCmd.ColorMask(false, false, false, false);
+ ctx.drawAll(VAO_PLAYER, ctx.vaoSceneCmd);
+ ctx.vaoSceneCmd.ColorMask(true, true, true, true);
- for (int zx = 0; zx < ctx.sizeX; ++zx)
- for (int zz = 0; zz < ctx.sizeZ; ++zz)
- ctx.zones[zx][zz].postAlphaPass();
- break;
+ for (int zx = 0; zx < ctx.sizeX; ++zx)
+ for (int zz = 0; zz < ctx.sizeZ; ++zz)
+ ctx.zones[zx][zz].postAlphaPass();
}
-
frameTimer.end(Timer.DRAW_PASS);
checkGLErrors();
} catch (Throwable ex) {
@@ -1117,11 +806,8 @@ public void draw(int overlayColor) {
}
frameTimer.begin(Timer.DRAW_SUBMIT);
- if (shouldRenderScene) {
- tiledLightingPass();
- directionalShadowPass();
- scenePass();
- }
+ if (shouldRenderScene)
+ renderPipeline.draw.execute(renderState);
if (sceneFboValid && plugin.sceneResolution != null && plugin.sceneViewport != null) {
glBindFramebuffer(GL_READ_FRAMEBUFFER, plugin.fboScene);
@@ -1179,6 +865,9 @@ public void draw(int overlayColor) {
log.error("Unable to swap buffers:", ex);
}
+ if(shouldRenderScene)
+ renderPipeline.postDraw.execute(renderState);
+
glBindFramebuffer(GL_FRAMEBUFFER, plugin.awtContext.getFramebuffer(false));
frameTimer.endFrameAndReset();
diff --git a/src/main/java/rs117/hd/renderer/zone/passes/DebugDrawPass.java b/src/main/java/rs117/hd/renderer/zone/passes/DebugDrawPass.java
new file mode 100644
index 0000000000..c536b0f48d
--- /dev/null
+++ b/src/main/java/rs117/hd/renderer/zone/passes/DebugDrawPass.java
@@ -0,0 +1,494 @@
+package rs117.hd.renderer.zone.passes;
+
+import java.io.IOException;
+import java.nio.IntBuffer;
+import java.util.Iterator;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import lombok.extern.slf4j.Slf4j;
+import org.lwjgl.system.MemoryStack;
+import org.lwjgl.system.MemoryUtil;
+import rs117.hd.HdPlugin;
+import rs117.hd.opengl.GLPrimitives;
+import rs117.hd.opengl.shader.DebugDrawShaderProgram;
+import rs117.hd.opengl.shader.DebugDrawShaderProgram.DebugDrawCubeShaderProgram;
+import rs117.hd.opengl.shader.DebugDrawShaderProgram.DebugDrawLineShaderProgram;
+import rs117.hd.opengl.shader.DebugDrawShaderProgram.DebugDrawSphereShaderProgram;
+import rs117.hd.opengl.shader.DebugDrawShaderProgram.DebugDrawTextShaderProgram;
+import rs117.hd.opengl.shader.ShaderException;
+import rs117.hd.opengl.shader.ShaderIncludes;
+import rs117.hd.overlays.FrameTimer;
+import rs117.hd.utils.DebugDraw;
+import rs117.hd.utils.RenderState;
+import rs117.hd.utils.buffer.GLBuffer;
+import rs117.hd.utils.collections.ConcurrentPool;
+
+import static org.lwjgl.opengl.GL11.GL_BLEND;
+import static org.lwjgl.opengl.GL11.GL_CULL_FACE;
+import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST;
+import static org.lwjgl.opengl.GL11.GL_FILL;
+import static org.lwjgl.opengl.GL11.GL_FLOAT;
+import static org.lwjgl.opengl.GL11.GL_FRONT_AND_BACK;
+import static org.lwjgl.opengl.GL11.GL_GEQUAL;
+import static org.lwjgl.opengl.GL11.GL_INT;
+import static org.lwjgl.opengl.GL11.GL_LINE;
+import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
+import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
+import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
+import static org.lwjgl.opengl.GL11.GL_UNSIGNED_INT;
+import static org.lwjgl.opengl.GL11.glPolygonMode;
+import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
+import static org.lwjgl.opengl.GL15.GL_DYNAMIC_DRAW;
+import static org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER;
+import static org.lwjgl.opengl.GL15.glBindBuffer;
+import static org.lwjgl.opengl.GL20.glEnableVertexAttribArray;
+import static org.lwjgl.opengl.GL20.glVertexAttribPointer;
+import static org.lwjgl.opengl.GL30.glBindVertexArray;
+import static org.lwjgl.opengl.GL30.glDeleteVertexArrays;
+import static org.lwjgl.opengl.GL30.glGenVertexArrays;
+import static org.lwjgl.opengl.GL30.glVertexAttribIPointer;
+import static org.lwjgl.opengl.GL30C.GL_DRAW_FRAMEBUFFER;
+import static org.lwjgl.opengl.GL31.glDrawElementsInstanced;
+import static org.lwjgl.opengl.GL33.glVertexAttribDivisor;
+import static rs117.hd.overlays.Timer.RENDER_DEBUG_DRAW;
+
+@Slf4j
+@Singleton
+public class DebugDrawPass implements RenderPass {
+ private static final ConcurrentPool POOL = new ConcurrentPool<>(Draw::new);
+
+ private static final int INITIAL_CAPACITY = 256;
+
+ private static final int CUBE_FLOATS = 7; // cx cy cz hx hy hz argb
+ private static final int SPHERE_FLOATS = 5; // cx cy cz r argb
+ private static final int LINE_FLOATS = 8; // x1 y1 z1 x2 y2 z2 thickness argb
+ private static final int TEXT_FLOATS = 7; // wx wy wz scale charCode charIndex argb
+
+ private final ConcurrentLinkedQueue lineQueue = new ConcurrentLinkedQueue<>();
+ private final ConcurrentLinkedQueue aabbQueue = new ConcurrentLinkedQueue<>();
+ private final ConcurrentLinkedQueue sphereQueue = new ConcurrentLinkedQueue<>();
+ private final ConcurrentLinkedQueue textQueue = new ConcurrentLinkedQueue<>();
+
+ private IntBuffer aabbBufSolid;
+ private IntBuffer aabbBufWire;
+ private IntBuffer sphereBufSolid;
+ private IntBuffer sphereBufWire;
+ private IntBuffer lineBuf;
+ private IntBuffer textBuf;
+
+ private PrimitiveDraw cubeDraw;
+ private PrimitiveDraw sphereDraw;
+ private PrimitiveDraw lineDraw;
+ private PrimitiveDraw textDraw;
+
+ @Inject
+ private HdPlugin plugin;
+
+ @Inject
+ private FrameTimer frameTimer;
+
+ @Inject
+ private DebugDrawCubeShaderProgram cubeShader;
+
+ @Inject
+ private DebugDrawSphereShaderProgram sphereShader;
+
+ @Inject
+ private DebugDrawLineShaderProgram lineShader;
+
+ @Inject
+ private DebugDrawTextShaderProgram textShader;
+
+ @Override
+ public RenderPassType getType() { return RenderPassType.DEBUG_DRAW; }
+
+ @Override
+ public void initialize() {
+ try (MemoryStack stack = MemoryStack.stackPush()) {
+ cubeDraw = new PrimitiveDraw(GLPrimitives.buildCube(stack), "Cube", cubeShader, CUBE_FLOATS);
+
+ glBindVertexArray(cubeDraw.vao);
+ glBindBuffer(GL_ARRAY_BUFFER, cubeDraw.mesh.getVbo().id);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, 3, GL_FLOAT, false, 3 * Float.BYTES, 0);
+
+ cubeDraw.instanceVbo.bind();
+ glEnableVertexAttribArray(1); // center
+ glVertexAttribPointer(1, 3, GL_FLOAT, false, cubeDraw.stride, 0);
+ glVertexAttribDivisor(1, 1);
+
+ glEnableVertexAttribArray(2); // halfExtents
+ glVertexAttribPointer(2, 3, GL_FLOAT, false, cubeDraw.stride, 3 * Float.BYTES);
+ glVertexAttribDivisor(2, 1);
+
+ glEnableVertexAttribArray(3); // argb
+ glVertexAttribIPointer(3, 1, GL_INT, cubeDraw.stride, 6 * Float.BYTES);
+ glVertexAttribDivisor(3, 1);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindVertexArray(0);
+ }
+
+ try (MemoryStack stack = MemoryStack.stackPush()) {
+ sphereDraw = new PrimitiveDraw(GLPrimitives.buildSphere(stack, 16, 32), "Sphere", sphereShader, SPHERE_FLOATS);
+
+ glBindVertexArray(sphereDraw.vao);
+ glBindBuffer(GL_ARRAY_BUFFER, sphereDraw.mesh.getVbo().id);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, 3, GL_FLOAT, false, 3 * Float.BYTES, 0);
+
+ sphereDraw.instanceVbo.bind();
+ glEnableVertexAttribArray(1); // center
+ glVertexAttribPointer(1, 3, GL_FLOAT, false, sphereDraw.stride, 0);
+ glVertexAttribDivisor(1, 1);
+
+ glEnableVertexAttribArray(2); // radius
+ glVertexAttribPointer(2, 1, GL_FLOAT, false, sphereDraw.stride, 3 * Float.BYTES);
+ glVertexAttribDivisor(2, 1);
+
+ glEnableVertexAttribArray(3); // argb
+ glVertexAttribIPointer(3, 1, GL_INT, sphereDraw.stride, 4 * Float.BYTES);
+ glVertexAttribDivisor(3, 1);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindVertexArray(0);
+ }
+
+ try (MemoryStack stack = MemoryStack.stackPush()) {
+ lineDraw = new PrimitiveDraw(GLPrimitives.buildLine(stack), "Line", lineShader, LINE_FLOATS);
+
+ glBindVertexArray(lineDraw.vao);
+ glBindBuffer(GL_ARRAY_BUFFER, lineDraw.mesh.getVbo().id);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, 3, GL_FLOAT, false, 3 * Float.BYTES, 0);
+
+ lineDraw.instanceVbo.bind();
+ glEnableVertexAttribArray(1); // start
+ glVertexAttribPointer(1, 3, GL_FLOAT, false, lineDraw.stride, 0);
+ glVertexAttribDivisor(1, 1);
+
+ glEnableVertexAttribArray(2); // end
+ glVertexAttribPointer(2, 3, GL_FLOAT, false, lineDraw.stride, 3 * Float.BYTES);
+ glVertexAttribDivisor(2, 1);
+
+ glEnableVertexAttribArray(3); // thickness
+ glVertexAttribPointer(3, 1, GL_FLOAT, false, lineDraw.stride, 6 * Float.BYTES);
+ glVertexAttribDivisor(3, 1);
+
+ glEnableVertexAttribArray(4); // argb
+ glVertexAttribIPointer(4, 1, GL_INT, lineDraw.stride, 7 * Float.BYTES);
+ glVertexAttribDivisor(4, 1);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindVertexArray(0);
+ }
+
+ try (MemoryStack stack = MemoryStack.stackPush()) {
+ textDraw = new PrimitiveDraw(GLPrimitives.buildQuad(stack), "Text", textShader, TEXT_FLOATS);
+
+ glBindVertexArray(textDraw.vao);
+ glBindBuffer(GL_ARRAY_BUFFER, textDraw.mesh.getVbo().id);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, 3, GL_FLOAT, false, 3 * Float.BYTES, 0);
+
+ textDraw.instanceVbo.bind();
+ glEnableVertexAttribArray(1); // aCenter
+ glVertexAttribPointer(1, 3, GL_FLOAT, false, textDraw.stride, 0);
+ glVertexAttribDivisor(1, 1);
+
+ glEnableVertexAttribArray(2); // aScale
+ glVertexAttribPointer(2, 1, GL_FLOAT, false, textDraw.stride, 3 * Float.BYTES);
+ glVertexAttribDivisor(2, 1);
+
+ glEnableVertexAttribArray(3); // aCharCode
+ glVertexAttribIPointer(3, 1, GL_INT, textDraw.stride, 4 * Float.BYTES);
+ glVertexAttribDivisor(3, 1);
+
+ glEnableVertexAttribArray(4); // aCharIndex
+ glVertexAttribIPointer(4, 1, GL_INT, textDraw.stride, 5 * Float.BYTES);
+ glVertexAttribDivisor(4, 1);
+
+ glEnableVertexAttribArray(5); // argb
+ glVertexAttribIPointer(5, 1, GL_INT, textDraw.stride, 6 * Float.BYTES);
+ glVertexAttribDivisor(5, 1);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindVertexArray(0);
+ }
+
+ aabbBufSolid = MemoryUtil.memAllocInt(INITIAL_CAPACITY * CUBE_FLOATS);
+ aabbBufWire = MemoryUtil.memAllocInt(INITIAL_CAPACITY * CUBE_FLOATS);
+ sphereBufSolid = MemoryUtil.memAllocInt(INITIAL_CAPACITY * SPHERE_FLOATS);
+ sphereBufWire = MemoryUtil.memAllocInt(INITIAL_CAPACITY * SPHERE_FLOATS);
+ lineBuf = MemoryUtil.memAllocInt(INITIAL_CAPACITY * LINE_FLOATS);
+ textBuf = MemoryUtil.memAllocInt(8192 * TEXT_FLOATS);
+
+ DebugDraw.INSTANCE = this;
+ }
+
+ @Override
+ public void initializeShaders(ShaderIncludes includes) throws ShaderException, IOException {
+ cubeShader.compile(includes);
+ sphereShader.compile(includes);
+ lineShader.compile(includes);
+ textShader.compile(includes);
+ }
+
+ @Override
+ public void destroyShaders() {
+ cubeShader.destroy();
+ sphereShader.destroy();
+ lineShader.destroy();
+ textShader.destroy();
+ }
+
+ @Override
+ public void destroy() {
+ DebugDraw.INSTANCE = null;
+
+ if(cubeDraw != null)
+ cubeDraw.destroy();
+ cubeDraw = null;
+
+ if(sphereDraw != null)
+ sphereDraw.destroy();
+ sphereDraw = null;
+
+ if(lineDraw != null)
+ lineDraw.destroy();
+ lineDraw = null;
+
+ if(textDraw != null)
+ textDraw.destroy();
+ textDraw = null;
+
+ if (aabbBufSolid != null) { MemoryUtil.memFree(aabbBufSolid); aabbBufSolid = null; }
+ if (aabbBufWire != null) { MemoryUtil.memFree(aabbBufWire); aabbBufWire = null; }
+ if (sphereBufSolid != null) { MemoryUtil.memFree(sphereBufSolid); sphereBufSolid = null; }
+ if (sphereBufWire != null) { MemoryUtil.memFree(sphereBufWire); sphereBufWire = null; }
+ if (lineBuf != null) { MemoryUtil.memFree(lineBuf); lineBuf = null; }
+
+ lineQueue.clear();
+ aabbQueue.clear();
+ sphereQueue.clear();
+ textQueue.clear();
+ }
+
+ @Override
+ public void draw(RenderState renderState) {
+ if (lineQueue.isEmpty() && aabbQueue.isEmpty() && sphereQueue.isEmpty() && textQueue.isEmpty())
+ return;
+
+ frameTimer.begin(RENDER_DEBUG_DRAW);
+
+ renderState.framebuffer.set(GL_DRAW_FRAMEBUFFER, plugin.fboScene);
+ renderState.viewport.set(0, 0, plugin.sceneResolution[0], plugin.sceneResolution[1]);
+ renderState.enable.set(GL_DEPTH_TEST);
+ renderState.depthFunc.set(GL_GEQUAL);
+ renderState.depthMask.set(false);
+ renderState.enable.set(GL_BLEND);
+ renderState.blendFunc.set(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ renderState.apply();
+
+ aabbBufSolid = ensureCapacity(aabbBufSolid, aabbQueue.size() * CUBE_FLOATS);
+ aabbBufWire = ensureCapacity(aabbBufWire, aabbQueue.size() * CUBE_FLOATS);
+ sphereBufSolid = ensureCapacity(sphereBufSolid, sphereQueue.size() * SPHERE_FLOATS);
+ sphereBufWire = ensureCapacity(sphereBufWire, sphereQueue.size() * SPHERE_FLOATS);
+ lineBuf = ensureCapacity(lineBuf, lineQueue.size() * LINE_FLOATS);
+
+ aabbBufSolid.clear();
+ aabbBufWire.clear();
+ sphereBufSolid.clear();
+ sphereBufWire.clear();
+ lineBuf.clear();
+ textBuf.clear();
+
+ for (Draw d : aabbQueue)
+ d.writeCube(d.filled ? aabbBufSolid : aabbBufWire);
+
+ for (Draw d : sphereQueue)
+ d.writeSphere(d.filled ? sphereBufSolid : sphereBufWire);
+
+ for (Draw d : lineQueue)
+ d.writeLine(lineBuf);
+
+ for (Draw d : textQueue)
+ d.writeText(textBuf);
+
+ renderState.enable.set(GL_CULL_FACE);
+ renderState.apply();
+
+ cubeDraw.uploadAndDraw(aabbBufSolid);
+ sphereDraw.uploadAndDraw(sphereBufSolid);
+ textDraw.uploadAndDraw(textBuf);
+
+ renderState.disable.set(GL_CULL_FACE);
+ renderState.apply();
+
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+
+ cubeDraw.uploadAndDraw(aabbBufWire);
+ sphereDraw.uploadAndDraw(sphereBufWire);
+
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+
+ lineDraw.uploadAndDraw(lineBuf);
+
+ renderState.disable.set(GL_DEPTH_TEST);
+ renderState.disable.set(GL_BLEND);
+ renderState.disable.set(GL_CULL_FACE);
+ renderState.depthMask.set(true);
+ renderState.apply();
+
+ frameTimer.end(RENDER_DEBUG_DRAW);
+
+ expireQueue(lineQueue);
+ expireQueue(aabbQueue);
+ expireQueue(sphereQueue);
+ expireQueue(textQueue);
+ }
+
+ public Draw pushDraw(PrimitiveDrawType type) {
+ Draw d = POOL.acquire();
+ switch (type) {
+ case AABB:
+ aabbQueue.add(d);
+ break;
+ case SPHERE:
+ sphereQueue.add(d);
+ break;
+ case LINE:
+ lineQueue.add(d);
+ break;
+ case TEXT:
+ textQueue.add(d);
+ break;
+ default:
+ throw new RuntimeException("Invalid draw type: " + type);
+ }
+ return d;
+ }
+
+ private void expireQueue(ConcurrentLinkedQueue queue) {
+ final float delta = plugin.deltaTime;
+ Iterator it = queue.iterator();
+ while (it.hasNext()) {
+ Draw d = it.next();
+ d.duration -= delta;
+ if (d.duration <= 0f) {
+ it.remove();
+ POOL.recycle(d);
+ }
+ }
+ }
+
+ private static IntBuffer ensureCapacity(IntBuffer buf, int requiredInts) {
+ if (buf.capacity() >= requiredInts) return buf;
+ MemoryUtil.memFree(buf);
+ int cap = Math.max(Integer.highestOneBit(requiredInts - 1) << 1, INITIAL_CAPACITY);
+ return MemoryUtil.memAllocInt(cap);
+ }
+
+ public static class Draw {
+ public float x1, y1, z1;
+ public float x2, y2, z2;
+ public float thickness;
+ public int rgb;
+ public float duration;
+ public boolean filled;
+ public String text;
+
+ private void writeCube(IntBuffer buf) {
+ buf.put(Float.floatToRawIntBits(x1))
+ .put(Float.floatToRawIntBits(y1))
+ .put(Float.floatToRawIntBits(z1))
+ .put(Float.floatToRawIntBits(x2))
+ .put(Float.floatToRawIntBits(y2))
+ .put(Float.floatToRawIntBits(z2))
+ .put(rgb);
+ }
+
+ private void writeSphere(IntBuffer buf) {
+ buf.put(Float.floatToRawIntBits(x1))
+ .put(Float.floatToRawIntBits(y1))
+ .put(Float.floatToRawIntBits(z1))
+ .put(Float.floatToRawIntBits(x2))
+ .put(rgb);
+ }
+
+ private void writeLine(IntBuffer buf) {
+ buf.put(Float.floatToRawIntBits(x1))
+ .put(Float.floatToRawIntBits(y1))
+ .put(Float.floatToRawIntBits(z1))
+ .put(Float.floatToRawIntBits(x2))
+ .put(Float.floatToRawIntBits(y2))
+ .put(Float.floatToRawIntBits(z2))
+ .put(Float.floatToRawIntBits(thickness))
+ .put(rgb);
+ }
+
+ private void writeText(IntBuffer buf) {
+ for(int c = 0; c < text.length(); c++) {
+ buf.put(Float.floatToRawIntBits(x1))
+ .put(Float.floatToRawIntBits(y1))
+ .put(Float.floatToRawIntBits(z1))
+ .put(Float.floatToRawIntBits(x2))
+ .put(text.charAt(c))
+ .put(c)
+ .put(rgb);
+ }
+ }
+ }
+
+ public enum PrimitiveDrawType {
+ AABB,
+ SPHERE,
+ LINE,
+ TEXT
+ };
+
+ private static class PrimitiveDraw {
+ final GLPrimitives.Mesh mesh;
+ final GLBuffer instanceVbo;
+ final DebugDrawShaderProgram shader;
+ final int vao;
+ final int floatsPerInstance;
+ final int stride;
+
+ PrimitiveDraw(GLPrimitives.Mesh mesh, String name, DebugDrawShaderProgram shader, int floatsPerInstance) {
+ this.mesh = mesh;
+ this.floatsPerInstance = floatsPerInstance;
+ this.stride = floatsPerInstance * Float.BYTES;
+ this.instanceVbo = new GLBuffer("VBO::" + name + "::Instances", GL_ARRAY_BUFFER, GL_DYNAMIC_DRAW);
+ this.instanceVbo.initialize((long) INITIAL_CAPACITY * stride);
+ this.shader = shader;
+ this.vao = glGenVertexArrays();
+ }
+
+ void uploadAndDraw(IntBuffer data) {
+ if(data.position() == 0)
+ return;
+ data.flip();
+
+ assert data.limit() % floatsPerInstance == 0;
+ final int count = data.limit() / floatsPerInstance;
+ if(count == 0)
+ return;
+
+ shader.use();
+ instanceVbo.upload(data);
+ glBindVertexArray(vao);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.getEbo().id);
+ glDrawElementsInstanced(GL_TRIANGLES, mesh.getIndexCount(), GL_UNSIGNED_INT, 0, count);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ glBindVertexArray(0);
+ }
+
+ void destroy() {
+ glDeleteVertexArrays(vao);
+ mesh.destroy();
+ instanceVbo.destroy();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/rs117/hd/renderer/zone/passes/DirectionalShadowPass.java b/src/main/java/rs117/hd/renderer/zone/passes/DirectionalShadowPass.java
new file mode 100644
index 0000000000..fa33382903
--- /dev/null
+++ b/src/main/java/rs117/hd/renderer/zone/passes/DirectionalShadowPass.java
@@ -0,0 +1,312 @@
+package rs117.hd.renderer.zone.passes;
+
+import java.io.IOException;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import lombok.extern.slf4j.Slf4j;
+import net.runelite.api.*;
+import net.runelite.api.hooks.*;
+import rs117.hd.HdPlugin;
+import rs117.hd.config.ShadowMode;
+import rs117.hd.opengl.shader.ShaderException;
+import rs117.hd.opengl.shader.ShaderIncludes;
+import rs117.hd.opengl.shader.ShadowShaderProgram;
+import rs117.hd.opengl.uniforms.UBOGlobal;
+import rs117.hd.overlays.FrameTimer;
+import rs117.hd.overlays.Timer;
+import rs117.hd.renderer.zone.SceneManager;
+import rs117.hd.renderer.zone.WorldViewContext;
+import rs117.hd.renderer.zone.Zone;
+import rs117.hd.renderer.zone.ZoneRenderer;
+import rs117.hd.scene.EnvironmentManager;
+import rs117.hd.scene.SceneCullingManager;
+import rs117.hd.scene.model_overrides.ModelOverride;
+import rs117.hd.utils.Camera;
+import rs117.hd.utils.CommandBuffer;
+import rs117.hd.utils.RenderState;
+import rs117.hd.utils.ShadowCasterVolume;
+
+import static net.runelite.api.Perspective.*;
+import static org.lwjgl.opengl.GL11C.GL_CULL_FACE;
+import static org.lwjgl.opengl.GL11C.GL_DEPTH_BUFFER_BIT;
+import static org.lwjgl.opengl.GL11C.GL_DEPTH_TEST;
+import static org.lwjgl.opengl.GL11C.GL_LEQUAL;
+import static org.lwjgl.opengl.GL11C.glClear;
+import static org.lwjgl.opengl.GL11C.glClearDepth;
+import static org.lwjgl.opengl.GL30C.GL_FRAMEBUFFER;
+import static org.lwjgl.opengl.GL30C.glBindVertexArray;
+import static rs117.hd.utils.MathUtils.*;
+
+@Slf4j
+@Singleton
+public class DirectionalShadowPass implements RenderPass {
+
+ public static final int DIRECTIONAL_CAMERA_ID = ZoneRenderer.CAMERA_COUNT++;
+
+ @Inject
+ private HdPlugin plugin;
+
+ @Inject
+ private ZoneRenderer renderer;
+
+ @Inject
+ private FrameTimer frameTimer;
+
+ @Inject
+ private SceneManager sceneManager;
+
+ @Inject
+ private EnvironmentManager environmentManager;
+
+ @Inject
+ private SceneCullingManager sceneCullingManager;
+
+ @Inject
+ private ShadowShaderProgram.Fast fastShadowProgram;
+
+ @Inject
+ private ShadowShaderProgram.Detailed detailedShadowProgram;
+
+ public final Camera directionalCamera = new Camera().setOrthographic(true).setCullingId(DIRECTIONAL_CAMERA_ID);
+ public final ShadowCasterVolume directionalShadowCasterVolume = new ShadowCasterVolume(directionalCamera);
+ public final CommandBuffer directionalCmd = new CommandBuffer("Directional");
+
+ private UBOGlobal uboGlobal;
+ private Camera sceneCamera;
+
+ private boolean isCameraAddedToCulling;
+ private boolean shouldDrawRoofShadows;
+ private boolean shouldClearShadowFbo;
+
+ @Override
+ public void initialize() {
+ uboGlobal = plugin.uboGlobal;
+ sceneCamera = renderer.sceneCamera;
+ shouldClearShadowFbo = true;
+
+ directionalCmd.setFrameTimer(frameTimer);
+ }
+
+ @Override
+ public void initializeShaders(ShaderIncludes includes) throws ShaderException, IOException {
+ fastShadowProgram.compile(includes);
+ detailedShadowProgram.compile(includes);
+ }
+
+ @Override
+ public void destroyShaders() {
+ fastShadowProgram.destroy();
+ detailedShadowProgram.destroy();
+ }
+
+ @Override
+ public void preSceneDraw(WorldViewContext ctx, boolean isTopLevel) {
+ if(!isTopLevel)
+ return;
+
+ directionalCamera.setPitch(environmentManager.currentSunAngles[0]);
+ directionalCamera.setYaw(PI - environmentManager.currentSunAngles[1]);
+ uboGlobal.lightDir.set(directionalCamera.getForwardDirection());
+
+ if(!plugin.configShadowsEnabled || sceneCamera.isOrthographic()) {
+ if(isCameraAddedToCulling)
+ sceneCullingManager.removeCamera(directionalCamera);
+ isCameraAddedToCulling = false;
+ return;
+ }
+
+ if(!isCameraAddedToCulling)
+ sceneCullingManager.addCamera(directionalCamera);
+ isCameraAddedToCulling = true;
+
+ float drawDistance = (float) plugin.getDrawDistance();
+ int shadowDrawDistance = 90 * LOCAL_TILE_SIZE;
+
+ final float[][] volumeCorners = directionalShadowCasterVolume
+ .build(sceneCamera, drawDistance * LOCAL_TILE_SIZE, shadowDrawDistance);
+
+ final float[] sceneCenter = new float[3];
+ for (float[] corner : volumeCorners)
+ add(sceneCenter, sceneCenter, corner);
+ divide(sceneCenter, sceneCenter, (float) volumeCorners.length);
+
+ // Reset position before transforming points
+ directionalCamera.setPosition(0, 0, 0);
+
+ float minX = Float.POSITIVE_INFINITY, maxX = Float.NEGATIVE_INFINITY;
+ float minY = Float.POSITIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY;
+ float minZ = Float.POSITIVE_INFINITY, maxZ = Float.NEGATIVE_INFINITY;
+ float radius = 0f;
+ for (float[] corner : volumeCorners) {
+ radius = max(radius, distance(sceneCenter, corner));
+
+ directionalCamera.transformPoint(corner, corner);
+
+ minX = min(minX, corner[0]);
+ maxX = max(maxX, corner[0]);
+
+ minY = min(minY, corner[1]);
+ maxY = max(maxY, corner[1]);
+
+ minZ = min(minZ, corner[2]);
+ maxZ = max(maxZ, corner[2]);
+ }
+
+ // Offset the Directional Camera by the radius of the scene
+ float[] directionalFwd = directionalCamera.getForwardDirection();
+ multiply(directionalFwd, directionalFwd, radius);
+ add(sceneCenter, sceneCenter, directionalFwd);
+
+ // Calculate directional size from the AABB of the scene frustum corners
+ // Then snap to the nearest multiple of `LOCAL_HALF_TILE_SIZE` to prevent shimmering
+ int directionalSize = (int) max(abs(maxY - minY), abs(maxX - minX), abs(maxZ - minZ));
+ directionalSize = Math.round(directionalSize / (float) LOCAL_HALF_TILE_SIZE) * LOCAL_HALF_TILE_SIZE;
+ directionalSize = max(8000, directionalSize); // Clamp the size to prevent going too small at reduced draw distances
+
+ // Ignore directional size changes below the change threshold to avoid inducing shimmering
+ int previousDirectionalSize = directionalCamera.getViewportWidth();
+ float changeThreshold = previousDirectionalSize * 0.05f; // 10% of the previous directional size
+ if (abs(directionalSize - previousDirectionalSize) < changeThreshold)
+ directionalSize = previousDirectionalSize;
+
+ // Snap Position to Shadow Texel Grid to prevent shimmering
+ directionalCamera.transformPoint(sceneCenter, sceneCenter);
+
+ float texelSize = (float) directionalSize / plugin.shadowMapResolution;
+ sceneCenter[0] = (float) floor(sceneCenter[0] / texelSize + 0.5f) * texelSize;
+ sceneCenter[1] = (float) floor(sceneCenter[1] / texelSize + 0.5f) * texelSize;
+
+ directionalCamera.setPosition(directionalCamera.inverseTransformPoint(sceneCenter, sceneCenter));
+ directionalCamera.setNearPlane(Math.max(0.1f, radius * 0.05f));
+ directionalCamera.setFarPlane(radius * 2.0f);
+ directionalCamera.setZoom(1.0f);
+ directionalCamera.setViewportWidth(directionalSize);
+ directionalCamera.setViewportHeight(directionalSize);
+
+ uboGlobal.lightProjectionMatrix.set(directionalCamera.getViewProjMatrix());
+ uboGlobal.upload();
+
+ shouldDrawRoofShadows =
+ plugin.configShadowsEnabled &&
+ plugin.configRoofShadows &&
+ environmentManager.allowRoofShadows();
+
+ directionalCmd.reset();
+ }
+
+ @Override
+ public boolean zoneInFrustum(Zone z, int zx, int zz, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
+ if(!plugin.configShadowsEnabled)
+ return false;
+
+ if(z.isVisible(sceneCamera))
+ return true;
+
+ boolean isVisible = z.isVisible(directionalCamera);
+ if (z.isVisible(directionalCamera) && plugin.configExpandShadowDraw) {
+ int centerX = minX + (maxX - minX) / 2;
+ int centerY = minY + (maxY - minY) / 2;
+ int centerZ = minZ + (maxZ - minZ) / 2;
+ isVisible = directionalShadowCasterVolume.intersectsPoint(centerX, centerY, centerZ);
+ }
+
+ return z.setVisibility(directionalCamera, isVisible);
+ }
+
+ @Override
+ public void drawZoneOpaque(WorldViewContext ctx, Zone z, int zx, int zz) {
+ if(!plugin.configShadowsEnabled)
+ return;
+
+ if(sceneManager.isRoot(ctx) && !z.isVisible(directionalCamera))
+ return;
+
+ final boolean isSquashed = ctx.uboWorldViewStruct != null && ctx.uboWorldViewStruct.isSquashed();
+ if (!isSquashed) {
+ directionalCmd.SetShader(fastShadowProgram);
+ z.renderOpaque(directionalCmd, ctx, directionalCamera, shouldDrawRoofShadows);
+ }
+ }
+
+ @Override
+ public void drawZoneAlpha(WorldViewContext ctx, Zone z, int level, int zx, int zz) {
+ if(!plugin.configShadowsEnabled)
+ return;
+
+ if(sceneManager.isRoot(ctx) && !z.isVisible(directionalCamera))
+ return;
+
+ if (z.sizeA == 0 || z.alphaModels.isEmpty())
+ return;
+
+ final int offset = ctx.sceneContext.sceneOffset >> 3;
+ final boolean isSquashed = ctx.uboWorldViewStruct != null && ctx.uboWorldViewStruct.isSquashed();
+ if (!isSquashed) {
+ directionalCmd.SetShader(plugin.configShadowMode == ShadowMode.DETAILED ? detailedShadowProgram : fastShadowProgram);
+ z.renderAlpha(directionalCmd, zx - offset, zz - offset, level, ctx, directionalCamera, true, shouldDrawRoofShadows);
+ }
+ }
+
+ @Override
+ public boolean dynamicInFrustum(WorldViewContext ctx, Renderable renderable, Model model, ModelOverride modelOverride, int x, int y, int z) {
+ if(!plugin.configShadowsEnabled || !modelOverride.castShadows)
+ return false;
+
+ return directionalShadowCasterVolume.intersectsPoint(x, y, z);
+ }
+
+ @Override
+ public void drawPass(WorldViewContext ctx, int pass) {
+ if(!plugin.configShadowsEnabled)
+ return;
+
+ if(pass == DrawCallbacks.PASS_OPAQUE) {
+ directionalCmd.SetShader(fastShadowProgram);
+ directionalCmd.ExecuteSubCommandBuffer(ctx.vaoDirectionalCmd);
+ }
+ }
+
+ public void draw(RenderState renderState) {
+ if(plugin.fboShadowMap == 0 || plugin.shadowMapResolution == 0)
+ return;
+
+ final boolean shouldRenderShadows =
+ plugin.configShadowsEnabled &&
+ environmentManager.currentDirectionalStrength > 0;
+
+ if (shouldRenderShadows || shouldClearShadowFbo) {
+ // Render to the shadow depth map
+ renderState.framebuffer.set(GL_FRAMEBUFFER, plugin.fboShadowMap);
+ renderState.viewport.set(0, 0, plugin.shadowMapResolution, plugin.shadowMapResolution);
+ renderState.apply();
+
+ glClearDepth(1);
+ glClear(GL_DEPTH_BUFFER_BIT);
+ shouldClearShadowFbo = false;
+ }
+
+ if (!shouldRenderShadows)
+ return;
+
+ frameTimer.begin(Timer.RENDER_SHADOWS);
+
+ renderState.enable.set(GL_DEPTH_TEST);
+ renderState.disable.set(GL_CULL_FACE);
+ renderState.depthFunc.set(GL_LEQUAL);
+ renderState.ido.set(renderer.indirectDrawCmds.id);
+
+ CommandBuffer.SKIP_DEPTH_MASKING = true;
+ directionalCmd.execute(renderState);
+ CommandBuffer.SKIP_DEPTH_MASKING = false;
+
+ glBindVertexArray(0);
+
+ renderState.disable.set(GL_DEPTH_TEST);
+
+ shouldClearShadowFbo = true;
+ frameTimer.end(Timer.RENDER_SHADOWS);
+ }
+
+ @Override
+ public RenderPassType getType() { return RenderPassType.DIRECTIONAL; }
+}
diff --git a/src/main/java/rs117/hd/renderer/zone/passes/RenderPass.java b/src/main/java/rs117/hd/renderer/zone/passes/RenderPass.java
new file mode 100644
index 0000000000..3581d47e23
--- /dev/null
+++ b/src/main/java/rs117/hd/renderer/zone/passes/RenderPass.java
@@ -0,0 +1,52 @@
+package rs117.hd.renderer.zone.passes;
+
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.Set;
+import net.runelite.api.*;
+import rs117.hd.opengl.shader.ShaderException;
+import rs117.hd.opengl.shader.ShaderIncludes;
+import rs117.hd.renderer.zone.WorldViewContext;
+import rs117.hd.renderer.zone.Zone;
+import rs117.hd.scene.model_overrides.ModelOverride;
+import rs117.hd.utils.RenderState;
+
+public interface RenderPass {
+ RenderPassType[] TYPES = RenderPassType.values();
+
+ RenderPassType getType();
+
+ default void initialize() {}
+
+ default void initializeShaders(ShaderIncludes includes) throws ShaderException, IOException {}
+
+ default void destroyShaders() {}
+
+ default void destroy() {}
+
+ default void addShaderIncludes(ShaderIncludes includes) {}
+
+ default void processConfigChanges(Set keys) {}
+
+ default boolean zoneInFrustum(Zone z, int zx, int zz, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
+ return false;
+ }
+
+ default boolean dynamicInFrustum(WorldViewContext ctx, Renderable renderable, Model model, ModelOverride modelOverride, int x, int y, int z) {
+ return false;
+ }
+
+ default void drawZoneOpaque(WorldViewContext ctx, Zone z, int zx, int zz) {}
+
+ default void drawZoneAlpha(WorldViewContext ctx, Zone z, int level, int zx, int zz) {}
+
+ default void drawPass(WorldViewContext ctx, int pass) {}
+
+ default void preSceneDraw(WorldViewContext ctx, boolean isTopLevel) {}
+
+ default void postSceneDraw(WorldViewContext ctx) {}
+
+ default void draw(RenderState renderState) {}
+
+ default void postDraw(RenderState renderState) {}
+}
diff --git a/src/main/java/rs117/hd/renderer/zone/passes/RenderPassType.java b/src/main/java/rs117/hd/renderer/zone/passes/RenderPassType.java
new file mode 100644
index 0000000000..3b2d5bf95b
--- /dev/null
+++ b/src/main/java/rs117/hd/renderer/zone/passes/RenderPassType.java
@@ -0,0 +1,20 @@
+package rs117.hd.renderer.zone.passes;
+
+import rs117.hd.overlays.Timer;
+
+public enum RenderPassType {
+ TILED_LIGHTING(TiledLightingPass.class, Timer.TILED_LIGHTING_PASS),
+ DIRECTIONAL(DirectionalShadowPass.class, Timer.DIRECTIONAL_PASS),
+ SCENE(ScenePass.class, Timer.SCENE_PASS),
+ DEBUG_DRAW(DebugDrawPass.class, Timer.DEBUG_DRAW_PASS);
+
+ public final Class extends RenderPass> clazz;
+ public final String name;
+ public final Timer timer;
+
+ RenderPassType(Class extends RenderPass> clazz, Timer timer) {
+ this.clazz = clazz;
+ this.timer = timer;
+ this.name = clazz.getSimpleName();
+ }
+}
diff --git a/src/main/java/rs117/hd/renderer/zone/passes/RenderPipeline.java b/src/main/java/rs117/hd/renderer/zone/passes/RenderPipeline.java
new file mode 100644
index 0000000000..f1f57c8eea
--- /dev/null
+++ b/src/main/java/rs117/hd/renderer/zone/passes/RenderPipeline.java
@@ -0,0 +1,390 @@
+package rs117.hd.renderer.zone.passes;
+
+import com.google.inject.Injector;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.runelite.api.*;
+import rs117.hd.HdPlugin;
+import rs117.hd.opengl.shader.ShaderIncludes;
+import rs117.hd.overlays.FrameTimer;
+import rs117.hd.renderer.zone.WorldViewContext;
+import rs117.hd.renderer.zone.Zone;
+import rs117.hd.scene.model_overrides.ModelOverride;
+import rs117.hd.utils.RenderState;
+
+import static rs117.hd.HdPlugin.checkGLErrors;
+import static rs117.hd.utils.collections.Util.quickSort;
+
+@Slf4j
+@Singleton
+public final class RenderPipeline {
+
+ @Inject
+ private Injector injector;
+
+ @Inject
+ private HdPlugin plugin;
+
+ @Inject
+ private FrameTimer frameTimer;
+
+ public final InitializeFunction initialize = new InitializeFunction();
+ public final InitializeShadersFunction initializeShaders = new InitializeShadersFunction();
+ public final DestroyFunction destroy = new DestroyFunction();
+ public final DestroyShadersFunction destroyShaders = new DestroyShadersFunction();
+ public final AddShaderIncludesFunction addShaderIncludes = new AddShaderIncludesFunction();
+ public final ProcessConfigChangesFunction processConfigChanges = new ProcessConfigChangesFunction();
+ public final PreSceneDrawFunction preSceneDraw = new PreSceneDrawFunction();
+ public final PostSceneDrawFunction postSceneDraw = new PostSceneDrawFunction();
+ public final ZoneInFrustumFunction zoneInFrustum = new ZoneInFrustumFunction();
+ public final DynamicInFrustumFunction dynamicInFrustum = new DynamicInFrustumFunction();
+ public final DrawZoneOpaqueFunction drawZoneOpaque = new DrawZoneOpaqueFunction();
+ public final DrawZoneAlphaFunction drawZoneAlpha = new DrawZoneAlphaFunction();
+ public final DrawPassFunction drawPass = new DrawPassFunction();
+ public final DrawFunction draw = new DrawFunction();
+ public final PostDrawFunction postDraw = new PostDrawFunction();
+
+ private final int passCount = RenderPass.TYPES.length;
+ private final RenderPassType[] types = new RenderPassType[passCount];
+ private final RenderPass[] passes = new RenderPass[passCount];
+
+ public void initialize() {
+ for(int i = 0; i < passCount; i++)
+ passes[i] = injector.getInstance(RenderPass.TYPES[i].clazz);
+ quickSort(passes, Comparator.comparingInt((A) -> A.getType().ordinal()));
+
+ for(int i = 0; i < passCount; i++)
+ types[i] = passes[i].getType();
+
+ initialize.execute();
+ }
+
+ public void destroy() {
+ destroy.execute();
+ Arrays.fill(passes, null);
+ }
+
+ private boolean internalExecute(BaseRenderPassFunction function) {
+ final boolean detailedTimers = plugin.enableDetailedTimers;
+ boolean result = false;
+
+ for (int i = 0; i < passCount; i++) {
+ final RenderPass renderPass = passes[i];
+ final RenderPassType type = types[i];
+
+ if (renderPass == null || type == null)
+ continue;
+
+ try {
+ if(detailedTimers)
+ frameTimer.begin(type.timer);
+
+ result |= function.consumer.accept(renderPass);
+ } catch (Throwable e) {
+ log.error("Error during {} for render pass {}:", function.action, type.name, e);
+ if (!function.handleExceptions)
+ throw new RuntimeException(e);
+ plugin.requestPluginStop();
+ } finally {
+ if(detailedTimers)
+ frameTimer.end(type.timer);
+
+ if (function.checkGL)
+ checkGLErrors(() -> type.name + "::" + function.action);
+ }
+ }
+ return result;
+ }
+
+ public final class InitializeFunction extends BaseRenderPassFunction {
+ private InitializeFunction() {
+ super("initializeShaders", false, false);
+ consumer = (renderPass) -> {
+ renderPass.initialize();
+ return true;
+ };
+ }
+ }
+
+ public final class InitializeShadersFunction extends BaseRenderPassFunction {
+
+ private ShaderIncludes includes;
+
+ private InitializeShadersFunction() {
+ super("initializeShaders", false, false);
+ consumer = renderPass -> {
+ renderPass.initializeShaders(includes);
+ return true;
+ };
+ }
+
+ public void execute(ShaderIncludes includes) {
+ this.includes = includes;
+ execute();
+ }
+ }
+
+ public final class DestroyFunction extends BaseRenderPassFunction {
+ private DestroyFunction() {
+ super("destroy", false, true);
+ consumer = renderPass -> {
+ renderPass.destroy();
+ return true;
+ };
+ }
+ }
+
+ public final class DestroyShadersFunction extends BaseRenderPassFunction {
+ private DestroyShadersFunction() {
+ super("destroyShaders", false, true);
+ consumer = renderPass -> {
+ renderPass.destroyShaders();
+ return true;
+ };
+ }
+ }
+
+ public final class AddShaderIncludesFunction extends BaseRenderPassFunction {
+
+ private ShaderIncludes includes;
+
+ private AddShaderIncludesFunction() {
+ super("addShaderIncludes", false, false);
+ consumer = renderPass -> {
+ renderPass.addShaderIncludes(includes);
+ return true;
+ };
+ }
+
+ public void execute(ShaderIncludes includes) {
+ this.includes = includes;
+ execute();
+ }
+ }
+
+ public final class ProcessConfigChangesFunction extends BaseRenderPassFunction {
+ private Set keys;
+
+ private ProcessConfigChangesFunction() {
+ super("processConfigChanges", false, false);
+ consumer = renderPass -> {
+ renderPass.processConfigChanges(keys);
+ return true;
+ };
+ }
+
+ public void execute(Set keys) {
+ this.keys = keys;
+ execute();
+ }
+ }
+
+ public final class PreSceneDrawFunction extends BaseRenderPassFunction {
+
+ private WorldViewContext ctx;
+ private boolean isTopLevel;
+
+ private PreSceneDrawFunction() {
+ super("preSceneDraw", true, false);
+ consumer = renderPass -> {
+ renderPass.preSceneDraw(ctx, isTopLevel);
+ return true;
+ };
+ }
+
+ public void execute(WorldViewContext ctx, boolean isTopLevel) {
+ this.ctx = ctx;
+ this.isTopLevel = isTopLevel;
+ execute();
+ }
+ }
+
+ public final class PostSceneDrawFunction extends BaseRenderPassFunction {
+
+ private WorldViewContext ctx;
+
+ private PostSceneDrawFunction() {
+ super("postSceneDraw", true, false);
+ consumer = renderPass -> {
+ renderPass.postSceneDraw(ctx);
+ return true;
+ };
+ }
+
+ public void execute(WorldViewContext ctx) {
+ this.ctx = ctx;
+ execute();
+ }
+ }
+
+ public final class ZoneInFrustumFunction extends BaseRenderPassFunction {
+
+ private Zone zone;
+ private int zx, zz, minX, minY, minZ, maxX, maxY, maxZ;
+
+ private ZoneInFrustumFunction() {
+ super("zoneInFrustum", false, true);
+ consumer = renderPass -> renderPass.zoneInFrustum(zone, zx, zz, minX, minY, minZ, maxX, maxY, maxZ);
+ }
+
+ public boolean execute(Zone zone, int zx, int zz, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
+ this.zone = zone;
+ this.zx = zx;
+ this.zz = zz;
+ this.minX = minX;
+ this.minY = minY;
+ this.minZ = minZ;
+ this.maxX = maxX;
+ this.maxY = maxY;
+ this.maxZ = maxZ;
+ return execute();
+ }
+ }
+
+ public final class DynamicInFrustumFunction extends BaseRenderPassFunction {
+
+ private WorldViewContext ctx;
+ private Renderable renderable;
+ private Model model;
+ private ModelOverride modelOverride;
+ private int x, y, z;
+
+ private DynamicInFrustumFunction() {
+ super("dynamicInFrustum", false, true);
+ consumer = renderPass -> renderPass.dynamicInFrustum(ctx, renderable, model, modelOverride, x, y, z);
+ }
+
+ public boolean execute(WorldViewContext ctx, Renderable renderable, Model model, ModelOverride modelOverride, int x, int y, int z) {
+ this.ctx = ctx;
+ this.renderable = renderable;
+ this.model = model;
+ this.modelOverride = modelOverride;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ return execute();
+ }
+ }
+
+ public final class DrawZoneOpaqueFunction extends BaseRenderPassFunction {
+ private WorldViewContext ctx;
+ private Zone zone;
+ private int zx, zz;
+
+ private DrawZoneOpaqueFunction() {
+ super("drawZoneOpaque", false, false);
+ consumer = renderPass -> {
+ renderPass.drawZoneOpaque(ctx, zone, zx, zz);
+ return true;
+ };
+ }
+
+ public void execute(WorldViewContext ctx, Zone zone, int zx, int zz) {
+ this.ctx = ctx;
+ this.zone = zone;
+ this.zx = zx;
+ this.zz = zz;
+ execute();
+ }
+ }
+
+ public final class DrawZoneAlphaFunction extends BaseRenderPassFunction {
+ private WorldViewContext ctx;
+ private Zone zone;
+ private int level, zx, zz;
+
+ private DrawZoneAlphaFunction() {
+ super("drawZoneAlpha", false, false);
+ consumer = (renderPass) -> {
+ renderPass.drawZoneAlpha(ctx, zone, level, zx, zz);
+ return true;
+ };
+ }
+
+ public void execute(WorldViewContext ctx, Zone zone, int level, int zx, int zz) {
+ this.ctx = ctx;
+ this.zone = zone;
+ this.level = level;
+ this.zx = zx;
+ this.zz = zz;
+ execute();
+ }
+ }
+
+ public final class DrawPassFunction extends BaseRenderPassFunction {
+
+ private WorldViewContext ctx;
+ private int pass;
+
+ private DrawPassFunction() {
+ super("drawPass", true, false);
+ consumer = (renderPass) -> {
+ renderPass.drawPass(ctx, pass);
+ return true;
+ };
+ }
+
+ public void execute(WorldViewContext ctx, int pass) {
+ this.ctx = ctx;
+ this.pass = pass;
+ execute();
+ }
+ }
+
+ public final class DrawFunction extends BaseRenderPassFunction {
+
+ private RenderState renderState;
+ private DrawFunction() {
+ super("draw", true, false);
+
+ consumer = (renderPass) -> {
+ renderPass.draw(renderState);
+ return true;
+ };
+ }
+
+ public void execute(RenderState renderState) {
+ this.renderState = renderState;
+ execute();
+ }
+ }
+
+ public final class PostDrawFunction extends BaseRenderPassFunction {
+ private RenderState renderState;
+
+ private PostDrawFunction() {
+ super("postDraw", true, false);
+ consumer = (renderPass) -> {
+ renderPass.postDraw(renderState);
+ return true;
+ };
+ }
+
+ public void execute(RenderState renderState) {
+ this.renderState = renderState;
+ execute();
+ }
+ }
+
+ @FunctionalInterface
+ private interface RenderPassConsumer {
+ boolean accept(RenderPass renderPass) throws Throwable;
+ }
+
+ @RequiredArgsConstructor
+ public abstract class BaseRenderPassFunction {
+ private final String action;
+ private final boolean checkGL;
+ private final boolean handleExceptions;
+ protected RenderPassConsumer consumer;
+
+ public boolean execute() {
+ return internalExecute(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/rs117/hd/renderer/zone/passes/ScenePass.java b/src/main/java/rs117/hd/renderer/zone/passes/ScenePass.java
new file mode 100644
index 0000000000..140e0b80fe
--- /dev/null
+++ b/src/main/java/rs117/hd/renderer/zone/passes/ScenePass.java
@@ -0,0 +1,234 @@
+package rs117.hd.renderer.zone.passes;
+
+import java.io.IOException;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import lombok.extern.slf4j.Slf4j;
+import net.runelite.api.*;
+import net.runelite.api.hooks.*;
+import rs117.hd.HdPlugin;
+import rs117.hd.opengl.shader.SceneShaderProgram;
+import rs117.hd.opengl.shader.ShaderException;
+import rs117.hd.opengl.shader.ShaderIncludes;
+import rs117.hd.overlays.FrameTimer;
+import rs117.hd.overlays.Timer;
+import rs117.hd.renderer.zone.ModelStreamingManager;
+import rs117.hd.renderer.zone.SceneManager;
+import rs117.hd.renderer.zone.WorldViewContext;
+import rs117.hd.renderer.zone.Zone;
+import rs117.hd.renderer.zone.ZoneRenderer;
+import rs117.hd.scene.EnvironmentManager;
+import rs117.hd.scene.SceneCullingManager;
+import rs117.hd.scene.model_overrides.ModelOverride;
+import rs117.hd.utils.Camera;
+import rs117.hd.utils.ColorUtils;
+import rs117.hd.utils.CommandBuffer;
+import rs117.hd.utils.RenderState;
+
+import static net.runelite.api.Perspective.*;
+import static org.lwjgl.opengl.GL11C.GL_BLEND;
+import static org.lwjgl.opengl.GL11C.GL_COLOR_BUFFER_BIT;
+import static org.lwjgl.opengl.GL11C.GL_CULL_FACE;
+import static org.lwjgl.opengl.GL11C.GL_DEPTH_BUFFER_BIT;
+import static org.lwjgl.opengl.GL11C.GL_DEPTH_TEST;
+import static org.lwjgl.opengl.GL11C.GL_GEQUAL;
+import static org.lwjgl.opengl.GL11C.GL_ONE;
+import static org.lwjgl.opengl.GL11C.GL_ONE_MINUS_SRC_ALPHA;
+import static org.lwjgl.opengl.GL11C.GL_SRC_ALPHA;
+import static org.lwjgl.opengl.GL11C.GL_ZERO;
+import static org.lwjgl.opengl.GL11C.glClear;
+import static org.lwjgl.opengl.GL11C.glClearColor;
+import static org.lwjgl.opengl.GL11C.glClearDepth;
+import static org.lwjgl.opengl.GL13C.GL_MULTISAMPLE;
+import static org.lwjgl.opengl.GL30C.GL_DRAW_FRAMEBUFFER;
+import static org.lwjgl.opengl.GL30C.glBindVertexArray;
+import static rs117.hd.renderer.zone.WorldViewContext.VAO_PRESCENE;
+import static rs117.hd.utils.MathUtils.*;
+
+@Slf4j
+@Singleton
+public class ScenePass implements RenderPass {
+
+ public static final int ZONE_VISIBILITY_PADDING = 4 * LOCAL_TILE_SIZE;
+
+ @Inject
+ private HdPlugin plugin;
+
+ @Inject
+ private ZoneRenderer renderer;
+
+ @Inject
+ private SceneManager sceneManager;
+
+ @Inject
+ private FrameTimer frameTimer;
+
+ @Inject
+ private EnvironmentManager environmentManager;
+
+ @Inject
+ private SceneCullingManager sceneCullingManager;
+
+ @Inject
+ private ModelStreamingManager modelStreamingManager;
+
+ @Inject
+ private SceneShaderProgram sceneProgram;
+
+ public final CommandBuffer sceneCmd = new CommandBuffer("Scene");
+ public final CommandBuffer gapFillerCmd = new CommandBuffer("GapFiller");
+
+ private Camera sceneCamera;
+
+ @Override
+ public void initialize() {
+ sceneCamera = renderer.sceneCamera;
+
+ sceneCullingManager.addCamera(sceneCamera);
+
+ sceneCmd.setFrameTimer(frameTimer);
+ gapFillerCmd.setFrameTimer(frameTimer);
+ }
+
+ @Override
+ public void initializeShaders(ShaderIncludes includes) throws ShaderException, IOException {
+ sceneProgram.compile(includes);
+ }
+
+ @Override
+ public void destroy() {
+ sceneCullingManager.removeCamera(sceneCamera);
+ }
+
+ @Override
+ public void destroyShaders() {
+ sceneProgram.destroy();
+ }
+
+ @Override
+ public void preSceneDraw(WorldViewContext ctx, boolean isTopLevel) {
+ final Scene scene = ctx.sceneContext.scene;
+ if(scene.getWorldViewId() != WorldView.TOPLEVEL)
+ return;
+
+ gapFillerCmd.reset();
+ sceneCmd.reset();
+
+ Model skybox = scene.getSkybox();
+ if (skybox != null) {
+ skybox.calculateBoundsCylinder();
+ modelStreamingManager.uploadTempModel(
+ ctx,
+ sceneCamera,
+ null,
+ skybox,
+ ModelOverride.UNLIT,
+ skybox,
+ null,
+ null,
+ true,
+ VAO_PRESCENE,
+ -1,
+ 0,
+ sceneCamera.getPositionX(), sceneCamera.getPositionY(), sceneCamera.getPositionZ()
+ );
+ }
+
+ sceneCmd.DepthMask(false);
+ ctx.drawAll(VAO_PRESCENE, sceneCmd);
+ sceneCmd.DepthMask(true);
+ }
+
+ @Override
+ public void drawZoneOpaque(WorldViewContext ctx, Zone z, int zx, int zz) {
+ if (sceneManager.isRoot(ctx) && !z.isVisible(sceneCamera))
+ return;
+
+ z.renderOpaque(sceneCmd, ctx, sceneCamera, false);
+
+ if (z.hasGapFiller)
+ z.renderOpaqueLevel(gapFillerCmd, Zone.LEVEL_GAP_FILLER);
+ }
+
+ @Override
+ public void drawZoneAlpha(WorldViewContext ctx, Zone z, int level, int zx, int zz) {
+ if (sceneManager.isRoot(ctx) && !z.isVisible(sceneCamera))
+ return;
+
+ if (level == 0 && z.hasWater)
+ z.renderOpaqueLevel(sceneCmd, Zone.LEVEL_WATER_SURFACE);
+
+ if (z.sizeA != 0 || !z.alphaModels.isEmpty()) {
+ final int offset = ctx.sceneContext.sceneOffset >> 3;
+ z.renderAlpha(sceneCmd, zx - offset, zz - offset, level, ctx, sceneCamera, false, false);
+ }
+ }
+
+ @Override
+ public void drawPass(WorldViewContext ctx, int pass) {
+ if(pass == DrawCallbacks.PASS_OPAQUE)
+ sceneCmd.ExecuteSubCommandBuffer(ctx.vaoSceneCmd);
+ }
+
+ @Override
+ public void draw(RenderState renderState) {
+ sceneProgram.use();
+
+ frameTimer.begin(Timer.DRAW_SCENE);
+ renderState.framebuffer.set(GL_DRAW_FRAMEBUFFER, plugin.fboScene);
+ if (plugin.msaaSamples > 1) {
+ renderState.enable.set(GL_MULTISAMPLE);
+ } else {
+ renderState.disable.set(GL_MULTISAMPLE);
+ }
+ renderState.viewport.set(0, 0, plugin.sceneResolution[0], plugin.sceneResolution[1]);
+ renderState.ido.set(renderer.indirectDrawCmds.id);
+ renderState.apply();
+
+ // Clear scene
+ frameTimer.begin(Timer.CLEAR_SCENE);
+
+ float[] fogColor = ColorUtils.linearToSrgb(environmentManager.currentFogColor);
+ float[] gammaCorrectedFogColor = pow(fogColor, plugin.getGammaCorrection());
+ glClearColor(
+ gammaCorrectedFogColor[0],
+ gammaCorrectedFogColor[1],
+ gammaCorrectedFogColor[2],
+ 1f
+ );
+ glClearDepth(0);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+ frameTimer.end(Timer.CLEAR_SCENE);
+
+ frameTimer.begin(Timer.RENDER_SCENE);
+
+ renderState.enable.set(GL_BLEND);
+ renderState.enable.set(GL_CULL_FACE);
+ renderState.enable.set(GL_DEPTH_TEST);
+ renderState.depthFunc.set(GL_GEQUAL);
+ renderState.blendFunc.set(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE);
+
+ if (!gapFillerCmd.isEmpty()) {
+ renderState.depthMask.set(false);
+ gapFillerCmd.execute(renderState);
+ renderState.depthMask.set(true);
+ }
+
+ sceneCmd.execute(renderState);
+
+ frameTimer.end(Timer.RENDER_SCENE);
+
+ glBindVertexArray(0);
+
+ // Done rendering the scene
+ renderState.disable.set(GL_BLEND);
+ renderState.disable.set(GL_CULL_FACE);
+ renderState.disable.set(GL_DEPTH_TEST);
+ renderState.apply();
+
+ frameTimer.end(Timer.DRAW_SCENE);
+ }
+
+ @Override
+ public RenderPassType getType() { return RenderPassType.SCENE; }
+}
diff --git a/src/main/java/rs117/hd/renderer/zone/passes/TiledLightingPass.java b/src/main/java/rs117/hd/renderer/zone/passes/TiledLightingPass.java
new file mode 100644
index 0000000000..a37b73fe29
--- /dev/null
+++ b/src/main/java/rs117/hd/renderer/zone/passes/TiledLightingPass.java
@@ -0,0 +1,63 @@
+package rs117.hd.renderer.zone.passes;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import lombok.extern.slf4j.Slf4j;
+import rs117.hd.HdPlugin;
+import rs117.hd.config.DynamicLights;
+import rs117.hd.overlays.FrameTimer;
+import rs117.hd.overlays.Timer;
+import rs117.hd.utils.RenderState;
+
+import static org.lwjgl.opengl.GL11C.GL_NONE;
+import static org.lwjgl.opengl.GL11C.GL_TRIANGLES;
+import static org.lwjgl.opengl.GL11C.glDrawArrays;
+import static org.lwjgl.opengl.GL30C.GL_COLOR_ATTACHMENT0;
+import static org.lwjgl.opengl.GL30C.GL_FRAMEBUFFER;
+
+@Slf4j
+@Singleton
+public class TiledLightingPass implements RenderPass {
+
+ @Inject
+ private HdPlugin plugin;
+
+ @Inject
+ private FrameTimer frameTimer;
+
+ @Override
+ public void draw(RenderState renderState) {
+ if (!plugin.configTiledLighting || plugin.configDynamicLights == DynamicLights.NONE)
+ return;
+
+ plugin.updateTiledLightingFbo();
+ assert plugin.fboTiledLighting != 0;
+
+ frameTimer.begin(Timer.RENDER_TILED_LIGHTING);
+
+ renderState.framebuffer.set(GL_FRAMEBUFFER, plugin.fboTiledLighting);
+ renderState.viewport.set(0, 0, plugin.tiledLightingResolution[0], plugin.tiledLightingResolution[1]);
+ renderState.vao.setVao(plugin.vaoTri);
+
+ if (plugin.tiledLightingImageStoreProgram.isValid()) {
+ renderState.program.set(plugin.tiledLightingImageStoreProgram);
+ renderState.drawBuffer.set(GL_NONE);
+ renderState.apply();
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+ } else {
+ renderState.drawBuffer.set(GL_COLOR_ATTACHMENT0);
+ int layerCount = plugin.configDynamicLights.getTiledLightingLayers();
+ for (int layer = 0; layer < layerCount; layer++) {
+ renderState.program.set(plugin.tiledLightingShaderPrograms.get(layer));
+ renderState.framebufferTextureLayer.set(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, plugin.texTiledLighting, 0, layer);
+ renderState.apply();
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+ }
+ }
+
+ frameTimer.end(Timer.RENDER_TILED_LIGHTING);
+ }
+
+ @Override
+ public RenderPassType getType() { return RenderPassType.TILED_LIGHTING; }
+}
diff --git a/src/main/java/rs117/hd/scene/SceneCullingManager.java b/src/main/java/rs117/hd/scene/SceneCullingManager.java
new file mode 100644
index 0000000000..9a1437a670
--- /dev/null
+++ b/src/main/java/rs117/hd/scene/SceneCullingManager.java
@@ -0,0 +1,332 @@
+package rs117.hd.scene;
+
+import com.google.inject.Inject;
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+import javax.inject.Singleton;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import net.runelite.api.*;
+import rs117.hd.HdPlugin;
+import rs117.hd.utils.Camera;
+import rs117.hd.utils.DebugDraw;
+import rs117.hd.utils.collections.ConcurrentPool;
+import rs117.hd.utils.jobs.Job;
+
+import static rs117.hd.utils.MathUtils.*;
+
+@Singleton
+@Slf4j
+public class SceneCullingManager {
+ private static final int SCRATCH_SIZE = 32;
+
+ private final ConcurrentPool CULLING_JOB_POOL = new ConcurrentPool<>(CullingJob::new);
+ private final ConcurrentPool CULLING_SPHERE_POOL = new ConcurrentPool<>(CullingSphere::new);
+ private final ConcurrentPool CULLING_AABB_POOL = new ConcurrentPool<>(CullingAABB::new);
+
+ private final List pendingCullingResults = new ArrayList<>();
+ private final List cullingCameras = new ArrayList<>();
+ private final float[] debugProjected = new float[4];
+ private final float[] debugScratch = new float[SCRATCH_SIZE];
+
+ @Inject
+ private Client client;
+
+ public void addCamera(Camera camera) {
+ assert cullingCameras.size() < 8 : "SceneCullingManager supports at most 8 cameras";
+ if(!cullingCameras.contains(camera))
+ cullingCameras.add(camera);
+ }
+
+ public void removeCamera(Camera camera) {
+ cullingCameras.remove(camera);
+ }
+
+ public synchronized CullingSphere obtainSphere(float x, float y, float z, float radius) {
+ final CullingSphere result = CULLING_SPHERE_POOL.acquire();
+ result.x = x;
+ result.y = y;
+ result.z = z;
+ result.radius = radius;
+
+ return result;
+ }
+
+ public synchronized CullingAABB obtainBox(
+ float minX,
+ float minY,
+ float minZ,
+ float maxX,
+ float maxY,
+ float maxZ
+ ) {
+ if (minX > maxX) {
+ float t = minX;
+ minX = maxX;
+ maxX = t;
+ }
+
+ if (minY > maxY) {
+ float t = minY;
+ minY = maxY;
+ maxY = t;
+ }
+
+ if (minZ > maxZ) {
+ float t = minZ;
+ minZ = maxZ;
+ maxZ = t;
+ }
+
+ final CullingAABB result = CULLING_AABB_POOL.acquire();
+
+ result.x = (minX + maxX) * 0.5f;
+ result.y = (minY + maxY) * 0.5f;
+ result.z = (minZ + maxZ) * 0.5f;
+
+ result.extentsX = (maxX - minX) * 0.5f;
+ result.extentsY = (maxY - minY) * 0.5f;
+ result.extentsZ = (maxZ - minZ) * 0.5f;
+
+ return result;
+ }
+
+ public void flush() {
+ if(pendingCullingResults.isEmpty() || cullingCameras.isEmpty())
+ return;
+
+ CullingJob job = CULLING_JOB_POOL.acquire();
+ job.id++;
+ job.cullingCameras.addAll(cullingCameras);
+
+ for (int i = 0; i < pendingCullingResults.size(); i++) {
+ final CullingResult result = pendingCullingResults.get(i);
+ result.job = job;
+ job.pendingCullingResults.add(result);
+ }
+ pendingCullingResults.clear();
+
+ job.queue();
+ }
+
+ public void debugDraw(CullingResult... results) {
+ for(int i = 0; i < results.length; i++)
+ debugDraw(results[i]);
+ }
+
+ public void debugDraw(CullingResult result) {
+ if(result == null)
+ return;
+
+ result.build(debugProjected, debugScratch);
+ result.debugDraw(debugScratch);
+ }
+
+ public class CullingJob extends Job {
+ private final List pendingCullingResults = new ArrayList<>();
+ private final List cullingCameras = new ArrayList<>();
+ private final float[] projected = new float[4];
+ private final float[] scratch = new float[SCRATCH_SIZE];
+ private int id;
+
+ @Override
+ protected void onRun() {
+ for (int i = 0; i < pendingCullingResults.size(); i++) {
+ CullingResult result = pendingCullingResults.get(i);
+ if (result == null)
+ continue;
+
+ result.build(projected, scratch);
+
+ byte newFlags = 0;
+ for (int camIdx = 0; camIdx < cullingCameras.size(); camIdx++) {
+ if (result.test(cullingCameras.get(camIdx), scratch))
+ newFlags |= (byte) (1 << camIdx);
+ }
+
+ result.visibilityFlags = newFlags;
+ }
+
+ pendingCullingResults.clear();
+ cullingCameras.clear();
+
+ CULLING_JOB_POOL.recycle(this);
+ }
+ }
+
+ public class CullingAABB extends CullingResult {
+ public float extentsX;
+ public float extentsY;
+ public float extentsZ;
+
+ @Override
+ protected void build(float[] p, float[] scratch) {
+ float worldX = offsetX + x;
+ float worldY = offsetY + y;
+ float worldZ = offsetZ + z;
+
+ if (projection == null) {
+ scratch[0] = worldX - extentsX;
+ scratch[1] = worldY - extentsY;
+ scratch[2] = worldZ - extentsZ;
+ scratch[3] = worldX + extentsX;
+ scratch[4] = worldY + extentsY;
+ scratch[5] = worldZ + extentsZ;
+ return;
+ }
+
+ float minX = Float.POSITIVE_INFINITY;
+ float minY = Float.POSITIVE_INFINITY;
+ float minZ = Float.POSITIVE_INFINITY;
+
+ float maxX = Float.NEGATIVE_INFINITY;
+ float maxY = Float.NEGATIVE_INFINITY;
+ float maxZ = Float.NEGATIVE_INFINITY;
+
+ for (int ix = -1; ix <= 1; ix += 2) {
+ for (int iy = -1; iy <= 1; iy += 2) {
+ for (int iz = -1; iz <= 1; iz += 2) {
+ projection.project(
+ worldX + ix * extentsX,
+ worldY + iy * extentsY,
+ worldZ + iz * extentsZ,
+ p
+ );
+
+ minX = min(minX, p[0]);
+ minY = min(minY, p[1]);
+ minZ = min(minZ, p[2]);
+
+ maxX = max(maxX, p[0]);
+ maxY = max(maxY, p[1]);
+ maxZ = max(maxZ, p[2]);
+ }
+ }
+ }
+
+ scratch[0] = minX;
+ scratch[1] = minY;
+ scratch[2] = minZ;
+
+ scratch[3] = maxX;
+ scratch[4] = maxY;
+ scratch[5] = maxZ;
+ }
+
+ @Override
+ protected boolean test(Camera camera, float[] scratch) {
+ return camera.intersectsAABB(scratch[0], scratch[1], scratch[2], scratch[3], scratch[4], scratch[5]);
+ }
+
+ @Override
+ protected void debugDraw(float[] scratch) {
+ DebugDraw.drawMinMax(scratch[0], scratch[1], scratch[2], scratch[3], scratch[4], scratch[5], isVisible() ? Color.GREEN : Color.RED, false);
+ }
+
+ @Override
+ public void release() {
+ super.release();
+ CULLING_AABB_POOL.recycle(this);
+ }
+ }
+
+ public class CullingSphere extends CullingResult {
+ public float radius;
+
+ @Override
+ protected void build(float[] p, float[] scratch) {
+ float worldX = offsetX + x;
+ float worldY = offsetY + y;
+ float worldZ = offsetZ + z;
+
+ if(projection == null) {
+ scratch[0] = worldX;
+ scratch[1] = worldY;
+ scratch[2] = worldZ;
+ scratch[3] = radius;
+ return;
+ }
+
+ projection.project(worldX, worldY, worldZ, p);
+ scratch[0] = p[0];
+ scratch[1] = p[0];
+ scratch[2] = p[0];
+
+ projection.project(worldX + radius, worldY + radius, worldZ + radius, p);
+ scratch[3] = max(
+ abs(p[0] - scratch[0]),
+ max(
+ abs(p[1] - scratch[1]),
+ abs(p[2] - scratch[2])
+ )
+ );
+ }
+
+ @Override
+ protected boolean test(Camera camera, float[] scratch) {
+ return camera.intersectsSphere( scratch[0], scratch[1], scratch[2], scratch[3]);
+ }
+
+ @Override
+ protected void debugDraw(float[] scratch) {
+ DebugDraw.drawSphere(scratch[0], scratch[1], scratch[2], scratch[3], isVisible() ? Color.GREEN : Color.RED, false);
+ }
+
+ @Override
+ public void release() {
+ super.release();
+ CULLING_SPHERE_POOL.recycle(this);
+ }
+ }
+
+ public abstract class CullingResult {
+ @Getter
+ private byte visibilityFlags = 0;
+
+ protected CullingJob job;
+ public Projection projection;
+
+ public float x, y, z;
+ public float offsetX, offsetY, offsetZ;
+ protected int cullingJobId;
+
+ protected abstract void build(float[] p, float[] scratch);
+ protected abstract boolean test(Camera camera, float[] scratch);
+ protected abstract void debugDraw(float[] scratch);
+
+ private void ensureJobCompletion() {
+ if(job == null)
+ return;
+
+ if(cullingJobId == job.id)
+ job.waitForCompletion();
+ job = null;
+ }
+
+ public boolean isVisible() {
+ ensureJobCompletion();
+ return visibilityFlags != 0;
+ }
+
+ public boolean isVisible(Camera camera) {
+ ensureJobCompletion();
+ return (visibilityFlags & camera.getCullingMask()) != 0;
+ }
+
+ public void queue() {
+ assert !pendingCullingResults.contains(this);
+ pendingCullingResults.add(this);
+
+ if(pendingCullingResults.size() >= HdPlugin.PROCESSOR_COUNT)
+ flush();
+ }
+
+ public void release() {
+ x = y = z = offsetX = offsetY = offsetZ = 0;
+ visibilityFlags = 0;
+ projection = null;
+ job = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/rs117/hd/utils/Camera.java b/src/main/java/rs117/hd/utils/Camera.java
index 6f91fcdbc7..0b367c1ed5 100644
--- a/src/main/java/rs117/hd/utils/Camera.java
+++ b/src/main/java/rs117/hd/utils/Camera.java
@@ -48,9 +48,13 @@ public final class Camera implements Projection {
@Getter
private float farPlane = 0.0f;
@Getter
+ private int cullingMask = 0;
+ @Getter
private boolean orthographic = false;
@Getter
private boolean reverseZ = false;
+ @Getter
+ private boolean flipY = false;
@Override
public float[] project(float x, float y, float z) {
@@ -98,6 +102,22 @@ public Camera setReverseZ(boolean newReverseZ) {
return this;
}
+ public Camera setFlipY(boolean newFlipY) {
+ if (flipY != newFlipY) {
+ synchronized (this) {
+ flipY = newFlipY;
+ dirtyFlags |= PROJ_CHANGED;
+ }
+ }
+ return this;
+ }
+
+ public Camera setCullingId(int id) {
+ assert id >= 0 && id <= 8;
+ cullingMask = 1 << id;
+ return this;
+ }
+
public Camera setViewportWidth(int newViewportWidth) {
if (viewportWidth != newViewportWidth) {
synchronized (this) {
@@ -416,6 +436,10 @@ private void calculateProjectionMatrix() {
}
}
}
+ if (flipY) {
+ for (int i = 1; i < 16; i += 4)
+ projectionMatrix[i] = -projectionMatrix[i];
+ }
try {
invProjectionMatrix = Mat4.inverse(projectionMatrix);
} catch (Exception ex) {
@@ -545,7 +569,7 @@ public float[][] getFrustumCorners() {
return getFrustumCorners(new float[8][3]);
}
- public boolean intersectsAABB(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
+ public boolean intersectsAABB(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) {
calculateFrustumPlanes();
return HDUtils.isAABBIntersectingFrustum(minX, minY, minZ, maxX, maxY, maxZ, frustumPlanes);
}
@@ -575,4 +599,4 @@ public void copyFrom(Camera other) {
dirtyFlags = PROJ_CHANGED | VIEW_CHANGED;
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/rs117/hd/utils/DebugDraw.java b/src/main/java/rs117/hd/utils/DebugDraw.java
new file mode 100644
index 0000000000..238e162e8f
--- /dev/null
+++ b/src/main/java/rs117/hd/utils/DebugDraw.java
@@ -0,0 +1,221 @@
+package rs117.hd.utils;
+
+import java.awt.Color;
+import rs117.hd.renderer.zone.passes.DebugDrawPass;
+import rs117.hd.renderer.zone.passes.DebugDrawPass.Draw;
+import rs117.hd.renderer.zone.passes.DebugDrawPass.PrimitiveDrawType;
+
+public class DebugDraw {
+ public static DebugDrawPass INSTANCE;
+
+ public static void drawText(
+ float x, float y, float z, String text, float scale,
+ Color color, float duration
+ ) {
+ DebugDrawPass inst = INSTANCE;
+ if (inst == null || color == null || text == null)
+ return;
+
+ final Draw d = inst.pushDraw(PrimitiveDrawType.TEXT);
+ d.x1 = x;
+ d.y1 = y;
+ d.z1 = z;
+ d.x2 = scale;
+ d.duration = duration;
+ d.text = text;
+ d.rgb = color.getRGB();
+ }
+
+ public static void drawText(float x, float y, float z, String text, float scale, Color color) {
+ drawText(x, y, z, text, scale, color, -1);
+ }
+
+ public static void drawAABB(
+ float cx, float cy, float cz, float hx, float hy, float hz,
+ Color color, float duration, boolean filled
+ ) {
+ DebugDrawPass inst = INSTANCE;
+ if (inst == null || color == null)
+ return;
+
+ final Draw d = inst.pushDraw(PrimitiveDrawType.AABB);
+ d.x1 = cx;
+ d.y1 = cy;
+ d.z1 = cz;
+ d.x2 = hx;
+ d.y2 = hy;
+ d.z2 = hz;
+ d.rgb = color.getRGB();
+ d.duration = duration;
+ d.filled = filled;
+ }
+
+ public static void drawAABB(
+ float cx, float cy, float cz, float hx, float hy, float hz,
+ Color color, boolean filled
+ ) {
+ drawAABB(cx, cy, cz, hx, hy, hz, color, -1, filled);
+ }
+
+ public static void drawMinMax(
+ float minX, float minY, float minZ,
+ float maxX, float maxY, float maxZ,
+ Color color, float duration, boolean filled
+ ) {
+ DebugDrawPass inst = INSTANCE;
+ if (inst == null || color == null)
+ return;
+
+ final Draw d = inst.pushDraw(PrimitiveDrawType.AABB);
+ d.x1 = (minX + maxX) / 2f;
+ d.y1 = (minY + maxY) / 2f;
+ d.z1 = (minZ + maxZ) / 2f;
+ d.x2 = (maxX - minX) / 2f;
+ d.y2 = (maxY - minY) / 2f;
+ d.z2 = (maxZ - minZ) / 2f;
+ d.rgb = color.getRGB();
+ d.duration = duration;
+ d.filled = filled;
+ }
+
+ public static void drawMinMax(
+ float minX, float minY, float minZ,
+ float maxX, float maxY, float maxZ,
+ Color color, boolean filled
+ ) {
+ drawMinMax(minX, minY, minZ, maxX, maxY, maxZ, color, -1, filled);
+ }
+
+ public static void drawSphere(
+ float cx, float cy, float cz, float radius,
+ Color color, float duration, boolean filled
+ ) {
+ DebugDrawPass inst = INSTANCE;
+ if (inst == null || color == null)
+ return;
+
+ final Draw d = inst.pushDraw(PrimitiveDrawType.SPHERE);
+ d.x1 = cx;
+ d.y1 = cy;
+ d.z1 = cz;
+ d.x2 = radius;
+ d.rgb = color.getRGB();
+ d.duration = duration;
+ d.filled = filled;
+ }
+
+ public static void drawSphere(
+ float cx, float cy, float cz, float radius,
+ Color color, boolean filled
+ ) {
+ drawSphere(cx, cy, cz, radius, color, -1, filled);
+ }
+
+ public static void drawLine(
+ float x1, float y1, float z1, float x2, float y2, float z2,
+ Color color, float duration
+ ) {
+ drawLine(x1, y1, z1, x2, y2, z2, 1f, color, duration);
+ }
+
+ public static void drawLine(
+ float x1, float y1, float z1, float x2, float y2, float z2,
+ float thickness, Color color
+ ) {
+ drawLine(x1, y1, z1, x2, y2, z2, thickness, color, -1);
+ }
+
+ public static void drawLine(
+ float x1, float y1, float z1, float x2, float y2, float z2,
+ float thickness, Color color, float duration
+ ) {
+ DebugDrawPass inst = INSTANCE;
+ if (inst == null || color == null) return;
+
+ final Draw d = inst.pushDraw(PrimitiveDrawType.LINE);
+ d.x1 = x1;
+ d.y1 = y1;
+ d.z1 = z1;
+ d.x2 = x2;
+ d.y2 = y2;
+ d.z2 = z2;
+ d.thickness = thickness;
+ d.rgb = color.getRGB();
+ d.duration = duration;
+ d.filled = false;
+ }
+
+ public static void drawArrow(
+ float x1, float y1, float z1,
+ float dx, float dy, float dz,
+ float headLength, float thickness,
+ Color color, float duration
+ ) {
+ DebugDrawPass inst = INSTANCE;
+ if (inst == null || color == null)
+ return;
+
+ float x2 = x1 + dx;
+ float y2 = y1 + dy;
+ float z2 = z1 + dz;
+
+ drawLine(x1, y1, z1, x2, y2, z2, thickness, color, duration);
+
+ float len = (float) Math.sqrt(dx * dx + dy * dy + dz * dz);
+ if (len < 1e-5f)
+ return;
+
+ float invLen = 1f / len;
+ float nx = dx * invLen;
+ float ny = dy * invLen;
+ float nz = dz * invLen;
+
+ // Pick an axis that's not parallel to the direction.
+ float ax = Math.abs(ny) < 0.999f ? 0 : 1;
+ float ay = Math.abs(ny) < 0.999f ? 1 : 0;
+ float az = 0;
+
+ // right = normalize(dir × axis)
+ float rx = ny * az - nz * ay;
+ float ry = nz * ax - nx * az;
+ float rz = nx * ay - ny * ax;
+
+ float invRLen = 1f / (float) Math.sqrt(rx * rx + ry * ry + rz * rz);
+ rx *= invRLen;
+ ry *= invRLen;
+ rz *= invRLen;
+
+ // up = dir × right
+ float ux = ny * rz - nz * ry;
+ float uy = nz * rx - nx * rz;
+ float uz = nx * ry - ny * rx;
+
+ float bx = x2 - nx * headLength;
+ float by = y2 - ny * headLength;
+ float bz = z2 - nz * headLength;
+ float hs = headLength * 0.5f;
+
+ addHead(bx, by, bz, rx, ry, rz, hs, x2, y2, z2, thickness, color, duration);
+ addHead(bx, by, bz, ux, uy, uz, hs, x2, y2, z2, thickness, color, duration);
+ }
+
+ private static void addHead(
+ float bx, float by, float bz,
+ float vx, float vy, float vz,
+ float hs,
+ float x2, float y2, float z2,
+ float thickness,
+ Color color,
+ float duration
+ ) {
+ drawLine(bx + vx * hs, by + vy * hs, bz + vz * hs, x2, y2, z2, thickness, color, duration);
+ drawLine(bx - vx * hs, by - vy * hs, bz - vz * hs, x2, y2, z2, thickness, color, duration);
+ }
+
+ public static void drawArrow(
+ float x1, float y1, float z1, float dx, float dy, float dz,
+ float headLength, float thickness, Color color
+ ) {
+ drawArrow(x1, y1, z1, dx, dy, dz, headLength, thickness, color, -1);
+ }
+}
diff --git a/src/main/java/rs117/hd/utils/DeveloperTools.java b/src/main/java/rs117/hd/utils/DeveloperTools.java
index 36d42b6171..caa3f11f0d 100644
--- a/src/main/java/rs117/hd/utils/DeveloperTools.java
+++ b/src/main/java/rs117/hd/utils/DeveloperTools.java
@@ -167,7 +167,16 @@ public void onCommandExecuted(CommandExecuted commandExecuted) {
plugin.renderer.reloadScene();
break;
case "culling":
- plugin.freezeCulling = !plugin.freezeCulling;
+ if(args.length < 2) {
+ log.debug("Usage: ::117hd culling freeze|show");
+ return;
+ }
+
+ if(args[1].equalsIgnoreCase("freeze"))
+ plugin.freezeCulling = !plugin.freezeCulling;
+
+ if(args[1].equalsIgnoreCase("show"))
+ plugin.showCulling = !plugin.showCulling;
break;
}
}
diff --git a/src/main/java/rs117/hd/utils/buffer/GLBuffer.java b/src/main/java/rs117/hd/utils/buffer/GLBuffer.java
index a8d8f7d301..d1d9c222d7 100644
--- a/src/main/java/rs117/hd/utils/buffer/GLBuffer.java
+++ b/src/main/java/rs117/hd/utils/buffer/GLBuffer.java
@@ -24,6 +24,7 @@
*/
package rs117.hd.utils.buffer;
+import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
@@ -355,6 +356,19 @@ public GLBuffer initialize() {
return initialize(0);
}
+ public GLBuffer initialize(Buffer data) {
+ initialize(data.limit());
+ if(data instanceof ByteBuffer)
+ upload((ByteBuffer) data);
+ else if(data instanceof IntBuffer)
+ upload((IntBuffer) data);
+ else if(data instanceof FloatBuffer)
+ upload((FloatBuffer) data);
+ else
+ throw new IllegalArgumentException("Unsupported buffer type: " + data.getClass());
+ return this;
+ }
+
public GLBuffer initialize(long initialCapacity) {
id = glGenBuffers();
// Initialize both GL and CL buffers to buffers of a single byte or more,
diff --git a/src/main/java/rs117/hd/utils/collections/PrimitiveIntArray.java b/src/main/java/rs117/hd/utils/collections/PrimitiveIntArray.java
index 197f9ae052..97a25867e8 100644
--- a/src/main/java/rs117/hd/utils/collections/PrimitiveIntArray.java
+++ b/src/main/java/rs117/hd/utils/collections/PrimitiveIntArray.java
@@ -25,6 +25,11 @@ public void put(int v) {
array[length++] = v;
}
+ public int pop() {
+ assert length > 0;
+ return array[--length];
+ }
+
public void putUnique(int v) {
if (length < array.length) {
for (int i = 0; i < length; i++)
diff --git a/src/main/resources/rs117/hd/debug_draw_frag.glsl b/src/main/resources/rs117/hd/debug_draw_frag.glsl
new file mode 100644
index 0000000000..5d321bd399
--- /dev/null
+++ b/src/main/resources/rs117/hd/debug_draw_frag.glsl
@@ -0,0 +1,27 @@
+#version 330
+
+#include
+
+#define PRIMITIVE_CUBE 0
+#define PRIMITIVE_SPHERE 1
+#define PRIMITIVE_LINE 2
+#define PRIMITIVE_TEXT 3
+
+#include PRIMITIVE_TYPE
+
+flat in vec4 fColor;
+
+#if PRIMITIVE_TYPE == PRIMITIVE_TEXT
+flat in int fChar;
+ in vec2 fUV;
+#endif
+
+out vec4 FragColor;
+
+void main() {
+#if PRIMITIVE_TYPE == PRIMITIVE_TEXT
+ if (!fontSample(fChar, fUV))
+ discard;
+#endif
+ FragColor = fColor;
+}
\ No newline at end of file
diff --git a/src/main/resources/rs117/hd/debug_draw_vert.glsl b/src/main/resources/rs117/hd/debug_draw_vert.glsl
new file mode 100644
index 0000000000..52f42c4e65
--- /dev/null
+++ b/src/main/resources/rs117/hd/debug_draw_vert.glsl
@@ -0,0 +1,133 @@
+#version 330
+
+#include
+#include
+
+#define PRIMITIVE_CUBE 0
+#define PRIMITIVE_SPHERE 1
+#define PRIMITIVE_LINE 2
+#define PRIMITIVE_TEXT 3
+
+#include PRIMITIVE_TYPE
+
+layout(location = 0) in vec3 aPosition;
+
+#if PRIMITIVE_TYPE == PRIMITIVE_CUBE
+layout(location = 1) in vec3 aCenter;
+layout(location = 2) in vec3 aHalfExtents;
+layout(location = 3) in int aArgb;
+
+#elif PRIMITIVE_TYPE == PRIMITIVE_SPHERE
+layout(location = 1) in vec3 aCenter;
+layout(location = 2) in float aRadius;
+layout(location = 3) in int aArgb;
+
+#elif PRIMITIVE_TYPE == PRIMITIVE_LINE
+layout(location = 1) in vec3 aStart;
+layout(location = 2) in vec3 aEnd;
+layout(location = 3) in float aThickness;
+layout(location = 4) in int aArgb;
+
+#elif PRIMITIVE_TYPE == PRIMITIVE_TEXT
+layout(location = 1) in vec3 aCenter;
+layout(location = 2) in float aCharScale;
+layout(location = 3) in int aCharCode;
+layout(location = 4) in int aCharIndex;
+layout(location = 5) in int aArgb;
+#endif
+
+flat out vec4 fColor;
+
+#if PRIMITIVE_TYPE == PRIMITIVE_TEXT
+flat out int fChar;
+ out vec2 fUV;
+#endif
+
+vec4 unpackArgb(int argb) {
+ return vec4(
+ ((argb >> 16) & 0xFF) / 255.0,
+ ((argb >> 8) & 0xFF) / 255.0,
+ ((argb >> 0) & 0xFF) / 255.0,
+ ((argb >> 24) & 0xFF) / 255.0
+ );
+}
+
+#if PRIMITIVE_TYPE == PRIMITIVE_LINE
+mat4 lineModelMatrix(vec3 start, vec3 end, float thickness) {
+ vec3 dir = end - start;
+ float len = length(dir);
+ vec3 mid = (start + end) * 0.5;
+
+ if (len < 1e-5)
+ return mat4(1.0);
+
+ vec3 yAxis = dir / len;
+ vec3 ref = abs(yAxis.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
+ vec3 xAxis = normalize(cross(ref, yAxis));
+ vec3 zAxis = cross(yAxis, xAxis);
+
+ return mat4(
+ vec4(xAxis * thickness * 0.5, 0.0),
+ vec4(yAxis * len * 0.5, 0.0),
+ vec4(zAxis * thickness * 0.5, 0.0),
+ vec4(mid, 1.0)
+ );
+}
+#endif
+
+void main() {
+#if PRIMITIVE_TYPE == PRIMITIVE_CUBE
+ mat4 model = mat4(
+ vec4(aHalfExtents.x, 0.0, 0.0, 0.0),
+ vec4(0.0, aHalfExtents.y, 0.0, 0.0),
+ vec4(0.0, 0.0, aHalfExtents.z, 0.0),
+ vec4(aCenter, 1.0)
+ );
+ fColor = unpackArgb(aArgb);
+ gl_Position = projectionMatrix * model * vec4(aPosition, 1.0);
+
+#elif PRIMITIVE_TYPE == PRIMITIVE_SPHERE
+ mat4 model = mat4(
+ vec4(aRadius, 0.0, 0.0, 0.0),
+ vec4(0.0, aRadius, 0.0, 0.0),
+ vec4(0.0, 0.0, aRadius, 0.0),
+ vec4(aCenter, 1.0)
+ );
+ fColor = unpackArgb(aArgb);
+ gl_Position = projectionMatrix * model * vec4(aPosition, 1.0);
+
+#elif PRIMITIVE_TYPE == PRIMITIVE_LINE
+ fColor = unpackArgb(aArgb);
+ gl_Position = projectionMatrix * lineModelMatrix(aStart, aEnd, aThickness) * vec4(aPosition, 1.0);
+
+#elif PRIMITIVE_TYPE == PRIMITIVE_TEXT
+ vec2 quad = aPosition.xy - vec2(0.5);
+
+ // Extract camera basis from the view-projection matrix.
+ // If you have a separate view matrix available, use that instead.
+ vec3 cameraRight = vec3(
+ projectionMatrix[0][0],
+ projectionMatrix[1][0],
+ projectionMatrix[2][0]
+ );
+
+ vec3 cameraUp = vec3(
+ projectionMatrix[0][1],
+ projectionMatrix[1][1],
+ projectionMatrix[2][1]
+ );
+
+ float glyphSize = aCharScale;
+
+ vec3 worldPos =
+ aCenter +
+ cameraRight * ((float(aCharIndex) + quad.x) * glyphSize) +
+ cameraUp * (quad.y * glyphSize);
+
+ fUV = aPosition.xy;
+ fColor = unpackArgb(aArgb);
+ fChar = aCharCode;
+
+ gl_Position = projectionMatrix * vec4(worldPos, 1.0);
+#endif
+}
\ No newline at end of file
diff --git a/src/main/resources/rs117/hd/utils/font.glsl b/src/main/resources/rs117/hd/utils/font.glsl
new file mode 100644
index 0000000000..8c1bd30c74
--- /dev/null
+++ b/src/main/resources/rs117/hd/utils/font.glsl
@@ -0,0 +1,208 @@
+#pragma once
+
+// 8x8 bitmap font, ASCII 32–127 (96 glyphs × 8 rows)
+// Each int is one row of 8 pixels, LSB = leftmost pixel
+const int FONT[768] = int[](
+ // 0x20 SPACE
+ 0,0,0,0,0,0,0,0,
+ // 0x21 !
+ 24,24,24,24,24,0,24,0,
+ // 0x22 "
+ 54,54,54,0,0,0,0,0,
+ // 0x23 #
+ 54,54,127,54,127,54,54,0,
+ // 0x24 $
+ 28,42,40,28,10,42,28,0,
+ // 0x25 %
+ 98,100,8,16,38,70,0,0,
+ // 0x26 &
+ 28,34,20,8,20,34,28,0,
+ // 0x27 '
+ 24,24,8,0,0,0,0,0,
+ // 0x28 (
+ 8,16,32,32,32,16,8,0,
+ // 0x29 )
+ 32,16,8,8,8,16,32,0,
+ // 0x2A *
+ 0,20,8,62,8,20,0,0,
+ // 0x2B +
+ 0,8,8,62,8,8,0,0,
+ // 0x2C ,
+ 0,0,0,0,24,24,8,16,
+ // 0x2D -
+ 0,0,0,62,0,0,0,0,
+ // 0x2E .
+ 0,0,0,0,0,24,24,0,
+ // 0x2F /
+ 2,4,8,16,32,64,0,0,
+ // 0x30 0
+ 28,34,38,42,50,34,28,0,
+ // 0x31 1
+ 8,24,8,8,8,8,28,0,
+ // 0x32 2
+ 28,34,2,4,8,16,62,0,
+ // 0x33 3
+ 28,34,2,12,2,34,28,0,
+ // 0x34 4
+ 4,12,20,36,62,4,4,0,
+ // 0x35 5
+ 62,32,60,2,2,34,28,0,
+ // 0x36 6
+ 14,16,32,60,34,34,28,0,
+ // 0x37 7
+ 62,2,4,8,16,16,16,0,
+ // 0x38 8
+ 28,34,34,28,34,34,28,0,
+ // 0x39 9
+ 28,34,34,30,2,4,56,0,
+ // 0x3A :
+ 0,24,24,0,24,24,0,0,
+ // 0x3B ;
+ 0,24,24,0,24,24,8,16,
+ // 0x3C
+ 4,8,16,32,16,8,4,0,
+ // 0x3D =
+ 0,0,62,0,62,0,0,0,
+ // 0x3E >
+ 32,16,8,4,8,16,32,0,
+ // 0x3F ?
+ 28,34,2,4,8,0,8,0,
+ // 0x40 @
+ 28,34,2,26,42,42,28,0,
+ // 0x41 A
+ 8,20,34,34,62,34,34,0,
+ // 0x42 B
+ 60,34,34,60,34,34,60,0,
+ // 0x43 C
+ 28,34,32,32,32,34,28,0,
+ // 0x44 D
+ 60,34,34,34,34,34,60,0,
+ // 0x45 E
+ 62,32,32,60,32,32,62,0,
+ // 0x46 F
+ 62,32,32,60,32,32,32,0,
+ // 0x47 G
+ 28,34,32,46,34,34,28,0,
+ // 0x48 H
+ 34,34,34,62,34,34,34,0,
+ // 0x49 I
+ 28,8,8,8,8,8,28,0,
+ // 0x4A J
+ 14,4,4,4,4,36,24,0,
+ // 0x4B K
+ 34,36,40,48,40,36,34,0,
+ // 0x4C L
+ 32,32,32,32,32,32,62,0,
+ // 0x4D M
+ 34,54,42,42,34,34,34,0,
+ // 0x4E N
+ 34,34,50,42,38,34,34,0,
+ // 0x4F O
+ 28,34,34,34,34,34,28,0,
+ // 0x50 P
+ 60,34,34,60,32,32,32,0,
+ // 0x51 Q
+ 28,34,34,34,42,36,26,0,
+ // 0x52 R
+ 60,34,34,60,40,36,34,0,
+ // 0x53 S
+ 28,34,32,28,2,34,28,0,
+ // 0x54 T
+ 62,8,8,8,8,8,8,0,
+ // 0x55 U
+ 34,34,34,34,34,34,28,0,
+ // 0x56 V
+ 34,34,34,34,34,20,8,0,
+ // 0x57 W
+ 34,34,34,42,42,54,34,0,
+ // 0x58 X
+ 34,34,20,8,20,34,34,0,
+ // 0x59 Y
+ 34,34,20,8,8,8,8,0,
+ // 0x5A Z
+ 62,2,4,8,16,32,62,0,
+ // 0x5B [
+ 28,16,16,16,16,16,28,0,
+ // 0x5C backslash
+ 64,32,16,8,4,2,0,0,
+ // 0x5D ]
+ 28,4,4,4,4,4,28,0,
+ // 0x5E ^
+ 8,20,34,0,0,0,0,0,
+ // 0x5F _
+ 0,0,0,0,0,0,62,0,
+ // 0x60 `
+ 16,8,0,0,0,0,0,0,
+ // 0x61 a
+ 0,0,28,2,30,34,30,0,
+ // 0x62 b
+ 32,32,60,34,34,34,60,0,
+ // 0x63 c
+ 0,0,28,32,32,32,28,0,
+ // 0x64 d
+ 2,2,30,34,34,34,30,0,
+ // 0x65 e
+ 0,0,28,34,62,32,28,0,
+ // 0x66 f
+ 12,18,16,56,16,16,16,0,
+ // 0x67 g
+ 0,0,30,34,34,30,2,28,
+ // 0x68 h
+ 32,32,60,34,34,34,34,0,
+ // 0x69 i
+ 8,0,24,8,8,8,28,0,
+ // 0x6A j
+ 4,0,12,4,4,4,36,24,
+ // 0x6B k
+ 32,32,34,36,56,36,34,0,
+ // 0x6C l
+ 24,8,8,8,8,8,28,0,
+ // 0x6D m
+ 0,0,54,42,42,42,34,0,
+ // 0x6E n
+ 0,0,60,34,34,34,34,0,
+ // 0x6F o
+ 0,0,28,34,34,34,28,0,
+ // 0x70 p
+ 0,0,60,34,34,60,32,32,
+ // 0x71 q
+ 0,0,30,34,34,30,2,2,
+ // 0x72 r
+ 0,0,46,48,32,32,32,0,
+ // 0x73 s
+ 0,0,28,32,28,2,60,0,
+ // 0x74 t
+ 16,16,56,16,16,18,12,0,
+ // 0x75 u
+ 0,0,34,34,34,34,30,0,
+ // 0x76 v
+ 0,0,34,34,34,20,8,0,
+ // 0x77 w
+ 0,0,34,34,42,42,20,0,
+ // 0x78 x
+ 0,0,34,20,8,20,34,0,
+ // 0x79 y
+ 0,0,34,34,34,30,2,28,
+ // 0x7A z
+ 0,0,62,4,8,16,62,0,
+ // 0x7B {
+ 12,16,16,32,16,16,12,0,
+ // 0x7C |
+ 8,8,8,8,8,8,8,0,
+ // 0x7D }
+ 48,8,8,4,8,8,48,0,
+ // 0x7E ~
+ 20,40,0,0,0,0,0,0,
+ // 0x7F DEL
+ 0,0,0,0,0,0,0,0
+);
+
+bool fontSample(int charCode, vec2 uv)
+{
+ int c = clamp(charCode - 32, 0, 95);
+ int col = clamp(int(uv.x * 8.0), 0, 7);
+ int row = clamp(int((1.0 - uv.y) * 8.0), 0, 7);
+
+ int bits = FONT[c * 8 + row];
+ return ((bits >> (7 - col)) & 1) != 0;
+}
\ No newline at end of file