diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/IndirectPrograms.java b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/IndirectPrograms.java index 39d211ef9..9d3ce83e0 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/IndirectPrograms.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/IndirectPrograms.java @@ -31,7 +31,9 @@ public class IndirectPrograms extends AtomicReferenceCounted { private static final ResourceLocation SCATTER_SHADER_MAIN = Flywheel.rl("internal/indirect/scatter.glsl"); private static final ResourceLocation DOWNSAMPLE_FIRST = Flywheel.rl("internal/indirect/downsample_first.glsl"); private static final ResourceLocation DOWNSAMPLE_SECOND = Flywheel.rl("internal/indirect/downsample_second.glsl"); - public static final List UTIL_SHADERS = List.of(APPLY_SHADER_MAIN, SCATTER_SHADER_MAIN, DOWNSAMPLE_FIRST, DOWNSAMPLE_SECOND); + + private static final ResourceLocation FULLSCREEN = Flywheel.rl("internal/indirect/fullscreen.vert"); + private static final ResourceLocation OIT_COMPOSITE = Flywheel.rl("internal/indirect/oit_composite.frag"); private static final Compile> CULL = new Compile<>(); private static final Compile UTIL = new Compile<>(); @@ -45,11 +47,13 @@ public class IndirectPrograms extends AtomicReferenceCounted { private final PipelineCompiler pipeline; private final CompilationHarness> culling; private final CompilationHarness utils; + private final CompilationHarness fullscreen; - private IndirectPrograms(PipelineCompiler pipeline, CompilationHarness> culling, CompilationHarness utils) { + private IndirectPrograms(PipelineCompiler pipeline, CompilationHarness> culling, CompilationHarness utils, CompilationHarness fullscreen) { this.pipeline = pipeline; this.culling = culling; this.utils = utils; + this.fullscreen = fullscreen; } private static List getExtensions(GlslVersion glslVersion) { @@ -88,8 +92,9 @@ public class IndirectPrograms extends AtomicReferenceCounted { var pipelineCompiler = PipelineCompiler.create(sources, Pipelines.INDIRECT, vertexComponents, fragmentComponents, EXTENSIONS); var cullingCompiler = createCullingCompiler(sources); var utilCompiler = createUtilCompiler(sources); + var fullscreenCompiler = createFullscreenCompiler(sources); - IndirectPrograms newInstance = new IndirectPrograms(pipelineCompiler, cullingCompiler, utilCompiler); + IndirectPrograms newInstance = new IndirectPrograms(pipelineCompiler, cullingCompiler, utilCompiler, fullscreenCompiler); setInstance(newInstance); } @@ -125,6 +130,17 @@ public class IndirectPrograms extends AtomicReferenceCounted { .harness("utilities", sources); } + private static CompilationHarness createFullscreenCompiler(ShaderSources sources) { + return UTIL.program() + .link(UTIL.shader(GlCompat.MAX_GLSL_VERSION, ShaderType.VERTEX) + .nameMapper($ -> "fullscreen/fullscreen") + .withResource(FULLSCREEN)) + .link(UTIL.shader(GlCompat.MAX_GLSL_VERSION, ShaderType.FRAGMENT) + .nameMapper(rl -> "fullscreen/" + ResourceUtil.toDebugFileNameNoExtension(rl)) + .withResource(s -> s)) + .harness("fullscreen", sources); + } + static void setInstance(@Nullable IndirectPrograms newInstance) { if (instance != null) { instance.release(); @@ -148,8 +164,8 @@ public class IndirectPrograms extends AtomicReferenceCounted { setInstance(null); } - public GlProgram getIndirectProgram(InstanceType instanceType, ContextShader contextShader, Material material) { - return pipeline.get(instanceType, contextShader, material); + public GlProgram getIndirectProgram(InstanceType instanceType, ContextShader contextShader, Material material, boolean oit) { + return pipeline.get(instanceType, contextShader, material, oit); } public GlProgram getCullingProgram(InstanceType instanceType) { @@ -172,10 +188,15 @@ public class IndirectPrograms extends AtomicReferenceCounted { return utils.get(DOWNSAMPLE_SECOND); } + public GlProgram getOitCompositeProgram() { + return fullscreen.get(OIT_COMPOSITE); + } + @Override protected void _delete() { pipeline.delete(); culling.delete(); utils.delete(); + fullscreen.delete(); } } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/InstancingPrograms.java b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/InstancingPrograms.java index c1e736b1e..db3014402 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/InstancingPrograms.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/InstancingPrograms.java @@ -70,7 +70,7 @@ public class InstancingPrograms extends AtomicReferenceCounted { } public GlProgram get(InstanceType instanceType, ContextShader contextShader, Material material) { - return pipeline.get(instanceType, contextShader, material); + return pipeline.get(instanceType, contextShader, material, false); } @Override diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/PipelineCompiler.java b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/PipelineCompiler.java index e752edfd2..e315dce35 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/compile/PipelineCompiler.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/compile/PipelineCompiler.java @@ -50,7 +50,7 @@ public final class PipelineCompiler { ALL.add(this); } - public GlProgram get(InstanceType instanceType, ContextShader contextShader, Material material) { + public GlProgram get(InstanceType instanceType, ContextShader contextShader, Material material, boolean oit) { var light = material.light(); var cutout = material.cutout(); var shaders = material.shaders(); @@ -66,7 +66,7 @@ public final class PipelineCompiler { MaterialShaderIndices.cutoutSources() .index(cutout.source()); - return harness.get(new PipelineProgramKey(instanceType, contextShader, light, shaders, cutout != CutoutShaders.OFF, FrameUniforms.debugOn())); + return harness.get(new PipelineProgramKey(instanceType, contextShader, light, shaders, cutout != CutoutShaders.OFF, FrameUniforms.debugOn(), oit)); } public void delete() { @@ -128,7 +128,8 @@ public final class PipelineCompiler { .source()); var debug = key.debugEnabled() ? "_debug" : ""; var cutout = key.useCutout() ? "_cutout" : ""; - return "pipeline/" + pipeline.compilerMarker() + "/frag/" + material + "/" + light + "_" + context + cutout + debug; + var oit = key.oit() ? "_oit" : ""; + return "pipeline/" + pipeline.compilerMarker() + "/frag/" + material + "/" + light + "_" + context + cutout + debug + oit; }) .requireExtensions(extensions) .enableExtension("GL_ARB_conservative_depth") @@ -146,6 +147,11 @@ public final class PipelineCompiler { comp.define("_FLW_USE_DISCARD"); } }) + .onCompile((key, comp) -> { + if (key.oit()) { + comp.define("_FLW_OIT"); + } + }) .withResource(API_IMPL_FRAG) .withResource(key -> key.materialShaders() .fragmentSource()) @@ -217,6 +223,7 @@ public final class PipelineCompiler { * @param light The light shader to use. */ public record PipelineProgramKey(InstanceType instanceType, ContextShader contextShader, LightShader light, - MaterialShaders materialShaders, boolean useCutout, boolean debugEnabled) { + MaterialShaders materialShaders, boolean useCutout, boolean debugEnabled, + boolean oit) { } } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/MaterialRenderState.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/MaterialRenderState.java index 43206153f..ec98d5c09 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/MaterialRenderState.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/MaterialRenderState.java @@ -45,6 +45,17 @@ public final class MaterialRenderState { setupWriteMask(material.writeMask()); } + public static void setupOit(Material material) { + setupTexture(material); + setupBackfaceCulling(material.backfaceCulling()); + setupPolygonOffset(material.polygonOffset()); + setupDepthTest(material.depthTest()); + + WriteMask mask = material.writeMask(); + boolean writeColor = mask.color(); + RenderSystem.colorMask(writeColor, writeColor, writeColor, writeColor); + } + private static void setupTexture(Material material) { Samplers.DIFFUSE.makeActive(); AbstractTexture texture = Minecraft.getInstance() diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectCullingGroup.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectCullingGroup.java index faec3b63a..aa605f017 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectCullingGroup.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectCullingGroup.java @@ -13,6 +13,7 @@ import java.util.List; import dev.engine_room.flywheel.api.instance.Instance; import dev.engine_room.flywheel.api.instance.InstanceType; import dev.engine_room.flywheel.api.material.Material; +import dev.engine_room.flywheel.api.material.Transparency; import dev.engine_room.flywheel.api.model.Model; import dev.engine_room.flywheel.backend.compile.ContextShader; import dev.engine_room.flywheel.backend.compile.IndirectPrograms; @@ -36,6 +37,7 @@ public class IndirectCullingGroup { private final List> instancers = new ArrayList<>(); private final List indirectDraws = new ArrayList<>(); private final List multiDraws = new ArrayList<>(); + private final List transparentDraws = new ArrayList<>(); private final IndirectPrograms programs; private final GlProgram cullProgram; @@ -130,6 +132,7 @@ public class IndirectCullingGroup { private void sortDraws() { multiDraws.clear(); + transparentDraws.clear(); // sort by visual type, then material indirectDraws.sort(DRAW_COMPARATOR); @@ -138,7 +141,9 @@ public class IndirectCullingGroup { // if the next draw call has a different VisualType or Material, start a new MultiDraw if (i == indirectDraws.size() - 1 || incompatibleDraws(draw1, indirectDraws.get(i + 1))) { - multiDraws.add(new MultiDraw(draw1.material(), draw1.isEmbedded(), start, i + 1)); + var dst = draw1.material() + .transparency() == Transparency.TRANSLUCENT ? transparentDraws : multiDraws; + dst.add(new MultiDraw(draw1.material(), draw1.isEmbedded(), start, i + 1)); start = i + 1; } } @@ -171,7 +176,7 @@ public class IndirectCullingGroup { needsDrawSort = true; } - public void submit() { + public void submitSolid() { if (nothingToDo()) { return; } @@ -183,7 +188,7 @@ public class IndirectCullingGroup { GlProgram lastProgram = null; for (var multiDraw : multiDraws) { - var drawProgram = programs.getIndirectProgram(instanceType, multiDraw.embedded ? ContextShader.EMBEDDED : ContextShader.DEFAULT, multiDraw.material); + var drawProgram = programs.getIndirectProgram(instanceType, multiDraw.embedded ? ContextShader.EMBEDDED : ContextShader.DEFAULT, multiDraw.material, false); if (drawProgram != lastProgram) { lastProgram = drawProgram; @@ -197,8 +202,34 @@ public class IndirectCullingGroup { } } + public void submitTransparent() { + if (nothingToDo()) { + return; + } + + buffers.bindForDraw(); + + drawBarrier(); + + GlProgram lastProgram = null; + + for (var multiDraw : transparentDraws) { + var drawProgram = programs.getIndirectProgram(instanceType, multiDraw.embedded ? ContextShader.EMBEDDED : ContextShader.DEFAULT, multiDraw.material, true); + if (drawProgram != lastProgram) { + lastProgram = drawProgram; + + // Don't need to do this unless the program changes. + drawProgram.bind(); + } + + MaterialRenderState.setupOit(multiDraw.material); + + multiDraw.submit(drawProgram); + } + } + public void bindForCrumbling(Material material) { - var program = programs.getIndirectProgram(instanceType, ContextShader.CRUMBLING, material); + var program = programs.getIndirectProgram(instanceType, ContextShader.CRUMBLING, material, false); program.bind(); diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectDrawManager.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectDrawManager.java index 5cc280750..a61677865 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectDrawManager.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/IndirectDrawManager.java @@ -48,6 +48,8 @@ public class IndirectDrawManager extends DrawManager> { private final DepthPyramid depthPyramid; + private final WboitFrameBuffer wboitFrameBuffer; + public IndirectDrawManager(IndirectPrograms programs) { this.programs = programs; programs.acquire(); @@ -62,6 +64,8 @@ public class IndirectDrawManager extends DrawManager> { matrixBuffer = new MatrixBuffer(); depthPyramid = new DepthPyramid(programs); + + wboitFrameBuffer = new WboitFrameBuffer(programs); } @Override @@ -138,9 +142,17 @@ public class IndirectDrawManager extends DrawManager> { Uniforms.bindAll(); for (var group : cullingGroups.values()) { - group.submit(); + group.submitSolid(); } + wboitFrameBuffer.setup(); + + for (var group : cullingGroups.values()) { + group.submitTransparent(); + } + + wboitFrameBuffer.composite(); + MaterialRenderState.reset(); TextureBinder.resetLightAndOverlay(); } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/WboitFrameBuffer.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/WboitFrameBuffer.java new file mode 100644 index 000000000..c76133709 --- /dev/null +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/indirect/WboitFrameBuffer.java @@ -0,0 +1,117 @@ +package dev.engine_room.flywheel.backend.engine.indirect; + +import org.lwjgl.opengl.ARBDrawBuffersBlend; +import org.lwjgl.opengl.GL32; +import org.lwjgl.opengl.GL46; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; + +import dev.engine_room.flywheel.backend.compile.IndirectPrograms; +import dev.engine_room.flywheel.backend.gl.GlTextureUnit; +import net.minecraft.client.Minecraft; + +public class WboitFrameBuffer { + + public final int fbo; + private final IndirectPrograms programs; + private final int vao; + + public int accum; + public int reveal; + + private int lastWidth = -1; + private int lastHeight = -1; + + public WboitFrameBuffer(IndirectPrograms programs) { + this.programs = programs; + fbo = GL46.glCreateFramebuffers(); + vao = GL46.glCreateVertexArrays(); + } + + public void setup() { + var mainRenderTarget = Minecraft.getInstance() + .getMainRenderTarget(); + + createTextures(mainRenderTarget.width, mainRenderTarget.height); + + // No depth writes, but we'll still use the depth test + GlStateManager._depthMask(false); + GlStateManager._enableBlend(); + ARBDrawBuffersBlend.glBlendFunciARB(0, GL46.GL_ONE, GL46.GL_ONE); // accumulation blend target + ARBDrawBuffersBlend.glBlendFunciARB(1, GL46.GL_ZERO, GL46.GL_ONE_MINUS_SRC_COLOR); // revealage blend target + GlStateManager._blendEquation(GL46.GL_FUNC_ADD); + + GL46.glNamedFramebufferTexture(fbo, GL46.GL_DEPTH_ATTACHMENT, mainRenderTarget.getDepthTextureId(), 0); + + GlStateManager._glBindFramebuffer(GL46.GL_FRAMEBUFFER, fbo); + + GL46.glClearBufferfv(GL46.GL_COLOR, 0, new float[]{0, 0, 0, 0}); + GL46.glClearBufferfv(GL46.GL_COLOR, 1, new float[]{1, 1, 1, 1}); + } + + public void composite() { + var mainRenderTarget = Minecraft.getInstance() + .getMainRenderTarget(); + + mainRenderTarget.bindWrite(false); + + var oitCompositeProgram = programs.getOitCompositeProgram(); + + GlStateManager._depthMask(false); + GlStateManager._depthFunc(GL46.GL_ALWAYS); + GlStateManager._enableBlend(); + RenderSystem.blendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); + + oitCompositeProgram.bind(); + + GlTextureUnit.T0.makeActive(); + GlStateManager._bindTexture(accum); + + GlTextureUnit.T1.makeActive(); + GlStateManager._bindTexture(reveal); + + // Empty VAO, the actual full screen triangle is generated in the vertex shader + GlStateManager._glBindVertexArray(vao); + + GL46.glDrawArrays(GL46.GL_TRIANGLES, 0, 3); + } + + public void delete() { + GL46.glDeleteTextures(accum); + GL46.glDeleteTextures(reveal); + GL46.glDeleteFramebuffers(fbo); + GL46.glDeleteVertexArrays(vao); + } + + private void createTextures(int width, int height) { + if (lastWidth == width && lastHeight == height) { + return; + } + + lastWidth = width; + lastHeight = height; + + GL46.glDeleteTextures(accum); + GL46.glDeleteTextures(reveal); + + accum = GL46.glCreateTextures(GL46.GL_TEXTURE_2D); + reveal = GL46.glCreateTextures(GL46.GL_TEXTURE_2D); + + GL46.glNamedFramebufferDrawBuffers(fbo, new int[]{GL46.GL_COLOR_ATTACHMENT0, GL46.GL_COLOR_ATTACHMENT1}); + + GL46.glNamedFramebufferTexture(fbo, GL46.GL_COLOR_ATTACHMENT0, accum, 0); + GL46.glNamedFramebufferTexture(fbo, GL46.GL_COLOR_ATTACHMENT1, reveal, 0); + + GL46.glTextureStorage2D(accum, 1, GL32.GL_RGBA32F, width, height); + GL46.glTextureStorage2D(reveal, 1, GL32.GL_R8, width, height); + + for (int tex : new int[]{accum, reveal}) { + GL46.glTextureParameteri(tex, GL32.GL_TEXTURE_MIN_FILTER, GL32.GL_NEAREST); + GL46.glTextureParameteri(tex, GL32.GL_TEXTURE_MAG_FILTER, GL32.GL_NEAREST); + GL46.glTextureParameteri(tex, GL32.GL_TEXTURE_COMPARE_MODE, GL32.GL_NONE); + GL46.glTextureParameteri(tex, GL32.GL_TEXTURE_WRAP_S, GL32.GL_CLAMP_TO_EDGE); + GL46.glTextureParameteri(tex, GL32.GL_TEXTURE_WRAP_T, GL32.GL_CLAMP_TO_EDGE); + } + } +} diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/common.frag b/common/src/backend/resources/assets/flywheel/flywheel/internal/common.frag index d2d4fc5d8..7bc4ec54d 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/common.frag +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/common.frag @@ -17,8 +17,20 @@ in vec2 _flw_crumblingTexCoord; flat in uvec2 _flw_ids; #endif +#ifdef _FLW_OIT + +// your first render target which is used to accumulate pre-multiplied color values +layout (location = 0) out vec4 accum; + +// your second render target which is used to store pixel revealage +layout (location = 1) out float reveal; + +#else + out vec4 _flw_outputColor; +#endif + float _flw_diffuseFactor() { if (flw_material.cardinalLightingMode == 2u) { return diffuseFromLightDirections(flw_vertexNormal); @@ -33,6 +45,11 @@ float _flw_diffuseFactor() { } } +float linearize_depth(float d, float zNear, float zFar) { + float z_n = 2.0 * d - 1.0; + return 2.0 * zNear * zFar / (zFar + zNear - z_n * (zFar - zNear)); +} + void _flw_main() { flw_sampleColor = texture(flw_diffuseTex, flw_vertexTexCoord); flw_fragColor = flw_vertexColor * flw_sampleColor; @@ -99,5 +116,31 @@ void _flw_main() { } #endif - _flw_outputColor = flw_fogFilter(color); + color = flw_fogFilter(color); + + color.a = 0.9; + + #ifdef _FLW_OIT + + float depth = linearize_depth(gl_FragCoord.z, _flw_cullData.znear, _flw_cullData.zfar); + + // insert your favorite weighting function here. the color-based factor + // avoids color pollution from the edges of wispy clouds. the z-based + // factor gives precedence to nearer surfaces + //float weight = clamp(pow(min(1.0, color.a * 10.0) + 0.01, 3.0) * 1e8 * pow(1.0 - gl_FragCoord.z * 0.9, 3.0), 1e-2, 3e3); + float weight = max(min(1.0, max(max(color.r, color.g), color.b) * color.a), color.a) * + clamp(0.03 / (1e-5 + pow(depth / 200, 4.0)), 1e-2, 3e3); + + // blend func: GL_ONE, GL_ONE + // switch to pre-multiplied alpha and weight + accum = vec4(color.rgb * color.a, color.a) * weight; + + // blend func: GL_ZERO, GL_ONE_MINUS_SRC_ALPHA + reveal = color.a; + + #else + + _flw_outputColor = color; + + #endif } diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/fullscreen.vert b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/fullscreen.vert new file mode 100644 index 000000000..46ae47150 --- /dev/null +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/fullscreen.vert @@ -0,0 +1,4 @@ +void main() { + vec2 vertices[3] = vec2[3](vec2(-1, -1), vec2(3, -1), vec2(-1, 3)); + gl_Position = vec4(vertices[gl_VertexID], 0, 1); +} diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/oit_composite.frag b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/oit_composite.frag new file mode 100644 index 000000000..6c5a67419 --- /dev/null +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/indirect/oit_composite.frag @@ -0,0 +1,48 @@ +// shader outputs +layout (location = 0) out vec4 frag; + +// color accumulation buffer +layout (binding = 0) uniform sampler2D accum; + +// revealage threshold buffer +layout (binding = 1) uniform sampler2D reveal; + +// epsilon number +const float EPSILON = 0.00001f; + +// calculate floating point numbers equality accurately +bool isApproximatelyEqual(float a, float b) { + return abs(a - b) <= (abs(a) < abs(b) ? abs(b) : abs(a)) * EPSILON; +} + +// get the max value between three values +float max3(vec3 v) { + return max(max(v.x, v.y), v.z); +} + +void main() { + // fragment coordination + ivec2 coords = ivec2(gl_FragCoord.xy); + + // fragment revealage + float revealage = texelFetch(reveal, coords, 0).r; + + // save the blending and color texture fetch cost if there is not a transparent fragment + if (isApproximatelyEqual(revealage, 1.0f)) { + discard; + } + + // fragment color + vec4 accumulation = texelFetch(accum, coords, 0); + + // suppress overflow + if (isinf(max3(abs(accumulation.rgb)))) { + accumulation.rgb = vec3(accumulation.a); + } + + // prevent floating point precision bug + vec3 average_color = accumulation.rgb / max(accumulation.a, EPSILON); + + // blend pixels + frag = vec4(average_color, 1.0f - revealage); +}