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