diff --git a/shaders/2d_do_nothing.frag b/shaders/2d_do_nothing.frag new file mode 100644 index 00000000..7146f5d6 --- /dev/null +++ b/shaders/2d_do_nothing.frag @@ -0,0 +1,10 @@ +#version 410 core + +in vec2 outputTextureCoordinate; +uniform sampler2D textureSampler; + +out vec4 fragColor; + +void main() { + fragColor = texture(textureSampler, outputTextureCoordinate); +} \ No newline at end of file diff --git a/shaders/2d_do_nothing.vert b/shaders/2d_do_nothing.vert new file mode 100644 index 00000000..1f094a53 --- /dev/null +++ b/shaders/2d_do_nothing.vert @@ -0,0 +1,12 @@ +#version 410 core + +layout (location = 0) in vec2 position; +layout (location = 1) in vec2 textureCoordinate; +layout (location = 2) in vec4 color; + +out vec2 outputTextureCoordinate; + +void main() { + outputTextureCoordinate = textureCoordinate; + gl_Position = vec4(position, 0, 1); +} \ No newline at end of file diff --git a/src/main/java/org/crafter/Main.java b/src/main/java/org/crafter/Main.java index 2ce8d335..8649454b 100644 --- a/src/main/java/org/crafter/Main.java +++ b/src/main/java/org/crafter/Main.java @@ -25,6 +25,8 @@ import org.crafter.engine.delta.Delta; import org.crafter.engine.gui.font.Font; import org.crafter.engine.mesh.MeshStorage; +import org.crafter.engine.postprocessing.PipelineItem; +import org.crafter.engine.postprocessing.PostProcessing; import org.crafter.engine.shader.ShaderStorage; import org.crafter.engine.texture.TextureStorage; import org.crafter.engine.window.Window; @@ -67,9 +69,15 @@ public static void main(String[] args) { initialize(); + PostProcessing postProcessing = new PostProcessing(); + try { while(!Window.shouldClose()) { + postProcessing.start(); mainLoop(); + postProcessing.end(); + + postProcessing.update(); } } catch (Exception e) { // Game must shut down external threads or it WILL hang @@ -100,6 +108,9 @@ private static void initialize() { ShaderStorage.createShader("2d", "shaders/2d_vertex.vert", "shaders/2d_fragment.frag"); ShaderStorage.createUniform("2d", new String[]{"cameraMatrix", "objectMatrix"}); + // Used for post-processing + ShaderStorage.createShader("2d_do_nothing", "shaders/2d_do_nothing.vert", "shaders/2d_do_nothing.frag"); + Font.createFont("fonts/totally_original", "mc", true); Font.setShadowOffset(0.75f,0.75f); diff --git a/src/main/java/org/crafter/engine/postprocessing/PipelineItem.java b/src/main/java/org/crafter/engine/postprocessing/PipelineItem.java new file mode 100644 index 00000000..b9344b02 --- /dev/null +++ b/src/main/java/org/crafter/engine/postprocessing/PipelineItem.java @@ -0,0 +1,136 @@ +package org.crafter.engine.postprocessing; + +import org.crafter.engine.shader.ShaderStorage; +import org.crafter.engine.window.Window; + +import java.nio.ByteBuffer; + +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.opengl.GL12.GL_CLAMP_TO_EDGE; +import static org.lwjgl.opengl.GL30.*; +import static org.lwjgl.opengl.GL30.glDeleteFramebuffers; + +public class PipelineItem implements AutoCloseable { + private int rectVAO; + private int rectVBO; + + private final int framebuffer; + private final int framebufferTexture; + private final String shaderID; + + /** + * Creates a pipeline item to be used in post-processing. + * @param shaderID The shader program to be used for post-processing. Any vertex shaders passed should not have a + * projection matrix, view matrix, model matrix, or any other matrix. + */ + public PipelineItem(String shaderID) { + this.shaderID = shaderID; + + this.framebuffer = glGenFramebuffers(); + this.bind(); + this.framebufferTexture = glGenTextures(); + this.refreshFramebufferTexture(); + + errorCheck(); + + this.unbind(); + + this.genRect(); + } + + /** + * Sets the rectVAO and rectVBO member variables. + */ + private void genRect() { + final float[] coords = new float[]{ + // Coords Tex coords + -1, 1, 0, 1, + -1, -1, 0, 0, + 1, -1, 1, 0, + + -1, 1, 0, 1, + 1, -1, 1, 0, + 1, 1, 1, 1, + }; + + this.rectVAO = glGenVertexArrays(); + this.rectVBO = glGenBuffers(); + + glBindVertexArray(this.rectVAO); + glBindBuffer(GL_ARRAY_BUFFER, this.rectVBO); + + int sizeOfFloat = Float.SIZE / 8; + + glBufferData(GL_ARRAY_BUFFER, coords, GL_STATIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, false, 4 * sizeOfFloat, 0); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, false, 4 * sizeOfFloat, 2 * sizeOfFloat); + } + + private void refreshFramebufferTexture() { + this.bind(); + + glBindTexture(GL_TEXTURE_2D, this.framebufferTexture); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, Window.getWindowWidth(), Window.getWindowHeight(), 0, GL_RGB, GL_UNSIGNED_BYTE, (ByteBuffer) null); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this.framebufferTexture, 0); + + this.unbind(); + } + + private static void errorCheck() { + int fboStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); + + if (fboStatus != GL_FRAMEBUFFER_COMPLETE) { + System.err.println("FBO status has an error. Error code: " + fboStatus); + } + } + + public String getShaderID() { + return this.shaderID; + } + + public void bind() { + glBindFramebuffer(GL_FRAMEBUFFER, this.framebuffer); + } + + public void unbind() { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + public void draw() { + glBindTexture(GL_TEXTURE_2D, this.framebufferTexture); + ShaderStorage.start(this.shaderID); + + glBindVertexArray(this.rectVAO); + glDrawArrays(GL_TRIANGLES, 0, 6); + + ShaderStorage.stop(); + glBindTexture(GL_TEXTURE_2D, 0); + } + + /** + * Changes the frame buffer texture size to the window's new size if it is rescaled + */ + public void refresh() { + this.refreshFramebufferTexture(); + } + + /** + * Warning: this doesn't close the shader program associated with the pipeline item since it is not created + * by the PipelineItem itself. Make sure to do this.getShaderProgram().close() before running this + */ + @Override + public void close() { + glDeleteFramebuffers(this.framebuffer); + glDeleteTextures(this.framebufferTexture); + + glDeleteVertexArrays(this.rectVAO); + glDeleteBuffers(this.rectVBO); + } +} \ No newline at end of file diff --git a/src/main/java/org/crafter/engine/postprocessing/PostProcessing.java b/src/main/java/org/crafter/engine/postprocessing/PostProcessing.java new file mode 100644 index 00000000..acf84938 --- /dev/null +++ b/src/main/java/org/crafter/engine/postprocessing/PostProcessing.java @@ -0,0 +1,106 @@ +package org.crafter.engine.postprocessing; + +import org.crafter.engine.window.Window; +import org.joml.Vector2f; + +import java.util.ArrayList; +import java.util.List; + +import static org.lwjgl.opengl.GL11.*; + +public class PostProcessing implements AutoCloseable { + private final List pipeline; + private Vector2f windowSize; + + public PostProcessing() { + this.pipeline = new ArrayList<>(); + + this.addToPipeline( + new PipelineItem("2d_do_nothing") + ); + } + + /** + * Adds a new pipeline item to the end of the pipeline. + * @param item The pipeline item to add. + */ + public void addToPipeline(PipelineItem item) { + pipeline.add(item); + } + + /** + * WARNING: the pipeline must have at least one item to render the screen properly. + * @return The list of pipeline items. You may modify this, but make sure that there is at least one pipeline item at all times + */ + public List getPipeline() { + return this.pipeline; + } + + /** + * Run this before rendering anything, including clearing the screen. + * @throws IllegalStateException If the pipeline has 0 items. + */ + public void start() throws IllegalStateException { + if (this.pipeline.size() == 0) { + throw new IllegalStateException("The pipeline must have at least one item"); + } + + this.pipeline.get(0).bind(); + } + + /** + * Run this at the end of the frame. + * @throws IllegalStateException If the pipeline has 0 items. + */ + public void end() throws IllegalStateException { + glDisable(GL_DEPTH_TEST); + // glDisable(GL_CULL_FACE); + + if (this.pipeline.size() == 0) { + throw new IllegalStateException("The pipeline must have at least one item"); + } + + for (int i = 1; i < this.pipeline.size(); i++) { + PipelineItem item = this.pipeline.get(i); + item.bind(); + this.pipeline.get(i - 1).draw(); + } + + this.pipeline.get(0).unbind(); // it doesn't matter which one to call to unbind + this.pipeline.get(this.pipeline.size() - 1).draw(); + + // glEnable(GL_DEPTH_TEST); + glEnable(GL_CULL_FACE); + } + + /** + * Changes the texture size if the window is resized. + */ + public void update() { + // Since Window.wasResized() always evaluates to false this is my workaround + + if (this.windowSize == null) { + this.windowSize = Window.getWindowSize(); + } + + if (this.windowSize.equals(Window.getWindowSize())) { + return; + } + + for (PipelineItem item : this.getPipeline()) { + item.refresh(); + } + + this.windowSize = Window.getWindowSize(); + } + + /** + * Closes all pipeline items. + */ + @Override + public void close() { + for (PipelineItem pipelineItem : this.getPipeline()) { + pipelineItem.close(); + } + } +}