From 0a2fb018a64b53dc057905ee1cd3fcb5cc7b9558 Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Fri, 13 Mar 2026 23:43:33 +0000 Subject: [PATCH 1/2] Player silhouette Occlusion Based player silhouette Calculate potentially visible samples Added Depth Only Shader Fixed Silhouette not applying to trees Support fading the canopy whilst the player is behind it Blend the CanopyFadeStrength Improved Silhouette * Fixed CommandBuffer Stencil Command Encoding/Decoding * Added Alpha Support for the silhouette without it doubling up * Removed Edge Highlight Added IsCanopy to materials & tagged materials with it Fixes Fixes Increase radius & discard if fully faded Draw Actors seperatly from Dynamic VAOs Support for drawing multiple Silhouettes * All NPCS & Players can have a Silhouettes * Moved Silhouette pass out of ZoneRenderer and into its own class * Added Config Options for Player & NPCs, defaulting to the least noisy options Fixes & Improvements Fix silhouetteThreshold conflicting key Fix NPE Clear Map before rebuild Remove Additional VAO in favour of doing fast depth draw for silhouettes added fading Added FadeTime constant Sync --- schemas/materials.schema.json | 4 + src/main/java/rs117/hd/HdPlugin.java | 11 +- src/main/java/rs117/hd/HdPluginConfig.java | 103 +++- .../rs117/hd/config/NPCSilhouetteMode.java | 8 + .../rs117/hd/config/PlayerSilhouetteMode.java | 8 + .../rs117/hd/opengl/GLOcclusionQueries.java | 142 ++++++ .../hd/opengl/shader/BasicSceneProgram.java | 13 + .../shader/DepthSceneShaderProgram.java | 11 + .../rs117/hd/opengl/shader/ShaderProgram.java | 21 + .../rs117/hd/opengl/uniforms/UBOGlobal.java | 3 + src/main/java/rs117/hd/overlays/Timer.java | 2 + .../hd/renderer/zone/DynamicModelVAO.java | 22 + .../renderer/zone/ModelStreamingManager.java | 15 +- .../java/rs117/hd/renderer/zone/Zone.java | 25 +- .../rs117/hd/renderer/zone/ZoneRenderer.java | 126 ++++- .../zone/renderpass/SilhouettePass.java | 475 ++++++++++++++++++ .../rs117/hd/scene/materials/Material.java | 2 + .../java/rs117/hd/utils/CommandBuffer.java | 61 ++- src/main/java/rs117/hd/utils/RenderState.java | 26 + .../resources/rs117/hd/basic_scene_frag.glsl | 12 + .../resources/rs117/hd/basic_scene_vert.glsl | 21 + .../resources/rs117/hd/depth_scene_frag.glsl | 3 + .../resources/rs117/hd/depth_scene_vert.glsl | 21 + .../resources/rs117/hd/scene/materials.json | 35 +- src/main/resources/rs117/hd/scene_frag.glsl | 27 + .../resources/rs117/hd/uniforms/global.glsl | 3 + .../rs117/hd/uniforms/materials.glsl | 4 + .../resources/rs117/hd/utils/constants.glsl | 2 + 28 files changed, 1167 insertions(+), 39 deletions(-) create mode 100644 src/main/java/rs117/hd/config/NPCSilhouetteMode.java create mode 100644 src/main/java/rs117/hd/config/PlayerSilhouetteMode.java create mode 100644 src/main/java/rs117/hd/opengl/GLOcclusionQueries.java create mode 100644 src/main/java/rs117/hd/opengl/shader/BasicSceneProgram.java create mode 100644 src/main/java/rs117/hd/opengl/shader/DepthSceneShaderProgram.java create mode 100644 src/main/java/rs117/hd/renderer/zone/renderpass/SilhouettePass.java create mode 100644 src/main/resources/rs117/hd/basic_scene_frag.glsl create mode 100644 src/main/resources/rs117/hd/basic_scene_vert.glsl create mode 100644 src/main/resources/rs117/hd/depth_scene_frag.glsl create mode 100644 src/main/resources/rs117/hd/depth_scene_vert.glsl diff --git a/schemas/materials.schema.json b/schemas/materials.schema.json index 4eee8abaf4..808a490ed1 100644 --- a/schemas/materials.schema.json +++ b/schemas/materials.schema.json @@ -58,6 +58,10 @@ "type": "boolean", "description": "Whether 117 HD should skip lighting the material, largely preserving the vanilla look." }, + "isCanopy": { + "type": "boolean", + "description": "Is part of the canopy and will be faded if the face occludes the player." + }, "brightness": { "type": "number", "description": "A brightness multiplier for the material's color." diff --git a/src/main/java/rs117/hd/HdPlugin.java b/src/main/java/rs117/hd/HdPlugin.java index b948e21af6..abb5989f02 100644 --- a/src/main/java/rs117/hd/HdPlugin.java +++ b/src/main/java/rs117/hd/HdPlugin.java @@ -881,6 +881,7 @@ public ShaderIncludes getShaderIncludes() { .define("MAX_LIGHT_COUNT", configTiledLighting ? UBOLights.MAX_LIGHTS : configDynamicLights.getMaxSceneLights()) .define("NORMAL_MAPPING", config.normalMapping()) .define("PARALLAX_OCCLUSION_MAPPING", config.parallaxOcclusionMapping()) + .define("PLAYER_CANOPY_FADE", config.playerCanopy()) .define("SHADOW_MODE", configShadowMode) .define("SHADOW_TRANSPARENCY", config.shadowTransparency()) .define("SHADOW_FILTERING", config.shadowFiltering()) @@ -1314,8 +1315,8 @@ public void updateSceneFbo() { // Create depth render buffer rboSceneDepth = glGenRenderbuffers(); glBindRenderbuffer(GL_RENDERBUFFER, rboSceneDepth); - glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaaSamples, GL_DEPTH_COMPONENT32F, sceneResolution[0], sceneResolution[1]); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboSceneDepth); + glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaaSamples, GL_DEPTH24_STENCIL8, sceneResolution[0], sceneResolution[1]); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rboSceneDepth); checkGLErrors(); // If necessary, create an FBO for resolving multisampling @@ -1329,6 +1330,11 @@ public void updateSceneFbo() { checkGLErrors(); } + int status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + log.error("FBO incomplete: {}\n", status); + } + // Reset glBindFramebuffer(GL_FRAMEBUFFER, awtContext.getFramebuffer(false)); glBindRenderbuffer(GL_RENDERBUFFER, 0); @@ -1781,6 +1787,7 @@ public void processPendingConfigChanges() { case KEY_WIREFRAME: case KEY_SHADOW_FILTERING: case KEY_WINDOWS_HDR_CORRECTION: + case KEY_PLAYER_CANOPY: recompilePrograms = true; break; case KEY_ANTI_ALIASING_MODE: diff --git a/src/main/java/rs117/hd/HdPluginConfig.java b/src/main/java/rs117/hd/HdPluginConfig.java index 5fae4ff224..4284c47c0a 100644 --- a/src/main/java/rs117/hd/HdPluginConfig.java +++ b/src/main/java/rs117/hd/HdPluginConfig.java @@ -25,6 +25,8 @@ */ package rs117.hd; +import java.awt.Color; +import net.runelite.client.config.Alpha; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; @@ -40,6 +42,8 @@ import rs117.hd.config.DynamicLights; import rs117.hd.config.FogDepthMode; import rs117.hd.config.InfernalCape; +import rs117.hd.config.NPCSilhouetteMode; +import rs117.hd.config.PlayerSilhouetteMode; import rs117.hd.config.Saturation; import rs117.hd.config.SceneScalingMode; import rs117.hd.config.SeasonalHemisphere; @@ -784,7 +788,7 @@ default boolean characterDisplacement() { section = environmentSettings ) default boolean hideVanillaWaterEffects() { return true; } - + String KEY_POH_THEME_ENVIRONMENTS = "pohThemeEnvironments"; @ConfigItem( keyName = KEY_POH_THEME_ENVIRONMENTS, @@ -796,12 +800,103 @@ default boolean characterDisplacement() { default boolean pohThemeEnvironments() { return true; } + /*====== Silhouette & Canopy settings ======*/ + + @ConfigSection( + name = "Silhouette & Canopy", + description = "Silhouette settings", + position = 4, + closedByDefault = true + ) + String playerSilhouetteSettings = "playerSilhouetteSettings"; + + String KEY_PLAYER_CANOPY = "playerCanopy"; + @ConfigItem( + keyName = KEY_PLAYER_CANOPY, + position = 0, + name = "Canopy Fade Enabled", + description = "Fades Canopy when it is occluding the player", + section = playerSilhouetteSettings + ) + default boolean playerCanopy() { + return true; + } + + String KEY_SILHOUETTE_THRESHOLD = "silhouetteThreshold"; + @Units(Units.PERCENT) + @Range(min = 1, max = 100) + @ConfigItem( + keyName = KEY_SILHOUETTE_THRESHOLD, + position = 1, + name = "Silhouette Threshold", + description = "Threshold of how much of the actor is occluded, at which the silhouette should begin being drawn at (Default 50%)", + section = playerSilhouetteSettings + ) + default int silhouetteThreshold() { + return 50; + } + + String KEY_PLAYER_SILHOUETTE = "playerSilhouette"; + @ConfigItem( + keyName = KEY_PLAYER_SILHOUETTE, + position = 2, + name = "Player Silhouette", + description = "Draws the players silhouette whilst they are occluded by scene geometry", + section = playerSilhouetteSettings + ) + default PlayerSilhouetteMode playerSilhouette() { return PlayerSilhouetteMode.Player; } + + String KEY_NPC_SILHOUETTE = "npcSilhouette"; + @ConfigItem( + keyName = KEY_NPC_SILHOUETTE, + position = 3, + name = "NPC Silhouette", + description = "Draws the npcs silhouette whilst they are occluded by scene geometry", + section = playerSilhouetteSettings + ) + default NPCSilhouetteMode npcSilhouette() { return NPCSilhouetteMode.Interacting; } + + String KEY_PLAYER_SILHOUETTE_COLOR = "playerSilhouetteColor"; + @Alpha + @ConfigItem( + keyName = KEY_PLAYER_SILHOUETTE_COLOR, + position = 4, + name = "Player Color", + description = "Controls the colour of the players silhouette", + section = playerSilhouetteSettings + ) + default Color playerSilhouetteColor() { return new Color(20, 20, 20, 128); } + + String KEY_FRIENDLY_PLAYER_SILHOUETTE_COLOR = "friendlyNpcSilhouetteColor"; + @Alpha + @ConfigItem( + keyName = KEY_FRIENDLY_PLAYER_SILHOUETTE_COLOR, + position = 5, + name = "Friendly NPC Color", + description = "Controls the colour of the players silhouette", + section = playerSilhouetteSettings + ) + default Color friendlyNpcSilhouetteColor() { return new Color(16, 44, 58, 128); } + + String KEY_HOSTILE_NPC_SILHOUETTE_COLOR = "hostileNpcSilhouetteColor"; + @Alpha + @ConfigItem( + keyName = KEY_HOSTILE_NPC_SILHOUETTE_COLOR, + position = 6, + name = "Hostile NPC Color", + description = "Controls the colour of the npcs silhouette", + section = playerSilhouetteSettings + ) + default Color hostileNpcSilhouetteColor() { + return new Color(39, 5, 5, 128); + } + /*====== Miscellaneous settings ======*/ @ConfigSection( name = "Miscellaneous", description = "Miscellaneous settings", - position = 4, + position = 5, closedByDefault = true ) String miscellaneousSettings = "miscellaneousSettings"; @@ -955,7 +1050,7 @@ default boolean windowsHdrCorrection() { @ConfigSection( name = "Legacy", description = "Legacy options. If you dislike a change, you might find an option to change it back here.", - position = 5, + position = 6, closedByDefault = true ) String legacySettings = "legacySettings"; @@ -1108,7 +1203,7 @@ default boolean legacyTzHaarReskin() { @ConfigSection( name = "Experimental", description = "Experimental features - if you're experiencing issues you should consider disabling these.", - position = 6, + position = 7, closedByDefault = true ) String experimentalSettings = "experimentalSettings"; diff --git a/src/main/java/rs117/hd/config/NPCSilhouetteMode.java b/src/main/java/rs117/hd/config/NPCSilhouetteMode.java new file mode 100644 index 0000000000..78ddc4c5c9 --- /dev/null +++ b/src/main/java/rs117/hd/config/NPCSilhouetteMode.java @@ -0,0 +1,8 @@ +package rs117.hd.config; + +public enum NPCSilhouetteMode { + Disabled, + Interacting, + Hostile, + All +} diff --git a/src/main/java/rs117/hd/config/PlayerSilhouetteMode.java b/src/main/java/rs117/hd/config/PlayerSilhouetteMode.java new file mode 100644 index 0000000000..64dbfc827d --- /dev/null +++ b/src/main/java/rs117/hd/config/PlayerSilhouetteMode.java @@ -0,0 +1,8 @@ +package rs117.hd.config; + +public enum PlayerSilhouetteMode { + Disabled, + Player, + Player_Follower, + Everyone +} diff --git a/src/main/java/rs117/hd/opengl/GLOcclusionQueries.java b/src/main/java/rs117/hd/opengl/GLOcclusionQueries.java new file mode 100644 index 0000000000..8601d2cf09 --- /dev/null +++ b/src/main/java/rs117/hd/opengl/GLOcclusionQueries.java @@ -0,0 +1,142 @@ +package rs117.hd.opengl; + +import rs117.hd.utils.Destructible; + +import static org.lwjgl.opengl.GL15.GL_QUERY_RESULT; +import static org.lwjgl.opengl.GL15.GL_QUERY_RESULT_AVAILABLE; +import static org.lwjgl.opengl.GL15.GL_SAMPLES_PASSED; +import static org.lwjgl.opengl.GL15.glBeginQuery; +import static org.lwjgl.opengl.GL15.glDeleteQueries; +import static org.lwjgl.opengl.GL15.glEndQuery; +import static org.lwjgl.opengl.GL15.glGenQueries; +import static org.lwjgl.opengl.GL15.glGetQueryObjecti; +import static org.lwjgl.opengl.GL33.GL_ANY_SAMPLES_PASSED; +import static org.lwjgl.opengl.GL33.GL_QUERY_NO_WAIT; +import static org.lwjgl.opengl.GL33.GL_QUERY_WAIT; +import static org.lwjgl.opengl.GL33.glBeginConditionalRender; +import static org.lwjgl.opengl.GL33.glEndConditionalRender; +import static rs117.hd.utils.MathUtils.*; + +public final class GLOcclusionQueries implements Destructible { + + private static final long ID_SHIFT = 32; + private static final long ISSUED_BIT = 1L << 31; + private static final long RESULT_MASK = 0x7FFFFFFFL; + + private long query0, query1, query2; + + private int currentQuery; + private int currentTarget; + + private static long packId(int id) { return ((long) id) << ID_SHIFT; } + + private static int getId(long packed) { return (int) (packed >>> ID_SHIFT); } + + private static boolean isIssued(long packed) { return (packed & ISSUED_BIT) != 0; } + + private static long setIssued(long packed) { return packed | ISSUED_BIT; } + + private static long setResult(long packed, int result) { return (packed & ~RESULT_MASK) | (result & RESULT_MASK); } + + private static int getResult(long packed) { return (int) (packed & RESULT_MASK); } + + public void initialize() { + query0 = packId(glGenQueries()); + query1 = packId(glGenQueries()); + query2 = packId(glGenQueries()); + } + + private long getSlot(int idx) { + return idx == 0 ? query0 : idx == 1 ? query1 : query2; + } + + private void setSlot(int idx, long value) { + if (idx == 0) query0 = value; + else if (idx == 1) query1 = value; + else query2 = value; + } + + private int readbackIndex() { return (currentQuery + 1) % 3; } + + public void beginQuery(boolean exactSamples) { + currentTarget = exactSamples ? GL_SAMPLES_PASSED : GL_ANY_SAMPLES_PASSED; + glBeginQuery(currentTarget, getId(getSlot(currentQuery))); + } + + public void endQuery() { + glEndQuery(currentTarget); + + long slot = setIssued(getSlot(currentQuery)); + setSlot(currentQuery, slot); + + currentQuery = (currentQuery + 1) % 3; + } + + private void readBackResult() { + int idx = readbackIndex(); + long slot = getSlot(idx); + + if (!isIssued(slot)) + return; + + int id = getId(slot); + + if (glGetQueryObjecti(id, GL_QUERY_RESULT_AVAILABLE) != 0) { + slot = setResult(slot, glGetQueryObjecti(id, GL_QUERY_RESULT)); + setSlot(idx, slot); + } + } + + public int getResult() { + readBackResult(); + return getResult(getSlot(readbackIndex())); + } + + public boolean isVisible() { return getResult() > 0; } + + public int getVisiblePixels(int msaaSamples) { + int result = getResult(); + return msaaSamples > 0 ? result / msaaSamples : result; + } + + public float getVisibilityRatio(int proxyPixelArea, int msaaSamples) { + if (proxyPixelArea <= 0) + return 0f; + + float samples = getResult(); + if(samples == 0) + return 0f; + + float denom = msaaSamples > 0 + ? proxyPixelArea * (float) msaaSamples + : proxyPixelArea; + float ratio = samples / denom; + return ratio < 0f ? 0f : min(ratio, 1f); + } + + public void beginConditionalRender(boolean wait) { + int idx = readbackIndex(); + long slot = getSlot(idx); + + if (!isIssued(slot)) + return; + + glBeginConditionalRender( + getId(slot), + wait ? GL_QUERY_WAIT : GL_QUERY_NO_WAIT + ); + } + + public void endConditionalRender() { glEndConditionalRender(); } + + public int getCurrentQuery() { return getId(getSlot(currentQuery)); } + + @Override + public void destroy() { + if (query0 != 0) glDeleteQueries(getId(query0)); + if (query1 != 0) glDeleteQueries(getId(query1)); + if (query2 != 0) glDeleteQueries(getId(query2)); + + query0 = query1 = query2 = 0; + } +} \ No newline at end of file diff --git a/src/main/java/rs117/hd/opengl/shader/BasicSceneProgram.java b/src/main/java/rs117/hd/opengl/shader/BasicSceneProgram.java new file mode 100644 index 0000000000..854bfeb01a --- /dev/null +++ b/src/main/java/rs117/hd/opengl/shader/BasicSceneProgram.java @@ -0,0 +1,13 @@ +package rs117.hd.opengl.shader; + +import static org.lwjgl.opengl.GL33C.*; + +public class BasicSceneProgram extends ShaderProgram { + public Uniform4f uniColor = addUniform4f("color"); + + public BasicSceneProgram() { + super(t -> t + .add(GL_VERTEX_SHADER, "basic_scene_vert.glsl") + .add(GL_FRAGMENT_SHADER, "basic_scene_frag.glsl")); + } +} diff --git a/src/main/java/rs117/hd/opengl/shader/DepthSceneShaderProgram.java b/src/main/java/rs117/hd/opengl/shader/DepthSceneShaderProgram.java new file mode 100644 index 0000000000..4052841d6c --- /dev/null +++ b/src/main/java/rs117/hd/opengl/shader/DepthSceneShaderProgram.java @@ -0,0 +1,11 @@ +package rs117.hd.opengl.shader; + +import static org.lwjgl.opengl.GL33C.*; + +public class DepthSceneShaderProgram extends ShaderProgram { + public DepthSceneShaderProgram() { + super(t -> t + .add(GL_VERTEX_SHADER, "depth_scene_vert.glsl") + .add(GL_FRAGMENT_SHADER, "depth_scene_frag.glsl")); + } +} diff --git a/src/main/java/rs117/hd/opengl/shader/ShaderProgram.java b/src/main/java/rs117/hd/opengl/shader/ShaderProgram.java index 651c923979..13b76cec92 100644 --- a/src/main/java/rs117/hd/opengl/shader/ShaderProgram.java +++ b/src/main/java/rs117/hd/opengl/shader/ShaderProgram.java @@ -1,5 +1,6 @@ package rs117.hd.opengl.shader; +import java.awt.Color; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -287,6 +288,26 @@ public void set(float x, float y, float z, float w) { glUniform4f(uniformIndex, x, y, z, w); } + public void set(Color color) { + assert program.isActive(); + if(color != null) + glUniform4f(uniformIndex, + color.getRed() / 255.0f, + color.getBlue() / 255.0f, + color.getGreen() / 255.0f, + color.getAlpha() / 255.0f); + } + + public void set(Color color, float alpha) { + assert program.isActive(); + if(color != null) + glUniform4f(uniformIndex, + color.getRed() / 255.0f, + color.getBlue() / 255.0f, + color.getGreen() / 255.0f, + alpha); + } + public void set(float... vec4) { assert program.isActive(); glUniform4fv(uniformIndex, vec4); diff --git a/src/main/java/rs117/hd/opengl/uniforms/UBOGlobal.java b/src/main/java/rs117/hd/opengl/uniforms/UBOGlobal.java index 1343819ecc..8ac1f4a661 100644 --- a/src/main/java/rs117/hd/opengl/uniforms/UBOGlobal.java +++ b/src/main/java/rs117/hd/opengl/uniforms/UBOGlobal.java @@ -61,6 +61,9 @@ public void initialize() { public Property invProjectionMatrix = addProperty(PropertyType.Mat4, "invProjectionMatrix"); public Property lightProjectionMatrix = addProperty(PropertyType.Mat4, "lightProjectionMatrix"); + public Property playerPosition = addProperty(PropertyType.FVec3, "playerPosition"); + public Property playerHeight = addProperty(PropertyType.Float, "playerHeight"); + public Property canopyFadeStrength = addProperty(PropertyType.Float, "canopyFadeStrength"); public Property lightningBrightness = addProperty(PropertyType.Float, "lightningBrightness"); public Property elapsedTime = addProperty(PropertyType.Float, "elapsedTime"); } diff --git a/src/main/java/rs117/hd/overlays/Timer.java b/src/main/java/rs117/hd/overlays/Timer.java index 5f998a8651..a81d57991b 100644 --- a/src/main/java/rs117/hd/overlays/Timer.java +++ b/src/main/java/rs117/hd/overlays/Timer.java @@ -24,6 +24,7 @@ public enum Timer { DRAW_TEMP, DRAW_POSTSCENE, DRAW_TILED_LIGHTING, + DRAW_SILHOUETTES, DRAW_SUBMIT, // Miscellaneous @@ -70,6 +71,7 @@ public enum Timer { CLEAR_SCENE(GPU_TIMER), RENDER_SHADOWS(GPU_TIMER), RENDER_SCENE(GPU_TIMER), + RENDER_SILHOUETTES(GPU_TIMER), RENDER_UI(GPU_TIMER, "Render UI"), ; diff --git a/src/main/java/rs117/hd/renderer/zone/DynamicModelVAO.java b/src/main/java/rs117/hd/renderer/zone/DynamicModelVAO.java index 057d0ed059..dbb4dbdd79 100644 --- a/src/main/java/rs117/hd/renderer/zone/DynamicModelVAO.java +++ b/src/main/java/rs117/hd/renderer/zone/DynamicModelVAO.java @@ -315,5 +315,27 @@ public int getVertexCount() { public void end() { endDraw(this); } + + public void draw(DrawCall draw) { + draw.vao = vao; + draw.offset = getStartOffset() / VERT_SIZE_INTS; + draw.count = getVertexCount(); + } + } + + public static final class DrawCall { + public int vao; + public int offset; + public int count; + + public void reset() { + vao = 0; + offset = 0; + count = 0; + } + + public boolean isValid() { + return vao != 0 && offset >= 0 && count > 0; + } } } diff --git a/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java b/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java index fc01cb4ebe..fd29fffcd5 100644 --- a/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java +++ b/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java @@ -18,6 +18,7 @@ import rs117.hd.config.ShadowMode; import rs117.hd.overlays.FrameTimer; import rs117.hd.overlays.Timer; +import rs117.hd.renderer.zone.renderpass.SilhouettePass; import rs117.hd.scene.ModelOverrideManager; import rs117.hd.scene.model_overrides.ModelOverride; import rs117.hd.utils.HDUtils; @@ -30,6 +31,7 @@ import static rs117.hd.HdPlugin.PROCESSOR_COUNT; import static rs117.hd.renderer.zone.WorldViewContext.VAO_ALPHA; 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_SHADOW; import static rs117.hd.utils.MathUtils.*; @@ -69,11 +71,19 @@ public class ModelStreamingManager { @Inject private ZoneRenderer renderer; + @Inject + private SilhouettePass silhouettePass; + private final ArrayList pending = new ArrayList<>(); private final StreamingContext[] streamingContexts = new StreamingContext[RL_RENDER_THREADS + 1]; private int numRenderThreads; private int playerDrawIndex; + private Renderable localPlayer; + public int localPlayerVaoOpaque; + public int localPlayerVaoAlpha; + public int localPlayerOpaqueVertexOffset, localPlayerOpaqueVertexCount; + static final class StreamingContext { final int[] worldPos = new int[3]; final float[] objectWorldPos = new float[4]; @@ -333,7 +343,8 @@ private void uploadTempModel( // opaque player faces have their own vao and are drawn in a separate pass from normal opaque faces // because they are not depth tested. transparent player faces don't need their own vao because normal // transparent faces are already not depth tested - final int alphaFaceCount = hasAlpha ? sceneUploader.tempModelAlphaFaces : 0; + final int vaoType = renderable instanceof Player ? VAO_PLAYER : VAO_OPAQUE; + final int alphaFaceCount = hasAlpha && vaoType == VAO_OPAQUE ? sceneUploader.tempModelAlphaFaces : 0; final int opaqueFaceCount = visibleFaces.length - alphaFaceCount; final DynamicModelVAO.View opaqueView; @@ -357,6 +368,8 @@ private void uploadTempModel( // Fix rendering projectiles from boats with hide roofs enabled int plane = Math.min(ctx.maxLevel, gameObject.getPlane()); + if (renderable instanceof Actor && silhouettePass.isSilhouetteEnabled((Actor) renderable)) + silhouettePass.addSilhouetteDraw(gameObject, (Actor) renderable, opaqueView, x, y, z); if (opaqueView != alphaView) { if (alphaView.getEndOffset() > alphaView.getStartOffset()) { zone.addTempAlphaModel( diff --git a/src/main/java/rs117/hd/renderer/zone/Zone.java b/src/main/java/rs117/hd/renderer/zone/Zone.java index 3a8a009c6f..27470c31b9 100644 --- a/src/main/java/rs117/hd/renderer/zone/Zone.java +++ b/src/main/java/rs117/hd/renderer/zone/Zone.java @@ -32,6 +32,8 @@ import static rs117.hd.HdPlugin.GL_CAPS; import static rs117.hd.HdPlugin.SUPPORTS_INDIRECT_DRAW; import static rs117.hd.HdPlugin.checkGLErrors; +import static rs117.hd.renderer.zone.ZoneRenderer.CANOPY_STENCIL_REF; +import static rs117.hd.renderer.zone.ZoneRenderer.OPAQUE_STENCIL_REF; import static rs117.hd.renderer.zone.ZoneRenderer.TEXTURE_UNIT_TEXTURED_FACES; import static rs117.hd.renderer.zone.ZoneRenderer.eboAlpha; import static rs117.hd.utils.MathUtils.*; @@ -371,6 +373,7 @@ void renderOpaque(CommandBuffer cmd, WorldViewContext ctx, boolean roofShadows) lastDrawMode = STATIC_UNSORTED; lastVao = glVao; lastTboF = tboF.getTexId(); + lastStencil = OPAQUE_STENCIL_REF; flush(cmd); } @@ -414,6 +417,7 @@ public static class AlphaModel { byte lx, lz, ux, uz; // lower/upper zone coords byte zofx, zofz; // for temp alpha models, offset of source zone from target zone byte flags; + byte stencil; // only set for static geometry as they require sorting int radius; @@ -500,6 +504,7 @@ void addAlphaModel( int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE; int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE; + boolean isCanopy = false; for (int f = 0; f < faceCount; ++f) { if (color3[f] == -2) continue; @@ -588,6 +593,7 @@ void addAlphaModel( int fy = (((int) (vertexY[indices1[f]] + vertexY[indices2[f]] + vertexY[indices3[f]]) / 3) - cy) >> shift; int fz = (((int) (vertexZ[indices1[f]] + vertexZ[indices2[f]] + vertexZ[indices3[f]]) / 3) - cz) >> shift; + isCanopy = isCanopy || material.isCanopy; radius = Math.max(radius, fx * fx + fy * fy + fz * fz); packedFaces[bufferIdx] = ((fx & ((1 << 11) - 1)) << 21) @@ -598,6 +604,7 @@ void addAlphaModel( m.radius = 2 + (int) Math.sqrt(radius); m.sortedFaces = new int[bufferIdx * 3]; + m.stencil = CANOPY_STENCIL_REF; assert packedFaces.length > 0; // Normally these will be equal, but transparency is used to hide faces in the TzHaar reskin @@ -624,6 +631,7 @@ synchronized void addTempAlphaModel(ModelOverride modelOverride, DynamicModelVAO m.lx = m.lz = m.ux = m.uz = -1; m.flags = 0; m.zofx = m.zofz = 0; + m.stencil = 0; alphaModels.add(m); } @@ -637,6 +645,7 @@ synchronized void postAlphaPass() { alphaModels.remove(i); m.packedFaces = null; m.sortedFaces = null; + m.stencil = 0; modelCache.add(m); } m.asyncSortIdx = -1; @@ -654,6 +663,7 @@ synchronized void postAlphaPass() { private static int lastVao; private static int lastTboF; private static int lastzx, lastzz; + private static byte lastStencil; private static final class AlphaSortPredicate implements ToIntFunction { int cx, cy, cz; @@ -700,7 +710,7 @@ void renderAlpha( int zz, int level, WorldViewContext ctx, - boolean isShadowPass, + boolean sorted, boolean includeRoof ) { if (alphaModels.isEmpty()) @@ -717,9 +727,7 @@ void renderAlpha( drawIdx = 0; - cmd.DepthMask(false); - - if (!isShadowPass) + if (sorted) sortedAlphaFacesUpload.waitForCompletion(); int eboAlphaStart = eboAlphaOffset = ZoneRenderer.eboAlphaWriter.getWrittenInts(); @@ -736,13 +744,14 @@ void renderAlpha( if (m.isTemp()) { // these are already sorted and so just requires a glMultiDrawArrays() from the active vao drawMode = TEMP; - } else if (isShadowPass || m.asyncSortIdx < 0) { + } else if (!sorted || m.asyncSortIdx < 0) { drawMode = STATIC_UNSORTED; } if (lastDrawMode != drawMode || lastVao != m.vao || lastTboF != m.tboF || + lastStencil != m.stencil || lastzx != (zx - m.zofx) || lastzz != (zz - m.zofz) ) { @@ -750,6 +759,7 @@ void renderAlpha( lastDrawMode = drawMode; lastVao = m.vao; lastTboF = m.tboF; + lastStencil = m.stencil; lastzx = zx - m.zofx; lastzz = zz - m.zofz; } @@ -782,8 +792,6 @@ void renderAlpha( } flush(cmd); - - cmd.DepthMask(true); } private void flush(CommandBuffer cmd) { @@ -793,6 +801,7 @@ private void flush(CommandBuffer cmd) { long byteOffset = 4L * (eboAlphaOffset - vertexCount); cmd.BindVertexArray(lastVao, eboAlpha); cmd.BindTextureUnit(GL_TEXTURE_BUFFER, lastTboF, TEXTURE_UNIT_TEXTURED_FACES); + cmd.StencilFunc(GL_ALWAYS, lastStencil, 0xFF); // The EBO & IDO is bound by in ZoneRenderer if (GL_CAPS.OpenGL40 && SUPPORTS_INDIRECT_DRAW) { cmd.DrawElementsIndirect(GL_TRIANGLES, vertexCount, (int) (byteOffset / 4L), ZoneRenderer.indirectDrawCmdsStaging); @@ -805,6 +814,7 @@ private void flush(CommandBuffer cmd) { convertForDraw(lastDrawMode == STATIC_UNSORTED ? VERT_SIZE : DynamicModelVAO.VERT_SIZE); cmd.BindVertexArray(lastVao); cmd.BindTextureUnit(GL_TEXTURE_BUFFER, lastTboF, TEXTURE_UNIT_TEXTURED_FACES); + cmd.StencilFunc(GL_ALWAYS, lastStencil, 0xFF); if (drawIdx == 1) { if (GL_CAPS.OpenGL40 && SUPPORTS_INDIRECT_DRAW) { cmd.DrawArraysIndirect(GL_TRIANGLES, drawOff[0], drawEnd[0], ZoneRenderer.indirectDrawCmdsStaging); @@ -884,6 +894,7 @@ synchronized void multizoneLocs(SceneContext ctx, int zx, int zz, Camera camera, m2.uz = m.uz; m2.zofx = (byte) (closestZoneX - zx); m2.zofz = (byte) (closestZoneZ - zz); + m2.stencil = m.stencil; m2.packedFaces = m.packedFaces; m2.radius = m.radius; diff --git a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java index 5a5e80837b..6174324357 100644 --- a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java +++ b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java @@ -42,6 +42,8 @@ import rs117.hd.config.ColorFilter; import rs117.hd.config.DynamicLights; import rs117.hd.config.ShadowMode; +import rs117.hd.opengl.shader.BasicSceneProgram; +import rs117.hd.opengl.shader.DepthSceneShaderProgram; import rs117.hd.opengl.shader.SceneShaderProgram; import rs117.hd.opengl.shader.ShaderException; import rs117.hd.opengl.shader.ShaderIncludes; @@ -51,6 +53,7 @@ import rs117.hd.overlays.FrameTimer; import rs117.hd.overlays.Timer; import rs117.hd.renderer.Renderer; +import rs117.hd.renderer.zone.renderpass.SilhouettePass; import rs117.hd.scene.EnvironmentManager; import rs117.hd.scene.LightManager; import rs117.hd.scene.ProceduralGenerator; @@ -86,6 +89,10 @@ @Slf4j @Singleton public class ZoneRenderer implements Renderer { + public static final byte OPAQUE_STENCIL_REF = 1; + public static final byte CANOPY_STENCIL_REF = 1 << 1; + public static final byte ACTOR_STENCIL_REF = 1 << 2; + public static final int FRAMES_IN_FLIGHT = 3; private static int TEXTURE_UNIT_COUNT = HdPlugin.TEXTURE_UNIT_COUNT; @@ -127,6 +134,12 @@ public class ZoneRenderer implements Renderer { @Inject private SceneShaderProgram sceneProgram; + @Inject + private DepthSceneShaderProgram depthSceneProgram; + + @Inject + private BasicSceneProgram basicSceneProgram; + @Inject private ShadowShaderProgram.Fast fastShadowProgram; @@ -139,15 +152,21 @@ public class ZoneRenderer implements Renderer { @Inject private UBOWorldViews uboWorldViews; + @Inject + private SilhouettePass silhouettePass; + public final Camera sceneCamera = new Camera().setReverseZ(true); public final Camera directionalCamera = new Camera().setOrthographic(true); public final ShadowCasterVolume directionalShadowCasterVolume = new ShadowCasterVolume(directionalCamera); public final RenderState renderState = new RenderState(); public final CommandBuffer sceneCmd = new CommandBuffer("Scene", renderState); + public final CommandBuffer alphaDepthCmd = new CommandBuffer("SceneAlphaDepth", renderState); public final CommandBuffer directionalCmd = new CommandBuffer("Directional", renderState); - private GLBuffer indirectDrawCmds; + private final float[] playerPosition = new float[3]; + + public GLBuffer indirectDrawCmds; public static GpuIntBuffer indirectDrawCmdsStaging; public static GLBuffer.EBO eboAlpha; @@ -188,6 +207,7 @@ public void initialize() { uboWorldViews.initialize(UNIFORM_BLOCK_WORLD_VIEWS); sceneManager.initialize(renderState, uboWorldViews); modelStreamingManager.initialize(); + silhouettePass.initialize(renderState, depthSceneProgram, basicSceneProgram); // Force updates that only run when the cameras change sceneCamera.setDirty(); @@ -202,6 +222,7 @@ public void destroy() { modelStreamingManager.destroy(); sceneManager.destroy(); uboWorldViews.destroy(); + silhouettePass.destroy(); if (SceneUploader.POOL != null) SceneUploader.POOL.destroy(); @@ -227,6 +248,8 @@ public void addShaderIncludes(ShaderIncludes includes) { @Override public void initializeShaders(ShaderIncludes includes) throws ShaderException, IOException { sceneProgram.compile(includes); + depthSceneProgram.compile(includes); + basicSceneProgram.compile(includes); fastShadowProgram.compile(includes); detailedShadowProgram.compile(includes); } @@ -234,6 +257,8 @@ public void initializeShaders(ShaderIncludes includes) throws ShaderException, I @Override public void destroyShaders() { sceneProgram.destroy(); + depthSceneProgram.destroy(); + basicSceneProgram.destroy(); fastShadowProgram.destroy(); detailedShadowProgram.destroy(); } @@ -269,6 +294,8 @@ private void destroyBuffers() { public void processConfigChanges(Set keys) { if (keys.contains(KEY_ASYNC_MODEL_PROCESSING) || keys.contains(KEY_ASYNC_MODEL_CACHE_SIZE)) modelStreamingManager.reinitialize(); + + silhouettePass.updateCachedConfigs(); } @Override @@ -583,6 +610,8 @@ private void preSceneDrawTopLevel( plugin.uboGlobal.underwaterCausticsColor.set(environmentManager.currentUnderwaterCausticsColor); plugin.uboGlobal.underwaterCausticsStrength.set(environmentManager.currentUnderwaterCausticsStrength); plugin.uboGlobal.elapsedTime.set((float) (plugin.elapsedTime % MAX_FLOAT_WITH_128TH_PRECISION)); + plugin.uboGlobal.canopyFadeStrength.set(silhouettePass.getCanopyFadeStrength()); + plugin.uboGlobal.upload(); if (plugin.configColorFilter != ColorFilter.NONE) { plugin.uboGlobal.colorFilter.set(plugin.configColorFilter.ordinal()); @@ -596,6 +625,7 @@ private void preSceneDrawTopLevel( // Reset buffers for the next frame indirectDrawCmdsStaging.clear(); sceneCmd.reset(); + alphaDepthCmd.reset(); directionalCmd.reset(); renderState.reset(); @@ -624,10 +654,6 @@ private void postDrawTopLevel() { return; sceneFboValid = true; - - // Upload world views before rendering - uboWorldViews.upload(); - if (eboAlphaWriter != null) eboAlphaWriter.flush(); @@ -706,6 +732,7 @@ private void directionalShadowPass() { renderState.enable.set(GL_DEPTH_TEST); renderState.disable.set(GL_CULL_FACE); + renderState.disable.set(GL_STENCIL_TEST); renderState.depthFunc.set(GL_LEQUAL); renderState.ido.set(indirectDrawCmds.id); @@ -733,6 +760,7 @@ private void scenePass() { } renderState.viewport.set(0, 0, plugin.sceneResolution[0], plugin.sceneResolution[1]); renderState.ido.set(indirectDrawCmds.id); + renderState.depthMask.set(true); renderState.apply(); // Clear scene @@ -747,7 +775,8 @@ private void scenePass() { 1f ); glClearDepth(0); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glClearStencil(0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); frameTimer.end(Timer.CLEAR_SCENE); frameTimer.begin(Timer.RENDER_SCENE); @@ -755,26 +784,66 @@ private void scenePass() { renderState.enable.set(GL_BLEND); renderState.enable.set(GL_CULL_FACE); renderState.enable.set(GL_DEPTH_TEST); + renderState.enable.set(GL_STENCIL_TEST); renderState.depthFunc.set(GL_GEQUAL); renderState.blendFunc.set(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE); + renderState.stencilMask.set(0xFF); + renderState.stencilFunc.set(GL_ALWAYS, OPAQUE_STENCIL_REF, 0xFF); + renderState.stencilOp.set(GL_KEEP, GL_KEEP, GL_REPLACE); + renderState.apply(); - // Render the scene sceneCmd.execute(); - // TODO: Filler tiles frameTimer.end(Timer.RENDER_SCENE); glBindVertexArray(0); // Done rendering the scene + renderState.stencilMask.set(0); renderState.disable.set(GL_BLEND); renderState.disable.set(GL_CULL_FACE); renderState.disable.set(GL_DEPTH_TEST); + renderState.disable.set(GL_STENCIL_TEST); renderState.apply(); frameTimer.end(Timer.DRAW_SCENE); } + private void sceneAlphaDepthPass() { + 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(); + + depthSceneProgram.use(); + + renderState.enable.set(GL_DEPTH_TEST); + renderState.enable.set(GL_BLEND); + renderState.enable.set(GL_CULL_FACE); + renderState.enable.set(GL_STENCIL_TEST); + renderState.colorMask.set(false, false, false, false); + renderState.depthMask.set(true); + renderState.stencilMask.set(0xFF); + renderState.stencilFunc.set(GL_ALWAYS, 0, 0xFF); + renderState.stencilOp.set(GL_KEEP, GL_KEEP, GL_REPLACE); + renderState.apply(); + + alphaDepthCmd.execute(); + + renderState.colorMask.set(true, true, true, true); + renderState.stencilMask.set(0); + renderState.disable.set(GL_BLEND); + renderState.disable.set(GL_STENCIL_TEST); + renderState.disable.set(GL_CULL_FACE); + renderState.disable.set(GL_DEPTH_TEST); + renderState.apply(); + } + @Override public boolean zoneInFrustum(int zx, int zz, int maxY, int minY) { if (!sceneManager.isTopLevelValid()) @@ -890,11 +959,16 @@ public void drawZoneAlpha(Projection entityProjection, Scene scene, int level, i 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); + z.renderAlpha(directionalCmd, zx - offset, zz - offset, level, ctx, false, shouldDrawRoofShadows); } - if (!sceneManager.isRoot(ctx) || z.inSceneFrustum) - z.renderAlpha(sceneCmd, zx - offset, zz - offset, level, ctx, false, false); + if (!sceneManager.isRoot(ctx) || z.inSceneFrustum) { + sceneCmd.DepthMask(false); + z.renderAlpha(sceneCmd, zx - offset, zz - offset, level, ctx, true, false); + sceneCmd.DepthMask(true); + + z.renderAlpha(alphaDepthCmd, zx - offset, zz - offset, level, ctx, false, false); + } } frameTimer.end(Timer.DRAW_ZONE_ALPHA); @@ -912,9 +986,10 @@ public void drawPass(Projection projection, Scene scene, int pass) { switch (pass) { case DrawCallbacks.PASS_OPAQUE: directionalCmd.SetShader(fastShadowProgram); - directionalCmd.ExecuteSubCommandBuffer(ctx.vaoDirectionalCmd); sceneCmd.ExecuteSubCommandBuffer(ctx.vaoSceneCmd); + directionalCmd.ExecuteSubCommandBuffer(ctx.vaoDirectionalCmd); + break; case DrawCallbacks.PASS_ALPHA: modelStreamingManager.ensureAsyncUploadsComplete(null); @@ -927,22 +1002,24 @@ public void drawPass(Projection projection, Scene scene, int pass) { 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 shadow-only models ctx.drawAll(VAO_SHADOW, ctx.vaoDirectionalCmd); + ctx.drawAll(VAO_OPAQUE, ctx.vaoSceneCmd); + // 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 + // Draw players opaque, writing only depth + ctx.vaoSceneCmd.SetShader(depthSceneProgram); ctx.vaoSceneCmd.ColorMask(false, false, false, false); + ctx.drawAll(VAO_PLAYER, ctx.vaoSceneCmd); + + ctx.vaoSceneCmd.SetShader(sceneProgram); ctx.vaoSceneCmd.ColorMask(true, true, true, true); for (int zx = 0; zx < ctx.sizeX; ++zx) @@ -981,6 +1058,15 @@ public void drawDynamic( @Override public void drawTemp(Projection worldProjection, Scene scene, GameObject gameObject, Model m, int orientation, int x, int y, int z) { frameTimer.begin(Timer.DRAW_TEMP); + + if(gameObject.getRenderable() == client.getLocalPlayer()) { + if(scene.getWorldViewId() != WorldView.TOPLEVEL) { + worldProjection.project(x, y, z, playerPosition); + } else { + vec3(playerPosition, x, y, z); + } + } + try { modelStreamingManager.drawTemp(worldProjection, scene, gameObject, m, orientation, x, y, z); } catch (Exception ex) { @@ -1010,9 +1096,15 @@ public void draw(int overlayColor) { frameTimer.begin(Timer.DRAW_SUBMIT); if (shouldRenderScene) { + plugin.uboGlobal.playerPosition.set(playerPosition); + plugin.uboGlobal.playerHeight.set((float) client.getLocalPlayer().getModelHeight()); + uboWorldViews.upload(); + tiledLightingPass(); directionalShadowPass(); scenePass(); + sceneAlphaDepthPass(); + silhouettePass.draw(); } if (sceneFboValid && plugin.sceneResolution != null && plugin.sceneViewport != null) { diff --git a/src/main/java/rs117/hd/renderer/zone/renderpass/SilhouettePass.java b/src/main/java/rs117/hd/renderer/zone/renderpass/SilhouettePass.java new file mode 100644 index 0000000000..55c66b5c3d --- /dev/null +++ b/src/main/java/rs117/hd/renderer/zone/renderpass/SilhouettePass.java @@ -0,0 +1,475 @@ +package rs117.hd.renderer.zone.renderpass; + +import java.awt.Color; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.function.ToIntFunction; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.Getter; +import net.runelite.api.*; +import rs117.hd.HdPlugin; +import rs117.hd.HdPluginConfig; +import rs117.hd.config.NPCSilhouetteMode; +import rs117.hd.config.PlayerSilhouetteMode; +import rs117.hd.opengl.GLOcclusionQueries; +import rs117.hd.opengl.shader.BasicSceneProgram; +import rs117.hd.opengl.shader.DepthSceneShaderProgram; +import rs117.hd.overlays.FrameTimer; +import rs117.hd.overlays.Timer; +import rs117.hd.renderer.zone.DynamicModelVAO; +import rs117.hd.renderer.zone.DynamicModelVAO.DrawCall; +import rs117.hd.renderer.zone.ZoneRenderer; +import rs117.hd.utils.Camera; +import rs117.hd.utils.CommandBuffer; +import rs117.hd.utils.ModelHash; +import rs117.hd.utils.RenderState; + +import static org.lwjgl.opengl.GL11C.GL_BLEND; +import static org.lwjgl.opengl.GL11C.GL_CULL_FACE; +import static org.lwjgl.opengl.GL11C.GL_DEPTH_TEST; +import static org.lwjgl.opengl.GL11C.GL_EQUAL; +import static org.lwjgl.opengl.GL11C.GL_GEQUAL; +import static org.lwjgl.opengl.GL11C.GL_KEEP; +import static org.lwjgl.opengl.GL11C.GL_LEQUAL; +import static org.lwjgl.opengl.GL11C.GL_NOTEQUAL; +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_REPLACE; +import static org.lwjgl.opengl.GL11C.GL_SRC_ALPHA; +import static org.lwjgl.opengl.GL11C.GL_STENCIL_TEST; +import static org.lwjgl.opengl.GL11C.GL_TRIANGLES; +import static org.lwjgl.opengl.GL11C.GL_ZERO; +import static org.lwjgl.opengl.GL13C.GL_MULTISAMPLE; +import static org.lwjgl.opengl.GL30C.GL_DRAW_FRAMEBUFFER; +import static rs117.hd.config.PlayerSilhouetteMode.Player_Follower; +import static rs117.hd.renderer.zone.ZoneRenderer.ACTOR_STENCIL_REF; +import static rs117.hd.renderer.zone.ZoneRenderer.CANOPY_STENCIL_REF; +import static rs117.hd.renderer.zone.ZoneRenderer.OPAQUE_STENCIL_REF; +import static rs117.hd.utils.MathUtils.*; + +@Singleton +public final class SilhouettePass { + @Inject + private FrameTimer frameTimer; + + @Inject + private ZoneRenderer renderer; + + @Inject + private HdPlugin plugin; + + @Inject + private HdPluginConfig config; + + @Inject + private Client client; + + private Player localPlayer; + private Actor localPlayerInteracting, localPlayerFollower; + private CommandBuffer cmd; + private RenderState renderState; + private DepthSceneShaderProgram depthSceneProgram; + private BasicSceneProgram basicSceneProgram; + + private boolean configPlayerCanopy; + private float configSilhouetteThreshold; + private Color configPlayerSilhouetteColor; + private Color configFriendlyNPCSilhouetteColor; + private Color configHostileNPCSilhouetteColor; + private PlayerSilhouetteMode configPlayerSilhouette; + private NPCSilhouetteMode configNPCSilhouette; + + private final AlphaSortPredicate alphaSortPred = new AlphaSortPredicate(); + private final Comparator alphaSortComparator = Comparator.comparingInt(alphaSortPred); + + private final SilhouetteDraw playerSilhouetteDraw = new SilhouetteDraw(true); + private final ArrayDeque freeDraws = new ArrayDeque<>(); + private final List activeDraws = new ArrayList<>(); + private final List pendingDraw = new ArrayList<>(); + private final HashMap actorToInteracting = new HashMap<>(); + + private static final class AlphaSortPredicate implements ToIntFunction { + int cx, cy, cz; + + @Override + public int applyAsInt(SilhouetteDraw m) { + return (m.x - cx) * (m.x - cx) + (m.y - cy) * (m.y - cy) + (m.z - cz) * (m.z - cz); + } + } + + public void initialize(RenderState renderState, DepthSceneShaderProgram depthSceneProgram, BasicSceneProgram basicSceneProgram) { + this.renderState = renderState; + this.depthSceneProgram = depthSceneProgram; + this.basicSceneProgram = basicSceneProgram; + + cmd = new CommandBuffer("Silhouette", renderState); + updateCachedConfigs(); + } + + public void updateCachedConfigs() { + configPlayerCanopy = config.playerCanopy(); + configSilhouetteThreshold = 1.0f - saturate(config.silhouetteThreshold() / 100.0f); + configPlayerSilhouette = config.playerSilhouette(); + configNPCSilhouette = config.npcSilhouette(); + configPlayerSilhouetteColor = config.playerSilhouetteColor(); + configFriendlyNPCSilhouetteColor = config.friendlyNpcSilhouetteColor(); + configHostileNPCSilhouetteColor = config.hostileNpcSilhouetteColor(); + } + + public void destroy() { + for(SilhouetteDraw actorSilhouette : activeDraws) + actorSilhouette.destroy(); + activeDraws.clear(); + + cmd = null; + renderState = null; + } + + private void putInteracting(Actor actor) { + Actor interacting = actor.getInteracting(); + if(interacting != null) + actorToInteracting.put(actor, interacting); + } + + private void buildInteractingMap() { + WorldView root = client.getTopLevelWorldView(); + if(root == null) + return; + + actorToInteracting.clear(); + + root.players().forEach(this::putInteracting); + root.npcs().forEach(this::putInteracting); + for (WorldEntity we : root.worldEntities()) { + WorldView wev = we.getWorldView(); + if(wev != null) { + wev.players().forEach(this::putInteracting); + wev.npcs().forEach(this::putInteracting); + } + } + } + + public void draw() { + if(configPlayerSilhouette == PlayerSilhouetteMode.Disabled && + configNPCSilhouette == NPCSilhouetteMode.Disabled && + !configPlayerCanopy) + return; + + localPlayer = client.getLocalPlayer(); + localPlayerInteracting = localPlayer != null ? client.getLocalPlayer().getInteracting() : null; + localPlayerFollower = localPlayer != null ? client.getFollower() : null; + + buildInteractingMap(); + + frameTimer.begin(Timer.DRAW_SILHOUETTES); + 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(); + + final Camera camera = renderer.sceneCamera; + alphaSortPred.cx = (int)camera.getPositionX(); + alphaSortPred.cy = (int)camera.getPositionY(); + alphaSortPred.cz = (int)camera.getPositionZ(); + pendingDraw.sort(alphaSortComparator); + + frameTimer.begin(Timer.RENDER_SILHOUETTES); + long currentTime = System.currentTimeMillis(); + for(int i = 0; i < pendingDraw.size(); i++) { + SilhouetteDraw silhouetteDraw = pendingDraw.get(i); + if(!silhouetteDraw.drawCall.isValid()) + continue; + + if(!silhouetteDraw.initialized) + silhouetteDraw.initialize(); + + silhouetteDraw.draw(); + silhouetteDraw.drawCall.reset(); + silhouetteDraw.lastDrawTimeMS = currentTime; + + renderState.disable.set(GL_DEPTH_TEST); + renderState.disable.set(GL_STENCIL_TEST); + renderState.disable.set(GL_BLEND); + renderState.depthMask.set(true); + renderState.colorMask.set(true, true, true, true); + renderState.stencilMask.set(0); + } + renderState.apply(); + pendingDraw.clear(); + + for(int i = activeDraws.size() - 1; i >= 0; --i) { + SilhouetteDraw silhouetteDraw = activeDraws.get(i); + if((currentTime - silhouetteDraw.lastDrawTimeMS) > 10000) { + freeDraws.add(silhouetteDraw); + activeDraws.remove(i); + } + } + + frameTimer.end(Timer.RENDER_SILHOUETTES); + frameTimer.end(Timer.DRAW_SILHOUETTES); + } + + private SilhouetteDraw getSilhouetteDraw(GameObject gameObject, Actor actor) { + if(actor == null) + return null; + + if(actor == localPlayer) + return playerSilhouetteDraw; + + final boolean isNpc = actor instanceof NPC; + final int actorId = ModelHash.getIdOrIndex(gameObject.getHash()); + + SilhouetteDraw silhouetteDraw; + for(int i = 0; i < activeDraws.size(); i++) { + silhouetteDraw = activeDraws.get(i); + if(silhouetteDraw.actorId == actorId && silhouetteDraw.isNPC == isNpc) + return silhouetteDraw; + } + + silhouetteDraw = freeDraws.poll(); + if(silhouetteDraw == null) + silhouetteDraw = new SilhouetteDraw(false); + silhouetteDraw.actorId = actorId; + silhouetteDraw.isNPC = isNpc; + silhouetteDraw.isHostile = isNpc && isNPCHostile((NPC) actor); + silhouetteDraw.silhouetteFadeAlpha = 0.0f; + activeDraws.add(silhouetteDraw); + return silhouetteDraw; + } + + private boolean isNPCHostile(NPC npc) { + if(localPlayer == null) + return false; + + var npcComposition = npc.getComposition(); + if(npcComposition == null || npcComposition.isFollower() || (npcComposition.getCombatLevel() * 2) < localPlayer.getCombatLevel()) + return false; + + final String[] actions = npcComposition.getActions(); + if(actions == null || actions.length == 0) + return false; + + for(int i = 0; i < actions.length; i++) { + if(actions[i] != null && actions[i].equalsIgnoreCase("Attack")) + return true; + } + + return false; + } + + public float getCanopyFadeStrength() { + return playerSilhouetteDraw.canopyFadeStrength; + } + + public boolean isSilhouetteEnabled(Actor actor) { + if(actor instanceof Player) { + switch (configPlayerSilhouette) { + case Player: + case Player_Follower: + return actor == localPlayer; + case Everyone: + return true; + case Disabled: + default: + return false; + } + } else if(actor instanceof NPC) { + if(configPlayerSilhouette == Player_Follower) { + if(actor == localPlayerFollower) + return true; + Actor interacting = actorToInteracting.get(actor); + if(interacting == localPlayer) + return true; + } + + switch (configNPCSilhouette) { + case Interacting: + return actor == localPlayerInteracting; + case Hostile: + return isNPCHostile((NPC) actor); + case All: + return true; + case Disabled: + default: + return false; + } + } + + return false; + } + + public synchronized void addSilhouetteDraw(GameObject gameObject, Actor actor, DynamicModelVAO.View drawView, int x, int y, int z) { + SilhouetteDraw silhouetteDraw = getSilhouetteDraw(gameObject, actor); + if(silhouetteDraw == null) + return; + silhouetteDraw.x = x; + silhouetteDraw.y = y; + silhouetteDraw.z = z; + drawView.draw(silhouetteDraw.drawCall); + assert !pendingDraw.contains(silhouetteDraw) : "SilhouetteDraw already pending!"; + pendingDraw.add(silhouetteDraw); + } + + final class SilhouetteDraw { + @Getter + private final DrawCall drawCall = new DrawCall(); + private final GLOcclusionQueries potentialQuery = new GLOcclusionQueries(); + private final GLOcclusionQueries opaqueQuery = new GLOcclusionQueries(); + private final GLOcclusionQueries canopyQuery; + + private int x, y, z; + private int actorId; + private long lastDrawTimeMS; + private float canopyFadeStrength; + private float silhouetteFadeAlpha; + private boolean initialized = false; + private boolean isNPC; + private boolean isHostile; + + public SilhouetteDraw(boolean canopy) { + this.canopyQuery = canopy ? new GLOcclusionQueries() : null; + } + + public void initialize() { + potentialQuery.initialize(); + opaqueQuery.initialize(); + if (canopyQuery != null) + canopyQuery.initialize(); + initialized = true; + } + + public void destroy() { + potentialQuery.destroy(); + opaqueQuery.destroy(); + if (canopyQuery != null) + canopyQuery.destroy(); + } + + public void draw() { + renderState.disable.set(GL_DEPTH_TEST); + renderState.enable.set(GL_CULL_FACE); + renderState.depthFunc.set(GL_GEQUAL); + renderState.depthMask.set(false); + renderState.colorMask.set(false, false, false, false); + renderState.stencilMask.set(0); + renderState.apply(); + + depthSceneProgram.use(); + + // Resue Scene Command Buffer since it has already been drawn + cmd.reset(); + cmd.BindVertexArray(drawCall.vao); + cmd.DrawArrays( + GL_TRIANGLES, + drawCall.offset, + drawCall.count + ); + + // Occlusion potentially with depth testing disabled + potentialQuery.beginQuery(true); + cmd.execute(); + potentialQuery.endQuery(); + + final int potentiallyVisiblePixels = potentialQuery.getVisiblePixels(plugin.msaaSamples) / 2; + if(potentiallyVisiblePixels > 0) { + renderState.enable.set(GL_DEPTH_TEST); + renderState.enable.set(GL_STENCIL_TEST); + + boolean drawSilhouette = isNPC ? configNPCSilhouette != NPCSilhouetteMode.Disabled : configPlayerSilhouette != PlayerSilhouetteMode.Disabled; + if (drawSilhouette) { + renderState.stencilMask.set(0xFF); + renderState.stencilFunc.set(GL_NOTEQUAL, ACTOR_STENCIL_REF, ACTOR_STENCIL_REF); + renderState.stencilOp.set(GL_KEEP, GL_KEEP, GL_REPLACE); + renderState.depthMask.set(true); + renderState.apply(); + + // Write the Actor Stencil Reference for testing Occlusion Against + cmd.execute(); + + renderState.stencilMask.set(0); + renderState.stencilFunc.set(GL_EQUAL, ACTOR_STENCIL_REF, ACTOR_STENCIL_REF); + renderState.stencilOp.set(GL_KEEP, GL_KEEP, GL_KEEP); + renderState.depthMask.set(false); + renderState.apply(); + + opaqueQuery.beginQuery(true); + cmd.execute(); + opaqueQuery.endQuery(); + + final float opaqueVisibleRatio = opaqueQuery.getVisibilityRatio( + potentiallyVisiblePixels, + plugin.msaaSamples + ); + + final float fadeTime = 1.0f / 0.1f; + if(opaqueVisibleRatio < configSilhouetteThreshold) { + silhouetteFadeAlpha = min(1.0f, silhouetteFadeAlpha + plugin.deltaTime * fadeTime); + } else { + silhouetteFadeAlpha = max(0.0f, silhouetteFadeAlpha - plugin.deltaTime * fadeTime); + } + + Color silhouetteColor = isNPC ? isHostile ? configHostileNPCSilhouetteColor : configFriendlyNPCSilhouetteColor : configPlayerSilhouetteColor; + float silhouetteAlpha = (silhouetteColor.getAlpha() / 255f) * silhouetteFadeAlpha * (1.0f - canopyFadeStrength); + + if(silhouetteFadeAlpha > 0.0f) { + renderState.enable.set(GL_BLEND); + renderState.depthFunc.set(GL_LEQUAL); + renderState.blendFunc.set(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + renderState.colorMask.set(true, true, true, true); + + renderState.stencilMask.set(OPAQUE_STENCIL_REF | CANOPY_STENCIL_REF | ACTOR_STENCIL_REF); + renderState.stencilFunc.set( + GL_EQUAL, + OPAQUE_STENCIL_REF, + OPAQUE_STENCIL_REF | CANOPY_STENCIL_REF | ACTOR_STENCIL_REF + ); + renderState.stencilOp.set(GL_KEEP, GL_KEEP, GL_ZERO); + renderState.apply(); + + basicSceneProgram.use(); + basicSceneProgram.uniColor.set(silhouetteColor, silhouetteAlpha); + + cmd.execute(); + + renderState.disable.set(GL_BLEND); + } + + if (configPlayerCanopy && canopyQuery != null) { + // Occlusion Test to see if we're behind canopy + renderState.stencilMask.set(0); + renderState.stencilFunc.set(GL_EQUAL, CANOPY_STENCIL_REF, CANOPY_STENCIL_REF); + renderState.stencilOp.set(GL_KEEP, GL_KEEP, GL_KEEP); + renderState.depthFunc.set(GL_LEQUAL); + renderState.depthMask.set(false); + renderState.colorMask.set(false, false, false, false); + renderState.apply(); + + depthSceneProgram.use(); + + canopyQuery.beginQuery(true); + cmd.execute(); + canopyQuery.endQuery(); + + canopyFadeStrength = mix + ( + canopyFadeStrength, + canopyQuery.getVisibilityRatio(potentiallyVisiblePixels, plugin.msaaSamples), + plugin.deltaTime * 4.0f + ); + } else { + canopyFadeStrength = 0.0f; + } + } + } + } + } +} diff --git a/src/main/java/rs117/hd/scene/materials/Material.java b/src/main/java/rs117/hd/scene/materials/Material.java index 1eaf988cff..2b7d28258f 100644 --- a/src/main/java/rs117/hd/scene/materials/Material.java +++ b/src/main/java/rs117/hd/scene/materials/Material.java @@ -57,6 +57,7 @@ public class Material { public boolean hasTransparency; private boolean overrideBaseColor; private boolean unlit; + public boolean isCanopy; @JsonAdapter(ColorUtils.LinearAdapter.class) public float brightness = 1; private float displacementScale = .1f; @@ -209,6 +210,7 @@ public void fillMaterialStruct( struct.flowMap.set(getTextureLayer(flowMap)); struct.shadowAlphaMap.set(getTextureLayer(shadowAlphaMap)); struct.flags.set( + (isCanopy ? 1 : 0) << 3 | (overrideBaseColor ? 1 : 0) << 2 | (unlit ? 1 : 0) << 1 | (hasTransparency ? 1 : 0) diff --git a/src/main/java/rs117/hd/utils/CommandBuffer.java b/src/main/java/rs117/hd/utils/CommandBuffer.java index 6e866f32b0..6d440b9f4c 100644 --- a/src/main/java/rs117/hd/utils/CommandBuffer.java +++ b/src/main/java/rs117/hd/utils/CommandBuffer.java @@ -36,13 +36,18 @@ public class CommandBuffer { private static final int GL_BIND_INDIRECT_ARRAY_TYPE = 8; private static final int GL_BIND_TEXTURE_UNIT_TYPE = 9; private static final int GL_DEPTH_MASK_TYPE = 10; - private static final int GL_COLOR_MASK_TYPE = 11; - private static final int GL_USE_PROGRAM = 12; + private static final int GL_DEPTH_FUNC_TYPE = 11; + private static final int GL_COLOR_MASK_TYPE = 12; + private static final int GL_USE_PROGRAM = 13; - private static final int GL_TOGGLE_TYPE = 13; // Combined glEnable & glDisable - private static final int GL_FENCE_SYNC = 14; + private static final int GL_STENCIL_MASK = 14; + private static final int GL_STENCIL_FUNC = 15; + private static final int GL_STENCIL_OP = 16; - private static final int GL_EXECUTE_SUB_COMMAND_BUFFER = 15; + private static final int GL_TOGGLE_TYPE = 17; // Combined glEnable & glDisable + private static final int GL_FENCE_SYNC = 18; + + private static final int GL_EXECUTE_SUB_COMMAND_BUFFER = 19; private static final long INT_MASK = 0xFFFF_FFFFL; private static final int DRAW_MODE_MASK = 0xF; @@ -134,6 +139,28 @@ public void DepthMask(boolean writeDepth) { cmd[writeHead++] = GL_DEPTH_MASK_TYPE & 0xFF | (writeDepth ? 1 : 0) << 8; } + public void DepthFunc(int depthMode) { + ensureCapacity(1); + cmd[writeHead++] = GL_DEPTH_FUNC_TYPE & 0xFF | (long) depthMode << 8; + } + + public void StencilMask(int mask) { + ensureCapacity(1); + cmd[writeHead++] = GL_STENCIL_MASK & 0xFF | (long) mask << 8; + } + + public void StencilFunc(int func, int ref, int mask) { + ensureCapacity(2); + cmd[writeHead++] = GL_STENCIL_FUNC & 0xFF | (long) func << 8; + cmd[writeHead++] = ref | (long) mask << 32; + } + + public void StencilOp(int sfail, int dpfail, int dppass) { + ensureCapacity(2); + cmd[writeHead++] = GL_STENCIL_OP & 0xFF | (long) sfail << 8; + cmd[writeHead++] = dppass | (long) dpfail << 32; + } + public void ColorMask(boolean writeRed, boolean writeGreen, boolean writeBlue, boolean writeAlpha) { ensureCapacity(1); cmd[writeHead++] = @@ -301,6 +328,30 @@ public void execute() { renderState.depthMask.set(state == 1); break; } + case GL_DEPTH_FUNC_TYPE: { + renderState.depthFunc.set((int) (data >> 8)); + break; + } + case GL_STENCIL_MASK: { + renderState.stencilMask.set((int) (data >> 8)); + break; + } + case GL_STENCIL_FUNC: { + long packed = cmd[readHead++]; + int func = (int) (data >> 8); + int ref = (int) packed; + int mask = (int) (packed >> 32); + renderState.stencilFunc.set(func, ref, mask); + break; + } + case GL_STENCIL_OP: { + long packed = cmd[readHead++]; + int sfail = (int) (data >> 8); + int dpfail = (int) packed; + int dppass = (int) (packed >> 32); + renderState.stencilOp.set(sfail, dpfail, dppass); + break; + } case GL_COLOR_MASK_TYPE: { boolean red = ((data >> 8) & 1) == 1; boolean green = ((data >> 9) & 1) == 1; diff --git a/src/main/java/rs117/hd/utils/RenderState.java b/src/main/java/rs117/hd/utils/RenderState.java index a5e06d5fa9..46baa43845 100644 --- a/src/main/java/rs117/hd/utils/RenderState.java +++ b/src/main/java/rs117/hd/utils/RenderState.java @@ -24,6 +24,9 @@ public final class RenderState { public final GLDepthFunc depthFunc = addState(GLDepthFunc::new); public final GLColorMask colorMask = addState(GLColorMask::new); public final GLBlendFunc blendFunc = addState(GLBlendFunc::new); + public final GLStencilMask stencilMask = addState(GLStencilMask::new); + public final GLStencilFunc stencilFunc = addState(GLStencilFunc::new); + public final GLStencilOp stencilOp = addState(GLStencilOp::new); public final GLEnable enable = addState(GLEnable::new); public final GLDisable disable = addState(GLDisable::new); @@ -132,6 +135,29 @@ private GLBlendFunc() { protected void applyValues(int[] values) { glBlendFuncSeparate(values[0], values[1], values[2], values[3]); } } + public static final class GLStencilMask extends GLState.Int { + @Override + protected void applyValue(int value) { glStencilMask(value); } + } + + public static final class GLStencilFunc extends GLState.IntArray { + private GLStencilFunc() { + super(3); + } + + @Override + protected void applyValues(int[] values) { glStencilFunc(values[0], values[1], values[2]); } + } + + public static final class GLStencilOp extends GLState.IntArray { + private GLStencilOp() { + super(3); + } + + @Override + protected void applyValues(int[] values) { glStencilOp(values[0], values[1], values[2]); } + } + public static final class GLColorMask extends GLState.BoolArray { private GLColorMask() { super(4); diff --git a/src/main/resources/rs117/hd/basic_scene_frag.glsl b/src/main/resources/rs117/hd/basic_scene_frag.glsl new file mode 100644 index 0000000000..92e28cb716 --- /dev/null +++ b/src/main/resources/rs117/hd/basic_scene_frag.glsl @@ -0,0 +1,12 @@ +#version 330 + +#include +#include + +uniform vec4 color; + +out vec4 FragColor; + +void main() { + FragColor = color; +} diff --git a/src/main/resources/rs117/hd/basic_scene_vert.glsl b/src/main/resources/rs117/hd/basic_scene_vert.glsl new file mode 100644 index 0000000000..aa0b013e00 --- /dev/null +++ b/src/main/resources/rs117/hd/basic_scene_vert.glsl @@ -0,0 +1,21 @@ +#version 330 + +#include +#include +#include + +layout (location = 0) in vec3 vPosition; + +layout (location = 7) in ivec2 vSceneBase; +layout (location = 6) in int vWorldViewId; + +void main() { + vec3 sceneOffset = vec3(vSceneBase.x, 0, vSceneBase.y); + vec3 worldPosition = sceneOffset + vPosition; + if (vWorldViewId != -1) { + mat4x3 worldViewProjection = mat4x3(getWorldViewProjection(vWorldViewId)); + worldPosition = worldViewProjection * vec4(worldPosition, 1.0); + } + + gl_Position = projectionMatrix * vec4(worldPosition, 1.0); +} diff --git a/src/main/resources/rs117/hd/depth_scene_frag.glsl b/src/main/resources/rs117/hd/depth_scene_frag.glsl new file mode 100644 index 0000000000..855de8b253 --- /dev/null +++ b/src/main/resources/rs117/hd/depth_scene_frag.glsl @@ -0,0 +1,3 @@ +#version 330 + +void main() {} diff --git a/src/main/resources/rs117/hd/depth_scene_vert.glsl b/src/main/resources/rs117/hd/depth_scene_vert.glsl new file mode 100644 index 0000000000..aa0b013e00 --- /dev/null +++ b/src/main/resources/rs117/hd/depth_scene_vert.glsl @@ -0,0 +1,21 @@ +#version 330 + +#include +#include +#include + +layout (location = 0) in vec3 vPosition; + +layout (location = 7) in ivec2 vSceneBase; +layout (location = 6) in int vWorldViewId; + +void main() { + vec3 sceneOffset = vec3(vSceneBase.x, 0, vSceneBase.y); + vec3 worldPosition = sceneOffset + vPosition; + if (vWorldViewId != -1) { + mat4x3 worldViewProjection = mat4x3(getWorldViewProjection(vWorldViewId)); + worldPosition = worldViewProjection * vec4(worldPosition, 1.0); + } + + gl_Position = projectionMatrix * vec4(worldPosition, 1.0); +} diff --git a/src/main/resources/rs117/hd/scene/materials.json b/src/main/resources/rs117/hd/scene/materials.json index 6647af0c7d..5224c78dad 100644 --- a/src/main/resources/rs117/hd/scene/materials.json +++ b/src/main/resources/rs117/hd/scene/materials.json @@ -78,6 +78,7 @@ "name": "LEAVES_SIDE", "vanillaTextureIndex": 8, "hasTransparency": true, + "isCanopy": true, "shadowAlphaMap": "LEAF_A", "textureScale": [ 1.025, @@ -194,12 +195,14 @@ "name": "TROPICAL_LEAF", "vanillaTextureIndex": 29, "hasTransparency": true, + "isCanopy": true, "brightness": 0.735 }, { "name": "WILLOW_LEAVES", "vanillaTextureIndex": 30, "hasTransparency": true, + "isCanopy": true, "textureScale": [ 1.025, 1.0, @@ -372,6 +375,7 @@ { "name": "LEAVES_TOP", "vanillaTextureIndex": 60, + "isCanopy": true, "shadowAlphaMap": "LEAF_A", "hasTransparency": true }, @@ -684,11 +688,13 @@ { "name": "LEAFY_ACACIA01", "vanillaTextureIndex": 123, + "isCanopy": true, "hasTransparency": true }, { "name": "LEAFY_STONEPINE01", "vanillaTextureIndex": 124, + "isCanopy": true, "hasTransparency": true }, { @@ -701,11 +707,13 @@ }, { "name": "LEAFYTREE_FROSTY", - "vanillaTextureIndex": 127 + "vanillaTextureIndex": 127, + "isCanopy": true }, { "name": "LEAFYTREE_TILED_FROSTY", - "vanillaTextureIndex": 128 + "vanillaTextureIndex": 128, + "isCanopy": true }, { "name": "WATER_OPEN", @@ -950,61 +958,73 @@ { "name": "LEAFY_JATOBA", "vanillaTextureIndex": 189, + "isCanopy": true, "hasTransparency": true }, { "name": "LEAFY_JATOBA_TILED", "vanillaTextureIndex": 190, + "isCanopy": true, "hasTransparency": true }, { "name": "LEAFY_JATOBA_DISEASED", "vanillaTextureIndex": 191, + "isCanopy": true, "hasTransparency": true }, { "name": "LEAFY_JATOBA_DISEASED_TILED", "vanillaTextureIndex": 192, + "isCanopy": true, "hasTransparency": true }, { "name": "LEAFY_CAMPHOR", "vanillaTextureIndex": 193, + "isCanopy": true, "hasTransparency": true }, { "name": "LEAFY_CAMPHOR_TILED", "vanillaTextureIndex": 194, + "isCanopy": true, "hasTransparency": true }, { "name": "LEAFY_CAMPHOR_DISEASED", "vanillaTextureIndex": 195, + "isCanopy": true, "hasTransparency": true }, { "name": "LEAFY_CAMPHOR_DISEASED_TILED", "vanillaTextureIndex": 196, + "isCanopy": true, "hasTransparency": true }, { "name": "LEAFY_IRONWOOD", "vanillaTextureIndex": 197, + "isCanopy": true, "hasTransparency": true }, { "name": "LEAFY_IRONWOOD_TILED", "vanillaTextureIndex": 198, + "isCanopy": true, "hasTransparency": true }, { "name": "LEAFY_IRONWOOD_DISEASED", "vanillaTextureIndex": 199, + "isCanopy": true, "hasTransparency": true }, { "name": "LEAFY_IRONWOOD_DISEASED_TILED", "vanillaTextureIndex": 200, + "isCanopy": true, "hasTransparency": true }, { @@ -2303,23 +2323,27 @@ "brightness": 1.277 }, { - "name": "LEAF_VEINS_N" + "name": "LEAF_VEINS_N", + "isCanopy": true }, { "name": "LEAF_VEINS", "normalMap": "LEAF_VEINS_N", "specularStrength": 0.15, + "isCanopy": true, "specularGloss": 20.0 }, { "name": "LEAF_VEINS_LIGHT", "parent": "LEAF_VEINS", + "isCanopy": true, "brightness": 1.083 }, { "name": "LEAF_VEINS_PALM", "parent": "LEAF_VEINS", "specularStrength": 0.4, + "isCanopy": true, "specularGloss": 60.0 }, { @@ -2327,21 +2351,25 @@ "parent": "LEAF_VEINS", "brightness": 1.18, "specularStrength": 0.4, + "isCanopy": true, "specularGloss": 60.0 }, { "name": "LEAF_VEINS_DARK", "parent": "LEAF_VEINS", + "isCanopy": true, "brightness": 0.881 }, { "name": "LEAF_VEINS_DARKER", "parent": "LEAF_VEINS", + "isCanopy": true, "brightness": 0.78 }, { "name": "LEAF_VEINS_DARKEST", "parent": "LEAF_VEINS", + "isCanopy": true, "brightness": 0.581 }, { @@ -3333,6 +3361,7 @@ "name": "WINTER_LEAVES_SIDE", "hasTransparency": true, "shadowAlphaMap": "LEAF_A", + "isCanopy": true, "textureScale": [ 1.025, 1.025, diff --git a/src/main/resources/rs117/hd/scene_frag.glsl b/src/main/resources/rs117/hd/scene_frag.glsl index bcc9ac06f0..a6f1f20791 100644 --- a/src/main/resources/rs117/hd/scene_frag.glsl +++ b/src/main/resources/rs117/hd/scene_frag.glsl @@ -31,6 +31,7 @@ #define DISPLAY_TANGENT 0 #define DISPLAY_SHADOWS 0 #define DISPLAY_LIGHTING 0 +#define DISPLAY_CANOPY 0 #include #include @@ -115,6 +116,31 @@ void main() { bool isWater = waterTypeIndex > 0 && !isUnderwater; vec4 outputColor = vec4(1); + float fade = 1.0; +#if PLAYER_CANOPY_FADE + if (canopyFadeStrength > 0.0 && getMaterialIsCanopy(material1)) { + vec3 camToPlayer = (playerPosition + vec3(0, -playerHeight / 2, 0)) - cameraPos; + float lineLength = length(camToPlayer); + vec3 lineDir = camToPlayer / lineLength; + + vec3 camToFrag = IN.position - cameraPos; + float t = clamp(dot(camToFrag, lineDir), 0.0, lineLength); + + vec3 closestPoint = cameraPos + lineDir * t; + float distToLine = max(length(IN.position - closestPoint) - 15.0, 0.0); + + float fadeRadius = mix(0.0, playerHeight, canopyFadeStrength); + #if DISPLAY_CANOPY + if(DISPLAY_CANOPY == 1) { + FragColor = vec4(distToLine / fadeRadius, 0.0, 0.0, 1.0); + return; + } + #endif + fade = smoothstep(0.0, fadeRadius, distToLine); + if(fade == 0) + discard; + } +#endif if (isWater) { outputColor = sampleWater(waterTypeIndex, viewDir); @@ -501,6 +527,7 @@ void main() { } outputColor.rgb = colorBlindnessCompensation(outputColor.rgb); + outputColor.a *= fade; #if APPLY_COLOR_FILTER outputColor.rgb = applyColorFilter(outputColor.rgb); diff --git a/src/main/resources/rs117/hd/uniforms/global.glsl b/src/main/resources/rs117/hd/uniforms/global.glsl index a50e951d99..bdd5591d3e 100644 --- a/src/main/resources/rs117/hd/uniforms/global.glsl +++ b/src/main/resources/rs117/hd/uniforms/global.glsl @@ -50,6 +50,9 @@ layout(std140) uniform UBOGlobal { mat4 invProjectionMatrix; mat4 lightProjectionMatrix; + vec3 playerPosition; + float playerHeight; + float canopyFadeStrength; float lightningBrightness; float elapsedTime; }; diff --git a/src/main/resources/rs117/hd/uniforms/materials.glsl b/src/main/resources/rs117/hd/uniforms/materials.glsl index 6f60529dbb..dfa0b9ada5 100644 --- a/src/main/resources/rs117/hd/uniforms/materials.glsl +++ b/src/main/resources/rs117/hd/uniforms/materials.glsl @@ -27,6 +27,10 @@ layout(std140) uniform UBOMaterials { #include MATERIAL_GETTER +bool getMaterialIsCanopy(const Material material) { + return (material.flags >> 3 & 1) == 1; +} + bool getMaterialShouldOverrideBaseColor(const Material material) { return (material.flags >> 2 & 1) == 1; } diff --git a/src/main/resources/rs117/hd/utils/constants.glsl b/src/main/resources/rs117/hd/utils/constants.glsl index 1ef390944e..556e36f941 100644 --- a/src/main/resources/rs117/hd/utils/constants.glsl +++ b/src/main/resources/rs117/hd/utils/constants.glsl @@ -28,6 +28,8 @@ #define MATERIAL_FLAG_VANILLA_UVS 1 #define MATERIAL_FLAG_IS_OVERLAY 0 +#include PLAYER_CANOPY_FADE + #include SHADOW_MODE #define SHADOW_MODE_OFF 0 #define SHADOW_MODE_FAST 1 From e499d227cda7f07607b898511c9df8514f0e31fa Mon Sep 17 00:00:00 2001 From: Ruffled <105522716+RuffledPlume@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:38:18 +0000 Subject: [PATCH 2/2] Dither out geom that is about the clip with the near plane --- src/main/resources/rs117/hd/scene_frag.glsl | 14 ++++++++++++++ src/main/resources/rs117/hd/scene_vert.glsl | 9 +++++---- src/main/resources/rs117/hd/utils/misc.glsl | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/main/resources/rs117/hd/scene_frag.glsl b/src/main/resources/rs117/hd/scene_frag.glsl index a6f1f20791..b5268d603a 100644 --- a/src/main/resources/rs117/hd/scene_frag.glsl +++ b/src/main/resources/rs117/hd/scene_frag.glsl @@ -33,6 +33,8 @@ #define DISPLAY_LIGHTING 0 #define DISPLAY_CANOPY 0 +#define NEAR_PLANE_DITHER_START 0.2 + #include #include #include @@ -56,6 +58,9 @@ flat in ivec3 fTerrainData; #endif in FragmentData { +#if ZONE_RENDERER + vec4 positionCS; +#endif vec3 position; vec2 uv; vec3 normal; @@ -85,6 +90,15 @@ vec2 worldUvs(float scale) { void main() { vec3 downDir = vec3(0, -1, 0); +#if ZONE_RENDERER + float viewZ = 1.0 - (0.5 + (IN.positionCS.z / IN.positionCS.w) * 0.5); + if(viewZ < NEAR_PLANE_DITHER_START) { + float fadeAmount = 1.0 - saturate(viewZ / NEAR_PLANE_DITHER_START); + if(orderedDither(gl_FragCoord.xy, pow(fadeAmount, 1.5) - 0.01, 1.75)) + discard; + } +#endif + // View & light directions are from the fragment to the camera/light vec3 viewDir = normalize(cameraPos - IN.position); diff --git a/src/main/resources/rs117/hd/scene_vert.glsl b/src/main/resources/rs117/hd/scene_vert.glsl index 06b5268582..725ca0d4e5 100644 --- a/src/main/resources/rs117/hd/scene_vert.glsl +++ b/src/main/resources/rs117/hd/scene_vert.glsl @@ -60,6 +60,7 @@ layout (location = 0) in vec3 vPosition; #endif out FragmentData { + vec4 positionCS; vec3 position; vec2 uv; vec3 normal; @@ -109,12 +110,12 @@ layout (location = 0) in vec3 vPosition; fFlatNormal = worldNormal; #endif - vec4 clipPosition = projectionMatrix * vec4(worldPosition, 1.0); int depthBias = (alphaBiasHsl >> 16) & 0xff; - if (projectionMatrix[2][3] != 0) // Disable depth bias for orthographic projection - clipPosition.z += depthBias / 128.0; + OUT.positionCS = projectionMatrix * vec4(worldPosition, 1.0); + if (projectionMatrix[2][3] != 0) // Disable depth bias for orthographic projection + OUT.positionCS.z += depthBias / 128.0; - gl_Position = clipPosition; + gl_Position = OUT.positionCS; } #else out vec3 gPosition; diff --git a/src/main/resources/rs117/hd/utils/misc.glsl b/src/main/resources/rs117/hd/utils/misc.glsl index 6e5406693b..b194fcff9c 100644 --- a/src/main/resources/rs117/hd/utils/misc.glsl +++ b/src/main/resources/rs117/hd/utils/misc.glsl @@ -112,6 +112,25 @@ void undoVanillaShading(inout int hsl, vec3 unrotatedNormal) { } #endif +const int DITHER_MAP_LEN = 4; +const float DITHER_MAP_SCALE = 16.0; + +const float DITHER_MAP[DITHER_MAP_LEN * DITHER_MAP_LEN] = float[]( + 0.0 / DITHER_MAP_SCALE, 8.0 / DITHER_MAP_SCALE, 2.0 / DITHER_MAP_SCALE, 10.0 / DITHER_MAP_SCALE, + 12.0 / DITHER_MAP_SCALE, 4.0 / DITHER_MAP_SCALE, 14.0 / DITHER_MAP_SCALE, 6.0 / DITHER_MAP_SCALE, + 3.0 / DITHER_MAP_SCALE, 11.0 / DITHER_MAP_SCALE, 1.0 / DITHER_MAP_SCALE, 9.0 / DITHER_MAP_SCALE, + 15.0 / DITHER_MAP_SCALE, 7.0 / DITHER_MAP_SCALE, 13.0 / DITHER_MAP_SCALE, 5.0 / DITHER_MAP_SCALE +); + +// ------------------------------------------------------------ +// Based on https://www.shadertoy.com/view/4t2cRt +// Returns a dither value (0.0 or 1.0) based on coords + opacity +// ------------------------------------------------------------ +bool orderedDither(vec2 pixelCoord, float opacity, float scaleFactor) { + ivec2 coord = ivec2(pixelCoord / scaleFactor) & (DITHER_MAP_LEN - 1); + return DITHER_MAP[coord.x + coord.y * DITHER_MAP_LEN] < clamp(opacity, 0.0, 1.0); +} + // 2D Random float hash(in vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);