From 2113ae54df2da867f553df3a9ee457c0a3856a33 Mon Sep 17 00:00:00 2001 From: bigfoot547 Date: Fri, 14 Jun 2024 20:29:43 -0500 Subject: initial commit --- .../dev/figboot/cuberender/state/BlendMode.java | 26 ++++ .../dev/figboot/cuberender/state/Framebuffer.java | 162 +++++++++++++++++++++ .../java/dev/figboot/cuberender/state/Mesh.java | 159 ++++++++++++++++++++ .../dev/figboot/cuberender/state/Sampleable.java | 10 ++ .../java/dev/figboot/cuberender/state/Texture.java | 14 ++ 5 files changed, 371 insertions(+) create mode 100644 src/main/java/dev/figboot/cuberender/state/BlendMode.java create mode 100644 src/main/java/dev/figboot/cuberender/state/Framebuffer.java create mode 100644 src/main/java/dev/figboot/cuberender/state/Mesh.java create mode 100644 src/main/java/dev/figboot/cuberender/state/Sampleable.java create mode 100644 src/main/java/dev/figboot/cuberender/state/Texture.java (limited to 'src/main/java/dev/figboot/cuberender/state') 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 s = (Sampleable)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 s = (Sampleable)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 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 implements Sampleable { + final Vector3f[] vertices; + final Vector3f[] normals; + final int[] indices; + + final Map 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 vertices = new ArrayList<>(); + private final List normals = new ArrayList<>(); + private final List indices = new ArrayList<>(); + private final List texCoords = new ArrayList<>(); + private int color; + private Texture texture; + + private final Map 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 { + int color; + + ColorMesh(Vector3f[] vertices, Vector3f[] normals, int[] indices, Map 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 { + Texture texture; + Vector2f[] texCoords; + + TextureMesh(Vector3f[] vertices, Vector3f[] normals, int[] indices, Map 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 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(); + } +} -- cgit v1.2.3-70-g09d2