abstract Image and add support for watching the image as its being rendered
This commit is contained in:
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.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"));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
public void render(@NotNull Scene scene, @NotNull Canvas canvas) {
|
||||
if (canvas.getWidth() != width || canvas.getHeight() != height) throw new IllegalArgumentException();
|
||||
|
||||
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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 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<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));
|
||||
}
|
||||
}
|
@ -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…
x
Reference in New Issue
Block a user