From 7875befa941efd47df0bb76052c9e044544f8c73 Mon Sep 17 00:00:00 2001 From: jbb01 <32650546+jbb01@users.noreply.github.com> Date: Sat, 3 Aug 2024 01:27:06 +0200 Subject: [PATCH] add camera, scene and skybox --- .../java/eu/jonahbauer/raytracing/Main.java | 15 +++- .../jonahbauer/raytracing/render/Camera.java | 73 +++++++++++++++++++ .../jonahbauer/raytracing/render/Color.java | 1 + .../jonahbauer/raytracing/render/Scene.java | 30 ++++++++ 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 src/main/java/eu/jonahbauer/raytracing/render/Camera.java create mode 100644 src/main/java/eu/jonahbauer/raytracing/render/Scene.java diff --git a/src/main/java/eu/jonahbauer/raytracing/Main.java b/src/main/java/eu/jonahbauer/raytracing/Main.java index 8c3927b..02d1e1b 100644 --- a/src/main/java/eu/jonahbauer/raytracing/Main.java +++ b/src/main/java/eu/jonahbauer/raytracing/Main.java @@ -1,7 +1,18 @@ package eu.jonahbauer.raytracing; +import eu.jonahbauer.raytracing.render.Camera; +import eu.jonahbauer.raytracing.render.ImageIO; +import eu.jonahbauer.raytracing.render.Scene; + +import java.io.IOException; +import java.nio.file.Path; + public class Main { - public static void main(String[] args) { - System.out.println("Hello world!"); + public static void main(String[] args) throws IOException { + var scene = new Scene(); + var camera = new Camera(256, 2, 16 / 9d); + + var image = scene.render(camera); + ImageIO.write(image, Path.of("scene-" + System.currentTimeMillis() + ".ppm")); } } \ No newline at end of file diff --git a/src/main/java/eu/jonahbauer/raytracing/render/Camera.java b/src/main/java/eu/jonahbauer/raytracing/render/Camera.java new file mode 100644 index 0000000..7bd740c --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/Camera.java @@ -0,0 +1,73 @@ +package eu.jonahbauer.raytracing.render; + +import eu.jonahbauer.raytracing.math.Ray; +import eu.jonahbauer.raytracing.math.Vec3; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public record Camera( + int width, int height, + double viewportWidth, double viewportHeight, + @NotNull Vec3 origin, @NotNull Vec3 direction +) { + public Camera { + if (width <= 0) throw new IllegalArgumentException("width must be positive"); + if (height <= 0) throw new IllegalArgumentException("height must be positive"); + if (viewportWidth <= 0 || !Double.isFinite(viewportWidth)) throw new IllegalArgumentException("viewportWidth must be positive"); + if (viewportHeight <= 0 || !Double.isFinite(viewportHeight)) throw new IllegalArgumentException("viewportHeight must be positive"); + Objects.requireNonNull(origin, "origin"); + Objects.requireNonNull(direction, "direction"); + } + + /** + * Creates a new camera with the given dimensions at the origin facing towards positive z with a focal length of 1. + * @param height the image height + * @param viewportHeight the viewport height + * @param aspectRatio the aspect ratio + */ + public Camera(int height, double viewportHeight, double aspectRatio) { + this((int) (height * aspectRatio), height, viewportHeight * aspectRatio, viewportHeight); + } + + /** + * Creates a new camera with the given dimensions at the origin facing towards positive z with a focal length of 1. + * @param width the image width + * @param height the image height + * @param viewportWidth the viewport width + * @param viewportHeight the viewport height + */ + public Camera(int width, int height, double viewportWidth, double viewportHeight) { + this(width, height, viewportWidth, viewportHeight, Vec3.ZERO, Vec3.UNIT_Z); + } + + public @NotNull Stream pixels() { + // project direction onto xz-plane + var d = direction.unit(); + var dXZ = direction.withY(0).unit(); + + var viewportU = new Vec3(dXZ.z(), 0, - dXZ.x()); // perpendicular to dXZ in xz-plane + var viewportV = d.cross(viewportU); // perpendicular to viewportU and direction + + viewportU = viewportU.times(viewportWidth); // vector along the width of the viewport + viewportV = viewportV.times(- viewportHeight); // vector along the height of the viewport + + var pixelU = viewportU.div(width); + var pixelV = viewportV.div(height); + + var pixel00 = origin.plus(direction) + .minus(viewportU.div(2)).minus(viewportV.div(2)) + .plus(pixelU.div(2)).plus(pixelV.div(2)); + + return IntStream.range(0, width * height).mapToObj(i -> { + var y = i / width; + var x = i % width; + var pixel = pixel00.plus(pixelU.times(x)).plus(pixelV.times(y)); + return new Pixel(x, y, new Ray(origin, pixel.minus(origin))); + }); + } + + public record Pixel(int x, int y, @NotNull Ray ray) {} +} diff --git a/src/main/java/eu/jonahbauer/raytracing/render/Color.java b/src/main/java/eu/jonahbauer/raytracing/render/Color.java index 4c31da1..98c17da 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/Color.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/Color.java @@ -5,6 +5,7 @@ import org.jetbrains.annotations.NotNull; 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 @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 new file mode 100644 index 0000000..1f0ea1e --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/Scene.java @@ -0,0 +1,30 @@ +package eu.jonahbauer.raytracing.render; + +import eu.jonahbauer.raytracing.math.Ray; +import org.jetbrains.annotations.NotNull; + +public record Scene() { + + public @NotNull Image render(@NotNull Camera camera) { + var image = new Image(camera.width(), camera.height()); + + camera.pixels().forEach(pixel -> { + var x = pixel.x(); + var y = pixel.y(); + var ray = pixel.ray(); + + image.set(x, y, getSkyboxColor(ray)); + }); + + return image; + } + + private @NotNull Color getSkyboxColor(@NotNull Ray ray) { + // altitude from -pi/2 to pi/2 + var alt = Math.copySign( + Math.acos(ray.direction().withY(0).unit().times(ray.direction().unit())), + ray.direction().y() + ); + return Color.lerp(Color.WHITE, Color.SKY, alt / Math.PI + 0.5); + } +}