aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/dev/figboot/cuberender/state
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/dev/figboot/cuberender/state')
-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
5 files changed, 371 insertions, 0 deletions
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();
+ }
+}