diff --git a/src/main/java/eu/jonahbauer/raytracing/Examples.java b/src/main/java/eu/jonahbauer/raytracing/Examples.java index 1c06fb5..464d5d0 100644 --- a/src/main/java/eu/jonahbauer/raytracing/Examples.java +++ b/src/main/java/eu/jonahbauer/raytracing/Examples.java @@ -6,6 +6,7 @@ import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.camera.SimpleCamera; import eu.jonahbauer.raytracing.render.material.*; import eu.jonahbauer.raytracing.render.texture.ImageTexture; +import eu.jonahbauer.raytracing.render.texture.PerlinTexture; import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Scene; import eu.jonahbauer.raytracing.scene.SkyBox; @@ -34,6 +35,7 @@ public class Examples { register("CORNELL_SMOKE", Examples::getCornellBoxSmoke); register("DIAGRAMM", Examples::getDiagramm); register("EARTH", Examples::getEarth); + register("PERLIN", Examples::getPerlin); } public static @NotNull IntFunction getByName(@NotNull String name) { @@ -274,6 +276,26 @@ public class Examples { ); } + public static @NotNull Example getPerlin(int height) { + if (height <= 0) height = 450; + + var material = new LambertianMaterial(new PerlinTexture(4)); + + return new Example( + new Scene( + getSkyBox(), + new Sphere(new Vec3(0, -1000, 0), 1000, material), + new Sphere(new Vec3(0, 2, 0), 2, material) + ), + SimpleCamera.builder() + .withImage(height * 16 / 9, height) + .withFieldOfView(Math.toRadians(20)) + .withPosition(new Vec3(13, 2, 3)) + .withTarget(Vec3.ZERO) + .build() + ); + } + private static @NotNull SkyBox getSkyBox() { return SkyBox.gradient(new Color(0.5, 0.7, 1.0), Color.WHITE); } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/texture/PerlinTexture.java b/src/main/java/eu/jonahbauer/raytracing/render/texture/PerlinTexture.java new file mode 100644 index 0000000..f8d0db0 --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/texture/PerlinTexture.java @@ -0,0 +1,146 @@ +package eu.jonahbauer.raytracing.render.texture; + +import eu.jonahbauer.raytracing.math.Vec3; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.Random; +import java.util.function.DoubleFunction; +import java.util.random.RandomGenerator; + +public final class PerlinTexture implements Texture { + private static final int POINT_COUNT = 256; + private static final @NotNull Random RANDOM = new Random(); + private static final @NotNull DoubleFunction GREYSCALE = t -> new Color(t, t, t); + + private final double scale; + private final int turbulence; + private final @NotNull DoubleFunction color; + + private final int mask; + private final Vec3[] randvec; + private final int[] permX; + private final int[] permY; + private final int[] permZ; + + public PerlinTexture() { + this(1.0); + } + + public PerlinTexture(double scale) { + this(scale, 7); + } + + public PerlinTexture(double scale, int turbulence) { + this(scale, turbulence, GREYSCALE); + } + + public PerlinTexture(double scale, int turbulence, @NotNull DoubleFunction color) { + this(scale, turbulence, color, POINT_COUNT, RANDOM); + } + + public PerlinTexture( + double scale, int turbulence, @NotNull DoubleFunction color, + int count, @NotNull RandomGenerator random + ) { + if ((count & (count - 1)) != 0) throw new IllegalArgumentException("count must be a power of two"); + if (turbulence <= 0) throw new IllegalArgumentException("turbulence must be positive"); + + this.scale = scale; + this.turbulence = turbulence; + this.color = Objects.requireNonNull(color, "color"); + + this.mask = count - 1; + this.randvec = new Vec3[count]; + for (int i = 0; i < count; i++) { + this.randvec[i] = Vec3.random(random, true); + } + this.permX = generatePerm(count, random); + this.permY = generatePerm(count, random); + this.permZ = generatePerm(count, random); + } + + private static int @NotNull[] generatePerm(int count, @NotNull RandomGenerator random) { + int[] p = new int[count]; + for (int i = 0; i < count; i++) { + p[i] = i; + } + permutate(p, random); + return p; + } + + private static void permutate(int @NotNull[] p, @NotNull RandomGenerator random) { + for (int i = p.length - 1; i > 0; i--) { + int target = random.nextInt(i); + int tmp = p[i]; + p[i] = p[target]; + p[target] = tmp; + } + } + + public double getNoise(@NotNull Vec3 p) { + var x = p.x() * scale; + var y = p.y() * scale; + var z = p.z() * scale; + + var u = x - Math.floor(x); + var v = y - Math.floor(y); + var w = z - Math.floor(z); + + int i = (int) Math.floor(x); + int j = (int) Math.floor(y); + int k = (int) Math.floor(z); + + var c = new Vec3[2][2][2]; + for (int di = 0; di < 2; di++) { + for (int dj = 0; dj < 2; dj++) { + for (int dk = 0; dk < 2; dk++) { + c[di][dj][dk] = randvec[permX[(i + di) & mask] ^ permY[(j + dj) & mask] ^ permZ[(k + dk) & mask]]; + } + } + } + + return interpolate(c, u, v, w); + } + + public double getNoise(@NotNull Vec3 p, int depth) { + var accum = 0.0; + var temp = p; + var weight = 1.0; + + for (int i = 0; i < depth; i++) { + accum += weight * getNoise(temp); + weight *= 0.5; + temp = temp.times(2); + } + + return accum; + } + + @Override + public @NotNull Color get(double u, double v, @NotNull Vec3 p) { + var noise = getNoise(p, turbulence); + var t = Math.fma(0.5, Math.sin(Math.PI * noise), 0.5); + return color.apply(t); + } + + private static double interpolate(Vec3[][][] c, double u, double v, double w) { + var uu = u * u * (3 - 2 * u); + var vv = v * v * (3 - 2 * v); + var ww = w * w * (3 - 2 * w); + + var accum = 0.0; + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 2; j++) { + for (int k = 0; k < 2; k++) { + var weight = new Vec3(u - i, v - j, w - k); + accum += (i * uu + (1 - i) * (1 - uu)) + * (j * vv + (1 - j) * (1 - vv)) + * (k * ww + (1 - k) * (1 - ww)) + * c[i][j][k].times(weight); + } + } + } + return accum; + } +}