1038 lines
40 KiB
Java
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);
|
|
}
|
|
}
|