diff --git a/client/src/main/java/nl/andrewl/aos2_client/Client.java b/client/src/main/java/nl/andrewl/aos2_client/Client.java index d09fae1..855308c 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/Client.java +++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java @@ -213,6 +213,7 @@ public class Client implements Runnable { player.updateModelTransform(); soundManager.playWalkingSounds(player, now); } + gameRenderer.getGuiRenderer().updateNamePlates(players.values()); } public void interpolateProjectiles(float dt) { diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java b/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java index 373db8d..019898f 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java @@ -171,6 +171,10 @@ public class GameRenderer { return camera; } + public GuiRenderer getGuiRenderer() { + return guiRenderer; + } + public void draw() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); chunkRenderer.draw(camera, client.getWorld().getChunkMeshesToDraw()); @@ -266,6 +270,7 @@ public class GameRenderer { // GUI rendering guiRenderer.start(); + guiRenderer.drawNameplates(myPlayer, camera.getViewTransformData(), perspectiveTransform.get(new float[16])); guiRenderer.drawNvg(screenWidth, screenHeight, myPlayer); guiRenderer.end(); diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/gui/GuiRenderer.java b/client/src/main/java/nl/andrewl/aos2_client/render/gui/GuiRenderer.java index da60c88..416372d 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/gui/GuiRenderer.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/gui/GuiRenderer.java @@ -1,8 +1,11 @@ package nl.andrewl.aos2_client.render.gui; +import nl.andrewl.aos2_client.Camera; import nl.andrewl.aos2_client.model.ClientPlayer; +import nl.andrewl.aos2_client.model.OtherPlayer; import nl.andrewl.aos2_client.render.ShaderProgram; import nl.andrewl.aos_core.FileUtils; +import nl.andrewl.aos_core.model.Player; import nl.andrewl.aos_core.model.item.BlockItem; import nl.andrewl.aos_core.model.item.BlockItemStack; import nl.andrewl.aos_core.model.item.Gun; @@ -12,11 +15,12 @@ import org.lwjgl.BufferUtils; import org.lwjgl.nanovg.NVGColor; import org.lwjgl.nanovg.NVGPaint; +import java.awt.*; +import java.awt.image.BufferedImage; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.FloatBuffer; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import static org.lwjgl.nanovg.NanoVG.*; import static org.lwjgl.nanovg.NanoVGGL3.*; @@ -49,7 +53,13 @@ public class GuiRenderer { private final Matrix4f transformMatrix; private final float[] transformMatrixData; - private final Map textures = new HashMap<>(); + private final ShaderProgram namePlateShaderProgram; + private final int namePlateTransformUniform; + private final int namePlateViewTransformUniform; + private final int namePlatePerspectiveTransformUniform; + private final int namePlateTextureSamplerUniform; + private final Font namePlateFont; + private final Map playerNamePlates = new HashMap<>(); public GuiRenderer() throws IOException { vgId = nvgCreate(NVG_ANTIALIAS); @@ -85,20 +95,26 @@ public class GuiRenderer { transformUniformLocation = shaderProgram.getUniform("transform"); textureSamplerUniform = shaderProgram.getUniform("guiTexture"); shaderProgram.bindAttribute(0, "position"); - this.transformMatrix = new Matrix4f(); - this.transformMatrixData = new float[16]; - } - public void loadTexture(String name, String resource) { - try { - textures.put(name, new GUITexture(resource)); - } catch (IOException e) { + // Shader program for rendering name plates. + namePlateShaderProgram = new ShaderProgram.Builder() + .withShader("shader/gui/nameplate_vertex.glsl", GL_VERTEX_SHADER) + .withShader("shader/gui/nameplate_fragement.glsl", GL_FRAGMENT_SHADER) + .build(); + namePlateTransformUniform = namePlateShaderProgram.getUniform("transform"); + namePlateViewTransformUniform = namePlateShaderProgram.getUniform("viewTransform"); + namePlatePerspectiveTransformUniform = namePlateShaderProgram.getUniform("perspectiveTransform"); + namePlateTextureSamplerUniform = namePlateShaderProgram.getUniform("guiTexture"); + namePlateShaderProgram.bindAttribute(0, "vertexPosition"); + + try (var in = FileUtils.getClasspathResource("text/JetBrainsMono-Regular.ttf")) { + namePlateFont = Font.createFont(Font.TRUETYPE_FONT, in).deriveFont(84f); + } catch (FontFormatException e) { throw new RuntimeException(e); } - } - public void addTexture(String name, GUITexture texture) { - textures.put(name, texture); + this.transformMatrix = new Matrix4f(); + this.transformMatrixData = new float[16]; } public void start() { @@ -122,14 +138,86 @@ public class GuiRenderer { glDrawArrays(GL_TRIANGLE_STRIP, 0, vertexCount); } + private void addNamePlate(OtherPlayer player) { + GUITexture texture = new GUITexture(generateNameplateImage(player.getUsername())); + playerNamePlates.put(player, texture); + } + + private void removeNamePlate(OtherPlayer player) { + GUITexture texture = playerNamePlates.remove(player); + texture.free(); + } + + private void retainNamePlates(Collection players) { + Set removalSet = new HashSet<>(playerNamePlates.keySet()); + removalSet.removeAll(players); + for (OtherPlayer playerToRemove : removalSet) { + removeNamePlate(playerToRemove); + } + } + + public void updateNamePlates(Collection players) { + for (OtherPlayer player : players) { + if (!playerNamePlates.containsKey(player)) { + addNamePlate(player); + } + } + retainNamePlates(players); + } + + private BufferedImage generateNameplateImage(String username) { + BufferedImage testImg = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + Graphics2D testGraphics = testImg.createGraphics(); + testGraphics.setFont(namePlateFont); + int textWidth = testGraphics.getFontMetrics(namePlateFont).stringWidth(username); + int textHeight = testGraphics.getFontMetrics(namePlateFont).getHeight(); + + int w = textWidth + 20; + int h = textHeight + 10; + BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = img.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setBackground(new Color(0, 0, 0, 0.5f)); + g.clearRect(0, 0, w, h); + g.setColor(Color.WHITE); + g.setFont(namePlateFont); + g.drawString(username, 10, h - 15); + return img; + } + + public void drawNameplates(ClientPlayer myPlayer, float[] viewTransformData, float[] perspectiveTransformData) { + shaderProgram.stopUsing(); + namePlateShaderProgram.use(); + glEnable(GL_DEPTH_TEST); + glActiveTexture(0); + glUniform1i(namePlateTextureSamplerUniform, 0); + glUniformMatrix4fv(namePlateViewTransformUniform, false, viewTransformData); + glUniformMatrix4fv(namePlatePerspectiveTransformUniform, false, perspectiveTransformData); + for (var entry : playerNamePlates.entrySet()) { + OtherPlayer player = entry.getKey(); + // Skip rendering far-away nameplates. + if (player.getPosition().distance(myPlayer.getPosition()) > 50) continue; + GUITexture texture = entry.getValue(); + float aspectRatio = (float) texture.getHeight() / (float) texture.getWidth(); + transformMatrix.identity() + .translate(player.getPosition().x(), player.getPosition().y() + Player.HEIGHT + 1f, player.getPosition().z()) + .rotate(myPlayer.getOrientation().x, Camera.UP) + .scale(1f, aspectRatio, 0f) + .get(transformMatrixData); + glUniformMatrix4fv(namePlateTransformUniform, false, transformMatrixData); + glBindTexture(GL_TEXTURE_2D, texture.getTextureId()); + glDrawArrays(GL_TRIANGLE_STRIP, 0, vertexCount); + } + + glBindTexture(GL_TEXTURE_2D, 0); + glDisable(GL_DEPTH_TEST); + namePlateShaderProgram.stopUsing(); + shaderProgram.use(); + } + public void drawNvg(float width, float height, ClientPlayer player) { nvgBeginFrame(vgId, width, height, width / height); nvgSave(vgId); - nvgFontSize(vgId, 60f); - nvgFontFaceId(vgId, jetbrainsMonoFont); - nvgTextAlign(vgId, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); - nvgFillColor(vgId, GuiUtils.rgba(1, 0, 0, 1, colorA)); - nvgText(vgId, 5, 5, "Hello world!"); drawCrosshair(width, height); drawHealthBar(width, height, player); @@ -150,7 +238,9 @@ public class GuiRenderer { public void free() { memFree(jetbrainsMonoFontData); nvgDelete(vgId); - for (var tex : textures.values()) tex.free(); + for (var texture : playerNamePlates.values()) { + texture.free(); + } glDeleteBuffers(vboId); glDeleteVertexArrays(vaoId); shaderProgram.free(); diff --git a/client/src/main/resources/shader/gui/nameplate_fragement.glsl b/client/src/main/resources/shader/gui/nameplate_fragement.glsl new file mode 100644 index 0000000..7d55bc5 --- /dev/null +++ b/client/src/main/resources/shader/gui/nameplate_fragement.glsl @@ -0,0 +1,10 @@ +#version 460 core + +in vec2 texturePosition; +out vec4 fragmentColor; + +uniform sampler2D guiTexture; + +void main() { + fragmentColor = texture(guiTexture, texturePosition); +} \ No newline at end of file diff --git a/client/src/main/resources/shader/gui/nameplate_vertex.glsl b/client/src/main/resources/shader/gui/nameplate_vertex.glsl new file mode 100644 index 0000000..bc878d6 --- /dev/null +++ b/client/src/main/resources/shader/gui/nameplate_vertex.glsl @@ -0,0 +1,17 @@ +#version 460 core + +in vec2 vertexPosition; + +uniform mat4 transform; +uniform mat4 viewTransform; +uniform mat4 perspectiveTransform; + +out vec2 texturePosition; + +void main() { + gl_Position = perspectiveTransform * viewTransform * transform * vec4(vertexPosition, 0.0, 1.0); + texturePosition = vec2( + (vertexPosition.x + 1.0) / 2.0, + 1 - (vertexPosition.y + 1.0) / 2.0 + ); +} \ No newline at end of file diff --git a/core/src/main/java/nl/andrewl/aos_core/FileUtils.java b/core/src/main/java/nl/andrewl/aos_core/FileUtils.java index 9b010cd..83e3391 100644 --- a/core/src/main/java/nl/andrewl/aos_core/FileUtils.java +++ b/core/src/main/java/nl/andrewl/aos_core/FileUtils.java @@ -47,4 +47,10 @@ public final class FileUtils { return buffer; } } + + public static InputStream getClasspathResource(String resource) throws IOException { + InputStream in = FileUtils.class.getClassLoader().getResourceAsStream(resource); + if (in == null) throw new IOException("Resource not found: " + resource); + return in; + } }