diff --git a/src/main/java/eu/jonahbauer/raytracing/Main.java b/src/main/java/eu/jonahbauer/raytracing/Main.java
index 02d1e1b..7efef97 100644
--- a/src/main/java/eu/jonahbauer/raytracing/Main.java
+++ b/src/main/java/eu/jonahbauer/raytracing/Main.java
@@ -3,13 +3,14 @@ package eu.jonahbauer.raytracing;
import eu.jonahbauer.raytracing.render.Camera;
import eu.jonahbauer.raytracing.render.ImageIO;
import eu.jonahbauer.raytracing.render.Scene;
+import eu.jonahbauer.raytracing.shape.Sphere;
import java.io.IOException;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
- var scene = new Scene();
+ var scene = new Scene(new Sphere(0, 0, 1, 0.5));
var camera = new Camera(256, 2, 16 / 9d);
var image = scene.render(camera);
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/Color.java b/src/main/java/eu/jonahbauer/raytracing/render/Color.java
index 98c17da..dee19d1 100644
--- a/src/main/java/eu/jonahbauer/raytracing/render/Color.java
+++ b/src/main/java/eu/jonahbauer/raytracing/render/Color.java
@@ -6,6 +6,7 @@ public record Color(double r, double g, double b) {
public static final @NotNull Color BLACK = new Color(0.0, 0.0, 0.0);
public static final @NotNull Color WHITE = new Color(1.0, 1.0, 1.0);
public static final @NotNull Color SKY = new Color(0.5, 0.7, 1.0);
+ public static final @NotNull Color RED = new Color(1.0, 0.0, 0.0);
public static @NotNull Color lerp(@NotNull Color a, @NotNull Color b, double t) {
if (t < 0) return a;
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/Scene.java b/src/main/java/eu/jonahbauer/raytracing/render/Scene.java
index 1f0ea1e..02f4dba 100644
--- a/src/main/java/eu/jonahbauer/raytracing/render/Scene.java
+++ b/src/main/java/eu/jonahbauer/raytracing/render/Scene.java
@@ -1,9 +1,20 @@
package eu.jonahbauer.raytracing.render;
import eu.jonahbauer.raytracing.math.Ray;
+import eu.jonahbauer.raytracing.shape.Shape;
import org.jetbrains.annotations.NotNull;
-public record Scene() {
+import java.util.List;
+
+public record Scene(@NotNull List<@NotNull Shape> shapes) {
+
+ public Scene {
+ shapes = List.copyOf(shapes);
+ }
+
+ public Scene(@NotNull Shape @NotNull ... shapes) {
+ this(List.of(shapes));
+ }
public @NotNull Image render(@NotNull Camera camera) {
var image = new Image(camera.width(), camera.height());
@@ -13,7 +24,15 @@ public record Scene() {
var y = pixel.y();
var ray = pixel.ray();
- image.set(x, y, getSkyboxColor(ray));
+ var result = shapes.stream()
+ .mapToDouble(shape -> shape.hit(ray))
+ .filter(Double::isFinite)
+ .min();
+ if (result.isPresent()) {
+ image.set(x, y, Color.RED);
+ } else {
+ image.set(x, y, getSkyboxColor(ray));
+ }
});
return image;
diff --git a/src/main/java/eu/jonahbauer/raytracing/shape/Shape.java b/src/main/java/eu/jonahbauer/raytracing/shape/Shape.java
new file mode 100644
index 0000000..ef3fe77
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/shape/Shape.java
@@ -0,0 +1,14 @@
+package eu.jonahbauer.raytracing.shape;
+
+import eu.jonahbauer.raytracing.math.Ray;
+import org.jetbrains.annotations.NotNull;
+
+public sealed interface Shape permits Sphere {
+
+ /**
+ * {@return the value t
such that ray.at(t)
is the intersection of this shaped closest to
+ * the ray origin, or Double.NaN
if the ray does not intersect this shape}
+ * @param ray a ray
+ */
+ double hit(@NotNull Ray ray);
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/shape/Sphere.java b/src/main/java/eu/jonahbauer/raytracing/shape/Sphere.java
new file mode 100644
index 0000000..020de3f
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/shape/Sphere.java
@@ -0,0 +1,51 @@
+package eu.jonahbauer.raytracing.shape;
+
+import eu.jonahbauer.raytracing.math.Ray;
+import eu.jonahbauer.raytracing.math.Vec3;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+public record Sphere(@NotNull Vec3 center, double radius) implements Shape {
+ public static final @NotNull Sphere UNIT = new Sphere(Vec3.ZERO, 1.0);
+
+ public Sphere {
+ Objects.requireNonNull(center, "center");
+ if (radius <= 0 || !Double.isFinite(radius)) throw new IllegalArgumentException("radius must be positive");
+ }
+
+ public Sphere(double x, double y, double z, double r) {
+ this(new Vec3(x, y, z), r);
+ }
+
+ @Override
+ public double hit(@NotNull Ray ray) {
+ var oc = ray.origin().minus(center());
+
+ var a = ray.direction().squared();
+ var b = 2 * ray.direction().times(oc);
+ var c = oc.squared() - radius * radius;
+
+ var discriminant = b * b - 4 * a * c;
+ if (discriminant < 0) return Double.NaN;
+
+ var sd = Math.sqrt(discriminant);
+
+ double t = (- b - sd) / (2 * a);
+ if (t < 0) t = (-b + sd) / (2 * a);
+ if (t < 0) t = Double.NaN;
+ return t;
+ }
+
+ public @NotNull Sphere withCenter(@NotNull Vec3 center) {
+ return new Sphere(center, radius);
+ }
+
+ public @NotNull Sphere withCenter(double x, double y, double z) {
+ return withCenter(new Vec3(x, y, z));
+ }
+
+ public @NotNull Sphere withRadius(double radius) {
+ return new Sphere(center, radius);
+ }
+}
diff --git a/src/test/java/eu/jonahbauer/raytracing/shape/SphereTest.java b/src/test/java/eu/jonahbauer/raytracing/shape/SphereTest.java
new file mode 100644
index 0000000..5bec826
--- /dev/null
+++ b/src/test/java/eu/jonahbauer/raytracing/shape/SphereTest.java
@@ -0,0 +1,25 @@
+package eu.jonahbauer.raytracing.shape;
+
+import eu.jonahbauer.raytracing.math.Ray;
+import eu.jonahbauer.raytracing.math.Vec3;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class SphereTest {
+
+ @Test
+ void hit() {
+ var center = new Vec3(1, 2, 3);
+ var radius = 5;
+ var sphere = new Sphere(center, radius);
+
+ var origin = new Vec3(6, 7, 8);
+ var direction = new Vec3(-1, -1, -1);
+ var ray = new Ray(origin, direction);
+
+ var t = sphere.hit(ray);
+ assertFalse(Double.isNaN(t));
+ assertEquals(center.plus(new Vec3(1, 1, 1).unit().times(radius)), ray.at(t));
+ }
+}
\ No newline at end of file