abstract Image and add support for watching the image as its being rendered

main
jbb01 6 months ago
parent 1080711229
commit bb326e82a6

@ -7,6 +7,8 @@ import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.Camera; import eu.jonahbauer.raytracing.render.Camera;
import eu.jonahbauer.raytracing.render.Color; import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.render.ImageFormat; 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.Scene;
import eu.jonahbauer.raytracing.scene.Sphere; import eu.jonahbauer.raytracing.scene.Sphere;
@ -32,7 +34,10 @@ public class Main {
.withBlurAngle(Math.toRadians(10)) .withBlurAngle(Math.toRadians(10))
.build(); .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")); ImageFormat.PNG.write(image, Path.of("scene-" + System.currentTimeMillis() + ".png"));
} }
} }

@ -3,11 +3,15 @@ package eu.jonahbauer.raytracing.render;
import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3; 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 eu.jonahbauer.raytracing.scene.Scene;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
public final class Camera { public final class Camera {
// image size // image size
@ -72,8 +76,16 @@ public final class Camera {
public @NotNull Image render(@NotNull Scene scene) { public @NotNull Image render(@NotNull Scene scene) {
var image = new Image(width, height); 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++) { for (int x = 0; x < width; x++) {
var r = 0d; var r = 0d;
var g = 0d; var g = 0d;
@ -87,15 +99,13 @@ public final class Camera {
b += color.b(); b += color.b();
} }
image.set(x, y, new Color( canvas.set(x, y, new Color(
Math.pow(r / samplesPerPixel, 1 / gamma), Math.pow(r / samplesPerPixel, 1 / gamma),
Math.pow(g / samplesPerPixel, 1 / gamma), Math.pow(g / samplesPerPixel, 1 / gamma),
Math.pow(b / samplesPerPixel, 1 / gamma) Math.pow(b / samplesPerPixel, 1 / gamma)
)); ));
} }
} });
return image;
} }
private @NotNull Ray getRay(int x, int y) { 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); 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 { public static class Builder {
private int imageWidth = 1920; private int imageWidth = 1920;
private int imageHeight = 1080; private int imageHeight = 1080;

@ -1,5 +1,6 @@
package eu.jonahbauer.raytracing.render; package eu.jonahbauer.raytracing.render;
import eu.jonahbauer.raytracing.render.canvas.Canvas;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.*; import java.io.*;
@ -13,12 +14,12 @@ import java.util.zip.DeflaterOutputStream;
public enum ImageFormat { public enum ImageFormat {
PPM { PPM {
@Override @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))) { try (var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.US_ASCII))) {
writer.write("P3\n"); writer.write("P3\n");
writer.write(String.valueOf(image.width())); writer.write(String.valueOf(image.getWidth()));
writer.write(" "); writer.write(" ");
writer.write(String.valueOf(image.height())); writer.write(String.valueOf(image.getHeight()));
writer.write("\n255\n"); writer.write("\n255\n");
var it = image.pixels().iterator(); var it = image.pixels().iterator();
@ -42,7 +43,7 @@ public enum ImageFormat {
private static final int IEND_TYPE = 0x49454E44; private static final int IEND_TYPE = 0x49454E44;
@Override @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()) { try (var data = new NoCloseDataOutputStream(out); var _ = data.closeable()) {
data.write(MAGIC); 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); data.writeInt(IHDR_LENGTH);
try ( try (
var crc = new CheckedOutputStream(data, new CRC32()); var crc = new CheckedOutputStream(data, new CRC32());
var ihdr = new DataOutputStream(crc) var ihdr = new DataOutputStream(crc)
) { ) {
ihdr.writeInt(IHDR_TYPE); ihdr.writeInt(IHDR_TYPE);
ihdr.writeInt(image.width()); // image width ihdr.writeInt(image.getWidth()); // image width
ihdr.writeInt(image.height()); // image height ihdr.writeInt(image.getHeight()); // image height
ihdr.writeByte(8); // bit depth ihdr.writeByte(8); // bit depth
ihdr.writeByte(2); // color type ihdr.writeByte(2); // color type
ihdr.writeByte(0); // compression method 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 ( try (
var baos = new ByteArrayOutputStream(); var baos = new ByteArrayOutputStream();
var crc = new CheckedOutputStream(baos, new CRC32()); var crc = new CheckedOutputStream(baos, new CRC32());
@ -81,7 +82,7 @@ public enum ImageFormat {
try (var deflate = new DataOutputStream(new DeflaterOutputStream(idat))) { try (var deflate = new DataOutputStream(new DeflaterOutputStream(idat))) {
var pixels = image.pixels().iterator(); 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 if (i == 0) deflate.writeByte(0); // filter type
var pixel = pixels.next(); var pixel = pixels.next();
deflate.writeByte(pixel.red()); 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(0);
data.writeInt(IEND_TYPE); data.writeInt(IEND_TYPE);
data.writeInt(0); 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)) { try (var out = Files.newOutputStream(path)) {
write(image, out); 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,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<Color> pixels() {
return IntStream.range(0, getHeight())
.mapToObj(y -> IntStream.range(0, getWidth()).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 org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Objects; 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 width;
private final int height; private final int height;
@ -22,35 +21,27 @@ public final class Image {
this.data = new Color[height][width]; this.data = new Color[height][width];
} }
public int width() { @Override
public int getWidth() {
return width; return width;
} }
public int height() { @Override
public int getHeight() {
return height; return height;
} }
@Override
public @NotNull Color get(int x, int y) { public @NotNull Color get(int x, int y) {
Objects.checkIndex(x, width); Objects.checkIndex(x, width);
Objects.checkIndex(y, height); Objects.checkIndex(y, height);
return Objects.requireNonNullElse(this.data[y][x], Color.BLACK); return Objects.requireNonNullElse(this.data[y][x], Color.BLACK);
} }
public @NotNull Stream<Color> pixels() { @Override
return Arrays.stream(data).flatMap(Arrays::stream).map(c -> Objects.requireNonNullElse(c, Color.BLACK));
}
public void set(int x, int y, @NotNull Color color) { public void set(int x, int y, @NotNull Color color) {
Objects.checkIndex(x, width); Objects.checkIndex(x, width);
Objects.checkIndex(y, height); Objects.checkIndex(y, height);
this.data[y][x] = Objects.requireNonNull(color); 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,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;
}
}
Loading…
Cancel
Save