add stratification to pixel sampling

main
jbb01 6 months ago
parent a31488bc78
commit 5f1e816edd

@ -18,7 +18,13 @@ public interface Camera {
/** /**
* Casts a ray through the given pixel. * Casts a ray through the given pixel.
* @param x the pixel x coordinate
* @param y the pixel y coordinate
* @param i the subpixel x coordinate
* @param j the subpixel y coordinate
* @param n the subpixel count (per side)
* @param random a random number generator
* @return a new ray * @return a new ray
*/ */
@NotNull Ray cast(int x, int y, @NotNull RandomGenerator random); @NotNull Ray cast(int x, int y, int i, int j, int n, @NotNull RandomGenerator random);
} }

@ -79,13 +79,20 @@ public final class SimpleCamera implements Camera {
/** /**
* {@inheritDoc} * {@inheritDoc}
* @param x {@inheritDoc}
* @param y {@inheritDoc}
* @param i {@inheritDoc}
* @param j {@inheritDoc}
* @param n {@inheritDoc}
* @param random {@inheritDoc}
*/ */
public @NotNull Ray cast(int x, int y, @NotNull RandomGenerator random) { @Override
public @NotNull Ray cast(int x, int y, int i, int j, int n, @NotNull RandomGenerator random) {
Objects.checkIndex(x, width); Objects.checkIndex(x, width);
Objects.checkIndex(y, height); Objects.checkIndex(y, height);
var origin = getRayOrigin(random); var origin = getRayOrigin(random);
var target = getRayTarget(x, y, random); var target = getRayTarget(x, y, i, j, n, random);
return new Ray(origin, target.minus(origin)); return new Ray(origin, target.minus(origin));
} }
@ -98,8 +105,8 @@ public final class SimpleCamera implements Camera {
if (blurRadius <= 0) return origin; if (blurRadius <= 0) return origin;
while (true) { while (true) {
var du = 2 * random.nextDouble() - 1; var du = Math.fma(2, random.nextDouble(), -1);
var dv = 2 * random.nextDouble() - 1; var dv = Math.fma(2, random.nextDouble(), -1);
if (du * du + dv * dv >= 1) continue; if (du * du + dv * dv >= 1) continue;
var ru = blurRadius * du; var ru = blurRadius * du;
@ -116,9 +123,10 @@ public final class SimpleCamera implements Camera {
/** /**
* {@return the target vector for a ray through the given pixel} The position is randomized within the pixel. * {@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, @NotNull RandomGenerator random) { private @NotNull Vec3 getRayTarget(int x, int y, int i, int j, int n, @NotNull RandomGenerator random) {
double dx = x + random.nextDouble() - 0.5; var factor = 1d / n;
double dy = y + random.nextDouble() - 0.5; var dx = x + Math.fma(factor, i + random.nextDouble(), -0.5);
var dy = y + Math.fma(factor, j + random.nextDouble(), -0.5);
return pixel00.plus(pixelU.times(dx)).plus(pixelV.times(dy)); return pixel00.plus(pixelU.times(dx)).plus(pixelV.times(dy));
} }

@ -14,7 +14,7 @@ import java.util.random.RandomGenerator;
import java.util.stream.IntStream; import java.util.stream.IntStream;
public final class SimpleRenderer implements Renderer { public final class SimpleRenderer implements Renderer {
private final int samplesPerPixel; private final int sqrtSamplesPerPixel;
private final int maxDepth; private final int maxDepth;
private final double gamma; private final double gamma;
@ -30,7 +30,7 @@ public final class SimpleRenderer implements Renderer {
} }
private SimpleRenderer(@NotNull Builder builder) { private SimpleRenderer(@NotNull Builder builder) {
this.samplesPerPixel = builder.samplesPerPixel; this.sqrtSamplesPerPixel = (int) Math.sqrt(builder.samplesPerPixel);
this.maxDepth = builder.maxDepth; this.maxDepth = builder.maxDepth;
this.gamma = builder.gamma; this.gamma = builder.gamma;
@ -38,6 +38,9 @@ public final class SimpleRenderer implements Renderer {
this.iterative = builder.iterative; this.iterative = builder.iterative;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) { public void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) {
if (canvas.getWidth() != camera.getWidth() || canvas.getHeight() != camera.getHeight()) { if (canvas.getWidth() != camera.getWidth() || canvas.getHeight() != camera.getHeight()) {
@ -45,19 +48,35 @@ public final class SimpleRenderer implements Renderer {
} }
if (iterative) { if (iterative) {
renderIterative(camera, scene, canvas);
} else {
renderNonIterative(camera, scene, canvas);
}
}
/**
* Renders the {@code scene} as seen by the {@code camera} to the {@code canvas}, taking one sample per pixel at
* a time and updating the canvas after each sample.
*/
private void renderIterative(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) {
var random = new Random(); var random = new Random();
// render one sample after the other // render one sample after the other
for (int i = 1 ; i <= samplesPerPixel; i++) { int i = 0;
var sample = i; for (int sj = 0; sj < sqrtSamplesPerPixel; sj++) {
for (int si = 0; si < sqrtSamplesPerPixel; si++) {
var sample = ++i;
var sif = si;
var sjf = sj;
getScanlineStream(camera.getHeight(), parallel).forEach(y -> { getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
for (int x = 0; x < camera.getWidth(); x++) { for (int x = 0; x < camera.getWidth(); x++) {
var ray = camera.cast(x, y, random); var ray = camera.cast(x, y, sif, sjf, sqrtSamplesPerPixel, random);
var c = getColor(scene, ray, random); var c = getColor(scene, ray, random);
canvas.set(x, y, Color.average(canvas.get(x, y), c, sample)); canvas.set(x, y, Color.average(canvas.get(x, y), c, sample));
} }
}); });
} }
}
// apply gamma correction // apply gamma correction
getScanlineStream(camera.getHeight(), parallel).forEach(y -> { getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
@ -65,23 +84,31 @@ public final class SimpleRenderer implements Renderer {
canvas.set(x, y, Color.gamma(canvas.get(x, y), gamma)); canvas.set(x, y, Color.gamma(canvas.get(x, y), gamma));
} }
}); });
} else { }
/**
* Renders the {@code scene} as seen by the {@code camera} to the {@code canvas}, taking some amount of samples
* per pixel and updating the canvas after each pixel.
*/
private void renderNonIterative(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) {
var splittable = new SplittableRandom(); var splittable = new SplittableRandom();
// render one pixel after the other // render one pixel after the other
getScanlineStream(camera.getHeight(), parallel).forEach(y -> { getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
var random = splittable.split(); var random = splittable.split();
for (int x = 0; x < camera.getWidth(); x++) { for (int x = 0; x < camera.getWidth(); x++) {
var color = Color.BLACK; var color = Color.BLACK;
for (int i = 1; i <= samplesPerPixel; i++) { int i = 0;
var ray = camera.cast(x, y, random); for (int sj = 0; sj < sqrtSamplesPerPixel; sj++) {
for (int si = 0; si < sqrtSamplesPerPixel; si++) {
var ray = camera.cast(x, y, si, sj, sqrtSamplesPerPixel, random);
var c = getColor(scene, ray, random); var c = getColor(scene, ray, random);
color = Color.average(color, c, i); color = Color.average(color, c, ++i);
}
} }
canvas.set(x, y, Color.gamma(color, gamma)); canvas.set(x, y, Color.gamma(color, gamma));
} }
}); });
} }
}
/** /**
* {@return the color of the given ray in the given scene} * {@return the color of the given ray in the given scene}

Loading…
Cancel
Save