separate camera from rendering
This commit is contained in:
parent
c17b9aedf5
commit
0c6db707e0
@ -5,11 +5,12 @@ import eu.jonahbauer.raytracing.material.LambertianMaterial;
|
|||||||
import eu.jonahbauer.raytracing.material.Material;
|
import eu.jonahbauer.raytracing.material.Material;
|
||||||
import eu.jonahbauer.raytracing.material.MetallicMaterial;
|
import eu.jonahbauer.raytracing.material.MetallicMaterial;
|
||||||
import eu.jonahbauer.raytracing.math.Vec3;
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
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.camera.SimpleCamera;
|
||||||
import eu.jonahbauer.raytracing.render.canvas.LiveCanvas;
|
import eu.jonahbauer.raytracing.render.canvas.LiveCanvas;
|
||||||
import eu.jonahbauer.raytracing.render.canvas.Image;
|
import eu.jonahbauer.raytracing.render.canvas.Image;
|
||||||
|
import eu.jonahbauer.raytracing.render.renderer.SimpleRenderer;
|
||||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||||
import eu.jonahbauer.raytracing.scene.Scene;
|
import eu.jonahbauer.raytracing.scene.Scene;
|
||||||
import eu.jonahbauer.raytracing.scene.Sphere;
|
import eu.jonahbauer.raytracing.scene.Sphere;
|
||||||
@ -23,13 +24,16 @@ public class Main {
|
|||||||
public static void main(String[] args) throws IOException {
|
public static void main(String[] args) throws IOException {
|
||||||
var scene = getScene();
|
var scene = getScene();
|
||||||
|
|
||||||
var camera = Camera.builder()
|
var camera = SimpleCamera.builder()
|
||||||
.withImage(1200, 675)
|
.withImage(1200, 675)
|
||||||
.withPosition(new Vec3(13, 2, 3))
|
.withPosition(new Vec3(13, 2, 3))
|
||||||
.withTarget(new Vec3(0, 0, 0))
|
.withTarget(new Vec3(0, 0, 0))
|
||||||
.withFieldOfView(Math.toRadians(20))
|
.withFieldOfView(Math.toRadians(20))
|
||||||
.withFocusDistance(10.0)
|
.withFocusDistance(10.0)
|
||||||
.withBlurAngle(Math.toRadians(0.6))
|
.withBlurAngle(Math.toRadians(0.6))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var renderer = SimpleRenderer.builder()
|
||||||
.withSamplesPerPixel(500)
|
.withSamplesPerPixel(500)
|
||||||
.withMaxDepth(50)
|
.withMaxDepth(50)
|
||||||
.build();
|
.build();
|
||||||
@ -37,7 +41,7 @@ public class Main {
|
|||||||
var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight()));
|
var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight()));
|
||||||
image.preview();
|
image.preview();
|
||||||
|
|
||||||
camera.render(scene, image);
|
renderer.render(camera, scene, image);
|
||||||
ImageFormat.PNG.write(image, Path.of("scene-" + System.currentTimeMillis() + ".png"));
|
ImageFormat.PNG.write(image, Path.of("scene-" + System.currentTimeMillis() + ".png"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +37,32 @@ public record Color(double r, double g, double b) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 static @NotNull Color gamma(@NotNull Color color, double gamma) {
|
||||||
|
if (gamma == 1.0) {
|
||||||
|
return color;
|
||||||
|
} else if (gamma == 2.0) {
|
||||||
|
return new Color(
|
||||||
|
Math.sqrt(color.r()),
|
||||||
|
Math.sqrt(color.g()),
|
||||||
|
Math.sqrt(color.b())
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new Color(
|
||||||
|
Math.pow(color.r(), 1 / gamma),
|
||||||
|
Math.pow(color.g(), 1 / gamma),
|
||||||
|
Math.pow(color.b(), 1 / gamma)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Color {
|
public Color {
|
||||||
if (r < 0 || r > 1 || g < 0 || g > 1 || b < 0 || b > 1) {
|
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");
|
throw new IllegalArgumentException("r, g and b must be in the range 0 to 1");
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -1,19 +1,13 @@
|
|||||||
package eu.jonahbauer.raytracing.render;
|
package eu.jonahbauer.raytracing.render.camera;
|
||||||
|
|
||||||
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 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 SimpleCamera implements Camera {
|
||||||
// image size
|
// image size
|
||||||
private final int width;
|
private final int width;
|
||||||
private final int height;
|
private final int height;
|
||||||
@ -22,9 +16,6 @@ public final class Camera {
|
|||||||
private final @NotNull Vec3 origin;
|
private final @NotNull Vec3 origin;
|
||||||
|
|
||||||
// rendering
|
// rendering
|
||||||
private final int samplesPerPixel;
|
|
||||||
private final int maxDepth;
|
|
||||||
private final double gamma;
|
|
||||||
private final double blurRadius;
|
private final double blurRadius;
|
||||||
|
|
||||||
// internal properties
|
// internal properties
|
||||||
@ -39,11 +30,11 @@ public final class Camera {
|
|||||||
return new Builder();
|
return new Builder();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Camera() {
|
public static @NotNull Camera withDefaults() {
|
||||||
this(new Builder());
|
return new Builder().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Camera(@NotNull Builder builder) {
|
private SimpleCamera(@NotNull Builder builder) {
|
||||||
this.width = builder.imageWidth;
|
this.width = builder.imageWidth;
|
||||||
this.height = builder.imageHeight;
|
this.height = builder.imageHeight;
|
||||||
|
|
||||||
@ -53,9 +44,6 @@ public final class Camera {
|
|||||||
this.origin = builder.position;
|
this.origin = builder.position;
|
||||||
var direction = (builder.direction == null ? builder.target.minus(builder.position).unit() : builder.direction);
|
var direction = (builder.direction == null ? builder.target.minus(builder.position).unit() : builder.direction);
|
||||||
|
|
||||||
this.samplesPerPixel = builder.samplePerPixel;
|
|
||||||
this.maxDepth = builder.maxDepth;
|
|
||||||
this.gamma = builder.gamma;
|
|
||||||
this.blurRadius = Math.tan(0.5 * builder.blurAngle) * builder.focusDistance;
|
this.blurRadius = Math.tan(0.5 * builder.blurAngle) * builder.focusDistance;
|
||||||
|
|
||||||
// project direction the horizontal plane
|
// project direction the horizontal plane
|
||||||
@ -74,100 +62,65 @@ public final class Camera {
|
|||||||
.plus(pixelU.div(2)).plus(pixelV.div(2));
|
.plus(pixelU.div(2)).plus(pixelV.div(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull Image render(@NotNull Scene scene) {
|
/**
|
||||||
var image = new Image(width, height);
|
* {@inheritDoc}
|
||||||
render(scene, image);
|
*/
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.set(x, y, new Color(
|
|
||||||
Math.pow(r / samplesPerPixel, 1 / gamma),
|
|
||||||
Math.pow(g / samplesPerPixel, 1 / gamma),
|
|
||||||
Math.pow(b / samplesPerPixel, 1 / gamma)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull Ray getRay(int x, int y) {
|
|
||||||
var origin = this.origin;
|
|
||||||
if (blurRadius > 0) {
|
|
||||||
double bu, bv;
|
|
||||||
do {
|
|
||||||
bu = 2 * Math.random() - 1;
|
|
||||||
bv = 2 * Math.random() - 1;
|
|
||||||
} while (bu * bu + bv * bv >= 1);
|
|
||||||
|
|
||||||
origin = origin.plus(u.times(blurRadius * bu)).plus(v.times(blurRadius * bv));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Ray(origin, getPixel(x, y).minus(origin));
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull Vec3 getPixel(int x, int y) {
|
|
||||||
Objects.checkIndex(x, width);
|
|
||||||
Objects.checkIndex(y, height);
|
|
||||||
|
|
||||||
double dx = x + Math.random() - 0.5;
|
|
||||||
double dy = y + Math.random() - 0.5;
|
|
||||||
return pixel00.plus(pixelU.times(dx)).plus(pixelV.times(dy));
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull Color getColor(@NotNull Scene scene, @NotNull Ray ray) {
|
|
||||||
return getColor(scene, ray, maxDepth);
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull Color getColor(@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(), getColor(scene, scatter.ray(), depth - 1)))
|
|
||||||
.orElse(Color.BLACK);
|
|
||||||
} else {
|
|
||||||
return getSkyboxColor(ray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 int getWidth() {
|
public int getWidth() {
|
||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
public int getHeight() {
|
public int getHeight() {
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public @NotNull Ray cast(int x, int y) {
|
||||||
|
Objects.checkIndex(x, width);
|
||||||
|
Objects.checkIndex(y, height);
|
||||||
|
|
||||||
|
var origin = getRayOrigin();
|
||||||
|
var target = getRayTarget(x, y);
|
||||||
|
return new Ray(origin, target.minus(origin));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the origin for a ray cast by this camera} The ray origin is randomized within a disk of
|
||||||
|
* radius {@link #blurRadius} centered on the camera position and perpendicular to the direction to simulate depth
|
||||||
|
* of field.
|
||||||
|
*/
|
||||||
|
private @NotNull Vec3 getRayOrigin() {
|
||||||
|
if (blurRadius <= 0) return origin;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
var du = 2 * Math.random() - 1;
|
||||||
|
var dv = 2 * Math.random() - 1;
|
||||||
|
if (du * du + dv * dv >= 1) continue;
|
||||||
|
|
||||||
|
var ru = blurRadius * du;
|
||||||
|
var rv = blurRadius * dv;
|
||||||
|
|
||||||
|
return new Vec3(
|
||||||
|
origin.x() + ru * u.x() + rv * v.x(),
|
||||||
|
origin.y() + ru * u.y() + rv * v.y(),
|
||||||
|
origin.z() + ru * u.z() + rv * v.z()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the target vector for a ray through the given pixel} The position is randomized within the pixel.
|
||||||
|
*/
|
||||||
|
private @NotNull Vec3 getRayTarget(int x, int y) {
|
||||||
|
double dx = x + Math.random() - 0.5;
|
||||||
|
double dy = y + Math.random() - 0.5;
|
||||||
|
return pixel00.plus(pixelU.times(dx)).plus(pixelV.times(dy));
|
||||||
|
}
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
private int imageWidth = 1920;
|
private int imageWidth = 1920;
|
||||||
private int imageHeight = 1080;
|
private int imageHeight = 1080;
|
||||||
@ -181,9 +134,6 @@ public final class Camera {
|
|||||||
private double focusDistance = 10;
|
private double focusDistance = 10;
|
||||||
private double blurAngle = 0.0;
|
private double blurAngle = 0.0;
|
||||||
|
|
||||||
private int samplePerPixel = 100;
|
|
||||||
private int maxDepth = 10;
|
|
||||||
private double gamma = 2.0;
|
|
||||||
|
|
||||||
private Builder() {}
|
private Builder() {}
|
||||||
|
|
||||||
@ -235,26 +185,8 @@ public final class Camera {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull Builder withSamplesPerPixel(int samples) {
|
public @NotNull SimpleCamera build() {
|
||||||
if (samples <= 0) throw new IllegalArgumentException("samples must be positive");
|
return new SimpleCamera(this);
|
||||||
this.samplePerPixel = 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 Camera build() {
|
|
||||||
return new Camera(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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…
x
Reference in New Issue
Block a user