diff --git a/src/main/java/eu/jonahbauer/raytracing/Main.java b/src/main/java/eu/jonahbauer/raytracing/Main.java index 43be90b..48c1a6f 100644 --- a/src/main/java/eu/jonahbauer/raytracing/Main.java +++ b/src/main/java/eu/jonahbauer/raytracing/Main.java @@ -7,6 +7,8 @@ import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.render.Camera; import eu.jonahbauer.raytracing.render.Color; import eu.jonahbauer.raytracing.render.ImageFormat; +import eu.jonahbauer.raytracing.render.canvas.LiveCanvas; +import eu.jonahbauer.raytracing.render.canvas.Image; import eu.jonahbauer.raytracing.scene.Scene; import eu.jonahbauer.raytracing.scene.Sphere; @@ -32,7 +34,10 @@ public class Main { .withBlurAngle(Math.toRadians(10)) .build(); - var image = camera.render(scene); + var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight())); + image.preview(); + + camera.render(scene, image); ImageFormat.PNG.write(image, Path.of("scene-" + System.currentTimeMillis() + ".png")); } } \ 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 index f49c118..1c39833 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/Camera.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/Camera.java @@ -3,11 +3,15 @@ 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.stream.IntStream; public final class Camera { // image size @@ -72,8 +76,16 @@ public final class Camera { public @NotNull Image render(@NotNull Scene scene) { var image = new Image(width, height); + render(scene, image); + return image; + } + + public void render(@NotNull Scene scene, @NotNull Canvas canvas) { + if (canvas.getWidth() != width || canvas.getHeight() != height) throw new IllegalArgumentException(); - for (int y = 0; y < height; y++) { + var lines = new AtomicInteger(); + IntStream.range(0, height).parallel().forEach(y -> { + System.out.println(lines.incrementAndGet()); for (int x = 0; x < width; x++) { var r = 0d; var g = 0d; @@ -87,15 +99,13 @@ public final class Camera { b += color.b(); } - image.set(x, y, new Color( + canvas.set(x, y, new Color( Math.pow(r / samplesPerPixel, 1 / gamma), Math.pow(g / samplesPerPixel, 1 / gamma), Math.pow(b / samplesPerPixel, 1 / gamma) )); } - } - - return image; + }); } private @NotNull Ray getRay(int x, int y) { @@ -150,6 +160,14 @@ public final class Camera { return Color.lerp(Color.WHITE, Color.SKY, alt / Math.PI + 0.5); } + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + public static class Builder { private int imageWidth = 1920; private int imageHeight = 1080; diff --git a/src/main/java/eu/jonahbauer/raytracing/render/ImageFormat.java b/src/main/java/eu/jonahbauer/raytracing/render/ImageFormat.java index 722b9c7..6a92e53 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/ImageFormat.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/ImageFormat.java @@ -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,12 +14,12 @@ 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())); + writer.write(String.valueOf(image.getWidth())); writer.write(" "); - writer.write(String.valueOf(image.height())); + writer.write(String.valueOf(image.getHeight())); writer.write("\n255\n"); var it = image.pixels().iterator(); @@ -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,15 +53,15 @@ 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()); var ihdr = new DataOutputStream(crc) ) { ihdr.writeInt(IHDR_TYPE); - ihdr.writeInt(image.width()); // image width - ihdr.writeInt(image.height()); // image height + ihdr.writeInt(image.getWidth()); // image width + ihdr.writeInt(image.getHeight()); // image height ihdr.writeByte(8); // bit depth ihdr.writeByte(2); // color type ihdr.writeByte(0); // compression method @@ -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()); @@ -81,7 +82,7 @@ public enum ImageFormat { try (var deflate = new DataOutputStream(new DeflaterOutputStream(idat))) { var pixels = image.pixels().iterator(); - for (int i = 0; pixels.hasNext(); i = (i + 1) % image.width()) { + for (int i = 0; pixels.hasNext(); i = (i + 1) % image.getWidth()) { if (i == 0) deflate.writeByte(0); // filter type var pixel = pixels.next(); deflate.writeByte(pixel.red()); @@ -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; } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/canvas/Canvas.java b/src/main/java/eu/jonahbauer/raytracing/render/canvas/Canvas.java new file mode 100644 index 0000000..c81ab7d --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/canvas/Canvas.java @@ -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 getWidth(); + int getHeight(); + + void set(int x, int y, @NotNull Color color); + @NotNull Color get(int x, int y); + + default @NotNull Stream pixels() { + return IntStream.range(0, getHeight()) + .mapToObj(y -> IntStream.range(0, getWidth()).mapToObj(x -> get(x, y))) + .flatMap(Function.identity()); + } +} diff --git a/src/main/java/eu/jonahbauer/raytracing/render/Image.java b/src/main/java/eu/jonahbauer/raytracing/render/canvas/Image.java similarity index 62% rename from src/main/java/eu/jonahbauer/raytracing/render/Image.java rename to src/main/java/eu/jonahbauer/raytracing/render/canvas/Image.java index 703fb3a..5f93eca 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/Image.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/canvas/Image.java @@ -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 final 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]; } - public int width() { + @Override + public int getWidth() { return width; } - public int height() { + @Override + public int getHeight() { 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 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)); - } } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/canvas/LiveCanvas.java b/src/main/java/eu/jonahbauer/raytracing/render/canvas/LiveCanvas.java new file mode 100644 index 0000000..402aebd --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/canvas/LiveCanvas.java @@ -0,0 +1,77 @@ +package eu.jonahbauer.raytracing.render.canvas; + +import eu.jonahbauer.raytracing.render.Color; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; + +public final class LiveCanvas implements Canvas { + private final @NotNull Canvas delegate; + private final @NotNull BufferedImage image; + + public LiveCanvas(@NotNull Canvas delegate) { + this.delegate = delegate; + this.image = new BufferedImage(delegate.getWidth(), delegate.getHeight(), BufferedImage.TYPE_INT_RGB); + } + + @Override + public int getWidth() { + return delegate.getWidth(); + } + + @Override + public int getHeight() { + return delegate.getHeight(); + } + + @Override + public void set(int x, int y, @NotNull Color color) { + delegate.set(x, y, color); + var rgb = color.red() << 16 | color.green() << 8 | color.blue(); + image.setRGB(x, y, rgb); + } + + @Override + public @NotNull Color get(int x, int y) { + return delegate.get(x, y); + } + + public @NotNull Thread preview() { + var frame = new JFrame(); + frame.setSize(getWidth(), getHeight()); + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + frame.setContentPane(new JPanel() { + @Override + protected void paintComponent(Graphics g) { + g.drawImage(image, 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.dispose(); + }); + + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + update.interrupt(); + } + }); + + return update; + } +}