package dev.figboot.cuberender.test; import dev.figboot.cuberender.math.*; import dev.figboot.cuberender.state.BlendMode; import dev.figboot.cuberender.state.Framebuffer; import dev.figboot.cuberender.state.Mesh; import dev.figboot.cuberender.state.Texture; import javax.imageio.ImageIO; import javax.swing.*; import java.awt.*; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.EnumMap; public class GraphicsPanel extends JPanel { private Framebuffer framebuffer; private final EnumMap> meshes = new EnumMap<>(BodyPart.class); private float xRot = 0, yRot = 0; private long[] clrTime = new long[32]; private long[] meshTime = new long[32]; private int tidx = 0; boolean rollOver = false; private static void addCuboid(Mesh.Builder mb, float x1, float y1, float z1, float x2, float y2, float z2, float tx, float ty, float tspanX, float tspanY, float tspanZ, int ibase) { mb.vertex(new Vector4f(x1, y1, z2), /* front */ new Vector4f(x1, y2, z2), new Vector4f(x2, y2, z2), new Vector4f(x2, y1, z2), new Vector4f(x2, y1, z2), /* +X side */ new Vector4f(x2, y2, z2), new Vector4f(x2, y2, z1), new Vector4f(x2, y1, z1), new Vector4f(x2, y1, z1), /* back */ new Vector4f(x2, y2, z1), new Vector4f(x1, y2, z1), new Vector4f(x1, y1, z1), new Vector4f(x1, y1, z1), /* -X side */ new Vector4f(x1, y2, z1), new Vector4f(x1, y2, z2), new Vector4f(x1, y1, z2), new Vector4f(x1, y1, z1), /* top */ new Vector4f(x1, y1, z2), new Vector4f(x2, y1, z2), new Vector4f(x2, y1, z1), new Vector4f(x1, y2, z1), /* bottom */ new Vector4f(x1, y2, z2), new Vector4f(x2, y2, z2), new Vector4f(x2, y2, z1)) .normals(new Vector4f(0, 0, 1, 0), new Vector4f(0, 0, 1, 0), new Vector4f(1, 0, 0, 0), new Vector4f(1, 0, 0, 0), new Vector4f(0, 0, -1, 0), new Vector4f(0, 0, -1, 0), new Vector4f(-1, 0, 0, 0), new Vector4f(-1, 0, 0, 0), new Vector4f(0, -1, 0, 0), new Vector4f(0, -1, 0, 0), new Vector4f(0, 1, 0, 0), new Vector4f(0, 1, 0, 0)) .texCoords(new Vector2f(tx, ty), new Vector2f(tx, ty - tspanY), new Vector2f(tx + tspanX, ty - tspanY), new Vector2f(tx + tspanX, ty), new Vector2f(tx + tspanX, ty), new Vector2f(tx + tspanX, ty - tspanY), new Vector2f(tx + tspanX + tspanZ, ty - tspanY), new Vector2f(tx + tspanX + tspanZ, ty), new Vector2f(tx + tspanX + tspanZ, ty), new Vector2f(tx + tspanX + tspanZ, ty - tspanY), new Vector2f(tx + 2 * tspanX + tspanZ, ty - tspanY), new Vector2f(tx + 2 * tspanX + tspanZ, ty), new Vector2f(tx - tspanZ, ty), new Vector2f(tx - tspanZ, ty - tspanY), new Vector2f(tx, ty - tspanY), new Vector2f(tx, ty), new Vector2f(tx, ty + tspanZ), new Vector2f(tx, ty), new Vector2f(tx + tspanX, ty), new Vector2f(tx + tspanX, ty + tspanZ), new Vector2f(tx + tspanX, ty + tspanZ), new Vector2f(tx + tspanX, ty), new Vector2f(tx + 2 * tspanX, ty), new Vector2f(tx + 2 * tspanX, ty + tspanZ)) .indices(ibase, ibase + 1, ibase + 2, ibase, ibase + 2, ibase + 3, ibase + 4, ibase + 5, ibase + 6, ibase + 4, ibase + 6, ibase + 7, ibase + 8, ibase + 9, ibase + 10, ibase + 8, ibase + 10, ibase + 11, ibase + 12, ibase + 13, ibase + 14, ibase + 12, ibase + 14, ibase + 15, ibase + 16, ibase + 17, ibase + 18, ibase + 16, ibase + 18, ibase + 19, ibase + 20, ibase + 21, ibase + 22, ibase + 20, ibase + 22, ibase + 23); } private void copyLimbFlipped(BufferedImage src, BufferedImage target) { for (int y = 4, maxY = src.getHeight(); y < maxY; ++y) { for (int x = 0; x < 4; ++x) { target.setRGB(x, y, src.getRGB(11 - x, y)); target.setRGB(x + 4, y, src.getRGB(7 - x, y)); target.setRGB(x + 8, y, src.getRGB(3 - x, y)); target.setRGB(x + 12, y, src.getRGB(15 - x, y)); } } for (int y = 0; y < 4; ++y) { for (int x = 0; x < 4; ++x) { target.setRGB(x + 4, y, src.getRGB(7 - x, y)); target.setRGB(x + 8, y, src.getRGB(11 - x, y)); } } } private BufferedImage convertToModernSkin(BufferedImage bi) { BufferedImage realBI = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = realBI.createGraphics(); g2d.drawImage(bi, 0, 0, 64, 32, null); // TODO: this is incomplete. the actual game mirrors the arm (so there is always an "inside" and an "outside") copyLimbFlipped(bi.getSubimage(0, 16, 16, 16), realBI.getSubimage(16, 48, 16, 16)); copyLimbFlipped(bi.getSubimage(40, 16, 16, 16), realBI.getSubimage(32, 48, 16, 16)); try { ImageIO.write(realBI, "PNG", new File("test.png")); } catch (IOException ex) { throw new RuntimeException(ex); } return realBI; } private static Mesh.Builder defaultBuilder() { return new Mesh.Builder().attach(Mesh.AttachmentType.LIGHT_FACTOR, 1f) .attach(Mesh.AttachmentType.LIGHT_VECTOR, new Vector4f(0, 0, 1, 0)); } public GraphicsPanel() { addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { handleResize(getWidth(), getHeight()); } }); BufferedImage bi; try (InputStream is = getClass().getResourceAsStream("/skin2.png")) { bi = ImageIO.read(is); } catch (IOException e) { throw new RuntimeException(e); } BufferedImage capeBI; try (InputStream is = getClass().getResourceAsStream("/cape.png")) { capeBI = ImageIO.read(is); } catch (IOException e) { throw new RuntimeException(e); } if (bi.getHeight() == 32) { bi = convertToModernSkin(bi); } Texture tex = new Texture(bi); Mesh.Builder bodyBuilder = defaultBuilder().texture(tex); // TODO: slim model boolean slim = false; float overlayOffset = 1/64f; int idxbase = -24; // head addCuboid(bodyBuilder, -4 / 16f, -16 / 16f, -4 / 16f, 4 / 16f, -8 / 16f, 4 / 16f, 1 / 8f, 7 / 8f, 1 / 8f, 1 / 8f, 1 / 8f, idxbase += 24); // torso addCuboid(bodyBuilder, -4 / 16f, -8 / 16f, -2 / 16f, 4 / 16f, 4 / 16f, 2 / 16f, 5 / 16f, 11 / 16f, 1 / 8f, 3 / 16f, 1 / 16f, idxbase += 24); // left+right arm addCuboid(bodyBuilder, (slim ? -7 : -8) / 16f, -8 / 16f, -2 / 16f, -4 / 16f, 4 / 16f, 2 / 16f, 11 / 16f, 11 / 16f, (slim ? 3 / 64f : 1 / 16f), 3 / 16f, 1 / 16f, idxbase += 24); addCuboid(bodyBuilder, 4 / 16f, -8 / 16f, -2 / 16f, (slim ? 7 : 8) / 16f, 4 / 16f, 2 / 16f, 9 / 16f, 3 / 16f, (slim ? 3 / 64f : 1 / 16f), 3 / 16f, 1 / 16f, idxbase += 24); // left+right leg addCuboid(bodyBuilder, -4 / 16f, 4 / 16f, -2 / 16f, 0 / 16f, 16 / 16f, 2 / 16f, 1 / 16f, 11 / 16f, 1 / 16f, 3 / 16f, 1 / 16f, idxbase += 24); addCuboid(bodyBuilder, 0 / 16f, 4 / 16f, -2 / 16f, 4 / 16f, 16 / 16f, 2 / 16f, 5 / 16f, 3 / 16f, 1 / 16f, 3 / 16f, 1 / 16f, idxbase += 24); meshes.put(BodyPart.MAIN, bodyBuilder.build()); Mesh.Builder hatBuilder = defaultBuilder().texture(tex); addCuboid(hatBuilder, -4 / 16f - overlayOffset, -16 / 16f - overlayOffset, -4 / 16f - overlayOffset, 4 / 16f + overlayOffset, -8 / 16f + overlayOffset, 4 / 16f + overlayOffset, 5 / 8f, 7 / 8f, 1 / 8f, 1 / 8f, 1 / 8f, 0); meshes.put(BodyPart.HAT, hatBuilder.build()); overlayOffset *= 1.01f; Mesh.Builder torsoBuilder = defaultBuilder().texture(tex); addCuboid(torsoBuilder, -4 / 16f - overlayOffset, -8 / 16f - overlayOffset, -2 / 16f - overlayOffset, 4 / 16f + overlayOffset, 4 / 16f + overlayOffset, 2 / 16f + overlayOffset, 5 / 16f, 7 / 16f, 1 / 8f, 3 / 16f, 1 / 16f, 0); meshes.put(BodyPart.TORSO_OVERLAY, torsoBuilder.build()); overlayOffset /= 1.01f; Mesh.Builder leftArmBuilder = defaultBuilder().texture(tex); addCuboid(leftArmBuilder, (slim ? -7 : -8) / 16f - overlayOffset, -8 / 16f - overlayOffset, -2 / 16f - overlayOffset, -4 / 16f + overlayOffset, 4 / 16f + overlayOffset, 2 / 16f + overlayOffset, 13 / 16f, 3 / 16f, (slim ? 3 / 64f : 1 / 16f), 3 / 16f, 1 / 16f, 0); meshes.put(BodyPart.LEFT_ARM_OVERLAY, leftArmBuilder.build()); Mesh.Builder rightArmBuilder = defaultBuilder().texture(tex); addCuboid(rightArmBuilder, 4 / 16f - overlayOffset, -8 / 16f - overlayOffset, -2 / 16f - overlayOffset, (slim ? 7 : 8) / 16f + overlayOffset, 4 / 16f + overlayOffset, 2 / 16f + overlayOffset, 11 / 16f, 7 / 16f, (slim ? 3 / 64f : 1 / 16f), 3 / 16f, 1 / 16f, 0); meshes.put(BodyPart.RIGHT_ARM_OVERLAY, rightArmBuilder.build()); overlayOffset /= 1.01f; Mesh.Builder leftLegBuilder = defaultBuilder().texture(tex); addCuboid(leftLegBuilder, -4 / 16f - overlayOffset, 4 / 16f - overlayOffset, -2 / 16f - overlayOffset, 0 / 16f + overlayOffset, 16 / 16f + overlayOffset, 2 / 16f + overlayOffset, 1 / 16f, 7 / 16f, 1 / 16f, 3 / 16f, 1 / 16f, 0); meshes.put(BodyPart.LEFT_LEG_OVERLAY, leftLegBuilder.build()); overlayOffset /= 1.01f; Mesh.Builder rightLegBuilder = defaultBuilder().texture(tex); addCuboid(rightLegBuilder, 0 / 16f - overlayOffset, 4 / 16f - overlayOffset, -2 / 16f - overlayOffset, 4 / 16f + overlayOffset, 16 / 16f + overlayOffset, 2 / 16f + overlayOffset, 1 / 16f, 3 / 16f, 1 / 16f, 3 / 16f, 1 / 16f, 0); meshes.put(BodyPart.RIGHT_LEG_OVERLAY, rightLegBuilder.build()); Mesh.Builder capeBuilder = defaultBuilder().texture(new Texture(capeBI)); addCuboid(capeBuilder, 0 / 16f - overlayOffset, 4 / 16f - overlayOffset, -2 / 16f - overlayOffset, 4 / 16f + overlayOffset, 16 / 16f + overlayOffset, 2 / 16f + overlayOffset, 1 / 16f, 3 / 16f, 1 / 16f, 3 / 16f, 1 / 16f, 0); meshes.put(BodyPart.RIGHT_LEG_OVERLAY, rightLegBuilder.build()); } private void handleResize(int width, int height) { framebuffer = new Framebuffer(width, height); updateTransform(); } private void updateTransform() { framebuffer.setTransform(Matrix4f.rotateY(yRot).times(Matrix4f.rotateX(xRot)).times(Matrix4f.scale(0.5f))); } @Override public void paintComponent(Graphics g) { if (framebuffer == null) handleResize(getWidth(), getHeight()); long start = System.nanoTime(); framebuffer.setBlendMode(BlendMode.DISABLE); framebuffer.clear(Framebuffer.FB_CLEAR_COLOR | Framebuffer.FB_CLEAR_DEPTH, 0xFF000000); long t1 = System.nanoTime(); framebuffer.setDepthMode(Framebuffer.FB_DEPTH_COMMIT | Framebuffer.FB_DEPTH_USE); framebuffer.drawMesh(meshes.get(BodyPart.MAIN)); framebuffer.setDepthMode(Framebuffer.FB_DEPTH_USE | Framebuffer.FB_DEPTH_COMMIT_TRANSPARENT); framebuffer.setBlendMode(BlendMode.BLEND_OVER); framebuffer.drawMesh(meshes.get(BodyPart.HAT)); framebuffer.drawMesh(meshes.get(BodyPart.TORSO_OVERLAY)); framebuffer.drawMesh(meshes.get(BodyPart.LEFT_ARM_OVERLAY)); framebuffer.drawMesh(meshes.get(BodyPart.RIGHT_ARM_OVERLAY)); framebuffer.drawMesh(meshes.get(BodyPart.LEFT_LEG_OVERLAY)); framebuffer.drawMesh(meshes.get(BodyPart.RIGHT_LEG_OVERLAY)); long t2 = System.nanoTime(); g.clearRect(0, 0, getWidth(), getHeight()); g.drawImage(framebuffer.getColor(), 0, 0, null); g.setColor(Color.RED); addTiming(t1 - start, t2 - t1); int y = -2; g.drawString(String.format("tot %.02fms", (t2 - start) / 1000000.), 10, y += 12); g.drawString(String.format("clr %.02fms", (t1 - start) / 1000000.), 10, y += 12); g.drawString(String.format("msh %.02fms", (t2 - t1) / 1000000.), 10, y += 12); g.drawString(getAvgClr(), 10, y += 12); g.drawString(String.format("%dx%d", framebuffer.getWidth(), framebuffer.getHeight()), 10, y += 12); } private void addTiming(long clr, long msh) { clrTime[tidx] = clr; meshTime[tidx] = msh; if (++tidx >= 32) { tidx = 0; rollOver = true; } } private String getAvgClr() { int n = rollOver ? 32 : tidx; if (n == 0) return "avg ???"; long sumClr = 0, sumMsh = 0; for (int i = 0; i < n; ++i) { sumClr += clrTime[i]; sumMsh += meshTime[i]; } return String.format("avg %.02fms clr %.02fms msh", sumClr / (double)n / 1000000, sumMsh / (double)n / 1000000); } public void setYRot(float rad) { this.yRot = rad; updateTransform(); } public void setXRot(float rad) { this.xRot = rad; updateTransform(); } public enum BodyPart { MAIN, HAT, TORSO_OVERLAY, LEFT_ARM_OVERLAY, RIGHT_ARM_OVERLAY, LEFT_LEG_OVERLAY, RIGHT_LEG_OVERLAY, CAPE } }