separate camera from rendering
parent
c17b9aedf5
commit
0c6db707e0
@ -0,0 +1,22 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.camera;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public interface Camera {
|
||||||
|
/**
|
||||||
|
* {@return the width of this camera in pixels}
|
||||||
|
*/
|
||||||
|
int getWidth();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the height of this camera in pixels}
|
||||||
|
*/
|
||||||
|
int getHeight();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Casts a ray through the given pixel.
|
||||||
|
* @return a new ray
|
||||||
|
*/
|
||||||
|
@NotNull Ray cast(int x, int y);
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.renderer;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.render.camera.Camera;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public interface Renderer {
|
||||||
|
default @NotNull Image render(@NotNull Camera camera, @NotNull Scene scene) {
|
||||||
|
var image = new Image(camera.getWidth(), camera.getHeight());
|
||||||
|
render(camera, scene, image);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the {@code scene} as seen by the {@code camera} to the {@code canvas}.
|
||||||
|
*/
|
||||||
|
void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas);
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.renderer;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Range;
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.render.Color;
|
||||||
|
import eu.jonahbauer.raytracing.render.camera.Camera;
|
||||||
|
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
||||||
|
import eu.jonahbauer.raytracing.scene.Scene;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import java.util.stream.LongStream;
|
||||||
|
|
||||||
|
public final class SimpleRenderer implements Renderer {
|
||||||
|
private final int samplesPerPixel;
|
||||||
|
private final int maxDepth;
|
||||||
|
private final double gamma;
|
||||||
|
|
||||||
|
public static @NotNull Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NotNull Renderer withDefaults() {
|
||||||
|
return new Builder().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimpleRenderer(@NotNull Builder builder) {
|
||||||
|
this.samplesPerPixel = builder.samplesPerPixel;
|
||||||
|
this.maxDepth = builder.maxDepth;
|
||||||
|
this.gamma = builder.gamma;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) {
|
||||||
|
if (canvas.getWidth() != camera.getWidth() || canvas.getHeight() != camera.getHeight()) {
|
||||||
|
throw new IllegalArgumentException("sizes of camera and canvas are different");
|
||||||
|
}
|
||||||
|
|
||||||
|
getPixelStream(camera.getWidth(), camera.getHeight()).parallel().forEach(pixel -> {
|
||||||
|
var y = (int) (pixel >> 32);
|
||||||
|
var x = (int) pixel;
|
||||||
|
|
||||||
|
var color = Color.BLACK;
|
||||||
|
for (int i = 1; i <= samplesPerPixel; i++) {
|
||||||
|
var ray = camera.cast(x, y);
|
||||||
|
var c = getColor(scene, ray);
|
||||||
|
color = Color.average(color, c, i);
|
||||||
|
}
|
||||||
|
canvas.set(x, y, Color.gamma(color, gamma));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the color of the given ray in the given scene}
|
||||||
|
*/
|
||||||
|
private @NotNull Color getColor(@NotNull Scene scene, @NotNull Ray ray) {
|
||||||
|
return getColor0(scene, ray, maxDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull Color getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth) {
|
||||||
|
if (depth <= 0) return Color.BLACK;
|
||||||
|
|
||||||
|
var optional = scene.hit(ray, new Range(0.001, Double.POSITIVE_INFINITY));
|
||||||
|
if (optional.isPresent()) {
|
||||||
|
var hit = optional.get();
|
||||||
|
var material = hit.material();
|
||||||
|
return material.scatter(ray, hit)
|
||||||
|
.map(scatter -> Color.multiply(scatter.attenuation(), getColor0(scene, scatter.ray(), depth - 1)))
|
||||||
|
.orElse(Color.BLACK);
|
||||||
|
} else {
|
||||||
|
return getSkyboxColor(ray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return a stream of the pixels in a canvas with the given size} The pixels {@code x} and {@code y} coordinate
|
||||||
|
* are encoded in the longs lower and upper 32 bits respectively.
|
||||||
|
*/
|
||||||
|
private static @NotNull LongStream getPixelStream(int width, int height) {
|
||||||
|
return IntStream.range(0, height)
|
||||||
|
.mapToObj(y -> IntStream.range(0, width).mapToLong(x -> (long) y << 32 | x))
|
||||||
|
.flatMapToLong(Function.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the color of the skybox for a given ray} The skybox color is a linear gradient based on the altitude of
|
||||||
|
* the ray above the horizon with {@link Color#SKY} at the top and {@link Color#WHITE} at the bottom.
|
||||||
|
*/
|
||||||
|
private static @NotNull Color getSkyboxColor(@NotNull Ray ray) {
|
||||||
|
// altitude from -pi/2 to pi/2
|
||||||
|
var alt = Math.copySign(
|
||||||
|
Math.acos(ray.direction().withY(0).unit().times(ray.direction().unit())),
|
||||||
|
ray.direction().y()
|
||||||
|
);
|
||||||
|
return Color.lerp(Color.WHITE, Color.SKY, alt / Math.PI + 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private int samplesPerPixel = 100;
|
||||||
|
private int maxDepth = 10;
|
||||||
|
private double gamma = 2.0;
|
||||||
|
|
||||||
|
public @NotNull Builder withSamplesPerPixel(int samples) {
|
||||||
|
if (samples <= 0) throw new IllegalArgumentException("samples must be positive");
|
||||||
|
this.samplesPerPixel = samples;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull Builder withMaxDepth(int depth) {
|
||||||
|
if (depth <= 0) throw new IllegalArgumentException("depth must be positive");
|
||||||
|
this.maxDepth = depth;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull Builder withGamma(double gamma) {
|
||||||
|
if (gamma <= 0 || !Double.isFinite(gamma)) throw new IllegalArgumentException("gamma must be positive");
|
||||||
|
this.gamma = gamma;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull SimpleRenderer build() {
|
||||||
|
return new SimpleRenderer(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue