package net.rustnotmc.client; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.FloatBuffer; import java.util.Properties; import java.util.Random; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.EXTFramebufferObject; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL13; import org.lwjgl.opengl.GL14; import org.lwjgl.opengl.GL20; import net.minecraft.client.Minecraft; import net.rustnotmc.loader.Log; /** * The post processing of the remake, redone on top of what the game has. * * The remake owns its whole renderer and runs its effects on a core profile * against G-buffers it fills itself. This game runs a fixed pipeline over a * 2.1 context, so the same ideas are rebuilt with the tools that are here: * the scene color and the scene depth are copied off the back buffer the * moment the world is drawn, and both effects run as full-screen passes in * programs of their own. * * Two effects, the way the remake has them: * * * * The server sends none of this and the world bridge sends no light that it * works from: every pass reads back what the client already drew, which is * the whole point of doing them here. * * Everything is wrapped. A driver that refuses one of the programs must cost * the player the effect, not the game. */ public final class RsxPostPipeline { private static boolean working = true; private static boolean programsBuilt; private static int outputWidth; private static int outputHeight; private static int colorCopyTexture; private static int depthCopyTexture; private static int ssaoTexture; private static int ssaoFramebuffer; private static int blurTexture; private static int blurFramebuffer; private static int[] bloomTextures; private static int[] bloomBuffers; private static int mips; private static Program thresholdProgram; private static Program downSampleProgram; private static Program combineProgram; private static Program bloomAddProgram; private static Program ssaoProgram; private static Program ssaoBlurProgram; private static Program ssaoCompositeProgram; /** The world projection as the camera left it, and its inverse. */ private static final float[] PROJECTION = new float[16]; private static final float[] INVERSE = new float[16]; private static final FloatBuffer PROJECTION_BUF = BufferUtils.createFloatBuffer(16); private static final FloatBuffer COLUMN_BUF = BufferUtils.createFloatBuffer(16); /* -------- settings, kept the way the remake keeps them -------- */ private static boolean buffered; private static boolean master = true; private static boolean useSSAO = true; private static boolean useBloom = true; private static float ssaoStrength = 1.0F; private static float ssaoRadius = 0.5F; private static float ssaoBias = 0.02F; private static float ssaoPower = 2.0F; private static float bloomIntensity = 1.5F; private static float bloomThreshold = 0.3F; private static float bloomKnee = 0.0F; private RsxPostPipeline() { } /** * A chat line beginning with /rsxpost turns the effects around. * *
     *   /rsxpost                 what is on right now
     *   /rsxpost on|off          the whole pipeline
     *   /rsxpost ssao on|off     the shading of corners
     *   /rsxpost bloom on|off    the glow of the sky
     *   /rsxpost strength 0.8    how strongly SSAO darkens
     *   /rsxpost radius 1.2      how far SSAO reaches, in blocks
     *   /rsxpost threshold 0.4   what bloom starts to pick up
     *   /rsxpost intensity 2.0   how bright the bloom lands
     * 
* * A line that is not one of ours is left alone, which matters: the chat * pre-fills commands the player typed before, and a half written * /rsxpostday has to go on to the world like any other word. */ public static boolean command(String line) { if (line == null || !line.startsWith("/rsxpost")) { return false; } if (line.length() > 8) { char next = line.charAt(8); if (next != ' ' && next != '\t') { return false; } } String[] words = line.trim().split("\\s+"); if (words.length < 2) { status(); return true; } String what = words[1].toLowerCase(); String value = words.length > 2 ? words[2] : null; boolean on = "on".equalsIgnoreCase(value); boolean off = "off".equalsIgnoreCase(value); if ("on".equals(what)) { setMaster(true); } else if ("off".equals(what)) { setMaster(false); } else if ("ssao".equals(what)) { if (on || off) { useSSAO = on; save(); say("SSAO " + (on ? "on" : "off")); } else { status(); } } else if ("bloom".equals(what)) { if (on || off) { useBloom = on; save(); say("bloom " + (on ? "on" : "off")); } else { status(); } } else if ("strength".equals(what) && value != null) { ssaoStrength = clamp(parse(value), 0.0F, 1.0F); useSSAO = true; save(); say("SSAO strength " + ssaoStrength); } else if ("radius".equals(what) && value != null) { ssaoRadius = clamp(parse(value), 0.1F, 2.0F); save(); say("SSAO radius " + ssaoRadius); } else if ("threshold".equals(what) && value != null) { bloomThreshold = clamp(parse(value), 0.0F, 1.0F); save(); say("bloom threshold " + bloomThreshold); } else if ("intensity".equals(what) && value != null) { bloomIntensity = clamp(parse(value), 0.0F, 8.0F); save(); say("bloom intensity " + bloomIntensity); } else { say("unknown: " + what + "; try on, off, ssao, bloom, strength," + " radius, threshold, intensity"); } return true; } private static float parse(String value) { try { return Float.parseFloat(value); } catch (NumberFormatException notANumber) { say("'" + value + "' is not a number"); return 0.0F; } } private static float clamp(float value, float least, float most) { if (value < least) { return least; } if (value > most) { return most; } return value; } private static void status() { say("post processing: master=" + master + " ssao=" + useSSAO + " bloom=" + useBloom + " strength=" + ssaoStrength + " radius=" + ssaoRadius + " threshold=" + bloomThreshold + " intensity=" + bloomIntensity); } private static void say(String text) { Minecraft minecraft = Minecraft.getMinecraft(); if (minecraft != null && minecraft.player != null) { minecraft.player.sendMessage( new net.minecraft.util.text.TextComponentString(text)); } } /* -------- one frame -------- */ /** * Runs the effects over what the world just drew. * * Called at the end of the world pass, after everything from the sky to * the water and before the hand, so the copies carry the whole world and * the hand stays sharp above them. Nothing runs when the pipeline is * switched off, and once it has failed it stays off: the effects are * worth less than the game they draw over. */ public static void apply(Minecraft minecraft) { if (!master || !working || minecraft.world == null) { return; } int w = minecraft.displayWidth; int h = minecraft.displayHeight; if (w <= 0 || h <= 0) { return; } load(); try { if (!programsBuilt || w != outputWidth || h != outputHeight) { build(w, h); } if (!working) { return; } copyTheBackBuffer(w, h); boolean blendWas = GL11.glIsEnabled(GL11.GL_BLEND); boolean depthWas = GL11.glIsEnabled(GL11.GL_DEPTH_TEST); boolean alphaWas = GL11.glIsEnabled(GL11.GL_ALPHA_TEST); int unitWas = GL11.glGetInteger(GL13.GL_ACTIVE_TEXTURE); java.nio.IntBuffer bindingBuf = BufferUtils.createIntBuffer(1); GL11.glGetInteger( EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT, bindingBuf); int binding = bindingBuf.get(0); GL11.glDisable(GL11.GL_BLEND); GL11.glDisable(GL11.GL_DEPTH_TEST); GL11.glDisable(GL11.GL_ALPHA_TEST); GL11.glDepthMask(false); if (useSSAO && ssaoProgram.complete()) { runSSAO(w, h); } if (useBloom && thresholdProgram.complete()) { runBloom(w, h); } GL11.glDepthMask(true); if (depthWas) { GL11.glEnable(GL11.GL_DEPTH_TEST); } if (blendWas) { GL11.glEnable(GL11.GL_BLEND); } if (alphaWas) { GL11.glEnable(GL11.GL_ALPHA_TEST); } GL13.glActiveTexture(unitWas); GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0); EXTFramebufferObject.glBindFramebufferEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT, binding); } catch (Throwable failure) { Log.error("post processing stopped", failure); working = false; } } /** * The projection of the world, taken at camera setup. * * Everything the SSAO maths does stands on this: positions are read back * out of the depth buffer with its inverse, samples are pushed back into * it with the matrix itself. */ public static void captureProjection() { PROJECTION_BUF.rewind(); GL11.glGetFloat(GL11.GL_PROJECTION_MATRIX, PROJECTION_BUF); PROJECTION_BUF.rewind(); for (int i = 0; i < 16; i++) { PROJECTION[i] = PROJECTION_BUF.get(); } // the matrices travel column major; a straight copy is enough inverted(PROJECTION, INVERSE); } /** One matrix, in the column major order the uniforms expect. */ private static FloatBuffer columnOf(float[] matrix) { COLUMN_BUF.rewind(); COLUMN_BUF.put(matrix); COLUMN_BUF.flip(); return COLUMN_BUF; } /* -------- the scene copies -------- */ /** * Copies color and depth into textures. * * The storage is allocated once and refilled with a copy every frame; * six repeated reallocations a second are how a machine that cannot * afford the effects says so. */ private static void copyTheBackBuffer(int w, int h) { GL11.glBindTexture(GL11.GL_TEXTURE_2D, colorCopyTexture); GL11.glCopyTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h); GL11.glBindTexture(GL11.GL_TEXTURE_2D, depthCopyTexture); GL11.glCopyTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h); } /* -------- SSAO -------- */ private static void runSSAO(int w, int h) { GL11.glViewport(0, 0, w, h); EXTFramebufferObject.glBindFramebufferEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT, ssaoFramebuffer); GL11.glClearColor(1.0F, 1.0F, 1.0F, 1.0F); GL11.glClear(GL11.GL_COLOR_BUFFER_BIT); ssaoProgram.use(); bind(0, depthCopyTexture); ssaoProgram.mat4("uProjection", columnOf(PROJECTION)); ssaoProgram.mat4("uInvProjection", columnOf(INVERSE)); for (int i = 0; i < 16; i++) { ssaoProgram.float3("uSamples[" + i + "]", SAMPLES[i]); } ssaoProgram.float1("uRadius", ssaoRadius); ssaoProgram.float1("uBias", ssaoBias); ssaoProgram.float1("uPower", ssaoPower); ssaoProgram.float2("uTexel", 1.0F / w, 1.0F / h); drawQuad(); ssaoProgram.close(); EXTFramebufferObject.glBindFramebufferEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT, blurFramebuffer); ssaoBlurProgram.use(); bind(0, ssaoTexture); bind(1, depthCopyTexture); ssaoBlurProgram.float2("uTexelSize", 1.0F / w, 1.0F / h); drawQuad(); ssaoBlurProgram.close(); // the AO lands on the whole scene in one pass; the hand comes after // and the interface after that, both are drawn on top of the result GL11.glViewport(0, 0, outputWidth, outputHeight); ssaoCompositeProgram.use(); bind(0, colorCopyTexture); bind(1, blurTexture); ssaoCompositeProgram.float1("uStrength", ssaoStrength); drawQuad(); ssaoCompositeProgram.close(); } /** * Sixteen directions spread over a hemisphere, farther out the later * they come, the same spread the remake's sampler was seeded with. */ private static final float[][] SAMPLES = buildSamples(); private static float[][] buildSamples() { Random random = new Random(42L); float[][] spread = new float[16][3]; for (int i = 0; i < 16; i++) { float x = random.nextFloat() * 2.0F - 1.0F; float y = random.nextFloat() * 2.0F - 1.0F; float z = 0.05F + random.nextFloat() * 0.9F; float len = (float) Math.sqrt(x * x + y * y + z * z); if (len < 1.0E-5F) { len = 1.0E-5F; } float eased = (float) i / 15.0F; eased = eased * eased; float scale = (0.1F + 0.9F * eased) * (0.1F + random.nextFloat() * 0.8F); spread[i][0] = x / len * scale / 2.0F; spread[i][1] = y / len * scale / 2.0F; spread[i][2] = z / len * scale / 2.0F; } return spread; } /* -------- bloom -------- */ private static void runBloom(int w, int h) { // down the chain: the threshold picks what shines, then each level // carries half of what the one above it held for (int level = 0; level < mips; level++) { GL11.glViewport(0, 0, mipWidth(level), mipHeight(level)); EXTFramebufferObject.glBindFramebufferEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT, bloomBuffers[level]); GL11.glClearColor(0.0F, 0.0F, 0.0F, 0.0F); GL11.glClear(GL11.GL_COLOR_BUFFER_BIT); if (level == 0) { thresholdProgram.use(); bind(0, colorCopyTexture); thresholdProgram.float2("uSrcTexelSize", 1.0F / w, 1.0F / h); thresholdProgram.float1("uThreshold", bloomThreshold); thresholdProgram.float1("uKnee", bloomKnee); drawQuad(); thresholdProgram.close(); } else { downSampleProgram.use(); bind(0, bloomTextures[level - 1]); downSampleProgram.float2("uSrcTexelSize", 1.0F / mipWidth(level - 1), 1.0F / mipHeight(level - 1)); drawQuad(); downSampleProgram.close(); } } // back up the chain: what the level below blurred is added to what // this level already holds, so halos keep the shape of their light for (int level = mips - 2; level >= 0; level--) { GL11.glViewport(0, 0, mipWidth(level), mipHeight(level)); EXTFramebufferObject.glBindFramebufferEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT, bloomBuffers[level]); combineProgram.use(); bind(0, bloomTextures[level + 1]); bind(1, bloomTextures[level]); combineProgram.float2("uBlurTexelSize", 1.0F / mipWidth(level + 1), 1.0F / mipHeight(level + 1)); drawQuad(); combineProgram.close(); } // and the sum lands on the screen one additive pass later GL11.glViewport(0, 0, outputWidth, outputHeight); GL11.glEnable(GL11.GL_BLEND); GL11.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE); bloomAddProgram.use(); bind(0, bloomTextures[0]); bloomAddProgram.float1("uIntensity", bloomIntensity); drawQuad(); bloomAddProgram.close(); GL11.glDisable(GL11.GL_BLEND); } private static int mipWidth(int level) { return Math.max(4, outputWidth >> (level + 1)); } private static int mipHeight(int level) { return Math.max(4, outputHeight >> (level + 1)); } /* -------- machinery -------- */ private static void bind(int unit, int texture) { GL13.glActiveTexture(GL13.GL_TEXTURE0 + unit); GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture); } private static void drawQuad() { GL11.glBegin(GL11.GL_QUADS); GL11.glVertex2f(-1.0F, -1.0F); GL11.glVertex2f(1.0F, -1.0F); GL11.glVertex2f(1.0F, 1.0F); GL11.glVertex2f(-1.0F, 1.0F); GL11.glEnd(); } /** * Builds everything once, and grows everything when the window grows. * * The programs survive a resize: their programs know nothing of pixels. * The textures do not exist until the first frame and are re-proposed at * every size, and the framebuffers keep pointing at the same names. */ private static void build(int w, int h) throws IOException { outputWidth = w; outputHeight = h; if (!programsBuilt) { captureIfFresh(); programsBuilt = true; bloomTextures = new int[BLOOM_MIP_LIMIT]; bloomBuffers = new int[BLOOM_MIP_LIMIT]; colorCopyTexture = GL11.glGenTextures(); depthCopyTexture = GL11.glGenTextures(); ssaoTexture = GL11.glGenTextures(); ssaoFramebuffer = EXTFramebufferObject.glGenFramebuffersEXT(); blurTexture = GL11.glGenTextures(); blurFramebuffer = EXTFramebufferObject.glGenFramebuffersEXT(); for (int level = 0; level < BLOOM_MIP_LIMIT; level++) { bloomTextures[level] = GL11.glGenTextures(); bloomBuffers[level] = EXTFramebufferObject .glGenFramebuffersEXT(); } thresholdProgram = program(COMMON_VERTEX, THRESHOLD_FRAGMENT); downSampleProgram = program(COMMON_VERTEX, DOWNSAMPLE_FRAGMENT); combineProgram = program(COMMON_VERTEX, COMBINE_FRAGMENT); bloomAddProgram = program(COMMON_VERTEX, BLOOM_ADD_FRAGMENT); ssaoProgram = program(COMMON_VERTEX, SSAO_FRAGMENT); ssaoBlurProgram = program(COMMON_VERTEX, SSAO_BLUR_FRAGMENT); ssaoCompositeProgram = program(COMMON_VERTEX, SSAO_COMPOSITE_FRAGMENT); } mips = 0; for (int level = 0; level < BLOOM_MIP_LIMIT; level++) { if (mipWidth(level) < 8 || mipHeight(level) < 8) { break; } mips++; } if (mips < 2) { mips = Math.min(2, BLOOM_MIP_LIMIT); } sizeTexture(colorCopyTexture, w, h, GL11.GL_RGB8, GL11.GL_LINEAR); GL11.glBindTexture(GL11.GL_TEXTURE_2D, depthCopyTexture); GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL14.GL_DEPTH_COMPONENT24, w, h, 0, GL11.GL_DEPTH_COMPONENT, GL11.GL_UNSIGNED_INT, (java.nio.ByteBuffer) null); nearest(GL11.GL_CLAMP); sizeTexture(ssaoTexture, w, h, GL11.GL_RGB8, GL11.GL_LINEAR); sizeTexture(blurTexture, w, h, GL11.GL_RGB8, GL11.GL_LINEAR); for (int level = 0; level < mips; level++) { sizeTexture(bloomTextures[level], mipWidth(level), mipHeight(level), GL11.GL_RGB8, GL11.GL_LINEAR); } attach(ssaoFramebuffer, ssaoTexture); attach(blurFramebuffer, blurTexture); for (int level = 0; level < mips; level++) { attach(bloomBuffers[level], bloomTextures[level]); } } /** * The projection may not be captured yet the first time around; after * that the camera has run long before the effects will. */ private static void captureIfFresh() { boolean any = false; for (float v : PROJECTION) { if (v != 0.0F) { any = true; break; } } if (!any) { identity(PROJECTION); identity(INVERSE); } } private static void sizeTexture(int texture, int w, int h, int format, int filter) { GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture); GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, format, w, h, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (java.nio.ByteBuffer) null); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, filter); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, filter); wrap(GL11.GL_CLAMP); } private static void wrap(int mode) { GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, mode); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, mode); } private static void nearest(int mode) { GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); wrap(mode); } private static final int BLOOM_MIP_LIMIT = 5; /** * Connects a texture to a framebuffer of the extension the game itself * uses everywhere a framebuffer is mentioned, so compatibility is the * same compatibility the world already proved. */ private static void attach(int framebuffer, int texture) { EXTFramebufferObject.glBindFramebufferEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT, framebuffer); EXTFramebufferObject.glFramebufferTexture2DEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, GL11.GL_TEXTURE_2D, texture, 0); int status = EXTFramebufferObject.glCheckFramebufferStatusEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT); if (status != EXTFramebufferObject.GL_FRAMEBUFFER_COMPLETE_EXT) { Log.error("post processing framebuffer is incomplete: " + status); working = false; } EXTFramebufferObject.glBindFramebufferEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0); } /** One program: the shaders go in, the uniforms come out by name. */ private static final class Program { int handle = 0; void use() { GL20.glUseProgram(handle); } void close() { GL20.glUseProgram(0); } boolean complete() { return handle != 0; } private int at(String name) { if (handle == 0) { return -1; } return GL20.glGetUniformLocation(handle, name); } void float1(String name, float value) { int position = at(name); if (position != -1) { GL20.glUniform1f(position, value); } } void float2(String name, float x, float y) { int position = at(name); if (position != -1) { GL20.glUniform2f(position, x, y); } } void float3(String name, float[] value) { int position = at(name); if (position != -1) { GL20.glUniform3f(position, value[0], value[1], value[2]); } } void mat4(String name, FloatBuffer value) { int position = at(name); if (position != -1) { GL20.glUniformMatrix4(position, false, value); } } } private static Program program(String vertex, String fragment) { Program made = new Program(); int vertexShader = GL20.glCreateShader(GL20.GL_VERTEX_SHADER); GL20.glShaderSource(vertexShader, vertex); GL20.glCompileShader(vertexShader); if (!compiled(vertexShader)) { GL20.glDeleteShader(vertexShader); return made; } int fragmentShader = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER); GL20.glShaderSource(fragmentShader, fragment); GL20.glCompileShader(fragmentShader); if (!compiled(fragmentShader)) { GL20.glDeleteShader(fragmentShader); GL20.glDeleteShader(vertexShader); return made; } int handle = GL20.glCreateProgram(); GL20.glAttachShader(handle, vertexShader); GL20.glAttachShader(handle, fragmentShader); GL20.glLinkProgram(handle); GL20.glDeleteShader(vertexShader); GL20.glDeleteShader(fragmentShader); if (GL20.glGetProgrami(handle, GL20.GL_LINK_STATUS) == 0) { Log.error("post processing program did not link: " + GL20.glGetProgramInfoLog(handle, 4096)); GL20.glDeleteProgram(handle); return made; } made.handle = handle; return made; } private static boolean compiled(int shader) { int state = GL20.glGetShaderi(shader, GL20.GL_COMPILE_STATUS); if (state == 0) { Log.error("post processing shader did not compile: " + GL20.glGetShaderInfoLog(shader, 4096)); return false; } return true; } /** The projection turned the other way, for positions read from depth. */ private static void inverted(float[] matrix, float[] into) { float a00 = matrix[0], a01 = matrix[1], a02 = matrix[2], a03 = matrix[3]; float a10 = matrix[4], a11 = matrix[5], a12 = matrix[6], a13 = matrix[7]; float a20 = matrix[8], a21 = matrix[9], a22 = matrix[10], a23 = matrix[11]; float a30 = matrix[12], a31 = matrix[13], a32 = matrix[14], a33 = matrix[15]; float b00 = a00 * a11 - a01 * a10; float b01 = a00 * a12 - a02 * a10; float b02 = a00 * a13 - a03 * a10; float b03 = a01 * a12 - a02 * a11; float b04 = a01 * a13 - a03 * a11; float b05 = a02 * a13 - a03 * a12; float b06 = a20 * a31 - a21 * a30; float b07 = a20 * a32 - a22 * a30; float b08 = a20 * a33 - a23 * a30; float b09 = a21 * a32 - a22 * a31; float b10 = a21 * a33 - a23 * a31; float b11 = a22 * a33 - a23 * a32; float determinant = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; if (Math.abs(determinant) < 1.0E-9F) { identity(into); return; } float over = 1.0F / determinant; into[0] = (a11 * b11 - a12 * b10 + a13 * b09) * over; into[1] = (-a01 * b11 + a02 * b10 - a03 * b09) * over; into[2] = (a31 * b05 - a32 * b04 + a33 * b03) * over; into[3] = (-a21 * b05 + a22 * b04 - a23 * b03) * over; into[4] = (-a10 * b11 + a12 * b08 - a13 * b07) * over; into[5] = (a00 * b11 - a02 * b08 + a03 * b07) * over; into[6] = (-a30 * b05 + a32 * b02 - a33 * b01) * over; into[7] = (a20 * b05 - a22 * b02 + a23 * b01) * over; into[8] = (a10 * b10 - a11 * b08 + a13 * b06) * over; into[9] = (-a00 * b10 + a01 * b08 - a03 * b06) * over; into[10] = (a30 * b04 - a31 * b02 + a33 * b00) * over; into[11] = (-a20 * b04 + a21 * b02 - a23 * b00) * over; into[12] = (-a10 * b09 + a11 * b07 - a12 * b06) * over; into[13] = (a00 * b09 - a01 * b07 + a02 * b06) * over; into[14] = (-a30 * b03 + a31 * b01 - a32 * b00) * over; into[15] = (a20 * b03 - a21 * b01 + a22 * b00) * over; } private static void identity(float[] into) { java.util.Arrays.fill(into, 0.0F); into[0] = 1.0F; into[5] = 1.0F; into[10] = 1.0F; into[15] = 1.0F; } /* -------- the shaders -------- */ private static final String COMMON_VERTEX = "#version 120\n" + "varying vec2 vUV;\n" + "void main() {\n" + " vUV = gl_Vertex.xy * 0.5 + 0.5;\n" + " gl_Position = vec4(gl_Vertex.xy, 0.0, 1.0);\n" + "}\n"; private static final String THRESHOLD_FRAGMENT = "#version 120\n" + "varying vec2 vUV;\n" + "uniform sampler2D uTex;\n" + "uniform vec2 uSrcTexelSize;\n" + "uniform float uThreshold;\n" + "uniform float uKnee;\n" + "vec3 gated(vec3 c) {\n" + " float br = max(c.r, max(c.g, c.b));\n" + " float soft = clamp(br - uThreshold + uKnee, 0.0, 2.0 * uKnee);\n" + " soft = (soft * soft) / (4.0 * uKnee + 0.00001);\n" + " float w = max(soft, br - uThreshold) / max(br, 0.00001);\n" + " return c * max(w, 0.0);\n" + "}\n" + "void main() {\n" + " vec2 ht = uSrcTexelSize * 0.5;\n" + " vec3 a = gated(texture2D(uTex, vUV + vec2(-ht.x, -ht.y)).rgb);\n" + " vec3 b = gated(texture2D(uTex, vUV + vec2( ht.x, -ht.y)).rgb);\n" + " vec3 c = gated(texture2D(uTex, vUV + vec2(-ht.x, ht.y)).rgb);\n" + " vec3 d = gated(texture2D(uTex, vUV + vec2( ht.x, ht.y)).rgb);\n" + " gl_FragColor = vec4((a + b + c + d) * 0.25, 1.0);\n" + "}\n"; private static final String DOWNSAMPLE_FRAGMENT = "#version 120\n" + "varying vec2 vUV;\n" + "uniform sampler2D uTex;\n" + "uniform vec2 uSrcTexelSize;\n" + "void main() {\n" + " vec2 ht = uSrcTexelSize * 0.5;\n" + " vec3 a = texture2D(uTex, vUV + vec2(-ht.x, -ht.y)).rgb;\n" + " vec3 b = texture2D(uTex, vUV + vec2( ht.x, -ht.y)).rgb;\n" + " vec3 c = texture2D(uTex, vUV + vec2(-ht.x, ht.y)).rgb;\n" + " vec3 d = texture2D(uTex, vUV + vec2( ht.x, ht.y)).rgb;\n" + " gl_FragColor = vec4((a + b + c + d) * 0.25, 1.0);\n" + "}\n"; private static final String COMBINE_FRAGMENT = "#version 120\n" + "varying vec2 vUV;\n" + "uniform sampler2D uBlurTex;\n" + "uniform sampler2D uPrevTex;\n" + "uniform vec2 uBlurTexelSize;\n" + "void main() {\n" + " vec2 t = uBlurTexelSize;\n" + " vec3 s = vec3(0.0);\n" + " s += texture2D(uBlurTex, vUV + vec2(-t.x, -t.y)).rgb;\n" + " s += texture2D(uBlurTex, vUV + vec2( 0.0, -t.y)).rgb * 2.0;\n" + " s += texture2D(uBlurTex, vUV + vec2( t.x, -t.y)).rgb;\n" + " s += texture2D(uBlurTex, vUV + vec2(-t.x, 0.0)).rgb * 2.0;\n" + " s += texture2D(uBlurTex, vUV ).rgb * 4.0;\n" + " s += texture2D(uBlurTex, vUV + vec2( t.x, 0.0)).rgb * 2.0;\n" + " s += texture2D(uBlurTex, vUV + vec2(-t.x, t.y)).rgb;\n" + " s += texture2D(uBlurTex, vUV + vec2( 0.0, t.y)).rgb * 2.0;\n" + " s += texture2D(uBlurTex, vUV + vec2( t.x, t.y)).rgb;\n" + " s /= 16.0;\n" + " vec3 prev = texture2D(uPrevTex, vUV).rgb;\n" + " gl_FragColor = vec4(prev + s, 1.0);\n" + "}\n"; private static final String BLOOM_ADD_FRAGMENT = "#version 120\n" + "varying vec2 vUV;\n" + "uniform sampler2D uBloom;\n" + "uniform float uIntensity;\n" + "void main() {\n" + " gl_FragColor = vec4(texture2D(uBloom, vUV).rgb * uIntensity, 0.0);\n" + "}\n"; private static final String SSAO_FRAGMENT = "#version 120\n" + "varying vec2 vUV;\n" + "uniform sampler2D uDepth;\n" + "uniform mat4 uProjection;\n" + "uniform mat4 uInvProjection;\n" + "uniform vec3 uSamples[16];\n" + "uniform vec2 uTexel;\n" + "uniform float uRadius;\n" + "uniform float uBias;\n" + "uniform float uPower;\n" + "vec3 viewPosFromDepth(float depth, vec2 uv) {\n" + " vec4 clipPos = vec4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);\n" + " vec4 viewPos = uInvProjection * clipPos;\n" + " return viewPos.xyz / viewPos.w;\n" + "}\n" + "vec3 hash3(vec2 seed) {\n" + " vec3 q = vec3(dot(seed, vec2(127.1, 311.7)),\n" + " dot(seed, vec2(269.5, 183.3)),\n" + " dot(seed, vec2(419.2, 371.9)));\n" + " return fract(sin(q) * 43758.5453) * 2.0 - 1.0;\n" + "}\n" + "void main() {\n" + " float centerDepth = texture2D(uDepth, vUV).r;\n" + " if (centerDepth >= 0.9999) { gl_FragColor = vec4(1.0); return; }\n" + " float dX = texture2D(uDepth, vUV + vec2(uTexel.x * 2.0, 0.0)).r;\n" + " float dY = texture2D(uDepth, vUV + vec2(0.0, uTexel.y * 2.0)).r;\n" + " if (dX >= 0.9999 || dY >= 0.9999) {\n" + " gl_FragColor = vec4(1.0); return;\n" + " }\n" + " vec3 center = viewPosFromDepth(centerDepth, vUV);\n" + " vec3 pX = viewPosFromDepth(dX, vUV + vec2(uTexel.x * 2.0, 0.0));\n" + " vec3 pY = viewPosFromDepth(dY, vUV + vec2(0.0, uTexel.y * 2.0));\n" + " vec3 n = normalize(cross(pX - center, pY - center));\n" + " if (dot(n, -center) < 0.0) { n = -n; }\n" + " vec3 randVec = normalize(hash3(gl_FragCoord.xy));\n" + " vec3 tangent = normalize(randVec - n * dot(randVec, n));\n" + " vec3 bitangent = cross(n, tangent);\n" + " mat3 tbn = mat3(tangent, bitangent, n);\n" + " float occlusion = 0.0;\n" + " for (int i = 0; i < 16; i++) {\n" + " vec3 samplePos = center + tbn * uSamples[i] * uRadius;\n" + " vec4 offset = uProjection * vec4(samplePos, 1.0);\n" + " offset.xyz /= offset.w;\n" + " vec2 ndc = offset.xy * 0.5 + 0.5;\n" + " if (offset.z >= 1.0 || ndc.x < 0.0 || ndc.x > 1.0\n" + " || ndc.y < 0.0 || ndc.y > 1.0) { continue; }\n" + " float depth = texture2D(uDepth, ndc).r;\n" + " if (depth >= 0.9999) { continue; }\n" + " vec3 sampleView = viewPosFromDepth(depth, ndc);\n" + " if (sampleView.z >= center.z + uBias) {\n" + " occlusion += smoothstep(0.0, 1.0, uRadius\n" + " / max(length(sampleView - center), 0.0001));\n" + " }\n" + " }\n" + " occlusion /= 16.0;\n" + " float ao = 1.0 - pow(occlusion, uPower);\n" + " gl_FragColor = vec4(ao, ao, ao, 1.0);\n" + "}\n"; private static final String SSAO_BLUR_FRAGMENT = "#version 120\n" + "varying vec2 vUV;\n" + "uniform sampler2D uSSAO;\n" + "uniform sampler2D uDepth;\n" + "uniform vec2 uTexelSize;\n" + "void main() {\n" + " float centerDepth = texture2D(uDepth, vUV).r;\n" + " if (centerDepth >= 0.9999) { gl_FragColor = vec4(1.0); return; }\n" + " float result = 0.0;\n" + " float weight = 0.0;\n" + " for (int x = -2; x <= 2; x++) {\n" + " for (int y = -2; y <= 2; y++) {\n" + " vec2 sampleUV = vUV + vec2(float(x), float(y)) * uTexelSize;\n" + " float sampleDepth = texture2D(uDepth, sampleUV).r;\n" + " float w = abs(centerDepth - sampleDepth) < 0.0002 ? 1.0 : 0.0;\n" + " result += texture2D(uSSAO, sampleUV).r * w;\n" + " weight += w;\n" + " }\n" + " }\n" + " float val = (weight > 0.0) ? result / weight : 1.0;\n" + " gl_FragColor = vec4(val, val, val, 1.0);\n" + "}\n"; private static final String SSAO_COMPOSITE_FRAGMENT = "#version 120\n" + "varying vec2 vUV;\n" + "uniform sampler2D uColor;\n" + "uniform sampler2D uSSAO;\n" + "uniform float uStrength;\n" + "void main() {\n" + " vec4 c = texture2D(uColor, vUV);\n" + " float ao = texture2D(uSSAO, vUV).r;\n" + " ao = mix(1.0, ao, clamp(uStrength, 0.0, 1.0));\n" + " gl_FragColor = vec4(c.rgb * ao, 1.0);\n" + "}\n"; /* -------- the file -------- */ /** Where the numbers sit when the game is closed. */ private static final String FILE = "rsxpost.properties"; private static void load() { if (buffered) { return; } buffered = true; File file = fileFor(); if (!file.isFile()) { save(); return; } Properties held = new Properties(); FileInputStream open = null; try { open = new FileInputStream(file); held.load(open); } catch (IOException couldNotRead) { Log.error("rsxpost.properties unreadable", couldNotRead); return; } finally { close(open); } master = Boolean.parseBoolean(held.getProperty("master", "true")); useSSAO = Boolean.parseBoolean(held.getProperty("ssao", "true")); useBloom = Boolean.parseBoolean(held.getProperty("bloom", "true")); ssaoStrength = floatValue(held, "strength", 1.0F); ssaoRadius = floatValue(held, "radius", 0.5F); ssaoBias = floatValue(held, "bias", 0.02F); ssaoPower = floatValue(held, "power", 2.0F); bloomIntensity = floatValue(held, "intensity", 1.5F); bloomThreshold = floatValue(held, "threshold", 0.3F); bloomKnee = floatValue(held, "knee", 0.0F); } private static float floatValue(Properties held, String key, float back) { try { return Float.parseFloat(held.getProperty(key, String.valueOf(back))); } catch (NumberFormatException notANumber) { return back; } } private static void save() { Properties held = new Properties(); held.setProperty("master", String.valueOf(master)); held.setProperty("ssao", String.valueOf(useSSAO)); held.setProperty("bloom", String.valueOf(useBloom)); held.setProperty("strength", String.valueOf(ssaoStrength)); held.setProperty("radius", String.valueOf(ssaoRadius)); held.setProperty("bias", String.valueOf(ssaoBias)); held.setProperty("power", String.valueOf(ssaoPower)); held.setProperty("intensity", String.valueOf(bloomIntensity)); held.setProperty("threshold", String.valueOf(bloomThreshold)); held.setProperty("knee", String.valueOf(bloomKnee)); FileOutputStream out = null; try { out = new FileOutputStream(fileFor()); held.store(out, "the remake's post processing, on the fork"); } catch (IOException couldNotWrite) { Log.error("rsxpost.properties unwritable", couldNotWrite); } finally { close(out); } } private static void setMaster(boolean on) { master = on; save(); if (on) { working = true; } say("post processing " + (on ? "on" : "off")); } private static void close(java.io.Closeable stream) { if (stream == null) { return; } try { stream.close(); } catch (IOException closedAnyway) { } } private static File fileFor() { Minecraft minecraft = Minecraft.getMinecraft(); File base = minecraft == null ? new File(".") : minecraft.gameDir; return new File(base, FILE); } }