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..b5268d603a 100644 --- a/src/main/resources/rs117/hd/scene_frag.glsl +++ b/src/main/resources/rs117/hd/scene_frag.glsl @@ -31,6 +31,9 @@ #define DISPLAY_TANGENT 0 #define DISPLAY_SHADOWS 0 #define DISPLAY_LIGHTING 0 +#define DISPLAY_CANOPY 0 + +#define NEAR_PLANE_DITHER_START 0.2 #include #include @@ -55,6 +58,9 @@ flat in ivec3 fTerrainData; #endif in FragmentData { +#if ZONE_RENDERER + vec4 positionCS; +#endif vec3 position; vec2 uv; vec3 normal; @@ -84,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); @@ -115,6 +130,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 +541,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/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/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 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);