From adf34b47421cf2d49416fca4fed8e0e94b99ca6f Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Wed, 12 Aug 2020 19:23:28 +0200 Subject: [PATCH] Drop-in palette lookup post-processing Directly usable for devs --- .../minestom/demo/largeframebuffers/Demo.java | 6 +- .../largeframebuffers/OpenGLRendering.java | 36 +-- .../map/framebuffers/MapColorRenderer.java | 257 ++++++++++++++++++ src/lwjgl/resources/shaders/fragment.glsl | 23 +- .../shaders/mapcolorconvert.fragment.glsl | 30 ++ .../shaders/mapcolorconvert.vertex.glsl | 10 + 6 files changed, 312 insertions(+), 50 deletions(-) create mode 100644 src/lwjgl/java/net/minestom/server/map/framebuffers/MapColorRenderer.java create mode 100644 src/lwjgl/resources/shaders/mapcolorconvert.fragment.glsl create mode 100644 src/lwjgl/resources/shaders/mapcolorconvert.vertex.glsl diff --git a/src/lwjgl/java/net/minestom/demo/largeframebuffers/Demo.java b/src/lwjgl/java/net/minestom/demo/largeframebuffers/Demo.java index 4607b36bb..8e6208f01 100644 --- a/src/lwjgl/java/net/minestom/demo/largeframebuffers/Demo.java +++ b/src/lwjgl/java/net/minestom/demo/largeframebuffers/Demo.java @@ -16,6 +16,7 @@ import net.minestom.server.map.MapColors; import net.minestom.server.map.framebuffers.LargeDirectFramebuffer; import net.minestom.server.map.framebuffers.LargeGLFWFramebuffer; import net.minestom.server.map.framebuffers.LargeGraphics2DFramebuffer; +import net.minestom.server.map.framebuffers.MapColorRenderer; import net.minestom.server.network.packet.server.play.MapDataPacket; import net.minestom.server.utils.Position; import net.minestom.server.utils.time.TimeUnit; @@ -43,17 +44,16 @@ public class Demo { LargeGraphics2DFramebuffer graphics2DFramebuffer = new LargeGraphics2DFramebuffer(512, 512); LargeGLFWFramebuffer glfwFramebuffer = new LargeGLFWFramebuffer(512, 512); - glfwFramebuffer.useMapColors(); - glfwFramebuffer.changeRenderingThreadToCurrent(); OpenGLRendering.init(); + MapColorRenderer renderer = new MapColorRenderer(glfwFramebuffer, OpenGLRendering::render); glfwFramebuffer.unbindContextFromThread(); // renderingLoop(0, directFramebuffer, Demo::directRendering); // renderingLoop(101, graphics2DFramebuffer, Demo::graphics2DRendering); renderingLoop(201, glfwFramebuffer, f -> {}); - glfwFramebuffer.setupRenderLoop(15, TimeUnit.MILLISECOND, () -> OpenGLRendering.render(glfwFramebuffer)); + glfwFramebuffer.setupRenderLoop(15, TimeUnit.MILLISECOND, renderer); for (int x = -2; x <= 2; x++) { for (int z = -2; z <= 2; z++) { diff --git a/src/lwjgl/java/net/minestom/demo/largeframebuffers/OpenGLRendering.java b/src/lwjgl/java/net/minestom/demo/largeframebuffers/OpenGLRendering.java index a4bb38c5d..6e7b3f6d9 100644 --- a/src/lwjgl/java/net/minestom/demo/largeframebuffers/OpenGLRendering.java +++ b/src/lwjgl/java/net/minestom/demo/largeframebuffers/OpenGLRendering.java @@ -1,14 +1,14 @@ package net.minestom.demo.largeframebuffers; -import net.minestom.server.map.MapColors; -import net.minestom.server.map.framebuffers.LargeGLFWFramebuffer; import org.joml.Matrix4f; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.GLUtil; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; -import java.io.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; import java.nio.ByteBuffer; import java.util.stream.Collectors; @@ -64,13 +64,11 @@ public final class OpenGLRendering { private static int projectionUniform; private static int viewUniform; private static int modelUniform; - private static int paletteTexture; private static int boxTexture; static void init() { GLUtil.setupDebugMessageCallback(); - paletteTexture = loadTexture("palette"); boxTexture = loadTexture("box"); vbo = glGenBuffers(); @@ -81,15 +79,6 @@ public final class OpenGLRendering { glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW); - glBindBuffer(GL_ARRAY_BUFFER, vbo); - - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, false, VERTEX_SIZE, 0); // position - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 2, GL_FLOAT, false, VERTEX_SIZE, 3*4); // color - - glBindBuffer(GL_ARRAY_BUFFER, 0); - // prepare matrices and shader renderShader = glCreateProgram(); projectionMatrix = new Matrix4f().setPerspective((float) (Math.PI/4f), 1f, 0.001f, 100f); @@ -107,8 +96,6 @@ public final class OpenGLRendering { projectionUniform = glGetUniformLocation(renderShader, "projection"); viewUniform = glGetUniformLocation(renderShader, "view"); modelUniform = glGetUniformLocation(renderShader, "model"); - int paletteSizeUniform = glGetUniformLocation(renderShader, "paletteSize"); - int paletteUniform = glGetUniformLocation(renderShader, "palette"); int boxUniform = glGetUniformLocation(renderShader, "box"); glUseProgram(renderShader); { @@ -116,9 +103,8 @@ public final class OpenGLRendering { uploadMatrix(viewUniform, viewMatrix); glUniform1i(boxUniform, 0); // texture unit 0 - glUniform1i(paletteUniform, 1); // texture unit 1 - glUniform1f(paletteSizeUniform, 236); } + glUseProgram(0); } private static int loadTexture(String filename) { @@ -166,7 +152,6 @@ public final class OpenGLRendering { private static int createShader(String filename, int type) { int shader = glCreateShader(type); try(BufferedReader reader = new BufferedReader(new InputStreamReader(OpenGLRendering.class.getResourceAsStream(filename)))) { - StringBuffer buffer = new StringBuffer(); String source = reader.lines().collect(Collectors.joining("\n")); glShaderSource(shader, source); glCompileShader(shader); @@ -180,7 +165,7 @@ public final class OpenGLRendering { private static int frame = 0; - static void render(LargeGLFWFramebuffer framebuffer) { + static void render() { if(frame % 100 == 0) { long time = System.currentTimeMillis(); long dt = time-lastTime; @@ -192,23 +177,24 @@ public final class OpenGLRendering { frame++; - glClearColor(MapColors.COLOR_BLACK.multiply53()/255.0f, 0f, 0f, 1f); + glClearColor(0f, 0f, 0f, 1f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_DEPTH_TEST); modelMatrix.rotateY((float) (Math.PI/60f)); - // TODO: render texture glUseProgram(renderShader); { - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, paletteTexture); - glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, boxTexture); uploadMatrix(modelUniform, modelMatrix); glBindBuffer(GL_ARRAY_BUFFER, vbo); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, false, VERTEX_SIZE, 0); // position + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, false, VERTEX_SIZE, 3*4); // color + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); glDrawElements(GL_TRIANGLES, indices.length, GL_UNSIGNED_INT, 0); } diff --git a/src/lwjgl/java/net/minestom/server/map/framebuffers/MapColorRenderer.java b/src/lwjgl/java/net/minestom/server/map/framebuffers/MapColorRenderer.java new file mode 100644 index 000000000..68d3fc658 --- /dev/null +++ b/src/lwjgl/java/net/minestom/server/map/framebuffers/MapColorRenderer.java @@ -0,0 +1,257 @@ +package net.minestom.server.map.framebuffers; + +import org.lwjgl.BufferUtils; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.util.stream.Collectors; + +import static org.lwjgl.opengl.GL30.*; + +/** + * Helper class designed to help OpenGL users to convert their RGB values to map colors inside a post processing pass + * with a shader provided by Minestom. + * + * When rendering to a {@link GLFWFramebuffer} or a {@link LargeGLFWFramebuffer}, wrap your rendering in a MapColorRenderer to render to the GLFW with map colors. + * + * {@link MapColorRenderer} sets up an OpenGL framebuffer with the size of the underlying framebuffer and renders to it. + * The initialization of the framebuffer is done in the constructor. + * Therefore, the constructor call should be done inside the thread linked to the OpenGL context. The context can + * be moved through {@link GLFWCapableBuffer#changeRenderingThreadToCurrent()} and {@link GLFWCapableBuffer#unbindContextFromThread()} + * + *
+ * Resources created in constructor are: + *
  • + * + * + * + * + * + * + * + *
  • + * + * The constructor also puts the given buffer in map color mode. + */ +public class MapColorRenderer implements Runnable { + + private final int fboID; + private final GLFWCapableBuffer framebuffer; + private final Runnable renderCode; + private final int colorTextureID; + private final int width; + private final int height; + private final int renderShader; + private final int screenQuadIndices; + private int paletteTexture; + private float paletteSize; + private final int screenQuadVAO; + + public MapColorRenderer(GLFWCapableBuffer framebuffer, Runnable renderCode) { + this(framebuffer, renderCode, MapColorRenderer.defaultFramebuffer(framebuffer.width(), framebuffer.height())); + } + + public MapColorRenderer(GLFWCapableBuffer framebuffer, Runnable renderCode, FboInitialization fboInitialization) { + this.framebuffer = framebuffer; + this.framebuffer.useMapColors(); + + this.renderCode = renderCode; + this.width = framebuffer.width(); + this.height = framebuffer.height(); + + this.fboID = glGenFramebuffers(); + glBindFramebuffer(GL_FRAMEBUFFER, fboID); + this.colorTextureID = fboInitialization.initFbo(fboID); + + if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + throw new RuntimeException("Framebuffer is not complete!"); + } + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + // create post-process shader + this.renderShader = glCreateProgram(); + int vertexShader = createShader("/shaders/mapcolorconvert.vertex.glsl", GL_VERTEX_SHADER); + int fragmentShader = createShader("/shaders/mapcolorconvert.fragment.glsl", GL_FRAGMENT_SHADER); + glAttachShader(renderShader, vertexShader); + glAttachShader(renderShader, fragmentShader); + glLinkProgram(renderShader); + if(glGetProgrami(renderShader, GL_LINK_STATUS) == 0) { + throw new RuntimeException("Link error: "+glGetProgramInfoLog(renderShader)); + } + + loadPalette("palette"); + + // create screen quad VAO + screenQuadVAO = glGenBuffers(); + glBindBuffer(GL_ARRAY_BUFFER, screenQuadVAO); + glBufferData(GL_ARRAY_BUFFER, new float[] { + -1f, -1f, + 1f, -1f, + 1f, 1f, + -1f, 1f + }, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + screenQuadIndices = glGenBuffers(); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, screenQuadIndices); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, new int[] {0,1,2, 2,3,0}, GL_STATIC_DRAW); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + int paletteSizeUniform = glGetUniformLocation(renderShader, "paletteSize"); + int paletteUniform = glGetUniformLocation(renderShader, "palette"); + int frameUniform = glGetUniformLocation(renderShader, "frame"); + + glUseProgram(renderShader); { + glUniform1i(frameUniform, 0); // texture unit 0 + glUniform1i(paletteUniform, 1); // texture unit 1 + glUniform1f(paletteSizeUniform, paletteSize); + } + glUseProgram(0); + } + + private static FboInitialization defaultFramebuffer(int width, int height) { + return fboId -> defaultFramebufferInit(fboId, width, height); + } + + private static int defaultFramebufferInit(int fbo, int width, int height) { + // color + int colorTexture = glGenTextures(); + glBindTexture(GL_TEXTURE_2D, colorTexture); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0L); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + // attach to framebuffer + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexture, 0); + glBindTexture(GL_TEXTURE_2D, 0); + + // depth + int depthStencilBuffer = glGenRenderbuffers(); + glBindRenderbuffer(GL_RENDERBUFFER, depthStencilBuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthStencilBuffer); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + return colorTexture; + } + + @Override + public void run() { + glViewport(0, 0, width, height); + // run user code inside of framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, fboID); + renderCode.run(); + + // run post processing to display on screen + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glClearColor(0f, 0f, 0f, 1f); // 0 on RED channel makes maps use NONE + glClear(GL_COLOR_BUFFER_BIT); + glDisable(GL_DEPTH_TEST); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, colorTextureID); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, paletteTexture); + + glUseProgram(renderShader); { + // render post processing quad + glBindBuffer(GL_ARRAY_BUFFER, screenQuadVAO); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, false, 2*4, 0); // position + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, screenQuadIndices); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); + }; + + glUseProgram(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, 0); + } + + /** + * Frees OpenGL resources used by this renderer. + * You should NOT render with this renderer after this call. + */ + public void cleanupResources() { + glDeleteFramebuffers(fboID); + glDeleteProgram(renderShader); + glDeleteTextures(paletteTexture); + // TODO: more cleanup + } + + private void loadPalette(String filename) { + int tex = glGenTextures(); + glBindTexture(GL_TEXTURE_2D, tex); + BufferedImage image; + try { + image = ImageIO.read(MapColorRenderer.class.getResourceAsStream("/textures/"+filename+".png")); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Missing image "+filename, e); + } + ByteBuffer pixels = BufferUtils.createByteBuffer(image.getWidth()*image.getHeight()*4); + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + int rgb = image.getRGB(x, y); + int alpha = (rgb >> 24) & 0xFF; + int red = (rgb >> 16) & 0xFF; + int green = (rgb >> 8) & 0xFF; + int blue = rgb & 0xFF; + pixels.put((byte) red); + pixels.put((byte) green); + pixels.put((byte) blue); + pixels.put((byte) alpha); + } + } + pixels.flip(); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.getWidth(), image.getHeight(), 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + + // closest neighbor required here, as pixels can have very different rgb values, and interpolation will break palette lookup + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glBindTexture(GL_TEXTURE_2D, 0); + + this.paletteTexture = tex; + this.paletteSize = image.getWidth(); + } + + private static int createShader(String filename, int type) { + int shader = glCreateShader(type); + try(BufferedReader reader = new BufferedReader(new InputStreamReader(MapColorRenderer.class.getResourceAsStream(filename)))) { + String source = reader.lines().collect(Collectors.joining("\n")); + glShaderSource(shader, source); + glCompileShader(shader); + } catch (IOException e) { + e.printStackTrace(); + } + return shader; + } + + @FunctionalInterface + public interface FboInitialization { + + /** + * Initializes the given framebuffer + * @param fboId + * @return the texture ID of the color texture, used for post processing. + */ + int initFbo(int fboId); + } + +} + + + diff --git a/src/lwjgl/resources/shaders/fragment.glsl b/src/lwjgl/resources/shaders/fragment.glsl index c17da90c7..587f22199 100644 --- a/src/lwjgl/resources/shaders/fragment.glsl +++ b/src/lwjgl/resources/shaders/fragment.glsl @@ -5,29 +5,8 @@ in vec2 uv; out vec4 fragColor; uniform sampler2D box; -uniform sampler2D palette; -uniform float paletteSize; void main() { vec3 vertexColor = texture(box, uv).rgb; - - - // render in map colors - int closest = 0; - uint closestDistance = uint(2147483647); - for(int i = 4; i < paletteSize; i++) { - vec3 mapColor = texture(palette, vec2((i+0.5f)/paletteSize, 0.0)).rgb; - int dr = int((mapColor.r - vertexColor.r)*255); - int dg = int((mapColor.g - vertexColor.g)*255); - int db = int((mapColor.b - vertexColor.b)*255); - - uint d = uint(dr*dr)+uint(dg*dg)+uint(db*db); - if(d < closestDistance) { - closestDistance = d; - closest = i; - } - } - - fragColor = vec4(closest/255.0, closest/255.0, closest/255.0, 1.0); - //fragColor = vec4(vertexColor, 1.0); + fragColor = vec4(vertexColor, 1.0); } \ No newline at end of file diff --git a/src/lwjgl/resources/shaders/mapcolorconvert.fragment.glsl b/src/lwjgl/resources/shaders/mapcolorconvert.fragment.glsl new file mode 100644 index 000000000..1eeef10ec --- /dev/null +++ b/src/lwjgl/resources/shaders/mapcolorconvert.fragment.glsl @@ -0,0 +1,30 @@ +#version 330 + +in vec2 fragCoords; +out vec4 fragColor; + +uniform sampler2D frame; +uniform sampler2D palette; +uniform float paletteSize; + +void main() { + vec3 fragmentColor = texture(frame, fragCoords).rgb; + + // render in map colors + int closest = 0; + uint closestDistance = uint(2147483647); + for(int i = 4; i < paletteSize; i++) { + vec3 mapColor = texture(palette, vec2((i+0.5f)/paletteSize, 0.0)).rgb; + int dr = int((mapColor.r - fragmentColor.r)*255); + int dg = int((mapColor.g - fragmentColor.g)*255); + int db = int((mapColor.b - fragmentColor.b)*255); + + uint d = uint(dr*dr)+uint(dg*dg)+uint(db*db); + if(d < closestDistance) { + closestDistance = d; + closest = i; + } + } + + fragColor = vec4(closest/255.0, closest/255.0, closest/255.0, 1.0); +} \ No newline at end of file diff --git a/src/lwjgl/resources/shaders/mapcolorconvert.vertex.glsl b/src/lwjgl/resources/shaders/mapcolorconvert.vertex.glsl new file mode 100644 index 000000000..b3d557dbe --- /dev/null +++ b/src/lwjgl/resources/shaders/mapcolorconvert.vertex.glsl @@ -0,0 +1,10 @@ +#version 330 + +layout(location = 0) in vec2 pos; + +out vec2 fragCoords; + +void main() { + fragCoords = (pos+vec2(1.0))/2.0; + gl_Position = vec4(pos, 0.0, 1.0); +} \ No newline at end of file