Large framebuffers for more than 128x128 rendering

This commit is contained in:
jglrxavpok 2020-08-11 00:35:25 +02:00
parent a7139d19b6
commit 6856904905
10 changed files with 521 additions and 93 deletions

View File

@ -0,0 +1,137 @@
package net.minestom.demo.largeframebuffers;
import fr.themode.demo.MainDemo;
import net.minestom.server.MinecraftServer;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.type.decoration.EntityItemFrame;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceManager;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.item.metadata.MapMeta;
import net.minestom.server.map.Framebuffer;
import net.minestom.server.map.LargeFramebuffer;
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.network.packet.server.play.MapDataPacket;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.time.TimeUnit;
import java.awt.*;
import java.util.Arrays;
import java.util.function.Consumer;
import static org.lwjgl.opengl.GL11.*;
public class Demo {
public static void main(String[] args) {
MainDemo.main(args); // used to avoid code duplication
initDemo();
}
private static void initDemo() {
MinecraftServer.getConnectionManager().addPlayerInitialization(player -> {
player.setGameMode(GameMode.CREATIVE);
});
InstanceManager instances = MinecraftServer.getInstanceManager();
Instance instance = instances.getInstances().stream().findAny().get();
LargeDirectFramebuffer directFramebuffer = new LargeDirectFramebuffer(512, 512);
LargeGraphics2DFramebuffer graphics2DFramebuffer = new LargeGraphics2DFramebuffer(512, 512);
LargeGLFWFramebuffer glfwFramebuffer = new LargeGLFWFramebuffer(512, 512);
renderingLoop(0, directFramebuffer, Demo::directRendering);
renderingLoop(101, graphics2DFramebuffer, Demo::graphics2DRendering);
renderingLoop(201, glfwFramebuffer, f -> {});
glfwFramebuffer.setupRenderLoop(30, TimeUnit.MILLISECOND, Demo::openGLRendering);
for (int x = -2; x <= 2; x++) {
for (int z = -2; z <= 2; z++) {
instance.loadChunk(x, z);
}
}
setupMaps(instance, 0, 10);
setupMaps(instance, 101, 20);
setupMaps(instance, 201, 30);
}
private static void createFrame(Instance instance, int id, int x, int y, int z) {
EntityItemFrame itemFrame = new EntityItemFrame(new Position(x, y, z), EntityItemFrame.ItemFrameOrientation.NORTH);
itemFrame.getPosition().setYaw(180f);
ItemStack map = new ItemStack(Material.FILLED_MAP, (byte)1);
map.setItemMeta(new MapMeta(id));
itemFrame.setItemStack(map);
itemFrame.setInstance(instance);
itemFrame.setCustomNameVisible(true);
itemFrame.setCustomName(ColoredText.of("MapID: "+id));
}
private static void setupMaps(Instance instance, int mapIDStart, int zCoordinate) {
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
createFrame(instance, mapIDStart+y*4+x, 2-x, 45-y, zCoordinate);
}
}
}
private static <T extends LargeFramebuffer> void renderingLoop(int mapIDStart, T framebuffer, Consumer<T> renderingCode) {
final Framebuffer[] subviews = new Framebuffer[4*4];
for (int i = 0; i < subviews.length; i++) {
int x = (i % 4)*128;
int y = (i / 4)*128;
subviews[i] = framebuffer.createSubView(x, y);
}
MinecraftServer.getSchedulerManager().buildTask(() -> {
renderingCode.accept(framebuffer);
for (int i = 0; i < subviews.length; i++) {
final MapDataPacket packet = new MapDataPacket();
packet.mapId = mapIDStart + i;
Framebuffer f = subviews[i];
f.preparePacket(packet);
sendPacket(packet);
}
}).repeat(30, TimeUnit.MILLISECOND).schedule();
}
private static void sendPacket(MapDataPacket packet) {
MinecraftServer.getConnectionManager().getOnlinePlayers().forEach(p -> p.getPlayerConnection().sendPacket(packet));
}
private static void directRendering(LargeDirectFramebuffer framebuffer) {
Arrays.fill(framebuffer.getColors(), 0, 512*40+128, MapColors.COLOR_CYAN.baseColor());
Arrays.fill(framebuffer.getColors(), 512*40+128, framebuffer.getColors().length, MapColors.COLOR_RED.baseColor());
}
private static void graphics2DRendering(LargeGraphics2DFramebuffer framebuffer) {
Graphics2D renderer = framebuffer.getRenderer();
renderer.setColor(Color.BLACK);
renderer.clearRect(0,0,512,512);
renderer.setColor(Color.WHITE);
renderer.drawString("Here's a very very long string that needs multiple maps to fit", 0, 100);
}
private static void openGLRendering() {
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();
}
}

View File

@ -0,0 +1,131 @@
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 java.nio.ByteBuffer;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.glfw.GLFW.glfwTerminate;
import static org.lwjgl.opengl.GL11.*;
abstract class GLFWCapableBuffer {
protected final byte[] colors;
private final ByteBuffer pixels;
private final long glfwWindow;
private final int width;
private final int height;
protected GLFWCapableBuffer(int width, int height) {
this(width, height, GLFW_NATIVE_CONTEXT_API, GLFW_OPENGL_API);
}
/**
* Creates the framebuffer and initializes a new context
*/
protected GLFWCapableBuffer(int width, int height, int apiContext, int clientAPI) {
this.width = width;
this.height = height;
this.colors = new byte[width*height];
this.pixels = BufferUtils.createByteBuffer(width*height*4);
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 GLFWCapableBuffer 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();
}
// TODO: provide shader that performs the conversion automatically, would be a lot faster
/**
* 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, width)*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, width)] = MapColors.closestColor(argb).getIndex();
}
}
}
public void cleanup() {
glfwTerminate();
}
public long getGLFWWindow() {
return glfwWindow;
}
public int width() {
return width;
}
public int height() {
return height;
}
}

View File

@ -1,20 +1,13 @@
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 java.nio.ByteBuffer;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.glfw.GLFW.GLFW_NATIVE_CONTEXT_API;
import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_API;
/**
* GLFW-based framebuffer.
@ -33,101 +26,20 @@ import static org.lwjgl.opengl.GL11.*;
*
* This framebuffer is meant to render to a single map (ie it is only compatible with 128x128 rendering)
*/
public class GLFWFramebuffer implements Framebuffer {
public class GLFWFramebuffer extends GLFWCapableBuffer 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
* Creates the framebuffer and initializes a new 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;
super(WIDTH, HEIGHT, apiContext, clientAPI);
}
@Override

View File

@ -0,0 +1,27 @@
package net.minestom.server.map.framebuffers;
import net.minestom.server.map.Framebuffer;
import net.minestom.server.map.LargeFramebuffer;
import static org.lwjgl.glfw.GLFW.GLFW_NATIVE_CONTEXT_API;
import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_API;
public class LargeGLFWFramebuffer extends GLFWCapableBuffer implements LargeFramebuffer {
public LargeGLFWFramebuffer(int width, int height) {
this(width, height, GLFW_NATIVE_CONTEXT_API, GLFW_OPENGL_API);
}
public LargeGLFWFramebuffer(int width, int height, int apiContext, int clientAPI) {
super(width, height, apiContext, clientAPI);
}
@Override
public Framebuffer createSubView(int left, int top) {
return new LargeFramebufferDefaultView(this, left, top);
}
@Override
public byte getMapColor(int x, int y) {
return colors[Framebuffer.index(x, y, width())];
}
}

View File

@ -22,6 +22,8 @@ public class EntityItemFrame extends ObjectEntity {
super(EntityType.ITEM_FRAME, spawnPosition);
this.orientation = orientation;
this.rotation = Rotation.NONE;
setNoGravity(true);
setGravity(0f);
}
@Override

View File

@ -0,0 +1,48 @@
package net.minestom.server.map;
import net.minestom.server.network.packet.server.play.MapDataPacket;
/**
* Framebuffer that is meant to be split in sub-framebuffers. Contrary to Framebuffer, LargeFramebuffer supports sizes over 128x128 pixels.
*/
public interface LargeFramebuffer {
int width();
int height();
/**
* Returns a new Framebuffer that represent a 128x128 sub-view of this framebuffer.
* Implementations are free (but not guaranteed) to throw exceptions if left & top produces out-of-bounds coordinates.
* @param left
* @param top
* @return
*/
Framebuffer createSubView(int left, int top);
byte getMapColor(int x, int y);
/**
* Prepares the packet to render a 128x128 sub view of this framebuffer
* @param packet
* @param left
* @param top
*/
default void preparePacket(MapDataPacket packet, int left, int top) {
byte[] colors = new byte[width()*height()];
int width = Math.min(width(), left+Framebuffer.WIDTH) - left;
int height = Math.min(height(), top+Framebuffer.HEIGHT) - top;
for (int y = top; y < height; y++) {
for (int x = left; x < width; x++) {
byte color = getMapColor(left, top);
colors[Framebuffer.index(x-left, y-top, width)] = color;
}
}
packet.columns = (short) width;
packet.rows = (short) height;
packet.icons = new MapDataPacket.Icon[0];
packet.x = 0;
packet.z = 0;
packet.data = colors;
}
}

View File

@ -1,6 +1,7 @@
package net.minestom.server.map.framebuffers;
import net.minestom.server.map.Framebuffer;
import net.minestom.server.map.LargeFramebuffer;
import net.minestom.server.map.MapColors;
import java.awt.*;

View File

@ -0,0 +1,65 @@
package net.minestom.server.map.framebuffers;
import net.minestom.server.map.Framebuffer;
import net.minestom.server.map.LargeFramebuffer;
import net.minestom.server.map.MapColors;
/**
* Large framebuffer with direct access to the colors array.
*
* This implementation does not throw errors when accessing out-of-bounds coordinates through sub-views, and will instead
* use {@link MapColors#NONE}. This is only the case for sub-views, access through {@link #setMapColor(int, int, byte)}
* and {@link #getMapColor(int, int)} will throw an exception if out-of-bounds coordinates are inputted.
*/
public class LargeDirectFramebuffer implements LargeFramebuffer {
private final int width;
private final int height;
private final byte[] colors;
/**
* Creates a new {@link LargeDirectFramebuffer} with the desired size
* @param width
* @param height
*/
public LargeDirectFramebuffer(int width, int height) {
this.width = width;
this.height = height;
this.colors = new byte[width*height];
}
@Override
public int width() {
return width;
}
@Override
public int height() {
return height;
}
@Override
public Framebuffer createSubView(int left, int top) {
return new LargeFramebufferDefaultView(this, left, top);
}
public LargeDirectFramebuffer setMapColor(int x, int y, byte color) {
if(!bounds(x, y)) throw new IndexOutOfBoundsException("Invalid x;y coordinate: "+x+";"+y);
colors[y*width+x] = color;
return this;
}
@Override
public byte getMapColor(int x, int y) {
if(!bounds(x, y)) throw new IndexOutOfBoundsException("Invalid x;y coordinate: "+x+";"+y);
return colors[y*width+x];
}
private boolean bounds(int x, int y) {
return x >= 0 && x < width && y >= 0 && y < height;
}
public byte[] getColors() {
return colors;
}
}

View File

@ -0,0 +1,37 @@
package net.minestom.server.map.framebuffers;
import net.minestom.server.map.Framebuffer;
import net.minestom.server.map.LargeFramebuffer;
import net.minestom.server.map.MapColors;
public class LargeFramebufferDefaultView implements Framebuffer {
private final LargeFramebuffer parent;
private final int x;
private final int y;
private final byte[] colors = new byte[WIDTH*HEIGHT];
public LargeFramebufferDefaultView(LargeFramebuffer parent, int x, int y) {
this.parent = parent;
this.x = x;
this.y = y;
}
private boolean bounds(int x, int y) {
return x >= 0 && x < parent.width() && y >= 0 && y < parent.height();
}
private byte colorOrNone(int x, int y) {
if(!bounds(x, y)) return MapColors.NONE.baseColor();
return parent.getMapColor(x, y);
}
@Override
public byte[] toMapColors() {
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
colors[Framebuffer.index(x, y)] = colorOrNone(x+this.x, y+this.y);
}
}
return colors;
}
}

View File

@ -0,0 +1,68 @@
package net.minestom.server.map.framebuffers;
import net.minestom.server.map.Framebuffer;
import net.minestom.server.map.LargeFramebuffer;
import net.minestom.server.map.MapColors;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
/**
* LargeFramebuffer that embeds a BufferedImage, allowing for rendering directly via Graphics2D or its pixel array
*/
public class LargeGraphics2DFramebuffer implements LargeFramebuffer {
private final byte[] colors;
private final BufferedImage backingImage;
private final Graphics2D renderer;
private final int[] pixels;
private final int width;
private final int height;
public LargeGraphics2DFramebuffer(int width, int height) {
this.width = width;
this.height = height;
colors = new byte[width*height];
backingImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
renderer = backingImage.createGraphics();
pixels = ((DataBufferInt)backingImage.getRaster().getDataBuffer()).getData();
}
public Graphics2D getRenderer() {
return renderer;
}
public BufferedImage getBackingImage() {
return backingImage;
}
public int get(int x, int z) {
return pixels[x+z*width]; // stride is always the width of the image
}
public LargeGraphics2DFramebuffer set(int x, int z, int rgb) {
pixels[x+z*width] = rgb;
return this;
}
@Override
public int width() {
return width;
}
@Override
public int height() {
return height;
}
@Override
public Framebuffer createSubView(int left, int top) {
return new LargeFramebufferDefaultView(this, left, top);
}
@Override
public byte getMapColor(int x, int y) {
return MapColors.closestColor(get(x, y)).getIndex();
}
}