add math and image
parent
2d2f5090ea
commit
028d19b118
@ -0,0 +1,21 @@
|
||||
package eu.jonahbauer.raytracing.math;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) {
|
||||
public Ray {
|
||||
Objects.requireNonNull(origin, "origin");
|
||||
Objects.requireNonNull(direction, "direction");
|
||||
}
|
||||
|
||||
public @NotNull Vec3 at(double t) {
|
||||
if (t < 0) throw new IllegalArgumentException("t must not be negative");
|
||||
return new Vec3(
|
||||
origin().x() + t * direction.x(),
|
||||
origin().y() + t * direction.y(),
|
||||
origin().z() + t * direction.z()
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package eu.jonahbauer.raytracing.math;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record Vec3(double x, double y, double z) {
|
||||
public static final Vec3 ZERO = new Vec3(0, 0, 0);
|
||||
public static final Vec3 UNIT_X = new Vec3(1, 0, 0);
|
||||
public static final Vec3 UNIT_Y = new Vec3(0, 1, 0);
|
||||
public static final Vec3 UNIT_Z = new Vec3(0, 0, 1);
|
||||
|
||||
public Vec3 {
|
||||
if (!Double.isFinite(x) || !Double.isFinite(y) || !Double.isFinite(z)) {
|
||||
throw new IllegalArgumentException("x, y and z must be finite");
|
||||
}
|
||||
}
|
||||
|
||||
public @NotNull Vec3 plus(@NotNull Vec3 b) {
|
||||
return new Vec3(this.x + b.x, this.y + b.y, this.z + b.z);
|
||||
}
|
||||
|
||||
public @NotNull Vec3 minus(@NotNull Vec3 b) {
|
||||
return new Vec3(this.x - b.x, this.y - b.y, this.z - b.z);
|
||||
}
|
||||
|
||||
public double times(@NotNull Vec3 b) {
|
||||
return this.x * b.x + this.y * b.y + this.z * b.z;
|
||||
}
|
||||
|
||||
public @NotNull Vec3 times(double b) {
|
||||
return new Vec3(this.x * b, this.y * b, this.z * b);
|
||||
}
|
||||
|
||||
public @NotNull Vec3 cross(@NotNull Vec3 b) {
|
||||
return new Vec3(
|
||||
this.y() * b.z() - b.y() * this.z(),
|
||||
this.z() * b.x() - b.z() * this.x(),
|
||||
this.x() * b.y() - b.x() * this.y()
|
||||
);
|
||||
}
|
||||
|
||||
public @NotNull Vec3 div(double b) {
|
||||
return new Vec3(this.x / b, this.y / b, this.z / b);
|
||||
}
|
||||
|
||||
public double squared() {
|
||||
return this.x * this.x + this.y * this.y + this.z * this.z;
|
||||
}
|
||||
|
||||
public double length() {
|
||||
return Math.sqrt(squared());
|
||||
}
|
||||
|
||||
public @NotNull Vec3 unit() {
|
||||
return div(length());
|
||||
}
|
||||
|
||||
public @NotNull Vec3 withX(double x) {
|
||||
return new Vec3(x, y, z);
|
||||
}
|
||||
|
||||
public @NotNull Vec3 withY(double y) {
|
||||
return new Vec3(x, y, z);
|
||||
}
|
||||
|
||||
public @NotNull Vec3 withZ(double z) {
|
||||
return new Vec3(x, y, z);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package eu.jonahbauer.raytracing.render;
|
||||
|
||||
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 @NotNull Color lerp(@NotNull Color a, @NotNull Color b, double t) {
|
||||
if (t < 0) return a;
|
||||
if (t > 1) return b;
|
||||
return new Color(
|
||||
(1 - t) * a.r + t * b.r,
|
||||
(1 - t) * a.g + t * b.g,
|
||||
(1 - t) * a.b + t * b.b
|
||||
);
|
||||
}
|
||||
|
||||
public Color {
|
||||
if (r < 0 || r > 1 || g < 0 || g > 1 || b < 0 || b > 1) {
|
||||
throw new IllegalArgumentException("r, g and b must be in the range 0 to 1");
|
||||
}
|
||||
}
|
||||
|
||||
public Color(int red, int green, int blue) {
|
||||
this(red / 255f, green / 255f, blue / 255f);
|
||||
}
|
||||
|
||||
public int red() {
|
||||
return (int) (255.99 * r);
|
||||
}
|
||||
|
||||
public int green() {
|
||||
return (int) (255.99 * g);
|
||||
}
|
||||
|
||||
public int blue() {
|
||||
return (int) (255.99 * b);
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package eu.jonahbauer.raytracing.render;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class Image {
|
||||
private final int width;
|
||||
private final int height;
|
||||
|
||||
private final Color[][] data;
|
||||
|
||||
public Image(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
if (width <= 0) throw new IllegalArgumentException("width must be positive");
|
||||
if (height <= 0) throw new IllegalArgumentException("height must be positive");
|
||||
|
||||
this.data = new Color[height][width];
|
||||
}
|
||||
|
||||
public int width() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int height() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public @NotNull Color get(int x, int y) {
|
||||
Objects.checkIndex(x, width);
|
||||
Objects.checkIndex(y, height);
|
||||
return Objects.requireNonNullElse(this.data[y][x], Color.BLACK);
|
||||
}
|
||||
|
||||
public @NotNull Stream<Color> pixels() {
|
||||
return Arrays.stream(data).flatMap(Arrays::stream).map(c -> Objects.requireNonNullElse(c, Color.BLACK));
|
||||
}
|
||||
|
||||
public void set(int x, int y, @NotNull Color color) {
|
||||
Objects.checkIndex(x, width);
|
||||
Objects.checkIndex(y, height);
|
||||
this.data[y][x] = Objects.requireNonNull(color);
|
||||
}
|
||||
|
||||
public void set(int x, int y, int red, int green, int blue) {
|
||||
set(x, y, new Color(red, green, blue));
|
||||
}
|
||||
|
||||
public void set(int x, int y, double r, double g, double b) {
|
||||
set(x, y, new Color(r, g, b));
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package eu.jonahbauer.raytracing.render;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class ImageIO {
|
||||
private ImageIO() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public static void write(@NotNull Image image, @NotNull Path path) throws IOException {
|
||||
try (var out = Files.newBufferedWriter(path)) {
|
||||
out.write("P3\n");
|
||||
out.write(String.valueOf(image.width()));
|
||||
out.write(" ");
|
||||
out.write(String.valueOf(image.height()));
|
||||
out.write("\n255\n");
|
||||
|
||||
var it = image.pixels().iterator();
|
||||
while (it.hasNext()) {
|
||||
var color = it.next();
|
||||
out.write(String.valueOf(color.red()));
|
||||
out.write(" ");
|
||||
out.write(String.valueOf(color.green()));
|
||||
out.write(" ");
|
||||
out.write(String.valueOf(color.blue()));
|
||||
out.write("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package eu.jonahbauer.raytracing.math;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class RayTest {
|
||||
|
||||
@Test
|
||||
void at() {
|
||||
var origin = new Vec3(0, 0, 0);
|
||||
var direction = new Vec3(1, 2, 3);
|
||||
var ray = new Ray(origin, direction);
|
||||
assertEquals(new Vec3(5, 10, 15), ray.at(5));
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package eu.jonahbauer.raytracing.math;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class Vec3Test {
|
||||
|
||||
@Test
|
||||
void plus() {
|
||||
var a = new Vec3(1, 2, 3);
|
||||
var b = new Vec3(-1, 1, -2);
|
||||
assertEquals(new Vec3(0, 3, 1), a.plus(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
void minus() {
|
||||
var a = new Vec3(1, 2, 3);
|
||||
var b = new Vec3(-1, 1, -2);
|
||||
assertEquals(new Vec3(2, 1, 5), a.minus(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
void timesVec() {
|
||||
var a = new Vec3(1, 2, 3);
|
||||
var b = new Vec3(-1, 1, -2);
|
||||
assertEquals(-5, a.times(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeScalar() {
|
||||
var a = new Vec3(1, 2, 3);
|
||||
var b = 2.5;
|
||||
assertEquals(new Vec3(2.5, 5, 7.5), a.times(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
void div() {
|
||||
var a = new Vec3(1, 2, 3);
|
||||
var b = 2;
|
||||
assertEquals(new Vec3(0.5, 1, 1.5), a.div(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
void squared() {
|
||||
var a = new Vec3(1, 2, 3);
|
||||
assertEquals(14, a.squared());
|
||||
}
|
||||
|
||||
@Test
|
||||
void length() {
|
||||
var a = new Vec3(3, 4, 0);
|
||||
assertEquals(5, a.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unit() {
|
||||
var a = new Vec3(3, 4, 0);
|
||||
assertEquals(new Vec3(0.6, 0.8, 0), a.unit());
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package eu.jonahbauer.raytracing.render;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class ImageTest {
|
||||
|
||||
@Test
|
||||
void test(@TempDir Path dir) throws IOException {
|
||||
var image = new Image(256, 256);
|
||||
|
||||
for (var y = 0; y < image.height(); y++) {
|
||||
for (var x = 0; x < image.width(); x++) {
|
||||
var r = (double) x / (image.width() - 1);
|
||||
var g = (double) y / (image.height() - 1);
|
||||
var b = 0;
|
||||
|
||||
image.set(x, y, r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println(dir);
|
||||
ImageIO.write(image, dir.resolve("img.ppm"));
|
||||
|
||||
String expected;
|
||||
String actual;
|
||||
|
||||
try (var in = Objects.requireNonNull(ImageTest.class.getResourceAsStream("simple_image.ppm"))) {
|
||||
expected = new String(in.readAllBytes(), StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
try (var in = Files.newInputStream(dir.resolve("img.ppm"))) {
|
||||
actual = new String(in.readAllBytes(), StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue