aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/dev/figboot
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/dev/figboot')
-rw-r--r--src/main/java/dev/figboot/cuberender/math/MathUtil.java9
-rw-r--r--src/main/java/dev/figboot/cuberender/math/Matrix3f.java121
-rw-r--r--src/main/java/dev/figboot/cuberender/math/Matrix4f.java149
-rw-r--r--src/main/java/dev/figboot/cuberender/math/Vector2f.java17
-rw-r--r--src/main/java/dev/figboot/cuberender/math/Vector3f.java42
-rw-r--r--src/main/java/dev/figboot/cuberender/math/Vector4f.java47
-rw-r--r--src/main/java/dev/figboot/cuberender/state/BlendMode.java26
-rw-r--r--src/main/java/dev/figboot/cuberender/state/Framebuffer.java162
-rw-r--r--src/main/java/dev/figboot/cuberender/state/Mesh.java159
-rw-r--r--src/main/java/dev/figboot/cuberender/state/Sampleable.java10
-rw-r--r--src/main/java/dev/figboot/cuberender/state/Texture.java14
-rw-r--r--src/main/java/dev/figboot/cuberender/test/GraphicsPanel.java387
-rw-r--r--src/main/java/dev/figboot/cuberender/test/TestWindow.java49
13 files changed, 1192 insertions, 0 deletions
diff --git a/src/main/java/dev/figboot/cuberender/math/MathUtil.java b/src/main/java/dev/figboot/cuberender/math/MathUtil.java
new file mode 100644
index 0000000..cf42a62
--- /dev/null
+++ b/src/main/java/dev/figboot/cuberender/math/MathUtil.java
@@ -0,0 +1,9 @@
+package dev.figboot.cuberender.math;
+
+public final class MathUtil {
+ private MathUtil() { }
+
+ public static float clamp(float f1, float min, float max) {
+ return Math.min(Math.max(f1, min), max);
+ }
+}
diff --git a/src/main/java/dev/figboot/cuberender/math/Matrix3f.java b/src/main/java/dev/figboot/cuberender/math/Matrix3f.java
new file mode 100644
index 0000000..521e68e
--- /dev/null
+++ b/src/main/java/dev/figboot/cuberender/math/Matrix3f.java
@@ -0,0 +1,121 @@
+package dev.figboot.cuberender.math;
+
+public class Matrix3f {
+ public float m00, m01, m02,
+ m10, m11, m12,
+ m20, m21, m22;
+
+ public Matrix3f() {
+ this(1, 0, 0, 0, 1, 0, 0, 0, 1);
+ }
+
+ public Matrix3f(Matrix3f m) {
+ this(m.m00, m.m01, m.m02, m.m10, m.m11, m.m12, m.m20, m.m21, m.m22);
+ }
+
+ public Matrix3f(float m00, float m01, float m02, float m10, float m11, float m12, float m20, float m21, float m22) {
+ this.m00 = m00;
+ this.m01 = m01;
+ this.m02 = m02;
+ this.m10 = m10;
+ this.m11 = m11;
+ this.m12 = m12;
+ this.m20 = m20;
+ this.m21 = m21;
+ this.m22 = m22;
+ }
+
+ public Matrix3f identity() {
+ return identity(this);
+ }
+
+ public Matrix3f identity(Matrix3f target) {
+ target.m00 = 1f;
+ target.m11 = 1f;
+ target.m22 = 1f;
+
+ target.m01 = target.m02 = target.m10 = target.m12 = target.m20 = target.m21 = 0;
+ return target;
+ }
+
+ public Matrix3f times(Matrix3f other) {
+ return times(other, this);
+ }
+
+ public Matrix3f times(Matrix3f other, Matrix3f target) {
+ float m00 = this.m00 * other.m00 + this.m01 * other.m10 + this.m02 * other.m20;
+ float m01 = this.m00 * other.m01 + this.m01 * other.m11 + this.m02 * other.m21;
+ float m02 = this.m00 * other.m02 + this.m01 * other.m21 + this.m02 * other.m22;
+
+ float m10 = this.m10 * other.m00 + this.m11 * other.m10 + this.m12 * other.m20;
+ float m11 = this.m10 * other.m01 + this.m11 * other.m11 + this.m12 * other.m21;
+ float m12 = this.m10 * other.m02 + this.m11 * other.m12 + this.m12 * other.m22;
+
+ float m20 = this.m20 * other.m00 + this.m21 * other.m10 + this.m22 * other.m20;
+ float m21 = this.m20 * other.m01 + this.m21 * other.m11 + this.m22 * other.m21;
+ float m22 = this.m20 * other.m02 + this.m21 * other.m12 + this.m22 * other.m22;
+
+ target.m00 = m00;
+ target.m01 = m01;
+ target.m02 = m02;
+
+ target.m10 = m10;
+ target.m11 = m11;
+ target.m12 = m12;
+
+ target.m20 = m20;
+ target.m21 = m21;
+ target.m22 = m22;
+
+ return target;
+ }
+
+ public Vector3f transform(Vector3f other) {
+ return new Vector3f(
+ this.m00 * other.x + this.m01 * other.y + this.m02 * other.z,
+ this.m10 * other.x + this.m11 * other.y + this.m12 * other.z,
+ this.m20 * other.x + this.m21 * other.y + this.m22 * other.z);
+ }
+
+ public static Matrix3f rotateX(float radians) {
+ float f1 = (float)Math.cos(radians);
+ float f2 = (float)Math.sin(radians);
+
+ return new Matrix3f(
+ 1, 0, 0,
+ 0, f1, f2,
+ 0, -f2, f1);
+ }
+
+ public static Matrix3f rotateY(float radians) {
+ float f1 = (float)Math.cos(radians);
+ float f2 = (float)Math.sin(radians);
+
+ return new Matrix3f(
+ f1, 0, -f2,
+ 0, 1, 0,
+ f2, 0, f1);
+ }
+
+ public static Matrix3f rotateZ(float radians) {
+ float f1 = (float)Math.cos(radians);
+ float f2 = (float)Math.sin(radians);
+
+ return new Matrix3f(
+ f1, -f2, 0,
+ f2, f1, 0,
+ 0, 0, 1);
+ }
+
+ public static Matrix3f scaleX(float by) {
+ return new Matrix3f(by, 0, 0, 0, 1, 0, 0, 0, 1);
+ }
+
+ public static Matrix3f scaleY(float by) {
+ return new Matrix3f(1, 0, 0, 0, by, 0, 0, 0, 1);
+ }
+
+ public static Matrix3f scaleZ(float by) {
+ return new Matrix3f(1, 0, 0, 0, 1, 0, 0, 0, by);
+ }
+}
diff --git a/src/main/java/dev/figboot/cuberender/math/Matrix4f.java b/src/main/java/dev/figboot/cuberender/math/Matrix4f.java
new file mode 100644
index 0000000..e86db41
--- /dev/null
+++ b/src/main/java/dev/figboot/cuberender/math/Matrix4f.java
@@ -0,0 +1,149 @@
+package dev.figboot.cuberender.math;
+
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+public class Matrix4f {
+ public float m00, m01, m02, m03,
+ m10, m11, m12, m13,
+ m20, m21, m22, m23,
+ m30, m31, m32, m33;
+
+ public Matrix4f() {
+ identity();
+ }
+
+ public Matrix4f(Matrix4f mat) {
+ this(mat.m00, mat.m01, mat.m02, mat.m03, mat.m10, mat.m11, mat.m12, mat.m13, mat.m20, mat.m21, mat.m22, mat.m23, mat.m30, mat.m31, mat.m32, mat.m33);
+ }
+
+ public Matrix4f identity() {
+ return identity(this);
+ }
+
+ public Matrix4f identity(Matrix4f target) {
+ target.m00 = target.m11 = target.m22 = target.m33 = 1f;
+ target.m01 = target.m02 = target.m03
+ = target.m10 = target.m12 = target.m13
+ = target.m20 = target.m21 = target.m23
+ = target.m30 = target.m31 = target.m32 = 0;
+
+ return target;
+ }
+
+ public Matrix4f times(Matrix4f right) {
+ return times(right, this);
+ }
+
+ public Matrix4f times(Matrix4f right, Matrix4f target) {
+ float m00 = this.m00 * right.m00 + this.m01 * right.m10 + this.m02 * right.m20 + this.m03 * this.m30;
+ float m01 = this.m00 * right.m01 + this.m01 * right.m11 + this.m02 * right.m21 + this.m03 * this.m31;
+ float m02 = this.m00 * right.m02 + this.m01 * right.m12 + this.m02 * right.m22 + this.m03 * this.m32;
+ float m03 = this.m00 * right.m03 + this.m01 * right.m13 + this.m02 * right.m23 + this.m03 * this.m33;
+
+ float m10 = this.m10 * right.m00 + this.m11 * right.m10 + this.m12 * right.m20 + this.m13 * this.m30;
+ float m11 = this.m10 * right.m01 + this.m11 * right.m11 + this.m12 * right.m21 + this.m13 * this.m31;
+ float m12 = this.m10 * right.m02 + this.m11 * right.m12 + this.m12 * right.m22 + this.m13 * this.m32;
+ float m13 = this.m10 * right.m03 + this.m11 * right.m13 + this.m12 * right.m23 + this.m13 * this.m33;
+
+ float m20 = this.m20 * right.m00 + this.m21 * right.m10 + this.m22 * right.m20 + this.m23 * this.m30;
+ float m21 = this.m20 * right.m01 + this.m21 * right.m11 + this.m22 * right.m21 + this.m23 * this.m31;
+ float m22 = this.m20 * right.m02 + this.m21 * right.m12 + this.m22 * right.m22 + this.m23 * this.m32;
+ float m23 = this.m20 * right.m03 + this.m21 * right.m13 + this.m22 * right.m23 + this.m23 * this.m33;
+
+ float m30 = this.m30 * right.m00 + this.m31 * right.m10 + this.m32 * right.m20 + this.m33 * this.m30;
+ float m31 = this.m30 * right.m01 + this.m31 * right.m11 + this.m32 * right.m21 + this.m33 * this.m31;
+ float m32 = this.m30 * right.m02 + this.m31 * right.m12 + this.m32 * right.m22 + this.m33 * this.m32;
+ float m33 = this.m30 * right.m03 + this.m31 * right.m13 + this.m32 * right.m23 + this.m33 * this.m33;
+
+ target.m00 = m00;
+ target.m01 = m01;
+ target.m02 = m02;
+ target.m03 = m03;
+
+ target.m10 = m10;
+ target.m11 = m11;
+ target.m12 = m12;
+ target.m13 = m13;
+
+ target.m20 = m20;
+ target.m21 = m21;
+ target.m22 = m22;
+ target.m23 = m23;
+
+ target.m30 = m30;
+ target.m31 = m31;
+ target.m32 = m32;
+ target.m33 = m33;
+
+ return target;
+ }
+
+ public Vector4f transform(Vector4f in) {
+ return transform(in, in);
+ }
+
+ public Vector4f transform(Vector4f in, Vector4f target) {
+ float x = in.x * m00 + in.y * m01 + in.z * m02 + in.w * m03;
+ float y = in.x * m10 + in.y * m11 + in.z * m12 + in.w * m13;
+ float z = in.x * m20 + in.y * m21 + in.z * m22 + in.w * m23;
+ float w = in.x * m30 + in.y * m31 + in.z * m32 + in.w * m33;
+
+ target.x = x;
+ target.y = y;
+ target.z = z;
+ target.w = w;
+
+ return target;
+ }
+
+ public static Matrix4f scale(float factor) {
+ return scale(factor, factor, factor);
+ }
+
+ public static Matrix4f scale(float x, float y, float z) {
+ Matrix4f mat = new Matrix4f();
+ mat.m00 = x;
+ mat.m11 = y;
+ mat.m22 = z;
+ return mat;
+ }
+
+ public static Matrix4f translate(float x, float y, float z) {
+ Matrix4f mat = new Matrix4f();
+ mat.m03 = x;
+ mat.m13 = y;
+ mat.m23 = z;
+ return mat;
+ }
+
+ public static Matrix4f rotateX(float rad) {
+ float cos = (float)Math.cos(rad);
+ float sin = (float)Math.sin(rad);
+ return new Matrix4f(
+ 1, 0, 0, 0,
+ 0, cos, sin, 0,
+ 0, -sin, cos, 0,
+ 0, 0, 0, 1);
+ }
+
+ public static Matrix4f rotateY(float rad) {
+ float cos = (float)Math.cos(rad);
+ float sin = (float)Math.sin(rad);
+ return new Matrix4f(
+ cos, 0, -sin, 0,
+ 0, 1, 0, 0,
+ sin, 0, cos, 0,
+ 0, 0, 0, 1);
+ }
+
+ public static Matrix4f rotateZ(float rad) {
+ float cos = (float)Math.cos(rad);
+ float sin = (float)Math.sin(rad);
+ return new Matrix4f(
+ cos, -sin, 0, 0,
+ sin, cos, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1);
+ }
+}
diff --git a/src/main/java/dev/figboot/cuberender/math/Vector2f.java b/src/main/java/dev/figboot/cuberender/math/Vector2f.java
new file mode 100644
index 0000000..66711d6
--- /dev/null
+++ b/src/main/java/dev/figboot/cuberender/math/Vector2f.java
@@ -0,0 +1,17 @@
+package dev.figboot.cuberender.math;
+
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+public class Vector2f {
+ public float x, y;
+
+ public Vector2f() {
+ this(0, 0);
+ }
+
+ public Vector2f(Vector2f vector) {
+ this.x = vector.x;
+ this.y = vector.y;
+ }
+}
diff --git a/src/main/java/dev/figboot/cuberender/math/Vector3f.java b/src/main/java/dev/figboot/cuberender/math/Vector3f.java
new file mode 100644
index 0000000..40720e8
--- /dev/null
+++ b/src/main/java/dev/figboot/cuberender/math/Vector3f.java
@@ -0,0 +1,42 @@
+package dev.figboot.cuberender.math;
+
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+public class Vector3f {
+ public float x, y, z;
+
+ public Vector3f() {
+ this(0, 0, 0);
+ }
+
+ public Vector3f(Vector3f vector) {
+ this.x = vector.x;
+ this.y = vector.y;
+ this.z = vector.z;
+ }
+
+ public float dot(Vector3f vector) {
+ return this.x * vector.x + this.y * vector.y + this.z * vector.z;
+ }
+
+ public float lengthSquared() {
+ return this.x * this.x + this.y * this.y + this.z * this.z;
+ }
+
+ public float length() {
+ return (float)Math.sqrt(lengthSquared());
+ }
+
+ public Vector3f normalize() {
+ return normalize(this);
+ }
+
+ public Vector3f normalize(Vector3f target) {
+ float len = length();
+ target.x /= len;
+ target.y /= len;
+ target.z /= len;
+ return target;
+ }
+}
diff --git a/src/main/java/dev/figboot/cuberender/math/Vector4f.java b/src/main/java/dev/figboot/cuberender/math/Vector4f.java
new file mode 100644
index 0000000..7a29010
--- /dev/null
+++ b/src/main/java/dev/figboot/cuberender/math/Vector4f.java
@@ -0,0 +1,47 @@
+package dev.figboot.cuberender.math;
+
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+public class Vector4f {
+ public float x, y, z, w;
+
+ public Vector4f() {
+ this(0, 0, 0, 0);
+ }
+
+ public Vector4f(Vector4f vec) {
+ this(vec.x, vec.y, vec.z, vec.w);
+ }
+
+ public Vector4f fromARGB(int argb) {
+ return fromARGB(argb, this);
+ }
+
+ public Vector4f fromARGB(int argb, Vector4f target) {
+ target.x = ((argb & 0x00FF0000) >>> 16) / 255f;
+ target.y = ((argb & 0x0000FF00) >>> 8) / 255f;
+ target.z = ((argb & 0x000000FF)) / 255f;
+ target.w = ((argb & 0xFF000000) >>> 24) / 255f;
+ return target;
+ }
+
+ public int toARGB() {
+ return ((int)(MathUtil.clamp(w, 0, 1) * 255) << 24)
+ | ((int)(MathUtil.clamp(x, 0, 1) * 255) << 16)
+ | ((int)(MathUtil.clamp(y, 0, 1) * 255) << 8)
+ | ((int)(MathUtil.clamp(z, 0, 1) * 255));
+ }
+
+ public Vector4f times(float fact) {
+ return times(fact, this);
+ }
+
+ public Vector4f times(float fact, Vector4f target) {
+ target.x *= fact;
+ target.y *= fact;
+ target.z *= fact;
+ target.w *= fact;
+ return target;
+ }
+}
diff --git a/src/main/java/dev/figboot/cuberender/state/BlendMode.java b/src/main/java/dev/figboot/cuberender/state/BlendMode.java
new file mode 100644
index 0000000..0c30998
--- /dev/null
+++ b/src/main/java/dev/figboot/cuberender/state/BlendMode.java
@@ -0,0 +1,26 @@
+package dev.figboot.cuberender.state;
+
+import dev.figboot.cuberender.math.Vector4f;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public enum BlendMode {
+ DISABLE((inOutColor, prev) -> inOutColor.w = 1),
+ BLEND_OVER((inOutColor, prev) -> {
+ float pAlphaFactor = prev.w * (1 - inOutColor.w);
+ float aOut = inOutColor.w + pAlphaFactor;
+
+ inOutColor.x = (inOutColor.x + prev.x * pAlphaFactor) / aOut;
+ inOutColor.y = (inOutColor.y + prev.y * pAlphaFactor) / aOut;
+ inOutColor.z = (inOutColor.z + prev.z * pAlphaFactor) / aOut;
+ inOutColor.w = aOut;
+ });
+
+ private final BlendFunction function;
+
+ public interface BlendFunction {
+ void blend(Vector4f inOutColor, Vector4f prev);
+ }
+}
diff --git a/src/main/java/dev/figboot/cuberender/state/Framebuffer.java b/src/main/java/dev/figboot/cuberender/state/Framebuffer.java
new file mode 100644
index 0000000..56e218e
--- /dev/null
+++ b/src/main/java/dev/figboot/cuberender/state/Framebuffer.java
@@ -0,0 +1,162 @@
+package dev.figboot.cuberender.state;
+
+import dev.figboot.cuberender.math.Matrix3f;
+import dev.figboot.cuberender.math.Vector3f;
+import dev.figboot.cuberender.math.Vector4f;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.util.Arrays;
+
+public class Framebuffer {
+ public static int FB_CLEAR_COLOR = 0x01;
+ public static int FB_CLEAR_DEPTH = 0x02;
+
+ public static int FB_DEPTH_USE = 0x01;
+ public static int FB_DEPTH_COMMIT = 0x02;
+ public static int FB_DEPTH_COMMIT_TRANSPARENT = 0x04;
+
+ @Getter private final int width, height;
+
+ @Getter private final BufferedImage color;
+ private final float[] depth;
+
+ private int depthMode = FB_DEPTH_USE | FB_DEPTH_COMMIT;
+
+ @Setter private Matrix3f transform;
+
+ @Setter private BlendMode blendMode = BlendMode.DISABLE;
+
+ public Framebuffer(int width, int height) {
+ this.width = width;
+ this.height = height;
+
+ this.color = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ depth = new float[width * height];
+ }
+
+ public void setDepthMode(int mode) {
+ this.depthMode = mode;
+ }
+
+ public void clear(int bits, int color) {
+ if ((bits & FB_CLEAR_COLOR) != 0) {
+ Graphics gfx = this.color.getGraphics();
+ gfx.setColor(new Color(color, true));
+ gfx.fillRect(0, 0, width, height);
+ }
+
+ if ((bits & FB_CLEAR_DEPTH) != 0) {
+ Arrays.fill(depth, Float.NEGATIVE_INFINITY);
+ }
+ }
+
+ public void drawMesh(Mesh<?> mesh) {
+ // this seems redundant but it saves us having to check it each loop iteration
+ if (mesh.indices != null) {
+ drawIndexedMesh(mesh);
+ } else {
+ drawFlatMesh(mesh);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void drawIndexedMesh(Mesh<?> mesh) {
+ int ntris = mesh.indices.length / 3;
+ Sampleable<Object> s = (Sampleable<Object>)mesh;
+
+ int i0, i1, i2;
+
+ for (int tri = 0; tri < ntris; ++tri) {
+ i0 = mesh.indices[tri * 3];
+ i1 = mesh.indices[tri * 3 + 1];
+ i2 = mesh.indices[tri * 3 + 2];
+
+ Vector3f vert0 = mesh.vertices[i0];
+ Vector3f vert1 = mesh.vertices[i1];
+ Vector3f vert2 = mesh.vertices[i2];
+
+ drawTriangle(vert0, vert1, vert2, mesh.normals[tri], s, i0, i1, i2);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void drawFlatMesh(Mesh<?> mesh) {
+ int ntris = mesh.vertices.length / 3;
+ Sampleable<Object> s = (Sampleable<Object>)mesh;
+
+ int i0, i1, i2;
+
+ for (int tri = 0; tri < ntris; ++tri) {
+ i0 = tri * 3;
+ i1 = tri * 3 + 1;
+ i2 = tri * 3 + 2;
+
+ Vector3f vert0 = mesh.vertices[i0];
+ Vector3f vert1 = mesh.vertices[i1];
+ Vector3f vert2 = mesh.vertices[i2];
+
+ drawTriangle(vert0, vert1, vert2, mesh.normals[tri], s, i0, i1, i2);
+ }
+ }
+
+ private float logToScrX(float x) {
+ return ((x + 1f) / 2) * width;
+ }
+
+ private float logToScrY(float y) {
+ return ((y + 1f) / 2) * height;
+ }
+
+ // triangles have flat normals (we don't need anything more than that in this renderer and it saves us the trouble of interpolating between 3 normal vectors)
+ private void drawTriangle(Vector3f vert0, Vector3f vert1, Vector3f vert2, Vector3f normal, Sampleable<Object> sampleable, int i0, int i1, int i2) {
+ Vector4f outColor = new Vector4f(), prevColor = new Vector4f();
+
+ vert0 = transform.transform(vert0);
+ vert1 = transform.transform(vert1);
+ vert2 = transform.transform(vert2);
+ normal = transform.transform(normal).normalize();
+
+ float sx0 = logToScrX(vert0.x), sy0 = logToScrY(vert0.y);
+ float sx1 = logToScrX(vert1.x), sy1 = logToScrY(vert1.y);
+ float sx2 = logToScrX(vert2.x), sy2 = logToScrY(vert2.y);
+
+ // optimization: Math.floor and Math.ceil convert float arguments to double
+ int minX = (int)Math.floor(Math.max(0, Math.min(sx0, Math.min(sx1, sx2))));
+ int maxX = (int)Math.ceil(Math.min(width - 1, Math.max(sx0, Math.max(sx1, sx2))));
+
+ int minY = (int)Math.floor(Math.max(0, Math.min(sy0, Math.min(sy1, sy2))));
+ int maxY = (int)Math.ceil(Math.min(height - 1, Math.max(sy0, Math.max(sy1, sy2))));
+
+ float area = (sy0 - sy2) * (sx1 - sx2) + (sy1 - sy2) * (sx2 - sx0);
+
+ for (int y = minY; y <= maxY; ++y) {
+ for (int x = minX; x < maxX; ++x) {
+ float b0 = ((y - sy2) * (sx1 - sx2) + (sy1 - sy2) * (sx2 - x)) / area;
+ float b1 = ((y - sy0) * (sx2 - sx0) + (sy2 - sy0) * (sx0 - x)) / area;
+ float b2 = ((y - sy1) * (sx0 - sx1) + (sy0 - sy1) * (sx1 - x)) / area;
+
+ if (b0 < 0 || b0 >= 1 || b1 < 0 || b1 >= 1 || b2 < 0 || b2 >= 1) continue;
+
+ float z = b0 * vert0.z + b1 * vert1.z + b2 * vert2.z;
+ if ((depthMode & FB_DEPTH_USE) != 0 && z <= depth[y * width + x]) continue;
+
+ if ((depthMode & FB_DEPTH_COMMIT) != 0 && outColor.w > 0) {
+ depth[y * width + x] = z;
+ }
+
+ prevColor.fromARGB(color.getRGB(x, y));
+ sampleable.sample(b0, b1, b2, normal, sampleable.extra(i0), sampleable.extra(i1), sampleable.extra(i2), outColor);
+
+ if ((depthMode & FB_DEPTH_COMMIT_TRANSPARENT) != 0 && outColor.w > 0) {
+ depth[y * width + x] = z;
+ }
+
+ blendMode.getFunction().blend(outColor, prevColor);
+ color.setRGB(x, y, outColor.toARGB());
+ }
+ }
+ }
+}
diff --git a/src/main/java/dev/figboot/cuberender/state/Mesh.java b/src/main/java/dev/figboot/cuberender/state/Mesh.java
new file mode 100644
index 0000000..2f28c16
--- /dev/null
+++ b/src/main/java/dev/figboot/cuberender/state/Mesh.java
@@ -0,0 +1,159 @@
+package dev.figboot.cuberender.state;
+
+import dev.figboot.cuberender.math.MathUtil;
+import dev.figboot.cuberender.math.Vector2f;
+import dev.figboot.cuberender.math.Vector3f;
+import dev.figboot.cuberender.math.Vector4f;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+
+import java.util.*;
+
+@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
+public abstract class Mesh<T> implements Sampleable<T> {
+ final Vector3f[] vertices;
+ final Vector3f[] normals;
+ final int[] indices;
+
+ final Map<AttachmentType, Object> attachments;
+
+ protected void applyLighting(Vector4f color, Vector3f normal) {
+ Float lightFact = (Float)attachments.get(AttachmentType.LIGHT_FACTOR);
+
+ if (lightFact == null) {
+ return;
+ }
+
+ float fact = 1 - (normal.dot((Vector3f)attachments.get(AttachmentType.LIGHT_VECTOR)) + 1) / 2;
+ fact *= lightFact; // lightFact should kinda set the "black level"
+ fact = 1 - fact;
+
+ fact = MathUtil.clamp(fact, 0, 1);
+
+ color.x *= fact;
+ color.y *= fact;
+ color.z *= fact;
+ }
+
+ public static class Builder {
+ private final List<Vector3f> vertices = new ArrayList<>();
+ private final List<Vector3f> normals = new ArrayList<>();
+ private final List<Integer> indices = new ArrayList<>();
+ private final List<Vector2f> texCoords = new ArrayList<>();
+ private int color;
+ private Texture texture;
+
+ private final Map<AttachmentType, Object> attachments = new EnumMap<>(AttachmentType.class);
+
+ public Builder() {
+ color = 0xFF000000;
+ texture = null;
+ }
+
+ public Builder color(int color) {
+ this.color = color;
+ return this;
+ }
+
+ public Builder texture(Texture t) {
+ this.texture = t;
+ return this;
+ }
+
+ public Builder texCoords(Vector2f... tex) {
+ texCoords.addAll(Arrays.asList(tex));
+ return this;
+ }
+
+ public Builder vertex(Vector3f... vert) {
+ vertices.addAll(Arrays.asList(vert));
+ return this;
+ }
+
+ public Builder normals(Vector3f... norm) {
+ normals.addAll(Arrays.asList(norm));
+ return this;
+ }
+
+ public Builder indices(int... indices) {
+ for (int idx : indices) {
+ this.indices.add(idx);
+ }
+ return this;
+ }
+
+ public Builder attach(AttachmentType type, Object o) {
+ attachments.put(type, o);
+ return this;
+ }
+
+ public Mesh<?> build() {
+ int[] idxArr;
+ if (indices.isEmpty()) {
+ idxArr = null;
+ } else {
+ idxArr = new int[indices.size()];
+ for (int i = 0, max = indices.size(); i < max; ++i) {
+ idxArr[i] = indices.get(i);
+ }
+ }
+
+ if (texture == null) {
+ return new ColorMesh(vertices.toArray(new Vector3f[0]), normals.toArray(new Vector3f[0]), idxArr, attachments, color);
+ } else {
+ return new TextureMesh(vertices.toArray(new Vector3f[0]), normals.toArray(new Vector3f[0]), idxArr, attachments, texture, texCoords.toArray(new Vector2f[0]));
+ }
+ }
+ }
+
+ private static class ColorMesh extends Mesh<Void> {
+ int color;
+
+ ColorMesh(Vector3f[] vertices, Vector3f[] normals, int[] indices, Map<AttachmentType, Object> attachments, int color) {
+ super(vertices, normals, indices, attachments);
+ this.color = color;
+ }
+
+ @Override
+ public Void extra(int idx) {
+ return null;
+ }
+
+ @Override
+ public void sample(float b0, float b1, float b2, Vector3f normal, Void u1, Void u2, Void u3, Vector4f outColor) {
+ applyLighting(outColor.fromARGB(color), normal);
+ }
+ }
+
+ private static class TextureMesh extends Mesh<Vector2f> {
+ Texture texture;
+ Vector2f[] texCoords;
+
+ TextureMesh(Vector3f[] vertices, Vector3f[] normals, int[] indices, Map<AttachmentType, Object> attachments, Texture tex, Vector2f[] texCoords) {
+ super(vertices, normals, indices, attachments);
+ this.texture = tex;
+ this.texCoords = texCoords;
+ }
+
+ @Override
+ public Vector2f extra(int idx) {
+ return texCoords[idx];
+ }
+
+ @Override
+ public void sample(float b0, float b1, float b2, Vector3f normal, Vector2f tc1, Vector2f tc2, Vector2f tc3, Vector4f color) {
+ float texX = b0 * tc1.x + b1 * tc2.x + b2 * tc3.x;
+ float texY = b0 * tc1.y + b1 * tc2.y + b2 * tc3.y;
+
+ int texiX = (int)Math.min(texture.width-1, Math.max(0, Math.floor(texX * texture.width)));
+ int texiY = (int)Math.min(texture.height-1, Math.max(0, Math.floor((1f - texY) * texture.height)));
+
+ applyLighting(color.fromARGB(texture.image.getRGB(texiX, texiY)), normal);
+ }
+ }
+
+ public enum AttachmentType {
+ LIGHT_FACTOR, // float
+ LIGHT_VECTOR // Vector3f
+ }
+}
diff --git a/src/main/java/dev/figboot/cuberender/state/Sampleable.java b/src/main/java/dev/figboot/cuberender/state/Sampleable.java
new file mode 100644
index 0000000..876a248
--- /dev/null
+++ b/src/main/java/dev/figboot/cuberender/state/Sampleable.java
@@ -0,0 +1,10 @@
+package dev.figboot.cuberender.state;
+
+import dev.figboot.cuberender.math.Vector3f;
+import dev.figboot.cuberender.math.Vector4f;
+
+public interface Sampleable<T> {
+ T extra(int idx);
+
+ void sample(float b0, float b1, float b2, Vector3f normal, T e1, T e2, T e3, Vector4f target);
+}
diff --git a/src/main/java/dev/figboot/cuberender/state/Texture.java b/src/main/java/dev/figboot/cuberender/state/Texture.java
new file mode 100644
index 0000000..843638d
--- /dev/null
+++ b/src/main/java/dev/figboot/cuberender/state/Texture.java
@@ -0,0 +1,14 @@
+package dev.figboot.cuberender.state;
+
+import java.awt.image.BufferedImage;
+
+public class Texture {
+ public final BufferedImage image;
+ public transient int width, height;
+
+ public Texture(BufferedImage image) {
+ this.image = image;
+ this.width = image.getWidth();
+ this.height = image.getHeight();
+ }
+}
diff --git a/src/main/java/dev/figboot/cuberender/test/GraphicsPanel.java b/src/main/java/dev/figboot/cuberender/test/GraphicsPanel.java
new file mode 100644
index 0000000..38dea64
--- /dev/null
+++ b/src/main/java/dev/figboot/cuberender/test/GraphicsPanel.java
@@ -0,0 +1,387 @@
+package dev.figboot.cuberender.test;
+
+import dev.figboot.cuberender.math.Matrix3f;
+import dev.figboot.cuberender.math.Vector2f;
+import dev.figboot.cuberender.math.Vector3f;
+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<BodyPart, Mesh<?>> 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 Vector3f(x1, y1, z2), /* front */
+ new Vector3f(x1, y2, z2),
+ new Vector3f(x2, y2, z2),
+ new Vector3f(x2, y1, z2),
+
+ new Vector3f(x2, y1, z2), /* +X side */
+ new Vector3f(x2, y2, z2),
+ new Vector3f(x2, y2, z1),
+ new Vector3f(x2, y1, z1),
+
+ new Vector3f(x2, y1, z1), /* back */
+ new Vector3f(x2, y2, z1),
+ new Vector3f(x1, y2, z1),
+ new Vector3f(x1, y1, z1),
+
+ new Vector3f(x1, y1, z1), /* -X side */
+ new Vector3f(x1, y2, z1),
+ new Vector3f(x1, y2, z2),
+ new Vector3f(x1, y1, z2),
+
+ new Vector3f(x1, y1, z1), /* top */
+ new Vector3f(x1, y1, z2),
+ new Vector3f(x2, y1, z2),
+ new Vector3f(x2, y1, z1),
+
+ new Vector3f(x1, y2, z1), /* bottom */
+ new Vector3f(x1, y2, z2),
+ new Vector3f(x2, y2, z2),
+ new Vector3f(x2, y2, z1))
+ .normals(new Vector3f(0, 0, 1),
+ new Vector3f(0, 0, 1),
+
+ new Vector3f(1, 0, 0),
+ new Vector3f(1, 0, 0),
+
+ new Vector3f(0, 0, -1),
+ new Vector3f(0, 0, -1),
+
+ new Vector3f(-1, 0, 0),
+ new Vector3f(-1, 0, 0),
+
+ new Vector3f(0, -1, 0),
+ new Vector3f(0, -1, 0),
+
+ new Vector3f(0, 1, 0),
+ new Vector3f(0, 1, 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 Vector3f(0, 0, 1));
+ }
+
+ 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(Matrix3f.rotateY(yRot).times(Matrix3f.rotateX(xRot)).times(Matrix3f.scaleX(0.5f).times(Matrix3f.scaleY(0.5f).times(Matrix3f.scaleZ(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
+ }
+}
diff --git a/src/main/java/dev/figboot/cuberender/test/TestWindow.java b/src/main/java/dev/figboot/cuberender/test/TestWindow.java
new file mode 100644
index 0000000..fc768a3
--- /dev/null
+++ b/src/main/java/dev/figboot/cuberender/test/TestWindow.java
@@ -0,0 +1,49 @@
+package dev.figboot.cuberender.test;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class TestWindow extends JFrame {
+ public TestWindow() {
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ setTitle("Graphics test");
+ setSize(300, 300);
+ setLocationRelativeTo(null);
+
+ JPanel panel = new JPanel();
+ JSlider sliderY = new JSlider();
+ JSlider sliderX = new JSlider();
+ GraphicsPanel gp = new GraphicsPanel();
+
+ sliderY.setMinimum(-180);
+ sliderY.setMaximum(180);
+
+ sliderX.setMinimum(-180);
+ sliderX.setMaximum(180);
+ sliderX.setOrientation(JSlider.VERTICAL);
+
+ sliderX.setValue(0);
+ sliderY.setValue(0);
+
+ panel.setLayout(new BorderLayout());
+ panel.add(gp, BorderLayout.CENTER);
+ panel.add(sliderY, BorderLayout.SOUTH);
+ panel.add(sliderX, BorderLayout.EAST);
+
+ setContentPane(panel);
+
+ sliderY.addChangeListener(e -> {
+ gp.setYRot((float)Math.toRadians(sliderY.getValue()));
+ gp.repaint();
+ });
+
+ sliderX.addChangeListener(e -> {
+ gp.setXRot((float)Math.toRadians(sliderX.getValue()));
+ gp.repaint();
+ });
+ }
+
+ public static void main(String[] args) {
+ new TestWindow().setVisible(true);
+ }
+}