diff --git a/src/main/java/rs117/hd/HdPlugin.java b/src/main/java/rs117/hd/HdPlugin.java index 01f211c80d..666095b269 100644 --- a/src/main/java/rs117/hd/HdPlugin.java +++ b/src/main/java/rs117/hd/HdPlugin.java @@ -83,6 +83,7 @@ import rs117.hd.config.ShadingMode; import rs117.hd.config.ShadowMode; import rs117.hd.config.VanillaShadowMode; +import rs117.hd.opengl.GLConstants; import rs117.hd.opengl.shader.ShaderException; import rs117.hd.opengl.shader.ShaderIncludes; import rs117.hd.opengl.shader.TiledLightingShaderProgram; @@ -132,6 +133,10 @@ import static net.runelite.api.Constants.*; import static org.lwjgl.opengl.GL33C.*; import static rs117.hd.HdPluginConfig.*; +import static rs117.hd.opengl.GLConstants.getMaxImageUnits; +import static rs117.hd.opengl.GLConstants.getMaxSamples; +import static rs117.hd.opengl.GLConstants.getMaxTextureSize; +import static rs117.hd.opengl.GLConstants.getMaxTextureUnits; import static rs117.hd.utils.MathUtils.*; import static rs117.hd.utils.ResourcePath.path; import static rs117.hd.utils.buffer.GLBuffer.MAP_WRITE; @@ -158,15 +163,15 @@ public class HdPlugin extends Plugin { public static final String INTEL_DRIVER_URL = "https://www.intel.com/content/www/us/en/support/detect.html"; public static final String NVIDIA_DRIVER_URL = "https://www.nvidia.com/en-us/geforce/drivers/"; - public static int MAX_TEXTURE_UNITS; public static int TEXTURE_UNIT_COUNT = 0; public static final int TEXTURE_UNIT_UI = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++; public static final int TEXTURE_UNIT_GAME = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++; public static final int TEXTURE_UNIT_SHADOW_MAP = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++; public static final int TEXTURE_UNIT_TILE_HEIGHT_MAP = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++; public static final int TEXTURE_UNIT_TILED_LIGHTING_MAP = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++; + public static final int TEXTURE_UNIT_SCENE_OPAQUE_DEPTH = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++; + public static final int TEXTURE_UNIT_SCENE_ALPHA_DEPTH = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++; - public static int MAX_IMAGE_UNITS; public static int IMAGE_UNIT_COUNT = 0; public static final int IMAGE_UNIT_TILED_LIGHTING = IMAGE_UNIT_COUNT++; @@ -367,10 +372,14 @@ public class HdPlugin extends Plugin { public int[] sceneResolution; public int fboScene; + public int fboSceneAlphaDepth; private int rboSceneColor; - private int rboSceneDepth; public int fboSceneResolve; + public int fboSceneDepthResolve = 0; + private int rboSceneDepthResolve = 0; private int rboSceneResolveColor; + private int texSceneDepth; + private int texSceneAlphaDepth; public int shadowMapResolution; public int fboShadowMap; @@ -398,6 +407,7 @@ public class HdPlugin extends Plugin { public boolean configModelBatching; public boolean configModelCaching; public boolean configShadowsEnabled; + public boolean configShadowTransparency; public boolean configRoofShadows; public boolean configExpandShadowDraw; public boolean configUseFasterModelHashing; @@ -509,7 +519,10 @@ protected void startUp() { fboScene = 0; rboSceneColor = 0; - rboSceneDepth = 0; + texSceneDepth = 0; + texSceneAlphaDepth = 0; + fboSceneDepthResolve = 0; + rboSceneDepthResolve = 0; fboSceneResolve = 0; rboSceneResolveColor = 0; fboShadowMap = 0; @@ -591,13 +604,10 @@ protected void startUp() { lwjglInitialized = true; checkGLErrors(); - MAX_TEXTURE_UNITS = glGetInteger(GL_MAX_TEXTURE_IMAGE_UNITS); // Not the fixed pipeline MAX_TEXTURE_UNITS - if (MAX_TEXTURE_UNITS < TEXTURE_UNIT_COUNT) - log.warn("The GPU only supports {} texture units", MAX_TEXTURE_UNITS); - MAX_IMAGE_UNITS = GL_CAPS.GL_ARB_shader_image_load_store ? - glGetInteger(ARBShaderImageLoadStore.GL_MAX_IMAGE_UNITS) : 0; - if (MAX_IMAGE_UNITS < IMAGE_UNIT_COUNT) - log.warn("The GPU only supports {} image units", MAX_IMAGE_UNITS); + if (getMaxTextureUnits() < TEXTURE_UNIT_COUNT) + log.warn("The GPU only supports {} texture units", getMaxTextureUnits()); + if (getMaxImageUnits() < IMAGE_UNIT_COUNT) + log.warn("The GPU only supports {} image units", getMaxImageUnits()); if (log.isDebugEnabled() && GL_CAPS.glDebugMessageControl != 0) { debugCallback = GLUtil.setupDebugMessageCallback(); @@ -866,6 +876,7 @@ public ShaderIncludes getShaderIncludes() { var includes = new ShaderIncludes() .addIncludePath(SHADER_PATH) .addInclude("VERSION_HEADER", OSType.getOSType() == OSType.Linux ? LINUX_VERSION_HEADER : WINDOWS_VERSION_HEADER) + .define("MSAA_SAMPLES", config.antiAliasingMode().getSamples()) .define("UI_SCALING_MODE", config.uiScalingMode()) .define("COLOR_BLINDNESS", config.colorBlindness()) .define("APPLY_COLOR_FILTER", configColorFilter != ColorFilter.NONE) @@ -1231,14 +1242,11 @@ public void updateSceneFbo() { client.getViewportHeight() }; - // Skip rendering when there's no viewport to render to, which happens while world hopping if (viewport[2] == 0 || viewport[3] == 0) return; - // DPI scaling and stretched mode also affects the game's viewport divide(sceneViewportScale, vec(actualUiResolution), vec(uiResolution)); if (sceneViewportScale[0] != 1 || sceneViewportScale[1] != 1) { - // Pad the viewport before scaling, so it always covers the game's viewport in the UI for (int i = 0; i < 2; i++) { viewport[i] -= 1; viewport[i + 2] += 2; @@ -1246,28 +1254,28 @@ public void updateSceneFbo() { viewport = round(multiply(vec(viewport), sceneViewportScale)); } - // Check if scene FBO needs to be recreated if (Arrays.equals(sceneViewport, viewport)) return; destroySceneFbo(); sceneViewport = viewport; - // Bind default FBO to check whether anti-aliasing is forced int defaultFramebuffer = awtContext.getFramebuffer(false); glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebuffer); - final int forcedAASamples = glGetInteger(GL_SAMPLES); - msaaSamples = forcedAASamples != 0 ? forcedAASamples : min(config.antiAliasingMode().getSamples(), glGetInteger(GL_MAX_SAMPLES)); - // Since there's seemingly no reliable way to check if the default framebuffer will do sRGB conversions with GL_FRAMEBUFFER_SRGB - // enabled, we always replace the default framebuffer with an sRGB one. We could technically support rendering to the default - // framebuffer when sRGB conversions aren't needed, but the goal is to transition to linear blending in the future anyway. - boolean sRGB = false; // This is currently unused + final int forcedAASamples = GLConstants.getForcedSamples(); + msaaSamples = forcedAASamples != 0 + ? forcedAASamples + : min(config.antiAliasingMode().getSamples(), getMaxSamples()); + + boolean sRGB = false; - // Some implementations (*cough* Apple) complain when blitting from an FBO without an alpha channel to a (default) FBO with alpha. - // To work around this, we select a format which includes an alpha channel, even though we don't need it. int defaultColorAttachment = defaultFramebuffer == 0 ? GL_BACK_LEFT : GL_COLOR_ATTACHMENT0; - int alphaBits = glGetFramebufferAttachmentParameteri(GL_FRAMEBUFFER, defaultColorAttachment, GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE); + int alphaBits = glGetFramebufferAttachmentParameteri( + GL_FRAMEBUFFER, + defaultColorAttachment, + GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE + ); checkGLErrors(); boolean alpha = alphaBits > 0; @@ -1278,22 +1286,33 @@ public void updateSceneFbo() { float resolutionScale = config.sceneResolutionScale() / 100f; sceneResolution = round(max(vec(1), multiply(slice(vec(sceneViewport), 2), resolutionScale))); uboGlobal.sceneResolution.set(sceneResolution); - uboGlobal.upload(); // Ensure this is up to date with rendering + uboGlobal.upload(); + + // ------------------------------------------------- + // Create scene FBO + // ------------------------------------------------- - // Create and bind the FBO fboScene = glGenFramebuffers(); glBindFramebuffer(GL_FRAMEBUFFER, fboScene); - // Create color render buffer + // ------------------------------------------------- + // Color RBO + // ------------------------------------------------- + rboSceneColor = glGenRenderbuffers(); glBindRenderbuffer(GL_RENDERBUFFER, rboSceneColor); - // Flush out all pending errors, so we can check whether the next step succeeds clearGLErrors(); int format = 0; for (int desiredFormat : desiredFormats) { - glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaaSamples, desiredFormat, sceneResolution[0], sceneResolution[1]); + glRenderbufferStorageMultisample( + GL_RENDERBUFFER, + msaaSamples, + desiredFormat, + sceneResolution[0], + sceneResolution[1] + ); if (glGetError() == GL_NO_ERROR) { format = desiredFormat; @@ -1304,31 +1323,174 @@ public void updateSceneFbo() { if (format == 0) throw new RuntimeException("No supported " + (sRGB ? "sRGB" : "linear") + " formats"); - // Found a usable format. Bind the RBO to the scene FBO - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rboSceneColor); - checkGLErrors(); - - // 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); + glFramebufferRenderbuffer( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + GL_RENDERBUFFER, + rboSceneColor + ); checkGLErrors(); - // If necessary, create an FBO for resolving multisampling if (msaaSamples > 1 && resolutionScale != 1) { fboSceneResolve = glGenFramebuffers(); glBindFramebuffer(GL_FRAMEBUFFER, fboSceneResolve); + rboSceneResolveColor = glGenRenderbuffers(); glBindRenderbuffer(GL_RENDERBUFFER, rboSceneResolveColor); - glRenderbufferStorageMultisample(GL_RENDERBUFFER, 0, format, sceneResolution[0], sceneResolution[1]); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rboSceneResolveColor); + glRenderbufferStorageMultisample( + GL_RENDERBUFFER, + 0, + format, + sceneResolution[0], + sceneResolution[1] + ); + + glFramebufferRenderbuffer( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + GL_RENDERBUFFER, + rboSceneResolveColor + ); + checkGLErrors(); + + glBindFramebuffer(GL_FRAMEBUFFER, fboScene); + } + + if (msaaSamples > 1) { + // Multisampled depth renderbuffer + rboSceneDepthResolve = glGenRenderbuffers(); + glBindRenderbuffer(GL_RENDERBUFFER, rboSceneDepthResolve); + glRenderbufferStorageMultisample( + GL_RENDERBUFFER, + msaaSamples, + GL_DEPTH_COMPONENT24, + sceneResolution[0], + sceneResolution[1] + ); + + glFramebufferRenderbuffer( + GL_FRAMEBUFFER, + GL_DEPTH_ATTACHMENT, + GL_RENDERBUFFER, + rboSceneDepthResolve + ); + + checkGLErrors(); + + // Single-sample depth texture (resolve target) + texSceneDepth = glGenTextures(); + glActiveTexture(TEXTURE_UNIT_SCENE_OPAQUE_DEPTH); + glBindTexture(GL_TEXTURE_2D, texSceneDepth); + + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_DEPTH_COMPONENT24, + sceneResolution[0], + sceneResolution[1], + 0, + GL_DEPTH_COMPONENT, + GL_FLOAT, + 0 + ); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + // Depth resolve FBO + fboSceneDepthResolve = glGenFramebuffers(); + glBindFramebuffer(GL_FRAMEBUFFER, fboSceneDepthResolve); + + glFramebufferTexture2D( + GL_FRAMEBUFFER, + GL_DEPTH_ATTACHMENT, + GL_TEXTURE_2D, + texSceneDepth, + 0 + ); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + throw new RuntimeException("Depth resolve FBO incomplete"); + + glBindFramebuffer(GL_FRAMEBUFFER, fboScene); + } + else { + // No MSAA: attach depth texture directly + texSceneDepth = glGenTextures(); + glActiveTexture(TEXTURE_UNIT_SCENE_OPAQUE_DEPTH); + glBindTexture(GL_TEXTURE_2D, texSceneDepth); + + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_DEPTH_COMPONENT24, + sceneResolution[0], + sceneResolution[1], + 0, + GL_DEPTH_COMPONENT, + GL_FLOAT, + 0 + ); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glFramebufferTexture2D( + GL_FRAMEBUFFER, + GL_DEPTH_ATTACHMENT, + GL_TEXTURE_2D, + texSceneDepth, + 0 + ); } + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + throw new RuntimeException("Scene FBO incomplete"); + + texSceneAlphaDepth = glGenTextures(); + glActiveTexture(TEXTURE_UNIT_SCENE_ALPHA_DEPTH); + glBindTexture(GL_TEXTURE_2D, texSceneAlphaDepth); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_DEPTH_COMPONENT16, + sceneResolution[0], + sceneResolution[1], + 0, + GL_DEPTH_COMPONENT, + GL_FLOAT, + 0 + ); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + fboSceneAlphaDepth = glGenFramebuffers(); + glBindFramebuffer(GL_FRAMEBUFFER, fboSceneAlphaDepth); + glFramebufferTexture2D( + GL_FRAMEBUFFER, + GL_DEPTH_ATTACHMENT, + GL_TEXTURE_2D, + texSceneAlphaDepth, + 0 + ); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + throw new RuntimeException("Alpha depth FBO incomplete"); + // Reset - glBindFramebuffer(GL_FRAMEBUFFER, awtContext.getFramebuffer(false)); + glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebuffer); glBindRenderbuffer(GL_RENDERBUFFER, 0); + glActiveTexture(TEXTURE_UNIT_UI); + glBindTexture(GL_TEXTURE_2D, 0); + glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0); } private void destroySceneFbo() { @@ -1342,14 +1504,26 @@ private void destroySceneFbo() { glDeleteRenderbuffers(rboSceneColor); rboSceneColor = 0; - if (rboSceneDepth != 0) - glDeleteRenderbuffers(rboSceneDepth); - rboSceneDepth = 0; + if (texSceneDepth != 0) + glDeleteTextures(texSceneDepth); + texSceneDepth = 0; + + if(texSceneAlphaDepth != 0) + glDeleteTextures(texSceneAlphaDepth); + texSceneAlphaDepth = 0; if (fboSceneResolve != 0) glDeleteFramebuffers(fboSceneResolve); fboSceneResolve = 0; + if(fboSceneDepthResolve != 0) + glDeleteFramebuffers(fboSceneDepthResolve); + fboSceneDepthResolve = 0; + + if(rboSceneDepthResolve != 0) + glDeleteRenderbuffers(rboSceneDepthResolve); + rboSceneDepthResolve = 0; + if (rboSceneResolveColor != 0) glDeleteRenderbuffers(rboSceneResolveColor); rboSceneResolveColor = 0; @@ -1371,10 +1545,9 @@ private void initializeShadowMapFbo() { glBindTexture(GL_TEXTURE_2D, texShadowMap); shadowMapResolution = config.shadowResolution().getValue(); - int maxResolution = glGetInteger(GL_MAX_TEXTURE_SIZE); - if (maxResolution < shadowMapResolution) { - log.info("Capping shadow resolution from {} to {}", shadowMapResolution, maxResolution); - shadowMapResolution = maxResolution; + if (getMaxTextureSize() < shadowMapResolution) { + log.info("Capping shadow resolution from {} to {}", shadowMapResolution, getMaxTextureSize()); + shadowMapResolution = getMaxTextureSize(); } glTexImage2D( @@ -1559,8 +1732,8 @@ public void drawUi(int overlayColor) { } /** - * Convert the front framebuffer to an Image - */ + * Convert the front framebuffer to an Image + */ public Image screenshot() { if (uiResolution == null) return null; @@ -1605,6 +1778,7 @@ public boolean isLoadingScene() { private void updateCachedConfigs() { configShadowMode = config.shadowMode(); configShadowsEnabled = configShadowMode != ShadowMode.OFF; + configShadowTransparency = config.shadowTransparency(); configRoofShadows = config.roofShadows(); configGroundTextures = config.groundTextures(); configGroundBlending = config.groundBlending(); @@ -1784,6 +1958,7 @@ public void processPendingConfigChanges() { case KEY_ANTI_ALIASING_MODE: case KEY_SCENE_RESOLUTION_SCALE: recreateSceneFbo = true; + recompilePrograms = true; break; case KEY_SHADOW_MODE: case KEY_SHADOW_RESOLUTION: diff --git a/src/main/java/rs117/hd/HdPluginConfig.java b/src/main/java/rs117/hd/HdPluginConfig.java index 2a2a000d2b..1ffa48fbf2 100644 --- a/src/main/java/rs117/hd/HdPluginConfig.java +++ b/src/main/java/rs117/hd/HdPluginConfig.java @@ -1196,6 +1196,17 @@ default boolean forceIndirectDraw() { return false; } + String KEY_OCCLUSION_CULLING = "experimentalOcclusionCulling"; + @ConfigItem( + keyName = KEY_OCCLUSION_CULLING, + name = "Occlusion Culling", + description = "", + section = experimentalSettings + ) + default boolean occlusionCulling() { + return true; + } + String KEY_ASYNC_MODEL_CACHE_SIZE = "asyncModelCacheSizeMiB"; @Range( min = 16, diff --git a/src/main/java/rs117/hd/config/DynamicLights.java b/src/main/java/rs117/hd/config/DynamicLights.java index 12c0da3ea2..c7d7394854 100644 --- a/src/main/java/rs117/hd/config/DynamicLights.java +++ b/src/main/java/rs117/hd/config/DynamicLights.java @@ -43,7 +43,6 @@ public enum DynamicLights static { int max = 0; for (var e : values()) { - assert e.tiledLightingLayers % 4 == 0; // Needs to be divisible by 4 max = max(max, e.tiledLightingLayers); } MAX_LAYERS_PER_TILE = max; diff --git a/src/main/java/rs117/hd/opengl/GLConstants.java b/src/main/java/rs117/hd/opengl/GLConstants.java new file mode 100644 index 0000000000..0bf748c818 --- /dev/null +++ b/src/main/java/rs117/hd/opengl/GLConstants.java @@ -0,0 +1,66 @@ +package rs117.hd.opengl; + +import static org.lwjgl.opengl.ARBShaderImageLoadStore.GL_MAX_IMAGE_UNITS; +import static org.lwjgl.opengl.GL11C.GL_MAX_TEXTURE_SIZE; +import static org.lwjgl.opengl.GL11C.glGetInteger; +import static org.lwjgl.opengl.GL13C.GL_SAMPLES; +import static org.lwjgl.opengl.GL20C.GL_MAX_TEXTURE_IMAGE_UNITS; +import static org.lwjgl.opengl.GL30C.GL_MAX_SAMPLES; +import static org.lwjgl.opengl.GL31C.GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT; +import static org.lwjgl.opengl.GL41.GL_NUM_PROGRAM_BINARY_FORMATS; +import static rs117.hd.HdPlugin.GL_CAPS; + +public class GLConstants { + + private static int FORCED_SAMPLES_VALUE = -1; + public static int getForcedSamples() { + if(FORCED_SAMPLES_VALUE == -1) + FORCED_SAMPLES_VALUE = glGetInteger(GL_SAMPLES); + return FORCED_SAMPLES_VALUE; + } + + private static int MAX_SAMPLES_VALUE = -1; + public static int getMaxSamples() { + if(MAX_SAMPLES_VALUE == -1) + MAX_SAMPLES_VALUE = glGetInteger(GL_MAX_SAMPLES); + return MAX_SAMPLES_VALUE; + } + + private static int MAX_IMAGE_UNITS_VALUE = -1; + public static int getMaxImageUnits() { + if(!GL_CAPS.GL_ARB_shader_image_load_store) + return 0; + if(MAX_IMAGE_UNITS_VALUE == -1) + MAX_IMAGE_UNITS_VALUE = glGetInteger(GL_MAX_IMAGE_UNITS); + return MAX_IMAGE_UNITS_VALUE; + } + + private static int MAX_TEXTURE_UNITS_VALUE = -1; + public static int getMaxTextureUnits() { + if(MAX_TEXTURE_UNITS_VALUE == -1) + MAX_TEXTURE_UNITS_VALUE = glGetInteger(GL_MAX_TEXTURE_IMAGE_UNITS); + return MAX_TEXTURE_UNITS_VALUE; + } + + private static int MAX_TEXTURE_SIZE_VALUE = -1; + public static int getMaxTextureSize() { + if(MAX_TEXTURE_SIZE_VALUE == -1) + MAX_TEXTURE_SIZE_VALUE = glGetInteger(GL_MAX_TEXTURE_SIZE); + return MAX_TEXTURE_SIZE_VALUE; + } + + private static int BUFFER_OFFSET_ALIGNMENT_VALUE = -1; + public static int getBufferOffsetAlignment() { + if(BUFFER_OFFSET_ALIGNMENT_VALUE == -1) + BUFFER_OFFSET_ALIGNMENT_VALUE = glGetInteger(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT); + + return BUFFER_OFFSET_ALIGNMENT_VALUE; + } + + private static int NUM_PROGRAM_BINARY_FORMATS_VALUE = -1; + public static int getNumProgramBinaryFormats() { + if(NUM_PROGRAM_BINARY_FORMATS_VALUE == -1) + NUM_PROGRAM_BINARY_FORMATS_VALUE = glGetInteger(GL_NUM_PROGRAM_BINARY_FORMATS); + return NUM_PROGRAM_BINARY_FORMATS_VALUE; + } +} diff --git a/src/main/java/rs117/hd/opengl/shader/DepthShaderProgram.java b/src/main/java/rs117/hd/opengl/shader/DepthShaderProgram.java new file mode 100644 index 0000000000..0b7da7db88 --- /dev/null +++ b/src/main/java/rs117/hd/opengl/shader/DepthShaderProgram.java @@ -0,0 +1,23 @@ +package rs117.hd.opengl.shader; + +import java.io.IOException; +import rs117.hd.HdPlugin; + +import static org.lwjgl.opengl.GL20.GL_FRAGMENT_SHADER; +import static org.lwjgl.opengl.GL20C.GL_FRAGMENT_SHADER_DERIVATIVE_HINT; +import static org.lwjgl.opengl.GL20C.GL_VERTEX_SHADER; +import static rs117.hd.HdPlugin.GL_CAPS; +import static rs117.hd.renderer.zone.ZoneRenderer.TEXTURE_UNIT_TEXTURED_FACES; + +public class DepthShaderProgram extends ShaderProgram { + public DepthShaderProgram() { + super(t -> t.add(GL_VERTEX_SHADER, "depth_vert.glsl")); + } + + @Override + public void compile(ShaderIncludes includes) throws ShaderException, IOException { + if(HdPlugin.APPLE || !GL_CAPS.OpenGL46) + shaderTemplate.add(GL_FRAGMENT_SHADER, "depth_frag.glsl"); + super.compile(includes); + } +} diff --git a/src/main/java/rs117/hd/opengl/shader/OcclusionShaderProgram.java b/src/main/java/rs117/hd/opengl/shader/OcclusionShaderProgram.java new file mode 100644 index 0000000000..d9fec265e3 --- /dev/null +++ b/src/main/java/rs117/hd/opengl/shader/OcclusionShaderProgram.java @@ -0,0 +1,32 @@ +package rs117.hd.opengl.shader; + +import java.io.IOException; +import rs117.hd.HdPlugin; + +import static org.lwjgl.opengl.GL20.GL_FRAGMENT_SHADER; +import static org.lwjgl.opengl.GL20C.GL_VERTEX_SHADER; +import static rs117.hd.HdPlugin.GL_CAPS; + +public class OcclusionShaderProgram extends ShaderProgram { + public OcclusionShaderProgram() { + super(t -> t.add(GL_VERTEX_SHADER, "occlusion_vert.glsl")); + } + + @Override + public void compile(ShaderIncludes includes) throws ShaderException, IOException { + if(HdPlugin.APPLE || !GL_CAPS.OpenGL46) + shaderTemplate.add(GL_FRAGMENT_SHADER, "depth_frag.glsl"); + super.compile(includes); + } + + public static class Debug extends OcclusionShaderProgram { + public Uniform1i queryId = addUniform1i("queryId"); + + @Override + public void compile(ShaderIncludes includes) throws ShaderException, IOException { + shaderTemplate.remove(GL_FRAGMENT_SHADER); + shaderTemplate.add(GL_FRAGMENT_SHADER, "occlusion_debug_frag.glsl"); + super.compile(includes); + } + } +} diff --git a/src/main/java/rs117/hd/opengl/shader/SceneShaderProgram.java b/src/main/java/rs117/hd/opengl/shader/SceneShaderProgram.java index 1c75140e78..e6d970c2a7 100644 --- a/src/main/java/rs117/hd/opengl/shader/SceneShaderProgram.java +++ b/src/main/java/rs117/hd/opengl/shader/SceneShaderProgram.java @@ -2,6 +2,8 @@ import static org.lwjgl.opengl.GL33C.*; import static rs117.hd.HdPlugin.TEXTURE_UNIT_GAME; +import static rs117.hd.HdPlugin.TEXTURE_UNIT_SCENE_ALPHA_DEPTH; +import static rs117.hd.HdPlugin.TEXTURE_UNIT_SCENE_OPAQUE_DEPTH; import static rs117.hd.HdPlugin.TEXTURE_UNIT_SHADOW_MAP; import static rs117.hd.HdPlugin.TEXTURE_UNIT_TILED_LIGHTING_MAP; import static rs117.hd.renderer.zone.ZoneRenderer.TEXTURE_UNIT_TEXTURED_FACES; @@ -11,6 +13,8 @@ public class SceneShaderProgram extends ShaderProgram { protected final UniformTexture uniShadowMap = addUniformTexture("shadowMap"); protected final UniformTexture uniTiledLightingTextureArray = addUniformTexture("tiledLightingArray"); protected final UniformTexture uniTextureFaces = addUniformTexture("textureFaces"); + protected final UniformTexture uniSceneOpaqueDepth = addUniformTexture("sceneOpaqueDepth"); + protected final UniformTexture uniSceneAlphaDepth = addUniformTexture("sceneAlphaDepth"); public SceneShaderProgram() { super(t -> t @@ -25,6 +29,8 @@ protected void initialize() { uniShadowMap.set(TEXTURE_UNIT_SHADOW_MAP); uniTiledLightingTextureArray.set(TEXTURE_UNIT_TILED_LIGHTING_MAP); uniTextureFaces.set(TEXTURE_UNIT_TEXTURED_FACES); + uniSceneOpaqueDepth.set(TEXTURE_UNIT_SCENE_OPAQUE_DEPTH); + uniSceneAlphaDepth.set(TEXTURE_UNIT_SCENE_ALPHA_DEPTH); } public static class Legacy extends SceneShaderProgram { diff --git a/src/main/java/rs117/hd/opengl/shader/ShaderTemplate.java b/src/main/java/rs117/hd/opengl/shader/ShaderTemplate.java index b3621b19d8..d2e814a3e2 100644 --- a/src/main/java/rs117/hd/opengl/shader/ShaderTemplate.java +++ b/src/main/java/rs117/hd/opengl/shader/ShaderTemplate.java @@ -32,6 +32,7 @@ import lombok.extern.slf4j.Slf4j; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.*; +import rs117.hd.opengl.GLConstants; import static org.lwjgl.opengl.GL33C.*; import static rs117.hd.opengl.shader.ShaderIncludes.SHADER_DUMP_PATH; @@ -103,9 +104,7 @@ public int compile(ShaderIncludes includes) throws ShaderException, IOException ok = true; if (SHADER_DUMP_PATH != null) { - int[] numFormats = { 0 }; - glGetIntegerv(GL41C.GL_NUM_PROGRAM_BINARY_FORMATS, numFormats); - if (numFormats[0] < 1) { + if (GLConstants.getNumProgramBinaryFormats() < 1) { log.error("OpenGL driver does not support any binary formats"); } else { int[] size = { 0 }; diff --git a/src/main/java/rs117/hd/opengl/shader/TiledLightingShaderProgram.java b/src/main/java/rs117/hd/opengl/shader/TiledLightingShaderProgram.java index b0cda8f703..1fae86d728 100644 --- a/src/main/java/rs117/hd/opengl/shader/TiledLightingShaderProgram.java +++ b/src/main/java/rs117/hd/opengl/shader/TiledLightingShaderProgram.java @@ -2,12 +2,17 @@ import static org.lwjgl.opengl.GL33C.*; import static rs117.hd.HdPlugin.IMAGE_UNIT_TILED_LIGHTING; +import static rs117.hd.HdPlugin.TEXTURE_UNIT_SCENE_ALPHA_DEPTH; +import static rs117.hd.HdPlugin.TEXTURE_UNIT_SCENE_OPAQUE_DEPTH; import static rs117.hd.HdPlugin.TEXTURE_UNIT_TILED_LIGHTING_MAP; public class TiledLightingShaderProgram extends ShaderProgram { private final UniformTexture uniTiledLightingTextureArray = addUniformTexture("tiledLightingArray"); private final UniformImage uniTiledLightingTextureStore = addUniformImage("tiledLightingImage"); + private final UniformTexture uniSceneOpaqueDepth = addUniformTexture("sceneOpaqueDepth"); + private final UniformTexture uniSceneAlphaDepth = addUniformTexture("sceneAlphaDepth"); + public TiledLightingShaderProgram() { super(t -> t .add(GL_VERTEX_SHADER, "tiled_lighting_vert.glsl") @@ -20,5 +25,8 @@ public TiledLightingShaderProgram() { protected void initialize() { uniTiledLightingTextureArray.set(TEXTURE_UNIT_TILED_LIGHTING_MAP); uniTiledLightingTextureStore.set(IMAGE_UNIT_TILED_LIGHTING); + + uniSceneOpaqueDepth.set(TEXTURE_UNIT_SCENE_OPAQUE_DEPTH); + uniSceneAlphaDepth.set(TEXTURE_UNIT_SCENE_ALPHA_DEPTH); } } diff --git a/src/main/java/rs117/hd/opengl/uniforms/UBOGlobal.java b/src/main/java/rs117/hd/opengl/uniforms/UBOGlobal.java index 1343819ecc..5578123307 100644 --- a/src/main/java/rs117/hd/opengl/uniforms/UBOGlobal.java +++ b/src/main/java/rs117/hd/opengl/uniforms/UBOGlobal.java @@ -56,6 +56,8 @@ public void initialize() { public Property pointLightsCount = addProperty(PropertyType.Int, "pointLightsCount"); public Property cameraPos = addProperty(PropertyType.FVec3, "cameraPos"); + public Property cameraNear = addProperty(PropertyType.Float, "cameraNear"); + public Property cameraFar = addProperty(PropertyType.Float, "cameraFar"); public Property viewMatrix = addProperty(PropertyType.Mat4, "viewMatrix"); public Property projectionMatrix = addProperty(PropertyType.Mat4, "projectionMatrix"); public Property invProjectionMatrix = addProperty(PropertyType.Mat4, "invProjectionMatrix"); diff --git a/src/main/java/rs117/hd/opengl/uniforms/UniformBuffer.java b/src/main/java/rs117/hd/opengl/uniforms/UniformBuffer.java index 0d1ca3104f..b7a7d1d6b3 100644 --- a/src/main/java/rs117/hd/opengl/uniforms/UniformBuffer.java +++ b/src/main/java/rs117/hd/opengl/uniforms/UniformBuffer.java @@ -11,17 +11,19 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.lwjgl.BufferUtils; +import rs117.hd.opengl.GLConstants; import rs117.hd.utils.RenderState; import rs117.hd.utils.buffer.GLBuffer; import rs117.hd.utils.buffer.SharedGLBuffer; import static org.lwjgl.opengl.GL33C.*; +import static rs117.hd.utils.HDUtils.align; import static rs117.hd.utils.MathUtils.*; @Slf4j public abstract class UniformBuffer { @RequiredArgsConstructor - protected enum PropertyType { + public enum PropertyType { Int(4, 4, 1), IVec2(8, 8, 2), IVec3(12, 16, 3), @@ -35,10 +37,10 @@ protected enum PropertyType { Mat3(48, 16, 9), Mat4(64, 16, 16); - private final int size; - private final int alignment; - private final int elementCount; - private final boolean isInt = name().startsWith("I"); + public final int size; + public final int alignment; + public final int elementCount; + public final boolean isInt = name().startsWith("I"); } @AllArgsConstructor @@ -47,6 +49,7 @@ public static class Property { private UniformBuffer owner; private int position; private int offset = -1; + @Getter private final PropertyType type; private final String name; @@ -343,6 +346,18 @@ public void bind(int bindingIndex) { glBindBufferBase(GL_UNIFORM_BUFFER, bindingIndex, glBuffer.id); } + public void bindRange(Property startProperty, Property endProperty) { + assert endProperty.offset >= startProperty.offset; + + long bufferAlignment = GLConstants.getBufferOffsetAlignment(); + long bytesOffset = (long)startProperty.offset * Integer.BYTES; + long bytesSize = ((endProperty.offset - startProperty.offset) + align(endProperty.type.size, endProperty.type.alignment, true)) * Integer.BYTES; + + long alignedBytesOffset = align(bytesOffset, bufferAlignment, false); + + glBindBufferRange(GL_UNIFORM_BUFFER, bindingIndex, glBuffer.id, alignedBytesOffset, bytesSize); + } + protected void preUpload() {} public final void upload() { diff --git a/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java b/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java index d596fc9efb..29cff8a61e 100644 --- a/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java +++ b/src/main/java/rs117/hd/overlays/FrameTimerOverlay.java @@ -17,6 +17,7 @@ import net.runelite.client.ui.overlay.components.LineComponent; import net.runelite.client.ui.overlay.components.TitleComponent; import rs117.hd.HdPlugin; +import rs117.hd.renderer.zone.OcclusionManager; import rs117.hd.renderer.zone.SceneManager; import rs117.hd.renderer.zone.WorldViewContext; import rs117.hd.renderer.zone.ZoneRenderer; @@ -206,6 +207,14 @@ public Dimension render(Graphics2D g) { .build()); } + var occlusionManager = OcclusionManager.getInstance(); + if(occlusionManager != null && occlusionManager.isActive()) { + children.add(LineComponent.builder() + .left("Visible Occlusion Queries:") + .right(String.format("%d/%d", occlusionManager.getPassedQueryCount(), occlusionManager.getQueryCount())) + .build()); + } + children.add(LineComponent.builder() .leftFont(boldFont) .left("Streaming Stats:") diff --git a/src/main/java/rs117/hd/overlays/Timer.java b/src/main/java/rs117/hd/overlays/Timer.java index 5f998a8651..29dc692123 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_OCCLUSION, DRAW_SUBMIT, // Miscellaneous @@ -31,6 +32,7 @@ public enum Timer { EXECUTE_COMMAND_BUFFER, MAP_UI_BUFFER("Map UI Buffer"), COPY_UI("Copy UI"), + OCCLUSION_READBACK, MODEL_UPLOAD_COMPLETE, // Logic @@ -68,6 +70,8 @@ public enum Timer { COMPUTE(GPU_TIMER), UNMAP_ROOT_CTX(GPU_TIMER), CLEAR_SCENE(GPU_TIMER), + RENDER_DEPTH_PRE_PASS(GPU_TIMER), + RENDER_OCCLUSION(GPU_TIMER), RENDER_SHADOWS(GPU_TIMER), RENDER_SCENE(GPU_TIMER), RENDER_UI(GPU_TIMER, "Render UI"), diff --git a/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java b/src/main/java/rs117/hd/renderer/zone/ModelStreamingManager.java index 7cdaba7cb3..2ed4a74505 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.OcclusionManager.OcclusionQuery; import rs117.hd.scene.ModelOverrideManager; import rs117.hd.scene.model_overrides.ModelOverride; import rs117.hd.utils.HDUtils; @@ -62,6 +63,9 @@ public class ModelStreamingManager { @Inject private ModelOverrideManager modelOverrideManager; + @Inject + private OcclusionManager occlusionManager; + @Inject private FrameTimer frameTimer; @@ -194,6 +198,10 @@ public void drawTemp(Projection worldProjection, Scene scene, GameObject gameObj )) { return; } + + final OcclusionQuery occlusionQuery = occlusionManager.obtainOcclusionQuery(ctx, gameObject.getId(), zone, orientation, false, m, x, y, z); + if(occlusionQuery != null && occlusionQuery.isOccluded()) + return; plugin.drawnTempRenderableCount++; final boolean isModelPartiallyVisible = sceneManager.isRoot(ctx) && modelClassification == 0; @@ -432,6 +440,10 @@ public void drawDynamic( )) { return; } + + final OcclusionQuery occlusionQuery = occlusionManager.obtainOcclusionQuery(ctx, tileObject.getHash(), zone, orient, true, m, x, y, z); + if(occlusionQuery != null && occlusionQuery.isOccluded()) + return; streamingContext.renderableCount++; final int preOrientation = HDUtils.getModelPreOrientation(HDUtils.getObjectConfig(tileObject)); diff --git a/src/main/java/rs117/hd/renderer/zone/OcclusionManager.java b/src/main/java/rs117/hd/renderer/zone/OcclusionManager.java new file mode 100644 index 0000000000..4b5ea5338b --- /dev/null +++ b/src/main/java/rs117/hd/renderer/zone/OcclusionManager.java @@ -0,0 +1,916 @@ +package rs117.hd.renderer.zone; + +import java.io.IOException; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import org.lwjgl.system.MemoryStack; +import rs117.hd.HdPlugin; +import rs117.hd.HdPluginConfig; +import rs117.hd.opengl.shader.OcclusionShaderProgram; +import rs117.hd.opengl.shader.ShaderException; +import rs117.hd.opengl.shader.ShaderIncludes; +import rs117.hd.opengl.uniforms.UBOWorldViews.WorldViewStruct; +import rs117.hd.overlays.FrameTimer; +import rs117.hd.overlays.Timer; +import rs117.hd.utils.HDUtils; +import rs117.hd.utils.RenderState; +import rs117.hd.utils.buffer.GLBuffer; +import rs117.hd.utils.buffer.GpuFloatBuffer; + +import static org.lwjgl.opengl.GL11.GL_TRIANGLES; +import static org.lwjgl.opengl.GL15C.GL_ARRAY_BUFFER; +import static org.lwjgl.opengl.GL15C.GL_STATIC_DRAW; +import static org.lwjgl.opengl.GL15C.glBindBuffer; +import static org.lwjgl.opengl.GL20C.glEnableVertexAttribArray; +import static org.lwjgl.opengl.GL20C.glVertexAttribPointer; +import static org.lwjgl.opengl.GL30C.glBindVertexArray; +import static org.lwjgl.opengl.GL30C.glDeleteVertexArrays; +import static org.lwjgl.opengl.GL30C.glGenVertexArrays; +import static org.lwjgl.opengl.GL33C.*; +import static org.lwjgl.opengl.GL43.GL_ANY_SAMPLES_PASSED_CONSERVATIVE; +import static rs117.hd.HdPlugin.GL_CAPS; +import static rs117.hd.HdPlugin.checkGLErrors; +import static rs117.hd.utils.MathUtils.*; + +@Slf4j +@Singleton +public final class OcclusionManager { + private static final int FRAMES_IN_FLIGHT = 2; + private static final int VISIBILITY_TEST_DELAY = 3; + + @Getter + private static OcclusionManager instance; + private final ConcurrentHashMap dynamicOcclusionQueries = new ConcurrentHashMap<>(); + private final ConcurrentHashMap tempOcclusionQueries = new ConcurrentHashMap<>(); + private final Set> dynamicOcclusionQuerySet = dynamicOcclusionQueries.entrySet(); + private final Set> tempOcclusionQuerySet = tempOcclusionQueries.entrySet(); + private final ConcurrentLinkedQueue freeQueries = new ConcurrentLinkedQueue<>(); + private final List queuedQueries = new ArrayList<>(); + private final List prevQueuedQueries = new ArrayList<>(); + private final float[] vec = new float[4]; + private final float[][] sceneFrustumPlanes = new float[6][4]; + private final float[] directionalFwd = new float[3]; + private final float[] projected = new float[6]; + @Inject + private HdPlugin plugin; + @Inject + private ZoneRenderer zoneRenderer; + @Inject + private HdPluginConfig config; + @Inject + private FrameTimer frameTimer; + @Inject + private OcclusionShaderProgram occlusionProgram; + @Inject + private OcclusionShaderProgram.Debug occlusionDebugProgram; + private RenderState renderState; + @Getter + private boolean active; + private int debugMode; + private int debugVisibility; + + @Getter + private int queryCount = 0; + @Getter + private int passedQueryCount; + + private GpuFloatBuffer aabbBuffer; + + private int fboOcclusionDepth = 0; + private int rboOcclusionDepth = 0; + + private int occlusionWidth = 0; + private int occlusionHeight = 0; + + private static final int OCCLUSION_DOWNSCALE = 4; + private static final int MIN_OCCLUSION_SIZE = 256; + private static final int MAX_OCCLUSION_SIZE = 1024; + + private int glCubeVAO; + private GLBuffer glCubeVBO; + private GLBuffer glCubeEBO; + private GLBuffer glCubeInstanceData; + private int anySamplesPassedTarget; + + public void toggleDebug() { debugMode = (debugMode + 1) % 3; } + public void toggleDebugVisibility() { + debugVisibility = (debugVisibility + 1) % 3; + } + + public void initialize(RenderState renderState) { + this.renderState = renderState; + + instance = this; + active = config.occlusionCulling(); + + aabbBuffer = new GpuFloatBuffer(1024); + + // Check if conservative queries are supported + if (GL_CAPS.GL_ARB_occlusion_query2) { + anySamplesPassedTarget = GL_ANY_SAMPLES_PASSED_CONSERVATIVE; + log.info("Using GL_ANY_SAMPLES_PASSED_CONSERVATIVE for occlusion queries"); + } else { + anySamplesPassedTarget = GL_ANY_SAMPLES_PASSED; + log.info("Using fallback GL_ANY_SAMPLES_PASSED for occlusion queries"); + } + + glCubeVBO = new GLBuffer("Occlusion VBO", GL_ARRAY_BUFFER, GL_STATIC_DRAW).initialize(); + glCubeEBO = new GLBuffer("Occlusion EBO", GL_ELEMENT_ARRAY_BUFFER, GL_STATIC_DRAW).initialize(); + glCubeInstanceData = new GLBuffer("Occlusion Instance Data", GL_ARRAY_BUFFER, GL_DYNAMIC_DRAW).initialize(); + + try (MemoryStack stack = MemoryStack.stackPush()) { + // Create cube VAO + glCubeVAO = glGenVertexArrays(); + glBindVertexArray(glCubeVAO); + + FloatBuffer vboCubeData = stack.mallocFloat(8 * 3) + .put(new float[] { + // 8 unique cube corners + -1, -1, -1, // 0 + 1, -1, -1, // 1 + 1, 1, -1, // 2 + -1, 1, -1, // 3 + -1, -1, 1, // 4 + 1, -1, 1, // 5 + 1, 1, 1, // 6 + -1, 1, 1 // 7 + }) + .flip(); + glCubeVBO.upload(vboCubeData); + + IntBuffer eboCubeData = stack.mallocInt(36) + .put(new int[] { + // Front face (-Z) + 0, 1, 2, + 0, 2, 3, + + // Back face (+Z) + 4, 6, 5, + 4, 7, 6, + + // Left face (-X) + 0, 3, 7, + 0, 7, 4, + + // Right face (+X) + 1, 5, 6, + 1, 6, 2, + + // Bottom face (-Y) + 0, 4, 5, + 0, 5, 1, + + // Top face (+Y) + 3, 2, 6, + 3, 6, 7 + }) + .flip(); + glCubeEBO.upload(eboCubeData); + + // position attribute + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, false, 3 * Float.BYTES, 0); + + // aabb center attribute + glEnableVertexAttribArray(1); + glVertexAttribDivisor(1, 1); + + // aabb scale attribute + glEnableVertexAttribArray(2); + glVertexAttribDivisor(2, 1); + + // reset + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glBindVertexArray(0); + } + } + + public void initializeShaders(ShaderIncludes includes) throws ShaderException, IOException { + occlusionProgram.compile(includes); + occlusionDebugProgram.compile(includes); + } + + public void destroyShaders() { + occlusionProgram.destroy(); + occlusionDebugProgram.destroy(); + } + + public void destroy() { + if (glCubeVAO != 0) + glDeleteVertexArrays(glCubeVAO); + glCubeVAO = 0; + + if (glCubeVBO != null) + glCubeVBO.destroy(); + glCubeVBO = null; + + if (glCubeEBO != null) + glCubeEBO.destroy(); + glCubeEBO = null; + + if (glCubeInstanceData != null) + glCubeInstanceData.destroy(); + glCubeInstanceData = null; + + if(aabbBuffer != null) + aabbBuffer.destroy(); + aabbBuffer = null; + + destroyOcclusionFbo(); + deleteQueries(freeQueries); + deleteQueries(queuedQueries); + deleteQueries(prevQueuedQueries); + } + + private void deleteQueries(Collection queries) { + for (OcclusionQuery query : queries) { + if (query.id[0] != 0) { + glDeleteQueries(query.id); + Arrays.fill(query.id, 0); + } + } + queries.clear(); + } + + public OcclusionQuery obtainOcclusionQuery( + WorldViewContext ctx, + long hash, + Zone zone, + int orientation, + boolean isDynamic, + Model m, + float x, + float y, + float z + ) { + final ConcurrentHashMap occlusionQueries = isDynamic ? dynamicOcclusionQueries : tempOcclusionQueries; + OcclusionQuery query = occlusionQueries.get(hash); + if (query == null) { + query = obtainQuery(); + occlusionQueries.put(hash, query); + } + query.setWorldView(ctx.uboWorldViewStruct); + query.queue(); + if (!query.resetThisFrame) + query.reset(); + if (active) { + query.addAABB(m.getAABB(orientation), x, y, z); + zone.additionalOcclusionQueries.add(query); + } + return query; + } + + public OcclusionQuery obtainQuery() { + OcclusionQuery query = freeQueries.poll(); + if (query == null) + query = new OcclusionQuery(); + return query; + } + + private void processDynamicOcclusionQueries(Iterator> iter) { + while (iter.hasNext()) { + final OcclusionQuery query = iter.next().getValue(); + if (query.isQueued()) { + query.resetThisFrame = false; + continue; + } + query.free(); + iter.remove(); + } + } + + private void destroyOcclusionFbo() { + if (rboOcclusionDepth != 0) { + glDeleteRenderbuffers(rboOcclusionDepth); + rboOcclusionDepth = 0; + } + + if (fboOcclusionDepth != 0) { + glDeleteFramebuffers(fboOcclusionDepth); + fboOcclusionDepth = 0; + } + + if (rboOcclusionDepth != 0) { + glDeleteRenderbuffers(rboOcclusionDepth); + rboOcclusionDepth = 0; + } + + if (fboOcclusionDepth != 0) { + glDeleteFramebuffers(fboOcclusionDepth); + fboOcclusionDepth = 0; + } + } + + private void ensureOcclusionFbo() { + int targetWidth = clamp(plugin.sceneResolution[0] / OCCLUSION_DOWNSCALE, MIN_OCCLUSION_SIZE, MAX_OCCLUSION_SIZE); + int targetHeight = clamp(plugin.sceneResolution[1] / OCCLUSION_DOWNSCALE, MIN_OCCLUSION_SIZE, MAX_OCCLUSION_SIZE); + + if (fboOcclusionDepth != 0 && + targetWidth == occlusionWidth && + targetHeight == occlusionHeight) { + return; + } + + destroyOcclusionFbo(); + + occlusionWidth = targetWidth; + occlusionHeight = targetHeight; + + fboOcclusionDepth = glGenFramebuffers(); + glBindFramebuffer(GL_FRAMEBUFFER, fboOcclusionDepth); + + rboOcclusionDepth = glGenRenderbuffers(); + glBindRenderbuffer(GL_RENDERBUFFER, rboOcclusionDepth); + + glRenderbufferStorage( + GL_RENDERBUFFER, + GL_DEPTH_COMPONENT24, + occlusionWidth, + occlusionHeight + ); + + glFramebufferRenderbuffer( + GL_FRAMEBUFFER, + GL_DEPTH_ATTACHMENT, + GL_RENDERBUFFER, + rboOcclusionDepth + ); + + glDrawBuffer(GL_NONE); + glReadBuffer(GL_NONE); + + int status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + throw new RuntimeException("Occlusion FBO incomplete: " + status); + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + public void readbackQueries() { + active = config.occlusionCulling() && occlusionProgram.isValid(); + + processDynamicOcclusionQueries(dynamicOcclusionQuerySet.iterator()); + processDynamicOcclusionQueries(tempOcclusionQuerySet.iterator()); + + if (prevQueuedQueries.isEmpty()) + return; + + frameTimer.begin(Timer.OCCLUSION_READBACK); + queryCount = prevQueuedQueries.size(); + passedQueryCount = 0; + for (int i = 0; i < queryCount; i++) { + final OcclusionQuery query = prevQueuedQueries.get(i); + if (!query.queued) + continue; + query.queued = false; + + final int id = query.getReadbackId(); + if (id == 0) + continue; + + if (query.frustumCulled) + continue; + + query.occluded = glGetQueryObjecti(id, GL_QUERY_RESULT) == 0; + if(query.occluded) + query.nextVisibilityTest = plugin.frame + VISIBILITY_TEST_DELAY; + else + passedQueryCount++; + } + frameTimer.end(Timer.OCCLUSION_READBACK); + prevQueuedQueries.clear(); + + checkGLErrors(); + } + + public void occlusionDebugPass() { + if (queuedQueries.isEmpty() || debugMode == 0) + return; + + renderState.viewport.set(0, 0, plugin.sceneResolution[0], plugin.sceneResolution[1]); + renderState.framebuffer.set(GL_DRAW_FRAMEBUFFER, plugin.fboScene); + renderState.depthFunc.set(GL_GEQUAL); + renderState.disable.set(GL_CULL_FACE); + renderState.enable.set(GL_BLEND); + renderState.enable.set(GL_DEPTH_TEST); + renderState.depthMask.set(false); + renderState.ebo.set(glCubeEBO.id); + renderState.vao.set(glCubeVAO); + renderState.apply(); + occlusionDebugProgram.use(); + + if (debugMode == 1) { + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + glLineWidth(2.5f); + } + + processQueries(queuedQueries, true); + + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + + renderState.ebo.set(0); + renderState.vao.set(0); + renderState.disable.set(GL_BLEND); + renderState.disable.set(GL_DEPTH_TEST); + renderState.depthMask.set(true); + renderState.apply(); + } + + public void occlusionPass() { + if (queuedQueries.isEmpty()) + return; + + frameTimer.begin(Timer.RENDER_OCCLUSION); + frameTimer.begin(Timer.DRAW_OCCLUSION); + + zoneRenderer.sceneCamera.getFrustumPlanes(sceneFrustumPlanes); + zoneRenderer.directionalCamera.getForwardDirection(directionalFwd); + normalize(directionalFwd, directionalFwd); + + ensureOcclusionFbo(); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, plugin.msaaSamples > 0 ? plugin.fboSceneDepthResolve : plugin.fboScene); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fboOcclusionDepth); + + glBlitFramebuffer( + 0, 0, + plugin.sceneResolution[0], plugin.sceneResolution[1], + 0, 0, + occlusionWidth, occlusionHeight, + GL_DEPTH_BUFFER_BIT, + GL_NEAREST + ); + + renderState.viewport.set(0, 0, occlusionWidth, occlusionHeight); + renderState.depthFunc.set(GL_GEQUAL); + renderState.disable.set(GL_CULL_FACE); + renderState.enable.set(GL_DEPTH_TEST); + renderState.depthMask.set(false); + renderState.colorMask.set(false, false, false, false); + renderState.ebo.set(glCubeEBO.id); + renderState.vao.set(glCubeVAO); + renderState.apply(); + occlusionProgram.use(); + + processQueries(queuedQueries, false); + + renderState.ebo.set(0); + renderState.vao.set(0); + renderState.disable.set(GL_DEPTH_TEST); + renderState.depthMask.set(true); + renderState.colorMask.set(true, true, true, true); + renderState.apply(); + + prevQueuedQueries.addAll(queuedQueries); + queuedQueries.clear(); + + frameTimer.end(Timer.DRAW_OCCLUSION); + frameTimer.end(Timer.RENDER_OCCLUSION); + + checkGLErrors(); + } + + private void processQueries(List queries, boolean isDebug) { + for (int i = 0; i < queries.size(); i++) { + final OcclusionQuery query = queries.get(i); + if (query.count == 0 || plugin.frame < query.nextVisibilityTest) + continue; + + if (query.id[0] == 0) + glGenQueries(query.id); + + buildQueryAABBs(query, isDebug); + } + + aabbBuffer.flip(); + glCubeInstanceData.upload(aabbBuffer); + glCubeInstanceData.bind(); + aabbBuffer.clear(); + + checkGLErrors(); + + for (int i = 0; i < queries.size(); i++) { + final OcclusionQuery query = queries.get(i); + if (query.count <= 0 || query.frustumCulled || plugin.frame < query.nextVisibilityTest) + continue; + + glVertexAttribPointer(1, 3, GL_FLOAT, false, 24, query.vboOffset); + glVertexAttribPointer(2, 3, GL_FLOAT, false, 24, query.vboOffset + 12); + + if (isDebug) { + if(debugVisibility > 0) { + if (debugVisibility == 1 && !query.isStatic) + continue; + + if (debugVisibility == 2 && query.isStatic) + continue; + } + occlusionDebugProgram.queryId.set(query.id[0]); + glDrawElementsInstanced(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0, query.count); + } else { + glBeginQuery(anySamplesPassedTarget, query.getSampleId()); + glDrawElementsInstanced(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0, query.count); + glEndQuery(anySamplesPassedTarget); + query.advance(); + } + } + checkGLErrors(); + } + + private void buildQueryAABBs(OcclusionQuery query, boolean isDebug) { + if (query.count <= 0) + return; + + final float EXPAND_FACTOR = 4.0f; + final float dirX = -abs(directionalFwd[0]); + final float dirY = -abs(directionalFwd[1]); + final float dirZ = -abs(directionalFwd[2]); + + boolean wasFrustumCulled = query.frustumCulled; + query.frustumCulled = false; + if (!isDebug && query.globalAABB) { + projectAABB(query, projected, + query.offsetX + (query.globalMinX + query.globalMaxX) * 0.5f, + query.offsetY + (query.globalMinY + query.globalMaxY) * 0.5f, + query.offsetZ + (query.globalMinZ + query.globalMaxZ) * 0.5f, + (query.globalMaxX - query.globalMinX) * 0.5f, + (query.globalMaxY - query.globalMinY) * 0.5f, + (query.globalMaxZ - query.globalMinZ) * 0.5f + ); + + if (plugin.configShadowsEnabled) + expandAABBAlongShadow(projected, dirX, dirY, dirZ, EXPAND_FACTOR); + + query.frustumCulled = !isAABBVisible(projected); + } + + if (query.frustumCulled) + return; + + query.vboOffset = (long)aabbBuffer.position() * Float.BYTES; + aabbBuffer.ensureCapacity(query.count * 8); + int aabbEnd = query.count * 6; + for (int base = 0; base < aabbEnd;) { + projectAABB(query, projected, + query.offsetX + query.aabb[base++], + query.offsetY + query.aabb[base++], + query.offsetZ + query.aabb[base++], + query.aabb[base++], + query.aabb[base++], + query.aabb[base++] + ); + + if (plugin.configShadowsEnabled && !isDebug) + expandAABBAlongShadow(projected, dirX, dirY, dirZ, EXPAND_FACTOR); + + if(query.count == 1) + query.frustumCulled = !isAABBVisible(projected); + + aabbBuffer.put(projected); + } + + if(wasFrustumCulled) // If we we're frustum culled & are no longer, retest the query ignoring existing delay + query.nextVisibilityTest = 0; + } + + private boolean isAABBVisible(float[] aabb) { + float minX = aabb[0] - aabb[3]; + float minY = aabb[1] - aabb[4]; + float minZ = aabb[2] - aabb[5]; + + float maxX = aabb[0] + aabb[3]; + float maxY = aabb[1] + aabb[4]; + float maxZ = aabb[2] + aabb[5]; + + return HDUtils.isAABBIntersectingFrustum(minX, minY, minZ, maxX, maxY, maxZ, sceneFrustumPlanes); + } + + private void expandAABBAlongShadow( + float[] aabb, + float dirX, float dirY, float dirZ, + float expandFactor + ) { + float sizeX = aabb[3]; + float sizeY = aabb[4]; + float sizeZ = aabb[5]; + + float projectedExtent = dirX * sizeX + dirY * sizeY + dirZ * sizeZ; + float offset = projectedExtent * expandFactor; + + aabb[3] = sizeX + dirX * offset; + aabb[4] = sizeY + dirY * offset; + aabb[5] = sizeZ + dirZ * offset; + + aabb[0] += dirX * offset; + aabb[1] += dirY * offset; + aabb[2] += dirZ * offset; + } + + private void projectAABB( + OcclusionQuery query, + float[] out, + float posX, float posY, float posZ, + float sizeX, float sizeY, float sizeZ + ) { + if (query.worldView == null) { + out[0] = posX; + out[1] = posY; + out[2] = posZ; + out[3] = sizeX; + out[4] = sizeY; + out[5] = sizeZ; + return; + } + + query.worldView.project(vec4( + vec, + posX - sizeX, + posY - sizeY, + posZ - sizeZ, + 1.0f + )); + + float minX = vec[0]; + float minY = vec[1]; + float minZ = vec[2]; + + query.worldView.project(vec4( + vec, + posX + sizeX, + posY + sizeY, + posZ + sizeZ, + 1.0f + )); + + float maxX = vec[0]; + float maxY = vec[1]; + float maxZ = vec[2]; + + out[0] = (minX + maxX) * 0.5f; + out[1] = (minY + maxY) * 0.5f; + out[2] = (minZ + maxZ) * 0.5f; + out[3] = (maxX - minX) * 0.5f; + out[4] = (maxY - minY) * 0.5f; + out[5] = (maxZ - minZ) * 0.5f; + } + + public final class OcclusionQuery { + private final int[] id = new int[FRAMES_IN_FLIGHT]; + private final boolean[] sampled = new boolean[FRAMES_IN_FLIGHT]; + + @Getter + private boolean queued; + + private boolean occluded; + private boolean frustumCulled; + private boolean resetThisFrame; + private boolean globalAABB; + private boolean isStatic; + + private int nextVisibilityTest; + private int activeId; + private long vboOffset; + + private float offsetX; + private float offsetY; + private float offsetZ; + + private float globalMinX = Float.POSITIVE_INFINITY; + private float globalMinY = Float.POSITIVE_INFINITY; + private float globalMinZ = Float.POSITIVE_INFINITY; + private float globalMaxX = Float.NEGATIVE_INFINITY; + private float globalMaxY = Float.NEGATIVE_INFINITY; + private float globalMaxZ = Float.NEGATIVE_INFINITY; + + @Setter + private WorldViewStruct worldView; + + private float[] aabb = new float[6]; + private int count = 0; + + private void advance() { + activeId = (activeId + 1) % FRAMES_IN_FLIGHT; + } + + private int getReadbackId() { + int idx = (activeId + 1) % FRAMES_IN_FLIGHT; + if (!sampled[idx]) + return 0; + + sampled[idx] = false; + return id[idx]; + } + + private int getSampleId() { + sampled[activeId] = true; + return id[activeId]; + } + + public boolean isOccluded() { + return (occluded || frustumCulled) && active; + } + + public boolean isVisible() { + return !isOccluded(); + } + + public void setOffset(float x, float y, float z) { + offsetX = x; + offsetY = y; + offsetZ = z; + } + + public void addSphere(float x, float y, float z, float radius) { + addAABB(x, y, z, radius, radius, radius); + } + + public void addAABB(AABB aabb) { + addAABB(aabb, 0, 0, 0); + } + + public void addAABB(AABB aabb, float x, float y, float z) { + addAABB( + x + aabb.getCenterX(), + y + aabb.getCenterY(), + z + aabb.getCenterZ(), + aabb.getExtremeX(), + aabb.getExtremeY(), + aabb.getExtremeZ() + ); + } + + public void addMinMax( + float minX, float minY, float minZ, + float maxX, float maxY, float maxZ + ) { + float sizeX = (maxX - minX) * 0.5f; + float sizeY = (maxY - minY) * 0.5f; + float sizeZ = (maxZ - minZ) * 0.5f; + + addAABB( + minX + sizeX, + minY + sizeY, + minZ + sizeZ, + sizeX, + sizeY, + sizeZ + ); + } + + public void addAABB( + float posX, float posY, float posZ, + float sizeX, float sizeY, float sizeZ + ) { + assert !isStatic; + + if (count * 6 >= aabb.length) { + aabb = Arrays.copyOf(aabb, aabb.length * 2); + } + + int base = count * 6; + + aabb[base] = posX; + aabb[base + 1] = posY; + aabb[base + 2] = posZ; + aabb[base + 3] = sizeX; + aabb[base + 4] = sizeY; + aabb[base + 5] = sizeZ; + + count++; + } + + public void setStatic() { + if (count == 0) + return; + + globalMinX = globalMinY = globalMinZ = Float.POSITIVE_INFINITY; + globalMaxX = globalMaxY = globalMaxZ = Float.NEGATIVE_INFINITY; + isStatic = true; + + int writeIndex = 0; + for (int i = 0; i < count; i++) { + int base1 = i * 6; + + float posX1 = aabb[base1]; + float posY1 = aabb[base1 + 1]; + float posZ1 = aabb[base1 + 2]; + float sizeX1 = aabb[base1 + 3]; + float sizeY1 = aabb[base1 + 4]; + float sizeZ1 = aabb[base1 + 5]; + + float minX1 = posX1 - sizeX1; + float minY1 = posY1 - sizeY1; + float minZ1 = posZ1 - sizeZ1; + float maxX1 = posX1 + sizeX1; + float maxY1 = posY1 + sizeY1; + float maxZ1 = posZ1 + sizeZ1; + + boolean encapsulated = false; + + for (int j = 0; j < count; j++) { + if (i == j) + continue; + + int base2 = j * 6; + float posX2 = aabb[base2]; + float posY2 = aabb[base2 + 1]; + float posZ2 = aabb[base2 + 2]; + float sizeX2 = aabb[base2 + 3]; + float sizeY2 = aabb[base2 + 4]; + float sizeZ2 = aabb[base2 + 5]; + + float minX2 = posX2 - sizeX2; + float minY2 = posY2 - sizeY2; + float minZ2 = posZ2 - sizeZ2; + float maxX2 = posX2 + sizeX2; + float maxY2 = posY2 + sizeY2; + float maxZ2 = posZ2 + sizeZ2; + + if (minX1 >= minX2 && maxX1 <= maxX2 && + minY1 >= minY2 && maxY1 <= maxY2 && + minZ1 >= minZ2 && maxZ1 <= maxZ2) { + encapsulated = true; + break; + } + } + + if (!encapsulated) { + // Move this surviving AABB in-place to the writeIndex position + if (writeIndex != i) { + int baseWrite = writeIndex * 6; + aabb[baseWrite] = posX1; + aabb[baseWrite + 1] = posY1; + aabb[baseWrite + 2] = posZ1; + aabb[baseWrite + 3] = sizeX1; + aabb[baseWrite + 4] = sizeY1; + aabb[baseWrite + 5] = sizeZ1; + } + writeIndex++; + + // Update global bounds + globalMinX = Math.min(globalMinX, minX1); + globalMinY = Math.min(globalMinY, minY1); + globalMinZ = Math.min(globalMinZ, minZ1); + globalMaxX = Math.max(globalMaxX, maxX1); + globalMaxY = Math.max(globalMaxY, maxY1); + globalMaxZ = Math.max(globalMaxZ, maxZ1); + } + } + globalAABB = true; + count = writeIndex; + } + + public void reset() { + count = 0; + resetThisFrame = true; + } + + public void queue() { + if (!active || queued) + return; + + queued = true; + + synchronized (queuedQueries) { + queuedQueries.add(this); + } + } + + public void free() { + count = nextVisibilityTest = 0; + queued = false; + occluded = false; + isStatic = false; + frustumCulled = false; + resetThisFrame = false; + worldView = null; + + offsetX = offsetY = offsetZ = 0f; + + globalMinX = globalMinY = globalMinZ = Float.POSITIVE_INFINITY; + globalMaxX = globalMaxY = globalMaxZ = Float.NEGATIVE_INFINITY; + globalAABB = false; + + Arrays.fill(sampled, false); + + freeQueries.add(this); + } + } +} diff --git a/src/main/java/rs117/hd/renderer/zone/SceneManager.java b/src/main/java/rs117/hd/renderer/zone/SceneManager.java index 6370b07e71..4ac8f7f164 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneManager.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneManager.java @@ -656,6 +656,7 @@ public void swapScene(Scene scene) { root.pendingCull.add(preZone); nextZone.setMetadata(ctx, nextSceneContext, x, z); + nextZone.setAlphaModelsOffset(ctx, nextSceneContext, x, z); } } diff --git a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java index d0b06b884d..10d2315701 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java @@ -24,6 +24,7 @@ */ package rs117.hd.renderer.zone; +import java.util.Arrays; import java.util.HashSet; import java.util.Set; import javax.inject.Inject; @@ -127,6 +128,10 @@ public interface OnBeforeProcessTileFunc { private short[][][] underlayIds; private int[][][] tileHeights; + private final float[] abbMin = new float[3]; + private final float[] abbMax = new float[3]; + private final float[] abbVec = new float[3]; + private final int[] worldPos = new int[3]; private final int[][] vertices = new int[4][3]; private final int[] vertexKeys = new int[4]; @@ -237,6 +242,9 @@ public void uploadZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) throws uploadZoneWater(ctx, zone, mzx, mzz, vb, fb); zone.levelOffsets[Zone.LEVEL_WATER_SURFACE] = vb.position(); } + + for(int i = 0; i < 5; i++) + zone.levelOcclusionQueries[i].setStatic(); } private void uploadZoneLevel( @@ -252,6 +260,8 @@ private void uploadZoneLevel( GpuIntBuffer fb ) throws InterruptedException { int ridx = 0; + Arrays.fill(abbMin, Float.MAX_VALUE); + Arrays.fill(abbMax, -Float.MAX_VALUE); // upload the roofs and save their positions for (int id : roofIds) { @@ -271,6 +281,11 @@ private void uploadZoneLevel( // upload everything else uploadZoneLevelRoof(ctx, zone, mzx, mzz, level, 0, visbelow, vb, ab, fb); + + if(abbMin[0] != Float.MAX_VALUE && abbMin[1] != Float.MAX_VALUE && abbMin[2] != Float.MAX_VALUE && + abbMax[0] != -Float.MAX_VALUE && abbMax[1] != -Float.MAX_VALUE && abbMax[2] != -Float.MAX_VALUE) { + zone.levelOcclusionQueries[level].addMinMax(abbMin[0], abbMin[1] - LOCAL_HALF_TILE_SIZE, abbMin[2], abbMax[0], abbMax[1] + LOCAL_HALF_TILE_SIZE, abbMax[2]); + } } private void uploadZoneLevelRoof( @@ -335,6 +350,9 @@ private void uploadZoneWater( this.basex = (mzx - (ctx.sceneOffset >> 3)) << 10; this.basez = (mzz - (ctx.sceneOffset >> 3)) << 10; + Arrays.fill(abbMin, Float.MAX_VALUE); + Arrays.fill(abbMax, -Float.MAX_VALUE); + for (int level = 0; level < MAX_Z; level++) { for (int xoff = 0; xoff < 8; ++xoff) { for (int zoff = 0; zoff < 8; ++zoff) { @@ -349,6 +367,11 @@ private void uploadZoneWater( } } } + + if(abbMin[0] != Float.MAX_VALUE && abbMin[1] != Float.MAX_VALUE && abbMin[2] != Float.MAX_VALUE && + abbMax[0] != -Float.MAX_VALUE && abbMax[1] != -Float.MAX_VALUE && abbMax[2] != -Float.MAX_VALUE) { + zone.levelOcclusionQueries[level].addMinMax(abbMin[0], abbMin[1] - LOCAL_HALF_TILE_SIZE, abbMin[2], abbMax[0], abbMax[1] + LOCAL_HALF_TILE_SIZE, abbMax[2]); + } } private void estimateZoneTileSize(ZoneSceneContext ctx, Zone z, Tile t) { @@ -730,7 +753,7 @@ private void uploadZoneRenderable( int alphaStart = alphaBuffer != null ? alphaBuffer.position() : 0; try { uploadStaticModel( - ctx, tile, model, modelOverride, uuid, + ctx, zone, tile, model, modelOverride, uuid, preOrientation, orient, x - basex, y, z - basez, opaqueBuffer, @@ -999,26 +1022,23 @@ private void uploadTilePaint( neMaterialData, nwMaterialData, seMaterialData, neTerrainData, nwTerrainData, seTerrainData ); - vb.putVertex( lx2, neHeight, lz2, uvx, uvy, 0, neNormals[0], neNormals[2], neNormals[1], - texturedFaceIdx + texturedFaceIdx, 0 ); - vb.putVertex( lx3, nwHeight, lz3, uvx - uvcos, uvy - uvsin, 0, nwNormals[0], nwNormals[2], nwNormals[1], - texturedFaceIdx + texturedFaceIdx, 0 ); - vb.putVertex( lx1, seHeight, lz1, uvx + uvsin, uvy - uvcos, 0, seNormals[0], seNormals[2], seNormals[1], - texturedFaceIdx + texturedFaceIdx, 0 ); texturedFaceIdx = fb.putFace( @@ -1026,27 +1046,42 @@ private void uploadTilePaint( swMaterialData, seMaterialData, nwMaterialData, swTerrainData, seTerrainData, nwTerrainData ); - vb.putVertex( lx0, swHeight, lz0, uvx - uvcos + uvsin, uvy - uvsin - uvcos, 0, swNormals[0], swNormals[2], swNormals[1], - texturedFaceIdx + texturedFaceIdx, 0 ); vb.putVertex( lx1, seHeight, lz1, uvx + uvsin, uvy - uvcos, 0, seNormals[0], seNormals[2], seNormals[1], - texturedFaceIdx + texturedFaceIdx, 0 ); vb.putVertex( lx3, nwHeight, lz3, uvx - uvcos, uvy - uvsin, 0, nwNormals[0], nwNormals[2], nwNormals[1], - texturedFaceIdx + texturedFaceIdx, 0 ); + + vec3(abbVec, lx2, neHeight, lz2); + min(abbMin, abbMin, abbVec); + max(abbMax, abbMax, abbVec); + + vec3(abbVec, lx3, nwHeight, lz3); + min(abbMin, abbMin, abbVec); + max(abbMax, abbMax, abbVec); + + vec3(abbVec, lx1, seHeight, lz1); + min(abbMin, abbMin, abbVec); + max(abbMax, abbMax, abbVec); + + vec3(abbVec, lx0, swHeight, lz0); + min(abbMin, abbMin, abbVec); + max(abbMax, abbMax, abbVec); } private void uploadTileModel( @@ -1315,28 +1350,41 @@ private void uploadTileModel( lx0, ly0, lz0, uvAx, uvAy, 0, normalsA[0], normalsA[2], normalsA[1], - texturedFaceIdx + texturedFaceIdx, 0 ); vb.putVertex( lx1, ly1, lz1, uvBx, uvBy, 0, normalsB[0], normalsB[2], normalsB[1], - texturedFaceIdx + texturedFaceIdx, 0 ); vb.putVertex( lx2, ly2, lz2, uvCx, uvCy, 0, normalsC[0], normalsC[2], normalsC[1], - texturedFaceIdx + texturedFaceIdx, 0 ); + + vec3(abbVec, lx0, ly0, lz0); + min(abbMin, abbMin, abbVec); + max(abbMax, abbMax, abbVec); + + vec3(abbVec, lx1, ly1, lz1); + min(abbMin, abbMin, abbVec); + max(abbMax, abbMax, abbVec); + + vec3(abbVec, lx2, ly2, lz2); + min(abbMin, abbMin, abbVec); + max(abbMax, abbMax, abbVec); } } // scene upload private int uploadStaticModel( ZoneSceneContext ctx, + Zone zone, Tile tile, Model model, ModelOverride modelOverride, @@ -1671,24 +1719,29 @@ private int uploadStaticModel( vx1, vy1, vz1, faceUVs[0], faceUVs[1], faceUVs[2], modelNormals[0], modelNormals[1], modelNormals[2], - texturedFaceIdx + texturedFaceIdx, depthBias ); vb.putStaticVertex( vx2, vy2, vz2, faceUVs[4], faceUVs[5], faceUVs[6], modelNormals[3], modelNormals[4], modelNormals[5], - texturedFaceIdx + texturedFaceIdx, depthBias ); vb.putStaticVertex( vx3, vy3, vz3, faceUVs[8], faceUVs[9], faceUVs[10], modelNormals[6], modelNormals[7], modelNormals[8], - texturedFaceIdx + texturedFaceIdx, depthBias ); + len += 3; } + + if(len > 0) + zone.levelOcclusionQueries[level].addAABB(model.getAABB(orientation), x, y, z); + writeCache.flush(); return len; } @@ -2090,19 +2143,19 @@ else if (color3 == -1) modelLocalI[vertexOffsetA], modelLocalI[vertexOffsetA + 1], modelLocalI[vertexOffsetA + 2], faceUVs[0], faceUVs[1], faceUVs[2], faceNormals[0], faceNormals[1], faceNormals[2], - texturedFaceIdx + texturedFaceIdx, depthBias ); vb.putVertex( modelLocalI[vertexOffsetB], modelLocalI[vertexOffsetB + 1], modelLocalI[vertexOffsetB + 2], faceUVs[4], faceUVs[5], faceUVs[6], faceNormals[3], faceNormals[4], faceNormals[5], - texturedFaceIdx + texturedFaceIdx, depthBias ); vb.putVertex( modelLocalI[vertexOffsetC], modelLocalI[vertexOffsetC + 1], modelLocalI[vertexOffsetC + 2], faceUVs[8], faceUVs[9], faceUVs[10], faceNormals[6], faceNormals[7], faceNormals[8], - texturedFaceIdx + texturedFaceIdx, depthBias ); } diff --git a/src/main/java/rs117/hd/renderer/zone/VertexWriteCache.java b/src/main/java/rs117/hd/renderer/zone/VertexWriteCache.java index 828d11e4f0..76084baa83 100644 --- a/src/main/java/rs117/hd/renderer/zone/VertexWriteCache.java +++ b/src/main/java/rs117/hd/renderer/zone/VertexWriteCache.java @@ -71,7 +71,7 @@ public void putVertex( int x, int y, int z, float u, float v, float w, int nx, int ny, int nz, - int textureFaceIdx + int textureFaceIdx, int depth ) { if (stagingPosition + 7 > stagingBuffer.length) flushAndGrow(); @@ -79,13 +79,15 @@ public void putVertex( final int[] stagingBuffer = this.stagingBuffer; final int stagingPosition = this.stagingPosition; + assert textureFaceIdx < 0xFFFFFF; + stagingBuffer[stagingPosition] = x; stagingBuffer[stagingPosition + 1] = y; stagingBuffer[stagingPosition + 2] = z; stagingBuffer[stagingPosition + 3] = float16(v) << 16 | float16(u); stagingBuffer[stagingPosition + 4] = (nx & 0xFFFF) << 16 | float16(w); stagingBuffer[stagingPosition + 5] = (nz & 0xFFFF) << 16 | ny & 0xFFFF; - stagingBuffer[stagingPosition + 6] = textureFaceIdx; + stagingBuffer[stagingPosition + 6] = (depth & 0xFF) << 24 | (textureFaceIdx & 0xFFFFFF); this.stagingPosition += 7; } @@ -94,7 +96,7 @@ public void putStaticVertex( int x, int y, int z, float u, float v, float w, int nx, int ny, int nz, - int textureFaceIdx + int textureFaceIdx, int depth ) { if (stagingPosition + 6 > stagingBuffer.length) flushAndGrow(); @@ -108,7 +110,7 @@ public void putStaticVertex( // Unnormalized normals, assumed to be within short max stagingBuffer[stagingPosition + 3] = (ny & 0xFFFF) << 16 | nx & 0xFFFF; stagingBuffer[stagingPosition + 4] = nz & 0xFFFF; - stagingBuffer[stagingPosition + 5] = textureFaceIdx; + stagingBuffer[stagingPosition + 5] = (depth & 0xFF) << 24 | (textureFaceIdx & 0xFFFFFF); this.stagingPosition += 6; } diff --git a/src/main/java/rs117/hd/renderer/zone/WorldViewContext.java b/src/main/java/rs117/hd/renderer/zone/WorldViewContext.java index a2737f2cee..a7f9d45957 100644 --- a/src/main/java/rs117/hd/renderer/zone/WorldViewContext.java +++ b/src/main/java/rs117/hd/renderer/zone/WorldViewContext.java @@ -163,7 +163,7 @@ void sortStaticAlphaModels(Camera camera) { for (int zx = 0; zx < sizeX; zx++) { for (int zz = 0; zz < sizeZ; zz++) { final Zone z = zones[zx][zz]; - if (z.alphaModels.isEmpty() || (worldViewId == -1 && !z.inSceneFrustum)) + if (z.alphaModels.isEmpty() || (worldViewId == -1 && !z.inSceneFrustum) || z.isFullyOccluded) continue; final int dx = camPosX - ((zx - offset) << 10); @@ -211,6 +211,7 @@ void handleZoneSwap(float deltaTime, int zx, int zz) { if (prevZone != curZone) { curZone.inSceneFrustum = prevZone.inSceneFrustum; curZone.inShadowFrustum = prevZone.inShadowFrustum; + clientThread.invoke(() -> zones[zx][zz].setAlphaModelsOffset(this, sceneContext, zx, zz)); pendingCull.add(prevZone); } } else if (uploadTask.wasCancelled() && !curZone.cull) { diff --git a/src/main/java/rs117/hd/renderer/zone/Zone.java b/src/main/java/rs117/hd/renderer/zone/Zone.java index 24dc3abba1..a71a16bd9d 100644 --- a/src/main/java/rs117/hd/renderer/zone/Zone.java +++ b/src/main/java/rs117/hd/renderer/zone/Zone.java @@ -17,6 +17,7 @@ import net.runelite.api.*; import org.lwjgl.system.MemoryStack; import rs117.hd.HdPlugin; +import rs117.hd.renderer.zone.OcclusionManager.OcclusionQuery; import rs117.hd.scene.MaterialManager; import rs117.hd.scene.SceneContext; import rs117.hd.scene.materials.Material; @@ -74,6 +75,8 @@ public class Zone { @Nullable public GLBuffer vboO, vboA, vboM; public GLTextureBuffer tboF; + public ConcurrentLinkedQueue additionalOcclusionQueries = new ConcurrentLinkedQueue<>(); + public boolean isFullyOccluded; public boolean initialized; // whether the zone vao and vbos are ready public boolean cull; // whether the zone is queued for deletion @@ -90,6 +93,7 @@ public class Zone { ZoneUploadJob uploadJob; int[] levelOffsets = new int[5]; // buffer pos in ints for the end of the level + OcclusionQuery[] levelOcclusionQueries = new OcclusionQuery[5]; int[][] rids; int[][] roofStart; @@ -121,6 +125,9 @@ public void initialize(GLBuffer o, GLBuffer a, GLTextureBuffer f, int eboShared) } tboF = f; + + for(int i = 0; i < 5; ++i) + levelOcclusionQueries[i] = OcclusionManager.getInstance().obtainQuery(); } public static void freeZones(@Nullable Zone[][] zones) { @@ -169,6 +176,18 @@ public void free() { uploadJob = null; } + for(int i = 0; i < 5; ++i){ + if(levelOcclusionQueries[i] != null) + levelOcclusionQueries[i].free(); + levelOcclusionQueries[i] = null; + } + + for(AlphaModel m : alphaModels) { + if(m.occlusionQuery != null) + m.occlusionQuery.free(); + m.occlusionQuery = null; + } + sortedAlphaFacesUpload.release(); sizeO = 0; @@ -194,6 +213,35 @@ public void free() { alphaModels.clear(); } + public void evaluateOcclusion(WorldViewContext ctx){ + isFullyOccluded = true; + for (int level = ctx.minLevel; level <= ctx.maxLevel; ++level) { + if(levelOcclusionQueries[level] == null || levelOcclusionQueries[level].isVisible()) + isFullyOccluded = false; + if(levelOcclusionQueries[level] != null) + levelOcclusionQueries[level].queue(); + } + + if(isFullyOccluded) { + // Check if any of the dynamic occlusion queries are not occluded + for(OcclusionQuery dynamicQuery : additionalOcclusionQueries) { + if(dynamicQuery.isVisible()) { + isFullyOccluded = false; + break; + } + } + + if(isFullyOccluded) { + // Zone is fully occluded, we need to requeue all dynamic queries since they are revelvant to if the zone is fully occluded + for(OcclusionQuery dynamicQuery : additionalOcclusionQueries) + dynamicQuery.queue(); + } + } + + if(!isFullyOccluded) // Dynamics will reappend when they are processed + additionalOcclusionQueries.clear(); + } + public static void processPendingDeletions() { int leakCount = 0; GLBuffer vbo; @@ -303,6 +351,18 @@ private void setupVao(int vao, int buffer, int metadata, int ebo) { glBindBuffer(GL_ARRAY_BUFFER, 0); } + public void setAlphaModelsOffset(WorldViewContext viewContext, SceneContext sceneContext, int mx, int mz) { + int baseX = (mx - (sceneContext.sceneOffset >> 3)) << 10; + int baseZ = (mz - (sceneContext.sceneOffset >> 3)) << 10; + + for(AlphaModel m : alphaModels) { + if(m.occlusionQuery != null) { + m.occlusionQuery.setOffset(baseX, 0, baseZ); + m.occlusionQuery.setWorldView(viewContext.uboWorldViewStruct); + } + } + } + public void setMetadata(WorldViewContext viewContext, SceneContext sceneContext, int mx, int mz) { if (vboM == null) return; @@ -310,6 +370,13 @@ public void setMetadata(WorldViewContext viewContext, SceneContext sceneContext, int baseX = (mx - (sceneContext.sceneOffset >> 3)) << 10; int baseZ = (mz - (sceneContext.sceneOffset >> 3)) << 10; + for(int i = 0; i < 5; i++) { + if(levelOcclusionQueries[i] != null) { + levelOcclusionQueries[i].setOffset(baseX, 0, baseZ); + levelOcclusionQueries[i].setWorldView(viewContext.uboWorldViewStruct); + } + } + try (MemoryStack stack = MemoryStack.stackPush()) { IntBuffer buf = stack.mallocInt(3) .put(viewContext.uboWorldViewStruct != null ? viewContext.uboWorldViewStruct.worldViewIdx + 1 : 0) @@ -367,6 +434,8 @@ void renderOpaque(CommandBuffer cmd, WorldViewContext ctx, boolean roofShadows) } for (int level = ctx.minLevel; level <= maxLevel; ++level) { + if(levelOcclusionQueries[level] != null && levelOcclusionQueries[level].isOccluded()) + continue; int[] rids = this.rids[level]; int[] roofStart = this.roofStart[level]; int[] roofEnd = this.roofEnd[level]; @@ -458,6 +527,7 @@ public static class AlphaModel { int[] packedFaces; int[] sortedFaces; int sortedFacesLen; + OcclusionQuery occlusionQuery; int dist; int asyncSortIdx = -1; @@ -638,6 +708,11 @@ void addAlphaModel( m.radius = 2 + (int) Math.sqrt(radius); m.sortedFaces = new int[bufferIdx * 3]; + if(bufferIdx >= 32) { + m.occlusionQuery = OcclusionManager.getInstance().obtainQuery(); + m.occlusionQuery.addSphere(x + cx, y + cy, z + cz, m.radius); + } + assert packedFaces.length > 0; // Normally these will be equal, but transparency is used to hide faces in the TzHaar reskin assert bufferIdx <= packedFaces.length : String.format("%d > %d", (int) bufferIdx, packedFaces.length); @@ -703,6 +778,7 @@ private void cleanAlphaModels(List alphaModels) { alphaModels.remove(i); m.packedFaces = null; m.sortedFaces = null; + m.occlusionQuery = null; modelCache.add(m); } m.asyncSortIdx = -1; @@ -775,6 +851,12 @@ void alphaStaticModelSort(Camera camera) { if ((m.flags & AlphaModel.SKIP) != 0 || m.isTemp()) continue; + if(m.occlusionQuery != null) { + m.occlusionQuery.queue(); + if(m.occlusionQuery.isOccluded()) + continue; + } + m.dist = dist; alphaSortingJob.addAlphaModel(m); } @@ -815,7 +897,7 @@ void renderAlpha( int zz, int level, WorldViewContext ctx, - boolean isShadowPass, + boolean isScenePass, boolean includeRoof ) { if (alphaModels.isEmpty()) @@ -832,12 +914,10 @@ void renderAlpha( drawIdx = 0; - cmd.DepthMask(false); - boolean shouldQueueUpload = false; for (int i = 0; i < alphaModels.size(); i++) { final AlphaModel m = alphaModels.get(i); - if ((m.flags & AlphaModel.SKIP) != 0 || m.level != level) + if ((m.flags & AlphaModel.SKIP) != 0 || m.level != level || (m.occlusionQuery != null && m.occlusionQuery.isOccluded())) continue; if (level < minLevel || level > maxLevel || @@ -859,7 +939,7 @@ void renderAlpha( continue; } - if (isShadowPass || m.asyncSortIdx < 0) { + if (!isScenePass || m.asyncSortIdx < 0) { lastDrawMode = STATIC_UNSORTED; pushRange(m.startpos, m.endpos); continue; @@ -890,8 +970,6 @@ void renderAlpha( } flush(cmd); - - cmd.DepthMask(true); } private void flush(CommandBuffer cmd) { @@ -953,7 +1031,8 @@ synchronized void multizoneLocs(SceneContext ctx, int zx, int zz, Camera camera, int zx2 = (centerX >> 10) + offset; int zz2 = (centerZ >> 10) + offset; if (zx2 >= 0 && zx2 < zones.length && zz2 >= 0 && zz2 < zones[0].length) { - if (zones[zx2][zz2].inSceneFrustum && zones[zx2][zz2].initialized) { + Zone z2 = zones[zx2][zz2]; + if(z2.inSceneFrustum && z2.initialized && !z2.isFullyOccluded) { max = distance; closestZoneX = centerX >> 10; closestZoneZ = centerZ >> 10; @@ -993,6 +1072,7 @@ synchronized void multizoneLocs(SceneContext ctx, int zx, int zz, Camera camera, m2.zofx = (byte) (closestZoneX - zx); m2.zofz = (byte) (closestZoneZ - zz); + m2.occlusionQuery = m.occlusionQuery; m2.packedFaces = m.packedFaces; m2.radius = m.radius; m2.asyncSortIdx = m.asyncSortIdx; diff --git a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java index d9efbc7fd2..37ff52cc27 100644 --- a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java +++ b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java @@ -33,6 +33,7 @@ import net.runelite.api.*; import net.runelite.api.events.*; import net.runelite.api.hooks.*; +import net.runelite.client.callback.RenderCallbackManager; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.ui.DrawManager; import org.lwjgl.opengl.*; @@ -41,6 +42,7 @@ import rs117.hd.config.ColorFilter; import rs117.hd.config.DynamicLights; import rs117.hd.config.ShadowMode; +import rs117.hd.opengl.shader.DepthShaderProgram; import rs117.hd.opengl.shader.SceneShaderProgram; import rs117.hd.opengl.shader.ShaderException; import rs117.hd.opengl.shader.ShaderIncludes; @@ -93,6 +95,7 @@ public class ZoneRenderer implements Renderer { private static int UNIFORM_BLOCK_COUNT = HdPlugin.UNIFORM_BLOCK_COUNT; public static final int UNIFORM_BLOCK_WORLD_VIEWS = UNIFORM_BLOCK_COUNT++; + public static final int UNIFORM_BLOCK_OCCLUSION = UNIFORM_BLOCK_COUNT++; @Inject private Client client; @@ -118,9 +121,18 @@ public class ZoneRenderer implements Renderer { @Inject private ModelStreamingManager modelStreamingManager; + @Inject + private OcclusionManager occlusionManager; + + @Inject + private RenderCallbackManager renderCallbackManager; + @Inject private FrameTimer frameTimer; + @Inject + private DepthShaderProgram depthProgram; + @Inject private SceneShaderProgram sceneProgram; @@ -137,13 +149,16 @@ public class ZoneRenderer implements Renderer { private UBOWorldViews uboWorldViews; public final Camera sceneCamera = new Camera().setReverseZ(true); + public final Camera directionalCamera = new Camera().setOrthographic(true); public final ShadowCasterVolume directionalShadowCasterVolume = new ShadowCasterVolume(directionalCamera); public final RenderState renderState = new RenderState(); + public final CommandBuffer tempCmd = new CommandBuffer("Temp", renderState); + public final CommandBuffer depthOpaqueCmd = new CommandBuffer("Depth Opaque", renderState); + public final CommandBuffer depthAlphaCmd = new CommandBuffer("Depth Alpha", renderState); public final CommandBuffer sceneCmd = new CommandBuffer("Scene", renderState); public final CommandBuffer directionalCmd = new CommandBuffer("Directional", renderState); - public final CommandBuffer playerCmd = new CommandBuffer("Player", renderState); private GLBuffer indirectDrawCmds; public static GpuIntBuffer indirectDrawCmdsStaging; @@ -176,11 +191,15 @@ public void initialize() { SceneUploader.POOL = new ConcurrentPool<>(plugin.getInjector(), SceneUploader.class); FacePrioritySorter.POOL = new ConcurrentPool<>(plugin.getInjector(), FacePrioritySorter.class); + depthOpaqueCmd.setFrameTimer(frameTimer); + depthAlphaCmd.setFrameTimer(frameTimer); sceneCmd.setFrameTimer(frameTimer); directionalCmd.setFrameTimer(frameTimer); - - jobSystem.startUp(config.cpuUsageLimit()); + uboWorldViews.initialize(UNIFORM_BLOCK_WORLD_VIEWS); + jobSystem.startUp(config.cpuUsageLimit()); + modelStreamingManager.initialize(); + occlusionManager.initialize(renderState); sceneManager.initialize(renderState, uboWorldViews); modelStreamingManager.initialize(); @@ -193,6 +212,7 @@ public void initialize() { public void destroy() { destroyBuffers(); + occlusionManager.destroy(); jobSystem.shutDown(); modelStreamingManager.destroy(); sceneManager.destroy(); @@ -218,16 +238,20 @@ public void addShaderIncludes(ShaderIncludes includes) { @Override public void initializeShaders(ShaderIncludes includes) throws ShaderException, IOException { + depthProgram.compile(includes); sceneProgram.compile(includes); fastShadowProgram.compile(includes); detailedShadowProgram.compile(includes); + occlusionManager.initializeShaders(includes); } @Override public void destroyShaders() { + depthProgram.destroy(); sceneProgram.destroy(); fastShadowProgram.destroy(); detailedShadowProgram.destroy(); + occlusionManager.destroyShaders(); } private void initializeBuffers() { @@ -297,12 +321,21 @@ public void preSceneDraw( ctx.completeInvalidation(); int offset = ctx.sceneContext.sceneOffset >> 3; - for (int zx = 0; zx < ctx.sizeX; ++zx) - for (int zz = 0; zz < ctx.sizeZ; ++zz) - ctx.zones[zx][zz].multizoneLocs(ctx.sceneContext, zx - offset, zz - offset, sceneCamera, ctx.zones); + for (int zx = 0; zx < ctx.sizeX; ++zx) { + for (int zz = 0; zz < ctx.sizeZ; ++zz) { + final Zone zone = ctx.zones[zx][zz]; + if(!zone.initialized) + continue; + zone.multizoneLocs(ctx.sceneContext, zx - offset, zz - offset, sceneCamera, ctx.zones); + zone.evaluateOcclusion(ctx); + } + } ctx.sortStaticAlphaModels(sceneCamera); + // Depth PrePass means opaque should only draw if its equal to the value stored in the mask + sceneCmd.DepthFunc(GL_EQUAL); + ctx.map(); frameTimer.end(Timer.DRAW_PRESCENE); } @@ -372,6 +405,8 @@ private void preSceneDrawTopLevel( frameTimer.begin(Timer.UPDATE_LIGHTS); lightManager.update(ctx.sceneContext, plugin.cameraShift, plugin.cameraFrustum); frameTimer.end(Timer.UPDATE_LIGHTS); + + occlusionManager.readbackQueries(); } catch (Exception ex) { log.error("Error while updating environment or lights:", ex); plugin.stopPlugin(); @@ -448,6 +483,8 @@ private void preSceneDrawTopLevel( } plugin.uboGlobal.cameraPos.set(plugin.cameraPosition); + plugin.uboGlobal.cameraNear.set(sceneCamera.getNearPlane()); + plugin.uboGlobal.cameraFar.set(sceneCamera.getFarPlane()); plugin.uboGlobal.viewMatrix.set(plugin.viewMatrix); plugin.uboGlobal.projectionMatrix.set(plugin.viewProjMatrix); plugin.uboGlobal.invProjectionMatrix.set(plugin.invViewProjMatrix); @@ -579,6 +616,8 @@ private void preSceneDrawTopLevel( // Reset buffers for the next frame indirectDrawCmdsStaging.clear(); + depthOpaqueCmd.reset(); + depthAlphaCmd.reset(); sceneCmd.reset(); directionalCmd.reset(); renderState.reset(); @@ -650,6 +689,89 @@ private void postDrawTopLevel() { checkGLErrors(); } + private void clearScene() { + renderState.framebuffer.set(GL_DRAW_FRAMEBUFFER, plugin.fboScene); + if (plugin.msaaSamples > 1) { + renderState.enable.set(GL_MULTISAMPLE); + } else { + renderState.disable.set(GL_MULTISAMPLE); + } + renderState.viewport.set(0, 0, plugin.sceneResolution[0], plugin.sceneResolution[1]); + renderState.ido.set(indirectDrawCmds.id); + renderState.apply(); + + // Clear scene + frameTimer.begin(Timer.CLEAR_SCENE); + + float[] fogColor = ColorUtils.linearToSrgb(environmentManager.currentFogColor); + float[] gammaCorrectedFogColor = pow(fogColor, plugin.getGammaCorrection()); + glClearColor( + gammaCorrectedFogColor[0], + gammaCorrectedFogColor[1], + gammaCorrectedFogColor[2], + 1f + ); + glClearDepth(0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + frameTimer.end(Timer.CLEAR_SCENE); + } + + private void depthPrePass() { + depthProgram.use(); + + frameTimer.begin(Timer.DRAW_SCENE); + + renderState.framebuffer.set(GL_DRAW_FRAMEBUFFER, plugin.fboScene); + renderState.viewport.set(0, 0, plugin.sceneResolution[0], plugin.sceneResolution[1]); + renderState.ido.set(indirectDrawCmds.id); + + renderState.enable.set(GL_CULL_FACE); + renderState.enable.set(GL_DEPTH_TEST); + renderState.depthFunc.set(GL_GEQUAL); + renderState.depthMask.set(true); + renderState.colorMask.set(false, false, false, false); + + renderState.apply(); + + glClearDepth(0.0); + glClear(GL_DEPTH_BUFFER_BIT); + + frameTimer.begin(Timer.RENDER_DEPTH_PRE_PASS); + + depthOpaqueCmd.execute(); + + renderState.framebuffer.set(GL_DRAW_FRAMEBUFFER, plugin.fboSceneAlphaDepth); + renderState.framebuffer.apply(); + + glClearDepth(0.0); + glClear(GL_DEPTH_BUFFER_BIT); + + depthAlphaCmd.execute(); + + frameTimer.end(Timer.RENDER_DEPTH_PRE_PASS); + renderState.disable.set(GL_CULL_FACE); + renderState.disable.set(GL_DEPTH_TEST); + renderState.colorMask.set(true, true, true, true); + renderState.apply(); + + if (plugin.msaaSamples > 1) { + glBindFramebuffer(GL_READ_FRAMEBUFFER, plugin.fboScene); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, plugin.fboSceneDepthResolve); + + glBlitFramebuffer( + 0, 0, plugin.sceneResolution[0], plugin.sceneResolution[1], + 0, 0, plugin.sceneResolution[0], plugin.sceneResolution[1], + GL_DEPTH_BUFFER_BIT, + GL_NEAREST + ); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + frameTimer.end(Timer.DRAW_SCENE); + } + private void tiledLightingPass() { if (!plugin.configTiledLighting || plugin.configDynamicLights == DynamicLights.NONE) return; @@ -703,9 +825,7 @@ private void directionalShadowPass() { renderState.disable.set(GL_CULL_FACE); renderState.depthFunc.set(GL_LEQUAL); - CommandBuffer.SKIP_DEPTH_MASKING = true; directionalCmd.execute(); - CommandBuffer.SKIP_DEPTH_MASKING = false; renderState.disable.set(GL_DEPTH_TEST); @@ -726,27 +846,11 @@ private void scenePass() { renderState.ido.set(indirectDrawCmds.id); renderState.apply(); - // Clear scene - frameTimer.begin(Timer.CLEAR_SCENE); - - float[] fogColor = ColorUtils.linearToSrgb(environmentManager.currentFogColor); - float[] gammaCorrectedFogColor = pow(fogColor, plugin.getGammaCorrection()); - glClearColor( - gammaCorrectedFogColor[0], - gammaCorrectedFogColor[1], - gammaCorrectedFogColor[2], - 1f - ); - glClearDepth(0); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - frameTimer.end(Timer.CLEAR_SCENE); - frameTimer.begin(Timer.RENDER_SCENE); renderState.enable.set(GL_BLEND); renderState.enable.set(GL_CULL_FACE); renderState.enable.set(GL_DEPTH_TEST); - renderState.depthFunc.set(GL_GEQUAL); renderState.blendFunc.set(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE); // Render the scene @@ -766,66 +870,62 @@ private void scenePass() { @Override public boolean zoneInFrustum(int zx, int zz, int maxY, int minY) { - if (!sceneManager.isTopLevelValid()) - return false; - - WorldViewContext ctx = sceneManager.getRoot(); - if (plugin.enableDetailedTimers) frameTimer.begin(Timer.VISIBILITY_CHECK); - int minX = zx * CHUNK_SIZE - ctx.sceneContext.sceneOffset; - int minZ = zz * CHUNK_SIZE - ctx.sceneContext.sceneOffset; - if (ctx.sceneContext.currentArea != null) { - var base = ctx.sceneContext.sceneBase; - assert base != null; - boolean inArea = ctx.sceneContext.currentArea.intersects( - true, base[0] + minX, base[1] + minZ, base[0] + minX + 7, base[1] + minZ + 7); - if (!inArea) { - if (plugin.enableDetailedTimers) frameTimer.end(Timer.VISIBILITY_CHECK); + try(var ignored = frameTimer.begin(Timer.VISIBILITY_CHECK)) { + if (!sceneManager.isTopLevelValid()) return false; + + WorldViewContext ctx = sceneManager.getRoot(); + int minX = zx * CHUNK_SIZE - ctx.sceneContext.sceneOffset; + int minZ = zz * CHUNK_SIZE - ctx.sceneContext.sceneOffset; + if (ctx.sceneContext.currentArea != null) { + var base = ctx.sceneContext.sceneBase; + assert base != null; + boolean inArea = ctx.sceneContext.currentArea.intersects( + true, base[0] + minX, base[1] + minZ, base[0] + minX + 7, base[1] + minZ + 7); + if (!inArea) { + return false; + } } - } - Zone zone = ctx.zones[zx][zz]; - if (plugin.freezeCulling) - return zone.inSceneFrustum || zone.inShadowFrustum; - - minX *= LOCAL_TILE_SIZE; - minZ *= LOCAL_TILE_SIZE; - int maxX = minX + CHUNK_SIZE * LOCAL_TILE_SIZE; - int maxZ = minZ + CHUNK_SIZE * LOCAL_TILE_SIZE; - if (zone.hasWater) { - maxY += ProceduralGenerator.MAX_DEPTH; - minY -= ProceduralGenerator.MAX_DEPTH; - } + Zone zone = ctx.zones[zx][zz]; + if (plugin.freezeCulling) + return zone.inSceneFrustum || zone.inShadowFrustum; - final int PADDING = 4 * LOCAL_TILE_SIZE; - zone.inSceneFrustum = sceneCamera.intersectsAABB( - minX - PADDING, minY, minZ - PADDING, maxX + PADDING, maxY, maxZ + PADDING); + if (zone.isFullyOccluded) + return zone.inSceneFrustum = zone.inShadowFrustum = false; - if (zone.inSceneFrustum) { - if (plugin.enableDetailedTimers) - frameTimer.end(Timer.VISIBILITY_CHECK); - return zone.inShadowFrustum = true; - } + minX *= LOCAL_TILE_SIZE; + minZ *= LOCAL_TILE_SIZE; + int maxX = minX + CHUNK_SIZE * LOCAL_TILE_SIZE; + int maxZ = minZ + CHUNK_SIZE * LOCAL_TILE_SIZE; + if (zone.hasWater) { + maxY += ProceduralGenerator.MAX_DEPTH; + minY -= ProceduralGenerator.MAX_DEPTH; + } + + final int PADDING = 4 * LOCAL_TILE_SIZE; + zone.inSceneFrustum = sceneCamera.intersectsAABB( + minX - PADDING, minY, minZ - PADDING, maxX + PADDING, maxY, maxZ + PADDING); - if (plugin.configShadowsEnabled && plugin.configExpandShadowDraw) { - zone.inShadowFrustum = directionalCamera.intersectsAABB(minX, minY, minZ, maxX, maxY, maxZ); - if (zone.inShadowFrustum) { - int centerX = minX + (maxX - minX) / 2; - int centerY = minY + (maxY - minY) / 2; - int centerZ = minZ + (maxZ - minZ) / 2; - zone.inShadowFrustum = directionalShadowCasterVolume.intersectsPoint(centerX, centerY, centerZ); + if (zone.inSceneFrustum) + return zone.inShadowFrustum = true; + + if (plugin.configShadowsEnabled && plugin.configExpandShadowDraw) { + zone.inShadowFrustum = directionalCamera.intersectsAABB(minX, minY, minZ, maxX, maxY, maxZ); + if (zone.inShadowFrustum) { + int centerX = minX + (maxX - minX) / 2; + int centerY = minY + (maxY - minY) / 2; + int centerZ = minZ + (maxZ - minZ) / 2; + zone.inShadowFrustum = directionalShadowCasterVolume.intersectsPoint(centerX, centerY, centerZ); + } + return zone.inShadowFrustum; } - if (plugin.enableDetailedTimers) - frameTimer.end(Timer.VISIBILITY_CHECK); - return zone.inShadowFrustum; - } - if (plugin.enableDetailedTimers) - frameTimer.end(Timer.VISIBILITY_CHECK); - if (plugin.orthographicProjection) - return zone.inSceneFrustum = true; + if (plugin.orthographicProjection) + return zone.inSceneFrustum = true; - return false; + return false; + } } @Override @@ -837,12 +937,18 @@ public void drawZoneOpaque(Projection entityProjection, Scene scene, int zx, int return; Zone z = ctx.zones[zx][zz]; - if (!z.initialized || z.sizeO == 0) + if(!z.initialized || z.sizeO == 0 ) return; frameTimer.begin(Timer.DRAW_ZONE_OPAQUE); - if (!sceneManager.isRoot(ctx) || z.inSceneFrustum) - z.renderOpaque(sceneCmd, ctx, false); + if (!sceneManager.isRoot(ctx) || z.inSceneFrustum) { + z.renderOpaque(tempCmd, ctx, false); + + depthOpaqueCmd.append(tempCmd); + sceneCmd.append(tempCmd); + + tempCmd.reset(); + } final boolean isSquashed = ctx.uboWorldViewStruct != null && ctx.uboWorldViewStruct.isSquashed(); if (!isSquashed && (!sceneManager.isRoot(ctx) || z.inShadowFrustum)) { @@ -861,13 +967,21 @@ public void drawZoneAlpha(Projection entityProjection, Scene scene, int level, i return; final Zone z = ctx.zones[zx][zz]; - if (!z.initialized) + if (!z.initialized || (z.levelOcclusionQueries[level] != null && z.levelOcclusionQueries[level].isOccluded())) return; frameTimer.begin(Timer.DRAW_ZONE_ALPHA); final boolean renderWater = z.inSceneFrustum && level == 0 && z.hasWater; - if (renderWater) - z.renderOpaqueLevel(sceneCmd, Zone.LEVEL_WATER_SURFACE); + if (renderWater) { + z.renderOpaqueLevel(tempCmd, Zone.LEVEL_WATER_SURFACE); + + depthAlphaCmd.append(tempCmd); + sceneCmd.DepthMask(false); + sceneCmd.append(tempCmd); + sceneCmd.DepthMask(true); + + tempCmd.reset(); + } modelStreamingManager.ensureAsyncUploadsComplete(z); @@ -881,11 +995,15 @@ 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, plugin.configRoofShadows); + z.renderAlpha(directionalCmd, zx - offset, zz - offset, level, ctx, false, plugin.configRoofShadows); } - 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); + z.renderAlpha(depthAlphaCmd, zx - offset, zz - offset, level, ctx, false, false); + sceneCmd.DepthMask(true); + } } frameTimer.end(Timer.DRAW_ZONE_ALPHA); @@ -904,6 +1022,9 @@ public void drawPass(Projection projection, Scene scene, int pass) { case DrawCallbacks.PASS_OPAQUE: directionalCmd.SetShader(fastShadowProgram); + // Switching to Alpha, so now it should only draw whilst greater than the depth value + sceneCmd.DepthFunc(GL_GREATER); + sceneCmd.ExecuteSubCommandBuffer(ctx.vaoSceneCmd); directionalCmd.ExecuteSubCommandBuffer(ctx.vaoDirectionalCmd); @@ -919,13 +1040,20 @@ public void drawPass(Projection projection, Scene scene, int pass) { if (sceneManager.isRoot(ctx)) frameTimer.end(Timer.UNMAP_ROOT_CTX); + // Equal for Dynamic Draw Opaque + ctx.vaoSceneCmd.DepthFunc(GL_EQUAL); + // Draw opaque + ctx.drawAll(VAO_OPAQUE, depthOpaqueCmd); ctx.drawAll(VAO_OPAQUE, ctx.vaoSceneCmd); ctx.drawAll(VAO_OPAQUE, ctx.vaoDirectionalCmd); // Draw shadow-only models ctx.drawAll(VAO_SHADOW, ctx.vaoDirectionalCmd); + // Greater for Players since they dont depth write during the pre-pass + ctx.vaoSceneCmd.DepthFunc(GL_GREATER); + final int offset = ctx.sceneContext.sceneOffset >> 3; for (int zx = 0; zx < ctx.sizeX; ++zx) { for (int zz = 0; zz < ctx.sizeZ; ++zz) { @@ -934,23 +1062,26 @@ public void drawPass(Projection projection, Scene scene, int pass) { if (!z.playerModels.isEmpty() && (!sceneManager.isRoot(ctx) || z.inSceneFrustum || z.inShadowFrustum)) { z.playerSort(zx - offset, zz - offset, sceneCamera); - z.renderPlayers(playerCmd, zx - offset, zz - offset); + z.renderPlayers(tempCmd, zx - offset, zz - offset); - if (!playerCmd.isEmpty()) { + if (!tempCmd.isEmpty()) { // Draw players shadow, with depth writes & alpha - ctx.vaoDirectionalCmd.append(playerCmd); + ctx.vaoDirectionalCmd.append(tempCmd); + + // Treat players as alpha depth + depthAlphaCmd.append(tempCmd); ctx.vaoSceneCmd.DepthMask(false); - ctx.vaoSceneCmd.append(playerCmd); + ctx.vaoSceneCmd.append(tempCmd); ctx.vaoSceneCmd.DepthMask(true); // Draw players opaque, writing only depth ctx.vaoSceneCmd.ColorMask(false, false, false, false); - ctx.vaoSceneCmd.append(playerCmd); + ctx.vaoSceneCmd.append(tempCmd); ctx.vaoSceneCmd.ColorMask(true, true, true, true); } - playerCmd.reset(); + tempCmd.reset(); } } } @@ -1020,9 +1151,12 @@ public void draw(int overlayColor) { frameTimer.begin(Timer.DRAW_SUBMIT); if (shouldRenderScene) { + clearScene(); + depthPrePass(); tiledLightingPass(); directionalShadowPass(); scenePass(); + occlusionManager.occlusionDebugPass(); } if (sceneFboValid && plugin.sceneResolution != null && plugin.sceneViewport != null) { @@ -1063,7 +1197,6 @@ public void draw(int overlayColor) { jobSystem.processPendingClientCallbacks(); - frameTimer.end(Timer.DRAW_FRAME); frameTimer.end(Timer.RENDER_FRAME); try { @@ -1077,11 +1210,12 @@ public void draw(int overlayColor) { // this might be AWT shutting down on VM shutdown, ignore it return; } - log.error("Unable to swap buffers:", ex); } + occlusionManager.occlusionPass(); glBindFramebuffer(GL_FRAMEBUFFER, plugin.awtContext.getFramebuffer(false)); + frameTimer.end(Timer.DRAW_FRAME); frameTimer.endFrameAndReset(); checkGLErrors(); diff --git a/src/main/java/rs117/hd/utils/Camera.java b/src/main/java/rs117/hd/utils/Camera.java index fc63b0d288..2f7dd49fba 100644 --- a/src/main/java/rs117/hd/utils/Camera.java +++ b/src/main/java/rs117/hd/utils/Camera.java @@ -203,8 +203,12 @@ public Camera setPosition(float[] newPosition) { return this; } + public float[] getPosition(float[] out) { + return copyTo(out, position); + } + public float[] getPosition() { - return copy(position); + return getPosition(new float[3]); } public float distanceTo(float[] point) { diff --git a/src/main/java/rs117/hd/utils/CommandBuffer.java b/src/main/java/rs117/hd/utils/CommandBuffer.java index 410711c79b..fa24651dfd 100644 --- a/src/main/java/rs117/hd/utils/CommandBuffer.java +++ b/src/main/java/rs117/hd/utils/CommandBuffer.java @@ -6,6 +6,7 @@ import java.util.stream.Collectors; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.lwjgl.opengl.*; import org.lwjgl.system.MemoryStack; import rs117.hd.opengl.GLFence; import rs117.hd.opengl.shader.ShaderProgram; @@ -21,8 +22,6 @@ @Slf4j public class CommandBuffer { - public static boolean SKIP_DEPTH_MASKING; - private static final int GL_MULTI_DRAW_ARRAYS_TYPE = 0; private static final int GL_MULTI_DRAW_ARRAYS_INDIRECT_TYPE = 1; private static final int GL_DRAW_ARRAYS_TYPE = 2; @@ -36,13 +35,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_BEGIN_QUERY_TYPE = 13; + private static final int GL_END_QUERY_TYPE = 14; + private static final int GL_CONDITIONAL_RENDERING_BEGIN_TYPE = 15; + private static final int GL_CONDITIONAL_RENDERING_END_TYPE = 16; + private static final int GL_USE_PROGRAM = 17; - private static final int GL_TOGGLE_TYPE = 13; // Combined glEnable & glDisable - private static final int GL_FENCE_SYNC = 14; + private static final int GL_TOGGLE_TYPE = 18; // Combined glEnable & glDisable + private static final int GL_FENCE_SYNC = 19; - private static final int GL_EXECUTE_SUB_COMMAND_BUFFER = 15; + private static final int GL_EXECUTE_SUB_COMMAND_BUFFER = 20; private static final long INT_MASK = 0xFFFF_FFFFL; private static final int DRAW_MODE_MASK = 0xF; @@ -134,6 +138,11 @@ public void DepthMask(boolean writeDepth) { cmd[writeHead++] = GL_DEPTH_MASK_TYPE & 0xFF | (writeDepth ? 1 : 0) << 8; } + public void DepthFunc(int depth) { + ensureCapacity(1); + cmd[writeHead++] = GL_DEPTH_FUNC_TYPE & 0xFF | (long) depth << 8; + } + public void ColorMask(boolean writeRed, boolean writeGreen, boolean writeBlue, boolean writeAlpha) { ensureCapacity(1); cmd[writeHead++] = @@ -277,6 +286,26 @@ public void Toggle(int capability, boolean enabled) { cmd[writeHead++] = (enabled ? 1L : 0) << 32 | capability & INT_MASK; } + public void BeginQuery(int mode, int query) { + ensureCapacity(1); + cmd[writeHead++] = GL_BEGIN_QUERY_TYPE & 0xFF | (long) mode << 8 | (long) query << 32; + } + + public void EndQuery(int mode) { + ensureCapacity(1); + cmd[writeHead++] = GL_END_QUERY_TYPE & 0xFF | (long) mode << 8; + } + + public void BeginConditionalRender(int query, int mode) { + ensureCapacity(1); + cmd[writeHead++] = GL_CONDITIONAL_RENDERING_BEGIN_TYPE & 0xFF | (long) mode << 8 | (long) query << 32; + } + + public void EndConditionalRender() { + ensureCapacity(1); + cmd[writeHead++] = GL_CONDITIONAL_RENDERING_END_TYPE & 0xFF; + } + public void append(CommandBuffer other) { if (other.isEmpty()) return; @@ -301,10 +330,11 @@ public void execute() { switch (type) { case GL_DEPTH_MASK_TYPE: { - int state = (int) (data >> 8) & 1; - if (SKIP_DEPTH_MASKING) - continue; - renderState.depthMask.set(state == 1); + renderState.depthMask.set(((int) (data >> 8) & 1) == 1); + break; + } + case GL_DEPTH_FUNC_TYPE: { + renderState.depthFunc.set((int) (data >> 8)); break; } case GL_COLOR_MASK_TYPE: { @@ -315,6 +345,26 @@ public void execute() { renderState.colorMask.set(red, green, blue, alpha); break; } + case GL_BEGIN_QUERY_TYPE: { + int mode = (int) data >> 8; + int query = (int) (data >> 32); + glBeginQuery(mode, query); + break; + } + case GL_END_QUERY_TYPE: { + glEndQuery((int) data >> 8); + break; + } + case GL_CONDITIONAL_RENDERING_BEGIN_TYPE: { + int mode = (int) data >> 8; + int query = (int) (data >> 32); + glBeginConditionalRender(query, mode); + break; + } + case GL_CONDITIONAL_RENDERING_END_TYPE: { + glEndConditionalRender(); + break; + } case GL_BIND_VERTEX_ARRAY_TYPE: { renderState.vao.set((int) (data >> 8)); break; diff --git a/src/main/java/rs117/hd/utils/DeveloperTools.java b/src/main/java/rs117/hd/utils/DeveloperTools.java index 36d42b6171..287e6b52c9 100644 --- a/src/main/java/rs117/hd/utils/DeveloperTools.java +++ b/src/main/java/rs117/hd/utils/DeveloperTools.java @@ -17,6 +17,7 @@ import rs117.hd.overlays.ShadowMapOverlay; import rs117.hd.overlays.TileInfoOverlay; import rs117.hd.overlays.TiledLightingOverlay; +import rs117.hd.renderer.zone.OcclusionManager; import rs117.hd.scene.AreaManager; import rs117.hd.scene.areas.AABB; import rs117.hd.scene.areas.Area; @@ -37,6 +38,8 @@ public class DeveloperTools implements KeyListener { private static final Keybind KEY_TOGGLE_ORTHOGRAPHIC = new Keybind(KeyEvent.VK_TAB, SHIFT_DOWN_MASK); private static final Keybind KEY_TOGGLE_HIDE_UI = new Keybind(KeyEvent.VK_H, CTRL_DOWN_MASK); private static final Keybind KEY_RELOAD_SCENE = new Keybind(KeyEvent.VK_R, CTRL_DOWN_MASK); + private static final Keybind KEY_TOGGLE_OCCLUSION = new Keybind(KeyEvent.VK_O, CTRL_DOWN_MASK); + private static final Keybind KEY_TOGGLE_OCCLUSION_VISIBILITY = new Keybind(KeyEvent.VK_O, CTRL_DOWN_MASK | SHIFT_DOWN_MASK); @Inject private ClientThread clientThread; @@ -50,6 +53,9 @@ public class DeveloperTools implements KeyListener { @Inject private HdPlugin plugin; + @Inject + private OcclusionManager occlusionManager; + @Inject private TileInfoOverlay tileInfoOverlay; @@ -169,6 +175,9 @@ public void onCommandExecuted(CommandExecuted commandExecuted) { case "culling": plugin.freezeCulling = !plugin.freezeCulling; break; + case "occlusion": + occlusionManager.toggleDebug(); + break; } } @@ -194,6 +203,10 @@ public void keyPressed(KeyEvent e) { hideUiEnabled = !hideUiEnabled; } else if (KEY_RELOAD_SCENE.matches(e)) { plugin.renderer.reloadScene(); + } else if(KEY_TOGGLE_OCCLUSION.matches(e)) { + occlusionManager.toggleDebug(); + } else if(KEY_TOGGLE_OCCLUSION_VISIBILITY.matches(e)) { + occlusionManager.toggleDebugVisibility(); } else { return; } diff --git a/src/main/java/rs117/hd/utils/HDUtils.java b/src/main/java/rs117/hd/utils/HDUtils.java index 0d9b1da7fe..db7f515cde 100644 --- a/src/main/java/rs117/hd/utils/HDUtils.java +++ b/src/main/java/rs117/hd/utils/HDUtils.java @@ -420,27 +420,27 @@ public static boolean isTriangleIntersectingFrustum( } public static boolean isAABBIntersectingFrustum( - int minX, - int minY, - int minZ, - int maxX, - int maxY, - int maxZ, + float minX, + float minY, + float minZ, + float maxX, + float maxY, + float maxZ, float[][] cullingPlanes ) { - for (float[] plane : cullingPlanes) { - if ( - plane[0] * minX + plane[1] * minY + plane[2] * minZ + plane[3] < 0 && - plane[0] * maxX + plane[1] * minY + plane[2] * minZ + plane[3] < 0 && - plane[0] * minX + plane[1] * maxY + plane[2] * minZ + plane[3] < 0 && - plane[0] * maxX + plane[1] * maxY + plane[2] * minZ + plane[3] < 0 && - plane[0] * minX + plane[1] * minY + plane[2] * maxZ + plane[3] < 0 && - plane[0] * maxX + plane[1] * minY + plane[2] * maxZ + plane[3] < 0 && - plane[0] * minX + plane[1] * maxY + plane[2] * maxZ + plane[3] < 0 && - plane[0] * maxX + plane[1] * maxY + plane[2] * maxZ + plane[3] < 0 - ) { + for (int i = 0; i < cullingPlanes.length; i++ ) { + final float[] plane = cullingPlanes[i]; + final float px = plane[0]; + final float py = plane[1]; + final float pz = plane[2]; + final float pw = plane[3]; + + final float pVertexX = px >= 0 ? maxX : minX; + final float pVertexY = py >= 0 ? maxY : minY; + final float pVertexZ = pz >= 0 ? maxZ : minZ; + + if (px * pVertexX + py * pVertexY + pz * pVertexZ + pw < 0) return false; - } } // Potentially visible @@ -508,4 +508,22 @@ public static JFrame getJFrame(Canvas canvas) { return null; } + + public static long align(long value, long alignment) { + return align(value, alignment, false); + } + + public static long align(long value, long alignment, boolean up) { + assert alignment > 0 : "Alignment must be positive"; + assert (alignment & (alignment - 1)) == 0 + : "Alignment must be a power of two"; + + if (up) { + // Align up + return (value + alignment - 1) & -alignment; + } else { + // Align down + return value & -alignment; + } + } } diff --git a/src/main/java/rs117/hd/utils/buffer/GpuIntBuffer.java b/src/main/java/rs117/hd/utils/buffer/GpuIntBuffer.java index 74013548ce..e18868f283 100644 --- a/src/main/java/rs117/hd/utils/buffer/GpuIntBuffer.java +++ b/src/main/java/rs117/hd/utils/buffer/GpuIntBuffer.java @@ -127,7 +127,7 @@ public void putVertex( int x, int y, int z, float u, float v, float w, int nx, int ny, int nz, - int textureFaceIdx + int textureFaceIdx, int depth ) { buffer.put((y & 0xFFFF) << 16 | x & 0xFFFF); buffer.put(float16(u) << 16 | z & 0xFFFF); @@ -135,7 +135,7 @@ public void putVertex( // Unnormalized normals, assumed to be within short max buffer.put((ny & 0xFFFF) << 16 | nx & 0xFFFF); buffer.put(nz & 0xFFFF); - buffer.put(textureFaceIdx); + buffer.put((depth & 0xFF) << 24 | (textureFaceIdx & 0xFFFFFF)); } public static int putFace( diff --git a/src/main/resources/rs117/hd/depth_frag.glsl b/src/main/resources/rs117/hd/depth_frag.glsl new file mode 100644 index 0000000000..5d2b16d99a --- /dev/null +++ b/src/main/resources/rs117/hd/depth_frag.glsl @@ -0,0 +1,5 @@ +#version 330 core + +layout(location = 0) out vec4 fragColor; + +void main() { /* MAC OS STUB */ } \ No newline at end of file diff --git a/src/main/resources/rs117/hd/depth_vert.glsl b/src/main/resources/rs117/hd/depth_vert.glsl new file mode 100644 index 0000000000..dc4af05a9b --- /dev/null +++ b/src/main/resources/rs117/hd/depth_vert.glsl @@ -0,0 +1,26 @@ +#version 330 + +#include +#include + +#include + +layout (location = 0) in vec3 vPosition; +layout (location = 3) in int vTextureFaceIdx; +layout (location = 6) in int vWorldViewId; +layout (location = 7) in ivec2 vSceneBase; + + 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); + } + + vec4 clipPosition = projectionMatrix * vec4(worldPosition, 1.0); + int depthBias = (vTextureFaceIdx >> 24) & 0xff; + clipPosition.z += depthBias * (1.0 / 128.0); + + gl_Position = clipPosition; +} \ No newline at end of file diff --git a/src/main/resources/rs117/hd/occlusion_debug_frag.glsl b/src/main/resources/rs117/hd/occlusion_debug_frag.glsl new file mode 100644 index 0000000000..0a9295d65d --- /dev/null +++ b/src/main/resources/rs117/hd/occlusion_debug_frag.glsl @@ -0,0 +1,12 @@ +#version 330 + +#include + +out vec4 fragColor; + +uniform int queryId; + +void main() { + float hue = float(queryId % 1024) / 1024.0; + fragColor = vec4(hsv2rgb(hue), 0.25); +} \ No newline at end of file diff --git a/src/main/resources/rs117/hd/occlusion_vert.glsl b/src/main/resources/rs117/hd/occlusion_vert.glsl new file mode 100644 index 0000000000..ff36ceee7a --- /dev/null +++ b/src/main/resources/rs117/hd/occlusion_vert.glsl @@ -0,0 +1,12 @@ +#version 330 + +#include + +layout (location = 0) in vec3 vVertex; +layout (location = 1) in vec3 vCenter; +layout (location = 2) in vec3 vScale; + + void main() { + vec3 worldPosition = vVertex * vScale + vCenter; + gl_Position = projectionMatrix * vec4(worldPosition, 1.0); +} \ No newline at end of file diff --git a/src/main/resources/rs117/hd/scene_frag.glsl b/src/main/resources/rs117/hd/scene_frag.glsl index bcc9ac06f0..e35833a1b2 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_DEPTH 0 #include #include @@ -43,6 +44,9 @@ uniform sampler2DArray textureArray; uniform sampler2D shadowMap; uniform usampler2DArray tiledLightingArray; +uniform sampler2D sceneOpaqueDepth; +uniform sampler2D sceneAlphaDepth; + // general HD settings flat in int fWorldViewId; @@ -116,6 +120,12 @@ void main() { vec4 outputColor = vec4(1); + #if DISPLAY_DEPTH + float depth = texelFetch(sceneOpaqueDepth, ivec2(gl_FragCoord.xy), 0).r; + FragColor = vec4(depth, 0.0, 0.0, 1.0); + if (DISPLAY_DEPTH == 1) return; // Redundant, for syntax highlighting in IntelliJ + #endif + if (isWater) { outputColor = sampleWater(waterTypeIndex, viewDir); } else { diff --git a/src/main/resources/rs117/hd/scene_vert.glsl b/src/main/resources/rs117/hd/scene_vert.glsl index 616639e6ca..7615331b9a 100644 --- a/src/main/resources/rs117/hd/scene_vert.glsl +++ b/src/main/resources/rs117/hd/scene_vert.glsl @@ -68,15 +68,14 @@ layout (location = 2) in vec3 vNormal; int vertex = gl_VertexID % 3; bool isProvoking = vertex == 2; int materialData = 0; - int alphaBiasHsl = 0; + int textureFaceIdx = vTextureFaceIdx & 0xFFFFFF; if (isProvoking) { // Only the Provoking vertex needs to fetch the face data - fAlphaBiasHsl = texelFetch(textureFaces, vTextureFaceIdx).xyz; - fMaterialData = texelFetch(textureFaces, vTextureFaceIdx + 1).xyz; - fTerrainData = texelFetch(textureFaces, vTextureFaceIdx + 2).xyz; + fAlphaBiasHsl = texelFetch(textureFaces, textureFaceIdx).xyz; + fMaterialData = texelFetch(textureFaces, textureFaceIdx + 1).xyz; + fTerrainData = texelFetch(textureFaces, textureFaceIdx + 2).xyz; fWorldViewId = vWorldViewId; - alphaBiasHsl = fAlphaBiasHsl[vertex]; materialData = fMaterialData[vertex]; } else { // All outputs must be written to for macOS compatibility @@ -84,8 +83,7 @@ layout (location = 2) in vec3 vNormal; fMaterialData = ivec3(0); fTerrainData = ivec3(0); fWorldViewId = 0; - alphaBiasHsl = texelFetch(textureFaces, vTextureFaceIdx)[vertex]; - materialData = texelFetch(textureFaces, vTextureFaceIdx + 1)[vertex]; + materialData = texelFetch(textureFaces, textureFaceIdx + 1)[vertex]; } vec3 sceneOffset = vec3(vSceneBase.x, 0, vSceneBase.y); @@ -108,7 +106,7 @@ layout (location = 2) in vec3 vNormal; #endif vec4 clipPosition = projectionMatrix * vec4(worldPosition, 1.0); - int depthBias = (alphaBiasHsl >> 16) & 0xff; + int depthBias = (vTextureFaceIdx >> 24) & 0xff; clipPosition.z += depthBias / 128.0; gl_Position = clipPosition; diff --git a/src/main/resources/rs117/hd/shadow_vert.glsl b/src/main/resources/rs117/hd/shadow_vert.glsl index f613a715fe..305f11ade0 100644 --- a/src/main/resources/rs117/hd/shadow_vert.glsl +++ b/src/main/resources/rs117/hd/shadow_vert.glsl @@ -52,9 +52,10 @@ layout (location = 1) in vec3 vUv; void main() { int vertex = gl_VertexID % 3; - int alphaBiasHsl = texelFetch(textureFaces, vTextureFaceIdx)[vertex]; - int materialData = texelFetch(textureFaces, vTextureFaceIdx + 1)[vertex]; - int terrainData = texelFetch(textureFaces, vTextureFaceIdx + 2)[vertex]; + int textureFaceIdx = vTextureFaceIdx & 0xFFFFFF; + int alphaBiasHsl = texelFetch(textureFaces, textureFaceIdx)[vertex]; + int materialData = texelFetch(textureFaces, textureFaceIdx + 1)[vertex]; + int terrainData = texelFetch(textureFaces, textureFaceIdx + 2)[vertex]; int waterTypeIndex = terrainData >> 3 & 0xFF; float opacity = 1 - (alphaBiasHsl >> 24 & 0xFF) / float(0xFF); diff --git a/src/main/resources/rs117/hd/tiled_lighting_frag.glsl b/src/main/resources/rs117/hd/tiled_lighting_frag.glsl index d0b26be2ba..a13f77f17d 100644 --- a/src/main/resources/rs117/hd/tiled_lighting_frag.glsl +++ b/src/main/resources/rs117/hd/tiled_lighting_frag.glsl @@ -2,6 +2,7 @@ #include TILED_LIGHTING_LAYER #include TILED_IMAGE_STORE +#define TILE_MIN_MAX 1 #if TILED_IMAGE_STORE #extension GL_EXT_shader_image_load_store : enable @@ -12,11 +13,15 @@ out uvec4 TiledData; #endif +#include +#include + +uniform sampler2D sceneOpaqueDepth; +uniform sampler2D sceneAlphaDepth; + #include #include -#include - in vec2 fUv; #if TILED_IMAGE_STORE @@ -60,12 +65,56 @@ uint packLightIndices(in SortedLight bin[SORTING_BIN_SIZE], in int binSize, inou return (idx0 <= 32767) ? uint(idx0 & 0x7FFF) : 0u; } +#define SAMPLE_DEPTH(FRAC_X, FRAC_Y) \ + pix = ivec2(int(mix(float(minPix.x), float(maxPix.x), FRAC_X)), int(mix(float(minPix.y), float(maxPix.y), FRAC_Y))); \ + depth = texelFetch(sceneOpaqueDepth, pix, 0).r; \ + if (depth > 0.0) { \ + minDepth = min(minDepth, depth); \ + maxDepth = max(maxDepth, depth); \ + } \ + depth = texelFetch(sceneAlphaDepth, pix, 0).r; \ + if(depth > 0.0) \ + maxDepth = max(maxDepth, depth); \ + + +bool calculateTileMinMax(vec2 bl, vec2 tr, out float tileMin, out float tileMax) { +#if TILE_MIN_MAX + ivec2 minPix = clamp(ivec2(floor(bl)), ivec2(0), ivec2(sceneResolution) - 1); + ivec2 maxPix = clamp(ivec2(ceil(tr)), ivec2(0), ivec2(sceneResolution)); + + // Instead of sampling 16x16x2, it's accurate enough just to sample the corners and mid points + float minDepth = 1e20; + float maxDepth = -1e20; + + ivec2 pix; + float depth; + SAMPLE_DEPTH(0.0, 0.0) + SAMPLE_DEPTH(0.5, 0.0) + SAMPLE_DEPTH(1.0, 0.0) + + SAMPLE_DEPTH(0.0, 0.5) + SAMPLE_DEPTH(0.5, 0.5) + SAMPLE_DEPTH(1.0, 0.5) + + SAMPLE_DEPTH(0.0, 1.0) + SAMPLE_DEPTH(0.5, 1.0) + SAMPLE_DEPTH(1.0, 1.0) + + if(minDepth > maxDepth) + return false; + + tileMin = depth01ToViewZ(minDepth, projectionMatrix); + tileMax = depth01ToViewZ(maxDepth, projectionMatrix); +#endif + return true; +} + void main() { ivec2 pixelCoord = ivec2(fUv * tiledLightingResolution); #if USE_LIGHTS_MASK int LightMaskSize = int(ceil(pointLightsCount / 32.0)); - uint LightsMask[32]; // 32 Words = 1024 Lights + uint LightsMask[(TILED_LIGHTING_LAYER + 1) * 4]; // 32 Words = 1024 Lights for (int i = 0; i < LightMaskSize; i++) LightsMask[i] = 0u; @@ -101,6 +150,20 @@ void main() { vec2 bl = tileOrigin; // bottom-left vec2 br = tileOrigin + vec2(tileSize.x, 0.0); // bottom-right +#if TILE_MIN_MAX + float tileMin = 0.0f; + float tileMax = 1.0f; + if(!calculateTileMinMax(bl, tr, tileMin, tileMax)) { + #if TILED_IMAGE_STORE + for (int layer = 0; layer < TILED_LIGHTING_LAYER_COUNT; layer++) + imageStore(tiledLightingImage, ivec3(pixelCoord, layer), uvec4(0.0)); + #else + TiledData = uvec4(0.0); + #endif + return; + } +#endif + vec2 ndcTL = (tl / sceneResolution) * 2.0 - 1.0; vec2 ndcTR = (tr / sceneResolution) * 2.0 - 1.0; vec2 ndcBL = (bl / sceneResolution) * 2.0 - 1.0; @@ -130,16 +193,15 @@ void main() { vec3 lightViewPos = lightData.xyz; float lightRadiusSqr = lightData.w; - float lightDistSqr = dot(lightViewPos, lightViewPos); - - vec3 lightCenterVec = (lightDistSqr > 0.0) ? lightViewPos / sqrt(lightDistSqr) : vec3(0.0); - - float lightSinSqr = clamp(lightRadiusSqr / max(lightDistSqr, 1e-6), 0.0, 1.0); - float lightCos = sqrt(0.999 - lightSinSqr); - float lightTileCos = dot(lightCenterVec, tileCenterVec); + #if TILE_MIN_MAX + float dz = max(lightViewPos.z - tileMin, tileMax - lightViewPos.z); + if (dz > 0.0 && dz * dz > lightRadiusSqr) + continue; + #endif - float sumCos = (lightRadiusSqr > lightDistSqr) ? -1.0 : (tileCos * lightCos - tileSin * sqrt(lightSinSqr)); - if (lightTileCos < sumCos) + float lightTileDot = dot(lightViewPos, tileCenterVec); + float lightDistSqr = dot(lightViewPos, lightViewPos); + if (lightTileDot * lightTileDot < lightDistSqr * tileCos * tileCos - lightRadiusSqr) continue; #if USE_LIGHTS_MASK @@ -150,8 +212,9 @@ void main() { #endif const float PROXIMITY_WEIGHT = 0.75; - float distanceScore = clamp(1.0 - sqrt(lightDistSqr) / (sqrt(lightRadiusSqr) + 1e-6), 0.0, 1.0); - float combinedScore = (lightTileCos * PROXIMITY_WEIGHT) + distanceScore * (1.0 - PROXIMITY_WEIGHT); + float distanceScore = clamp(1.0 - lightDistSqr / (lightRadiusSqr + 1e-6), 0.0, 1.0); + float angularScore = lightTileDot * lightTileDot / (lightDistSqr + 1e-6); + float combinedScore = (angularScore * PROXIMITY_WEIGHT) + distanceScore * (1.0 - PROXIMITY_WEIGHT); int idx = 0; for (; idx < sortingBinSize; idx++) { diff --git a/src/main/resources/rs117/hd/uniforms/global.glsl b/src/main/resources/rs117/hd/uniforms/global.glsl index a50e951d99..280a527edc 100644 --- a/src/main/resources/rs117/hd/uniforms/global.glsl +++ b/src/main/resources/rs117/hd/uniforms/global.glsl @@ -45,6 +45,8 @@ layout(std140) uniform UBOGlobal { int pointLightsCount; vec3 cameraPos; + float cameraNear; + float cameraFar; mat4 viewMatrix; mat4 projectionMatrix; mat4 invProjectionMatrix; diff --git a/src/main/resources/rs117/hd/utils/color_utils.glsl b/src/main/resources/rs117/hd/utils/color_utils.glsl index 163f6c2d24..5a434c6a82 100644 --- a/src/main/resources/rs117/hd/utils/color_utils.glsl +++ b/src/main/resources/rs117/hd/utils/color_utils.glsl @@ -110,6 +110,26 @@ float linearToSrgb(float rgb) { step(0.0031308, rgb)); } +vec3 hsv2rgb(float h) { + float s = 1.0; + float v = 1.0; + + float i = floor(h * 6.0); + float f = h * 6.0 - i; + float p = v * (1.0 - s); + float q = v * (1.0 - f * s); + float t = v * (1.0 - (1.0 - f) * s); + + int modI = int(mod(i, 6.0)); + + if (modI == 0) return vec3(v, t, p); + if (modI == 1) return vec3(q, v, p); + if (modI == 2) return vec3(p, v, t); + if (modI == 3) return vec3(p, q, v); + if (modI == 4) return vec3(t, p, v); + return vec3(v, p, q); +} + // https://web.archive.org/web/20230619214343/https://en.wikipedia.org/wiki/HSL_and_HSV#Color_conversion_formulae vec3 srgbToHsl(vec3 srgb) { float V = max(max(srgb.r, srgb.g), srgb.b); diff --git a/src/main/resources/rs117/hd/utils/constants.glsl b/src/main/resources/rs117/hd/utils/constants.glsl index cc3bae3ca4..761a147a62 100644 --- a/src/main/resources/rs117/hd/utils/constants.glsl +++ b/src/main/resources/rs117/hd/utils/constants.glsl @@ -54,6 +54,7 @@ #define SHADOW_DEFAULT_OPACITY_THRESHOLD 0.71 // Lowest while keeping Prifddinas glass walkways transparent #endif +#include MSAA_SAMPLES #include VANILLA_COLOR_BANDING #include UNDO_VANILLA_SHADING #include LEGACY_GREY_COLORS diff --git a/src/main/resources/rs117/hd/utils/misc.glsl b/src/main/resources/rs117/hd/utils/misc.glsl index 6e5406693b..930f9cc045 100644 --- a/src/main/resources/rs117/hd/utils/misc.glsl +++ b/src/main/resources/rs117/hd/utils/misc.glsl @@ -210,3 +210,18 @@ vec2 getPoissonDisk(int idx) { default: return vec2( 0.14383161, -0.14100790); } } + +float depth01ToViewZ(float depth01, mat4 proj) { + // depth buffer [0,1] -> NDC z [-1,1] + float zNdc = depth01 * 2.0 - 1.0; + + // Invert the perspective projection mapping for z: + // z_view = P[3][2] / (z_ndc - P[2][2]) + float denom = zNdc - proj[2][2]; + + // Guard against division by ~0 (can show up with infinite-far / extreme values) + if (abs(denom) < 1e-20) + denom = (denom < 0.0) ? -1e-20 : 1e-20; + + return proj[3][2] / denom; +}