diff options
Diffstat (limited to 'src')
19 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); + } +} diff --git a/src/main/resources/cape.png b/src/main/resources/cape.png Binary files differnew file mode 100644 index 0000000..9a3c129 --- /dev/null +++ b/src/main/resources/cape.png diff --git a/src/main/resources/skin2.png b/src/main/resources/skin2.png Binary files differnew file mode 100644 index 0000000..e510b1f --- /dev/null +++ b/src/main/resources/skin2.png diff --git a/src/main/resources/skin3.png b/src/main/resources/skin3.png Binary files differnew file mode 100644 index 0000000..c435612 --- /dev/null +++ b/src/main/resources/skin3.png diff --git a/src/main/resources/skin4.png b/src/main/resources/skin4.png Binary files differnew file mode 100644 index 0000000..74d3a5d --- /dev/null +++ b/src/main/resources/skin4.png diff --git a/src/main/resources/skinSlim.png b/src/main/resources/skinSlim.png Binary files differnew file mode 100644 index 0000000..93e9838 --- /dev/null +++ b/src/main/resources/skinSlim.png diff --git a/src/main/resources/translucent.png b/src/main/resources/translucent.png Binary files differnew file mode 100644 index 0000000..8649f7c --- /dev/null +++ b/src/main/resources/translucent.png |
