Files

1038 lines
40 KiB
Java

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:
*
* <ul>
* <li>SSAO. Sixteen hemisphere samples around each pixel's view-space
* position, the surface normal reconstructed from depth neighbours, softened
* by a five by five blur that stops at depth edges, then applied as a
* multiplier on the scene color.</li>
* <li>Bloom. A threshold pass picks what shines, a chain of downsamples
* spreads it, and the chain walks back up adding what it blurred into what
* lies beneath, landing as one additive layer over the screen.</li>
* </ul>
*
* 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.
*
* <pre>
* /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
* </pre>
*
* 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);
}
}