Compare commits
3 Commits
d67f877428
...
18b9a52404
Author | SHA1 | Date | |
---|---|---|---|
18b9a52404 | |||
b40cc15a9f | |||
87a7fbfcff |
@ -2,37 +2,114 @@ package eu.jonahbauer.raytracing;
|
||||
|
||||
import eu.jonahbauer.raytracing.material.DielectricMaterial;
|
||||
import eu.jonahbauer.raytracing.material.LambertianMaterial;
|
||||
import eu.jonahbauer.raytracing.material.Material;
|
||||
import eu.jonahbauer.raytracing.material.MetallicMaterial;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.Camera;
|
||||
import eu.jonahbauer.raytracing.render.*;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.ImageFormat;
|
||||
import eu.jonahbauer.raytracing.render.canvas.BufferedImageCanvas;
|
||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||
import eu.jonahbauer.raytracing.scene.Scene;
|
||||
import eu.jonahbauer.raytracing.scene.Sphere;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) throws IOException {
|
||||
var scene = new Scene(
|
||||
new Sphere(0, -100.5, - 1, 100, new LambertianMaterial(new Color(0.8, 0.8, 0.0))),
|
||||
new Sphere(0, 0, - 1.2, 0.5, new LambertianMaterial(new Color(0.1, 0.2, 0.5))),
|
||||
new Sphere(-1, 0, - 1, 0.5, new DielectricMaterial(1.5)),
|
||||
new Sphere(-1, 0, - 1, 0.4, new DielectricMaterial(1 / 1.5)),
|
||||
new Sphere(1, 0, - 1, 0.5, new MetallicMaterial(new Color(0.8, 0.6, 0.2), 1.0))
|
||||
);
|
||||
|
||||
var scene = getScene();
|
||||
var camera = Camera.builder()
|
||||
// .withImage(1920, 1080)
|
||||
.withImage(800, 450)
|
||||
.withPosition(new Vec3(-2, 2, 1))
|
||||
.withTarget(new Vec3(0, 0, -1))
|
||||
.withPosition(new Vec3(13, 2, 3))
|
||||
.withTarget(new Vec3(0, 0, 0))
|
||||
.withSamplesPerPixel(100)
|
||||
.withMaxDepth(10)
|
||||
.withFieldOfView(Math.toRadians(20))
|
||||
.withFocusDistance(3.4)
|
||||
.withBlurAngle(Math.toRadians(10))
|
||||
.withBlurAngle(Math.toRadians(0.6))
|
||||
.withFocusDistance(10.0)
|
||||
.build();
|
||||
|
||||
var image = camera.render(scene);
|
||||
ImageFormat.PNG.write(image, Path.of("scene-" + System.currentTimeMillis() + ".png"));
|
||||
var canvas = new BufferedImageCanvas(camera.width(), camera.height());
|
||||
preview(canvas);
|
||||
|
||||
camera.render(scene, canvas, Camera.RenderMode.SAMPLES);
|
||||
ImageFormat.PNG.write(canvas, Path.of("scene-" + System.currentTimeMillis() + ".png"));
|
||||
}
|
||||
|
||||
private static Thread preview(@NotNull BufferedImageCanvas canvas) {
|
||||
var frame = new JFrame();
|
||||
frame.setSize(canvas.width(), canvas.height());
|
||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
frame.setContentPane(new JPanel() {
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
g.drawImage(canvas.getImage(), 0, 0, null);
|
||||
}
|
||||
});
|
||||
frame.setResizable(false);
|
||||
frame.setVisible(true);
|
||||
|
||||
var update = Thread.ofVirtual().start(() -> {
|
||||
while (!Thread.interrupted()) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException ex) {
|
||||
break;
|
||||
}
|
||||
frame.repaint();
|
||||
}
|
||||
});
|
||||
|
||||
frame.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
update.interrupt();
|
||||
}
|
||||
});
|
||||
|
||||
return update;
|
||||
}
|
||||
|
||||
private static @NotNull Scene getScene() {
|
||||
var objects = new ArrayList<Hittable>();
|
||||
objects.add(new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.5, 0.5, 0.5))));
|
||||
|
||||
for (int a = -11; a < 11; a++) {
|
||||
for (int b = -11; b < 11; b++) {
|
||||
var center = new Vec3(a + 0.9 * Math.random(), 0.2, b + 0.9 * Math.random());
|
||||
if (Vec3.distance(center, new Vec3(4, 0.2, 0)) <= 0.9) continue;
|
||||
|
||||
Material material;
|
||||
var rnd = Math.random();
|
||||
if (rnd < 0.8) {
|
||||
// diffuse
|
||||
var albedo = Color.multiply(Color.random(), Color.random());
|
||||
material = new LambertianMaterial(albedo);
|
||||
} else if (rnd < 0.95) {
|
||||
// metal
|
||||
var albedo = Color.random(0.5, 1.0);
|
||||
var fuzz = Math.random() * 0.5;
|
||||
material = new MetallicMaterial(albedo, fuzz);
|
||||
} else {
|
||||
// glass
|
||||
material = new DielectricMaterial(1.5);
|
||||
}
|
||||
|
||||
objects.add(new Sphere(center, 0.2, material));
|
||||
}
|
||||
}
|
||||
|
||||
objects.add(new Sphere(new Vec3(0, 1, 0), 1.0, new DielectricMaterial(1.5)));
|
||||
objects.add(new Sphere(new Vec3(-4, 1, 0), 1.0, new LambertianMaterial(new Color(0.4, 0.2, 0.1))));
|
||||
objects.add(new Sphere(new Vec3(4, 1, 0), 1.0, new MetallicMaterial(new Color(0.7, 0.6, 0.5))));
|
||||
|
||||
return new Scene(objects);
|
||||
}
|
||||
}
|
@ -50,6 +50,10 @@ public record Vec3(double x, double y, double z) {
|
||||
return vec.plus(vxp.times(Math.sin(angle))).plus(vxvxp.times(1 - Math.cos(angle)));
|
||||
}
|
||||
|
||||
public static double distance(@NotNull Vec3 a, @NotNull Vec3 b) {
|
||||
return a.minus(b).length();
|
||||
}
|
||||
|
||||
public @NotNull Vec3 plus(@NotNull Vec3 b) {
|
||||
return new Vec3(this.x + b.x, this.y + b.y, this.z + b.z);
|
||||
}
|
||||
|
@ -3,11 +3,17 @@ package eu.jonahbauer.raytracing.render;
|
||||
import eu.jonahbauer.raytracing.math.Range;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
||||
import eu.jonahbauer.raytracing.render.canvas.Image;
|
||||
import eu.jonahbauer.raytracing.scene.Scene;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.LongStream;
|
||||
|
||||
public final class Camera {
|
||||
// image size
|
||||
@ -72,32 +78,24 @@ public final class Camera {
|
||||
|
||||
public @NotNull Image render(@NotNull Scene scene) {
|
||||
var image = new Image(width, height);
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
var r = 0d;
|
||||
var g = 0d;
|
||||
var b = 0d;
|
||||
|
||||
for (int i = 0; i < samplesPerPixel; i++) {
|
||||
var ray = getRay(x, y);
|
||||
var color = getColor(scene, ray);
|
||||
r += color.r();
|
||||
g += color.g();
|
||||
b += color.b();
|
||||
}
|
||||
|
||||
image.set(x, y, new Color(
|
||||
Math.pow(r / samplesPerPixel, 1 / gamma),
|
||||
Math.pow(g / samplesPerPixel, 1 / gamma),
|
||||
Math.pow(b / samplesPerPixel, 1 / gamma)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
render(scene, image);
|
||||
return image;
|
||||
}
|
||||
|
||||
public void render(@NotNull Scene scene, @NotNull Canvas canvas) {
|
||||
render(scene, canvas, RenderMode.SEQUENTIEL);
|
||||
}
|
||||
|
||||
public void render(@NotNull Scene scene, @NotNull Canvas canvas, @NotNull RenderMode mode) {
|
||||
if (canvas.width() != width || canvas.height() != height) throw new IllegalArgumentException();
|
||||
|
||||
var progress = new AtomicInteger();
|
||||
mode.render(this, scene, canvas, () -> {
|
||||
var val = progress.incrementAndGet();
|
||||
if (val % 1000 == 0) System.out.println(val);
|
||||
});
|
||||
}
|
||||
|
||||
private @NotNull Ray getRay(int x, int y) {
|
||||
var origin = this.origin;
|
||||
if (blurRadius > 0) {
|
||||
@ -150,6 +148,77 @@ public final class Camera {
|
||||
return Color.lerp(Color.WHITE, Color.SKY, alt / Math.PI + 0.5);
|
||||
}
|
||||
|
||||
public int width() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int height() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public enum RenderMode {
|
||||
SEQUENTIEL {
|
||||
@Override
|
||||
public void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas, @NotNull Runnable onProgressUpdate) {
|
||||
for (int y = 0; y < camera.height; y++) {
|
||||
for (int x = 0; x < camera.width; x++) {
|
||||
Color color = Color.BLACK;
|
||||
for (int i = 1; i <= camera.samplesPerPixel; i++) {
|
||||
var ray = camera.getRay(x, y);
|
||||
var c = camera.getColor(scene, ray);
|
||||
color = Color.average(color, c, i);
|
||||
}
|
||||
canvas.set(x, y, color);
|
||||
onProgressUpdate.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
PARALLEL {
|
||||
@Override
|
||||
public void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas, @NotNull Runnable onProgressUpdate) {
|
||||
coordinates(camera.width, camera.height).parallel().forEach(pos -> {
|
||||
var x = (int) pos;
|
||||
var y = (int) (pos >> 32);
|
||||
Color color = Color.BLACK;
|
||||
for (int i = 1; i <= camera.samplesPerPixel; i++) {
|
||||
var ray = camera.getRay(x, y);
|
||||
var c = camera.getColor(scene, ray);
|
||||
color = Color.average(color, c, i);
|
||||
}
|
||||
canvas.set(x, y, color);
|
||||
onProgressUpdate.run();
|
||||
});
|
||||
}
|
||||
},
|
||||
SAMPLES {
|
||||
@Override
|
||||
public void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas, @NotNull Runnable onProgressUpdate) {
|
||||
for (int i = 1; i <= camera.samplesPerPixel; i++) {
|
||||
var sample = i;
|
||||
coordinates(camera.width, camera.height).forEach(pos -> {
|
||||
var x = (int) pos;
|
||||
var y = (int) (pos >> 32);
|
||||
var ray = camera.getRay(x, y);
|
||||
var color = Color.average(canvas.get(x, y), camera.getColor(scene, ray), sample);
|
||||
canvas.set(x, y, color);
|
||||
});
|
||||
onProgressUpdate.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
;
|
||||
|
||||
private static LongStream coordinates(int width, int height) {
|
||||
return IntStream.range(0, height)
|
||||
.mapToObj(y -> IntStream.range(0, width).mapToLong(x -> (long) y << 32 | x))
|
||||
.flatMapToLong(Function.identity());
|
||||
}
|
||||
|
||||
public abstract void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas, @NotNull Runnable onProgressUpdate);
|
||||
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private int imageWidth = 1920;
|
||||
private int imageHeight = 1080;
|
||||
|
@ -24,6 +24,27 @@ public record Color(double r, double g, double b) {
|
||||
return new Color(a.r() * b.r(), a.g() * b.g(), a.b() * b.b());
|
||||
}
|
||||
|
||||
public static @NotNull Color random() {
|
||||
return new Color(Math.random(), Math.random(), Math.random());
|
||||
}
|
||||
|
||||
public static @NotNull Color random(double min, double max) {
|
||||
var span = max - min;
|
||||
return new Color(
|
||||
Math.fma(Math.random(), span, min),
|
||||
Math.fma(Math.random(), span, min),
|
||||
Math.fma(Math.random(), span, min)
|
||||
);
|
||||
}
|
||||
|
||||
public static @NotNull Color average(@NotNull Color current, @NotNull Color next, int index) {
|
||||
return new Color(
|
||||
current.r() + (next.r() - current.r()) / index,
|
||||
current.g() + (next.g() - current.g()) / index,
|
||||
current.b() + (next.b() - current.b()) / index
|
||||
);
|
||||
}
|
||||
|
||||
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");
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.jonahbauer.raytracing.render;
|
||||
|
||||
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.*;
|
||||
@ -13,7 +14,7 @@ import java.util.zip.DeflaterOutputStream;
|
||||
public enum ImageFormat {
|
||||
PPM {
|
||||
@Override
|
||||
public void write(@NotNull Image image, @NotNull OutputStream out) throws IOException {
|
||||
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
|
||||
try (var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.US_ASCII))) {
|
||||
writer.write("P3\n");
|
||||
writer.write(String.valueOf(image.width()));
|
||||
@ -42,7 +43,7 @@ public enum ImageFormat {
|
||||
private static final int IEND_TYPE = 0x49454E44;
|
||||
|
||||
@Override
|
||||
public void write(@NotNull Image image, @NotNull OutputStream out) throws IOException {
|
||||
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
|
||||
try (var data = new NoCloseDataOutputStream(out); var _ = data.closeable()) {
|
||||
data.write(MAGIC);
|
||||
|
||||
@ -52,7 +53,7 @@ public enum ImageFormat {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeIHDR(@NotNull Image image, @NotNull DataOutputStream data) throws IOException {
|
||||
private void writeIHDR(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
|
||||
data.writeInt(IHDR_LENGTH);
|
||||
try (
|
||||
var crc = new CheckedOutputStream(data, new CRC32());
|
||||
@ -71,7 +72,7 @@ public enum ImageFormat {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeIDAT(@NotNull Image image, @NotNull DataOutputStream data) throws IOException {
|
||||
private void writeIDAT(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
|
||||
try (
|
||||
var baos = new ByteArrayOutputStream();
|
||||
var crc = new CheckedOutputStream(baos, new CRC32());
|
||||
@ -97,7 +98,7 @@ public enum ImageFormat {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeIEND(@NotNull Image image, @NotNull DataOutputStream data) throws IOException {
|
||||
private void writeIEND(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
|
||||
data.writeInt(0);
|
||||
data.writeInt(IEND_TYPE);
|
||||
data.writeInt(0);
|
||||
@ -120,11 +121,11 @@ public enum ImageFormat {
|
||||
},
|
||||
;
|
||||
|
||||
public void write(@NotNull Image image, @NotNull Path path) throws IOException {
|
||||
public void write(@NotNull Canvas image, @NotNull Path path) throws IOException {
|
||||
try (var out = Files.newOutputStream(path)) {
|
||||
write(image, out);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void write(@NotNull Image image, @NotNull OutputStream out) throws IOException;
|
||||
public abstract void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException;
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
package eu.jonahbauer.raytracing.render.canvas;
|
||||
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
public class BufferedImageCanvas extends Image {
|
||||
private final @NotNull BufferedImage image;
|
||||
|
||||
public BufferedImageCanvas(int width, int height) {
|
||||
super(width, height);
|
||||
this.image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
}
|
||||
|
||||
public @NotNull BufferedImage getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, @NotNull Color color) {
|
||||
super.set(x, y, color);
|
||||
var rgb = color.red() << 16 | color.green() << 8 | color.blue();
|
||||
image.setRGB(x, y, rgb);
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package eu.jonahbauer.raytracing.render.canvas;
|
||||
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public interface Canvas {
|
||||
int width();
|
||||
int height();
|
||||
|
||||
void set(int x, int y, @NotNull Color color);
|
||||
@NotNull Color get(int x, int y);
|
||||
|
||||
default @NotNull Stream<Color> pixels() {
|
||||
return IntStream.range(0, height())
|
||||
.mapToObj(y -> IntStream.range(0, width()).mapToObj(x -> get(x, y)))
|
||||
.flatMap(Function.identity());
|
||||
}
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
package eu.jonahbauer.raytracing.render;
|
||||
package eu.jonahbauer.raytracing.render.canvas;
|
||||
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class Image {
|
||||
public class Image implements Canvas {
|
||||
private final int width;
|
||||
private final int height;
|
||||
|
||||
@ -22,35 +21,27 @@ public final class Image {
|
||||
this.data = new Color[height][width];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int width() {
|
||||
return width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int height() {
|
||||
return height;
|
||||
}
|
||||
|
||||
@Override
|
||||
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));
|
||||
}
|
||||
|
||||
@Override
|
||||
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));
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package eu.jonahbauer.raytracing.render;
|
||||
|
||||
import eu.jonahbauer.raytracing.render.canvas.Image;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
@ -23,12 +24,12 @@ class ImageTest {
|
||||
var g = (double) y / (image.height() - 1);
|
||||
var b = 0;
|
||||
|
||||
image.set(x, y, r, g, b);
|
||||
image.set(x, y, new Color(r, g, b));
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println(dir);
|
||||
ImageIO.write(image, dir.resolve("img.ppm"));
|
||||
ImageFormat.PPM.write(image, dir.resolve("img.ppm"));
|
||||
|
||||
String expected;
|
||||
String actual;
|
||||
|
Loading…
x
Reference in New Issue
Block a user