From 247dfeefc86f16f1df8f65383158ea7cf7e2242e Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Tue, 4 Aug 2020 23:01:35 +0200 Subject: [PATCH] OpenGL rendering on maps --- build.gradle | 32 +++++ .../fr/themode/demo/map/MapAnimationDemo.java | 44 ++++-- .../map/framebuffers/GLFWFramebuffer.java | 136 ++++++++++++++++++ 3 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 src/main/java/net/minestom/server/map/framebuffers/GLFWFramebuffer.java diff --git a/build.gradle b/build.gradle index fc57cfc04..04b0b65d3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,28 @@ +import org.gradle.internal.os.OperatingSystem + plugins { id 'java-library' id 'net.ltgt.apt' version '0.10' id 'org.jetbrains.kotlin.jvm' version '1.3.72' } +project.ext.lwjglVersion = "3.2.3" + +switch (OperatingSystem.current()) { + case OperatingSystem.LINUX: + def osArch = System.getProperty("os.arch") + project.ext.lwjglNatives = osArch.startsWith("arm") || osArch.startsWith("aarch64") + ? "natives-linux-${osArch.contains("64") || osArch.startsWith("armv8") ? "arm64" : "arm32"}" + : "natives-linux" + break + case OperatingSystem.MAC_OS: + project.ext.lwjglNatives = "natives-macos" + break + case OperatingSystem.WINDOWS: + project.ext.lwjglNatives = System.getProperty("os.arch").contains("64") ? "natives-windows" : "natives-windows-x86" + break +} + allprojects { repositories { mavenCentral() @@ -74,4 +93,17 @@ dependencies { api "org.jetbrains.kotlin:kotlin-stdlib-jdk8" api 'com.github.jglrxavpok:Hephaistos:v1.0.4' + + // LWJGL, for map rendering + implementation platform("org.lwjgl:lwjgl-bom:$lwjglVersion") + + api "org.lwjgl:lwjgl" + api "org.lwjgl:lwjgl-egl" + api "org.lwjgl:lwjgl-opengl" + api "org.lwjgl:lwjgl-opengles" + api "org.lwjgl:lwjgl-glfw" + runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-opengles::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-glfw::$lwjglNatives" } diff --git a/src/main/java/fr/themode/demo/map/MapAnimationDemo.java b/src/main/java/fr/themode/demo/map/MapAnimationDemo.java index a82b6f614..a2c7f9504 100644 --- a/src/main/java/fr/themode/demo/map/MapAnimationDemo.java +++ b/src/main/java/fr/themode/demo/map/MapAnimationDemo.java @@ -5,21 +5,23 @@ import net.minestom.server.event.player.PlayerSpawnEvent; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.item.metadata.MapMeta; -import net.minestom.server.map.MapColors; +import net.minestom.server.map.framebuffers.GLFWFramebuffer; import net.minestom.server.map.framebuffers.Graphics2DFramebuffer; import net.minestom.server.network.packet.server.play.MapDataPacket; import net.minestom.server.timer.SchedulerManager; import net.minestom.server.utils.time.TimeUnit; -import javax.imageio.ImageIO; import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; + +import static org.lwjgl.opengl.GL11.*; public class MapAnimationDemo { public static final int MAP_ID = 1; + public static final int EGL_MAP_ID = 2; + + private static final Graphics2DFramebuffer framebuffer = new Graphics2DFramebuffer(); + private static final GLFWFramebuffer glfwFramebuffer = new GLFWFramebuffer(); public static void init() { SchedulerManager scheduler = MinecraftServer.getSchedulerManager(); @@ -30,11 +32,31 @@ public class MapAnimationDemo { ItemStack map = new ItemStack(Material.FILLED_MAP, (byte) 1); map.setItemMeta(new MapMeta(MAP_ID)); player.getInventory().addItemStack(map); + + ItemStack map2 = new ItemStack(Material.FILLED_MAP, (byte) 1); + map2.setItemMeta(new MapMeta(EGL_MAP_ID)); + player.getInventory().addItemStack(map2); }); }); - } - private static final Graphics2DFramebuffer framebuffer = new Graphics2DFramebuffer(); + glfwFramebuffer.setupRenderLoop(16, TimeUnit.MILLISECOND, () -> { + glClearColor(0f, 0f, 0f, 1f); + glClear(GL_COLOR_BUFFER_BIT); + + glBegin(GL_TRIANGLES); + + glVertex2f(0, -0.75f); + glColor3f(1f, 0f, 0f); + + glVertex2f(0.75f, 0.75f); + glColor3f(0f, 1f, 0f); + + glVertex2f(-0.75f, 0.75f); + glColor3f(0f, 0f, 1f); + + glEnd(); + }); + } private static float time = 0f; private static long lastTime = System.currentTimeMillis(); @@ -68,7 +90,13 @@ public class MapAnimationDemo { MapDataPacket mapDataPacket = new MapDataPacket(); mapDataPacket.mapId = MAP_ID; - framebuffer.preparePacket(mapDataPacket, 32, 32, 64+32, 64+32); + framebuffer.preparePacket(mapDataPacket); + MinecraftServer.getConnectionManager().getOnlinePlayers().forEach(p -> { + p.getPlayerConnection().sendPacket(mapDataPacket); + }); + + mapDataPacket.mapId = EGL_MAP_ID; + glfwFramebuffer.preparePacket(mapDataPacket); MinecraftServer.getConnectionManager().getOnlinePlayers().forEach(p -> { p.getPlayerConnection().sendPacket(mapDataPacket); }); diff --git a/src/main/java/net/minestom/server/map/framebuffers/GLFWFramebuffer.java b/src/main/java/net/minestom/server/map/framebuffers/GLFWFramebuffer.java new file mode 100644 index 000000000..6ba82a0c1 --- /dev/null +++ b/src/main/java/net/minestom/server/map/framebuffers/GLFWFramebuffer.java @@ -0,0 +1,136 @@ +package net.minestom.server.map.framebuffers; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.map.Framebuffer; +import net.minestom.server.map.MapColors; +import net.minestom.server.timer.Task; +import net.minestom.server.utils.time.TimeUnit; +import org.lwjgl.BufferUtils; +import org.lwjgl.PointerBuffer; +import org.lwjgl.glfw.GLFWErrorCallback; +import org.lwjgl.opengl.GL; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; + +import java.nio.ByteBuffer; + +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.glfw.GLFW.*; + +/** + * GLFW-based framebuffer. + * + * Due to its interfacing with OpenGL(-ES), extra care needs to be applied when using this framebuffer. + * Rendering to this framebuffer should only be done via the thread on which the context is present. + * To perform map conversion at the end of a frame, it is advised to use {@link #render(Runnable)} to render to the map. + * + * Use {@link #changeRenderingThreadToCurrent} in a thread to switch the thread on which to render. + * + * Use {@link #setupRenderLoop} with a callback to setup a task in the {@link net.minestom.server.timer.SchedulerManager} + * to automatically render to the offscreen buffer on a specialized thread. + * + * GLFWFramebuffer does not provide guarantee that the result of {@link #toMapColors()} is synchronized with rendering, but + * it will be updated after each frame rendered through {@link #render(Runnable)} or {@link #setupRenderLoop(long, TimeUnit, Runnable)}. + */ +public class GLFWFramebuffer implements Framebuffer { + + private final byte[] colors = new byte[WIDTH*HEIGHT]; + private final ByteBuffer pixels = BufferUtils.createByteBuffer(WIDTH*HEIGHT*4); + private final long glfwWindow; + + public GLFWFramebuffer() { + this(GLFW_NATIVE_CONTEXT_API, GLFW_OPENGL_API); + } + + /** + * Creates the framebuffer and initializes a new EGL context + */ + public GLFWFramebuffer(int apiContext, int clientAPI) { + if(!glfwInit()) { + throw new RuntimeException("Failed to init GLFW"); + } + + GLFWErrorCallback.createPrint().set(); + glfwDefaultWindowHints(); + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + + glfwWindowHint(GLFW_CONTEXT_CREATION_API, apiContext); + glfwWindowHint(GLFW_CLIENT_API, clientAPI); + + this.glfwWindow = glfwCreateWindow(WIDTH, HEIGHT, "", 0L, 0L); + if(glfwWindow == 0L) { + try(var stack = MemoryStack.stackPush()) { + PointerBuffer desc = stack.mallocPointer(1); + int errcode = glfwGetError(desc); + throw new RuntimeException("("+errcode+") Failed to create GLFW Window."); + } + } + } + + public GLFWFramebuffer unbindContextFromThread() { + glfwMakeContextCurrent(0L); + return this; + } + + public void changeRenderingThreadToCurrent() { + glfwMakeContextCurrent(glfwWindow); + GL.createCapabilities(); + } + + public Task setupRenderLoop(long period, TimeUnit unit, Runnable rendering) { + return MinecraftServer.getSchedulerManager() + .buildTask(new Runnable() { + private boolean first = true; + + @Override + public void run() { + if(first) { + changeRenderingThreadToCurrent(); + first = false; + } + render(rendering); + } + }) + .repeat(period, unit) + .schedule(); + } + + public void render(Runnable rendering) { + rendering.run(); + glfwSwapBuffers(glfwWindow); + prepareMapColors(); + } + + /** + * Called in render after glFlush to read the pixel buffer contents and convert it to map colors. + * Only call if you do not use {@link #render(Runnable)} nor {@link #setupRenderLoop(long, TimeUnit, Runnable)} + */ + public void prepareMapColors() { + glReadPixels(0, 0, WIDTH, HEIGHT, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + for (int y = 0; y < HEIGHT; y++) { + for (int x = 0; x < WIDTH; x++) { + int i = Framebuffer.index(x, y)*4; + int red = pixels.get(i) & 0xFF; + int green = pixels.get(i+1) & 0xFF; + int blue = pixels.get(i+2) & 0xFF; + int alpha = pixels.get(i+3) & 0xFF; + int argb = (alpha << 24) | (red << 16) | (green << 8) | blue; + colors[Framebuffer.index(x, y)] = MapColors.closestColor(argb).getIndex(); + } + } + } + + public void cleanup() { + glfwTerminate(); + } + + public long getGLFWWindow() { + return glfwWindow; + } + + @Override + public byte[] toMapColors() { + return colors; + } +}