Skip to content

Commit

Permalink
Refactored noise interpolation code into data pipeline
Browse files Browse the repository at this point in the history
  • Loading branch information
Martomate committed Sep 14, 2023
1 parent a06c1e4 commit 40e2447
Show file tree
Hide file tree
Showing 13 changed files with 184 additions and 125 deletions.
41 changes: 41 additions & 0 deletions src/main/scala/hexacraft/math/noise/Data2D.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package hexacraft.math.noise

import hexacraft.math.Range2D

import org.joml.Math.biLerp

case class Data2D(sizeX: Int, sizeY: Int, values: Array[Double]) {
def apply(x: Int, y: Int): Double =
values(x + y * sizeX)
}

object Data2D {
def evaluate(indices: Range2D, fn: (Int, Int) => Double): Data2D =
val Range2D(xs, ys) = indices
val values = for (y <- ys; x <- xs) yield fn(x, y)
Data2D(xs.length, ys.length, values.toArray)

def interpolate(scaleX: Int, scaleY: Int, data: Data2D): Data2D =
val xs = 0 until (data.sizeX - 1) * scaleX
val ys = 0 until (data.sizeY - 1) * scaleY

Data2D.evaluate(
Range2D(xs, ys),
(x, y) => {
val ii = x / scaleX
val ij = y / scaleY
val fi = (x % scaleX) / scaleX.toDouble
val fj = (y % scaleY) / scaleY.toDouble

biLerp(
data(ii, ij),
data(ii, ij + 1),
data(ii + 1, ij),
data(ii + 1, ij + 1),
fj,
fi
)
}
)

}
48 changes: 48 additions & 0 deletions src/main/scala/hexacraft/math/noise/Data3D.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package hexacraft.math.noise

import hexacraft.math.Range3D

import org.joml.Math.triLerp

case class Data3D(sizeX: Int, sizeY: Int, sizeZ: Int, values: Array[Double]) {
def apply(x: Int, y: Int, z: Int): Double =
values(x + y * sizeX + z * sizeX * sizeY)
}

object Data3D {
def evaluate(indices: Range3D, fn: (Int, Int, Int) => Double): Data3D =
val Range3D(xs, ys, zs) = indices
val values = for (z <- zs; y <- ys; x <- xs) yield fn(x, y, z)
Data3D(xs.length, ys.length, zs.length, values.toArray)

def interpolate(scaleX: Int, scaleY: Int, scaleZ: Int, data: Data3D): Data3D =
val xs = 0 until (data.sizeX - 1) * scaleX
val ys = 0 until (data.sizeY - 1) * scaleY
val zs = 0 until (data.sizeZ - 1) * scaleZ

Data3D.evaluate(
Range3D(xs, ys, zs),
(x, y, z) => {
val ii = x / scaleX
val ij = y / scaleY
val ik = z / scaleZ
val fi = (x % scaleX) / scaleX.toDouble
val fj = (y % scaleY) / scaleY.toDouble
val fk = (z % scaleZ) / scaleZ.toDouble

triLerp(
data(ii, ij, ik),
data(ii, ij, ik + 1),
data(ii, ij + 1, ik),
data(ii, ij + 1, ik + 1),
data(ii + 1, ij, ik),
data(ii + 1, ij, ik + 1),
data(ii + 1, ij + 1, ik),
data(ii + 1, ij + 1, ik + 1),
fk,
fj,
fi
)
}
)
}
2 changes: 1 addition & 1 deletion src/main/scala/hexacraft/math/noise/NoiseGenerator3D.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import hexacraft.world.coord.fp.CylCoords
import java.util.Random

class NoiseGenerator3D(random: Random, val numOctaves: Int, val scale: Double) {
private[this] val noiseGens = Seq.fill(numOctaves)(new SingleNoiseGen3D(random))
private[this] val noiseGens = Seq.fill(numOctaves)(PerlinNoise3D(random))

def genNoise(x: Double, y: Double, z: Double): Double = {
var amp = 1d
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/hexacraft/math/noise/NoiseGenerator4D.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import hexacraft.world.coord.fp.CylCoords
import java.util.Random

class NoiseGenerator4D(random: Random, val numOctaves: Int, val scale: Double) {
private[this] val noiseGens = Seq.fill(numOctaves)(new SingleNoiseGen4D(random))
private[this] val noiseGens = Seq.fill(numOctaves)(PerlinNoise4D(random))

def genNoise(x: Double, y: Double, z: Double, w: Double): Double = {
var amp = 1d
Expand Down
22 changes: 0 additions & 22 deletions src/main/scala/hexacraft/math/noise/NoiseInterpolator2D.scala

This file was deleted.

35 changes: 0 additions & 35 deletions src/main/scala/hexacraft/math/noise/NoiseInterpolator3D.scala

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import org.joml.Math.triLerp
import java.util.Random

// Improved Perlin Noise: http://mrl.nyu.edu/~perlin/noise/
class SingleNoiseGen3D(random: Random) { // Apparently SimplexNoise exists in joml
class PerlinNoise3D(random: Random) { // Apparently SimplexNoise exists in joml
private[this] val perm = {
val arr = (0 until 256).toArray
shuffleArray(arr, random)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import org.joml.Math.{lerp, triLerp}
import java.util.Random

// Improved Perlin Noise: http://mrl.nyu.edu/~perlin/noise/
class SingleNoiseGen4D(random: Random) { // Apparently SimplexNoise exists in joml
class PerlinNoise4D(random: Random) { // Apparently SimplexNoise exists in joml
// format: off
private val grad4 = Array(
0, 1, 1, 1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, -1, -1,
Expand Down
17 changes: 17 additions & 0 deletions src/main/scala/hexacraft/math/ranges.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package hexacraft.math

extension (xs: Range) {
def offset(dx: Int): Range =
val (start, end, step) = (xs.start + dx, xs.end + dx, xs.step)
if xs.isInclusive then start to end by step else start until end by step
}

case class Range2D(xs: Range, ys: Range) {
def offset(dx: Int, dy: Int): Range2D =
Range2D(xs.offset(dx), ys.offset(dy))
}

case class Range3D(xs: Range, ys: Range, zs: Range) {
def offset(dx: Int, dy: Int, dz: Int): Range3D =
Range3D(xs.offset(dx), ys.offset(dy), zs.offset(dz))
}
66 changes: 44 additions & 22 deletions src/main/scala/hexacraft/world/gen/WorldGenerator.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package hexacraft.world.gen

import hexacraft.math.{Range2D, Range3D}
import hexacraft.math.noise.{Data2D, Data3D, NoiseGenerator3D, NoiseGenerator4D}
import hexacraft.world.CylinderSize
import hexacraft.world.coord.fp.BlockCoords
import hexacraft.world.coord.integer.{ChunkRelWorld, ColumnRelWorld}
import hexacraft.world.settings.WorldGenSettings

import com.flowpowered.nbt.CompoundTag
import hexacraft.math.noise.{NoiseGenerator3D, NoiseGenerator4D, NoiseInterpolator2D, NoiseInterpolator3D}

import java.util.Random

Expand All @@ -23,28 +24,49 @@ class WorldGenerator(worldGenSettings: WorldGenSettings)(using CylinderSize) {
private val biomeHeightVariationGenerator =
new NoiseGenerator3D(random, 4, worldGenSettings.biomeHeightVariationGenScale)

def getHeightmapInterpolator(coords: ColumnRelWorld): NoiseInterpolator2D =
new NoiseInterpolator2D(
4,
4,
(i, j) => {
val c = BlockCoords(coords.X.toInt * 16 + i * 4, 0, coords.Z.toInt * 16 + j * 4).toCylCoords
val height = biomeHeightGenerator.genNoiseFromCylXZ(c)
val heightVariation = biomeHeightVariationGenerator.genNoiseFromCylXZ(c)
heightMapGenerator.genNoiseFromCylXZ(c) * heightVariation * 100 + height * 100
}
)
def getHeightmapInterpolator(coords: ColumnRelWorld): Data2D =
WorldGenerator.makeHeightmapInterpolator(coords, terrainHeight)

def getBlockInterpolator(coords: ChunkRelWorld): Data3D =
WorldGenerator.makeBlockInterpolator(coords, blockNoise)

private def blockNoise(x: Int, y: Int, z: Int) =
val c = BlockCoords(x, y, z).toCylCoords
blockGenerator.genNoiseFromCyl(c) + blockDensityGenerator.genNoiseFromCyl(c) * 0.4

def getBlockInterpolator(coords: ChunkRelWorld): NoiseInterpolator3D = new NoiseInterpolator3D(
4,
4,
4,
(i, j, k) => {
val c =
BlockCoords(coords.X.toInt * 16 + i * 4, coords.Y.toInt * 16 + j * 4, coords.Z.toInt * 16 + k * 4).toCylCoords
blockGenerator.genNoiseFromCyl(c) + blockDensityGenerator.genNoiseFromCyl(c) * 0.4
}
)
private def terrainHeight(x: Int, z: Int) =
val c = BlockCoords(x, 0, z).toCylCoords
val height = biomeHeightGenerator.genNoiseFromCylXZ(c)
val heightVariation = biomeHeightVariationGenerator.genNoiseFromCylXZ(c)
heightMapGenerator.genNoiseFromCylXZ(c) * heightVariation * 100 + height * 100

def toNBT: CompoundTag = worldGenSettings.toNBT
}

object WorldGenerator {
def makeHeightmapInterpolator(coords: ColumnRelWorld, terrainHeight: (Int, Int) => Double): Data2D =
val samplingPoints = Range2D(
0 to 16 by 4,
0 to 16 by 4
).offset(
coords.X.toInt * 16,
coords.Z.toInt * 16
)

val samples = Data2D.evaluate(samplingPoints, terrainHeight)
Data2D.interpolate(4, 4, samples)

def makeBlockInterpolator(coords: ChunkRelWorld, noise: (Int, Int, Int) => Double): Data3D =
val samplingPoints = Range3D(
0 to 16 by 4,
0 to 16 by 4,
0 to 16 by 4
).offset(
coords.X.toInt * 16,
coords.Y.toInt * 16,
coords.Z.toInt * 16
)

val samples = Data3D.evaluate(samplingPoints, noise)
Data3D.interpolate(4, 4, 4, samples)
}
20 changes: 0 additions & 20 deletions src/test/scala/hexacraft/math/noise/NoiseInterpolator2DTest.scala

This file was deleted.

22 changes: 0 additions & 22 deletions src/test/scala/hexacraft/math/noise/NoiseInterpolator3DTest.scala

This file was deleted.

30 changes: 30 additions & 0 deletions src/test/scala/hexacraft/world/gen/WorldGeneratorTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package hexacraft.world.gen

import hexacraft.world.CylinderSize
import hexacraft.world.coord.integer.{ChunkRelWorld, ColumnRelWorld}

import munit.FunSuite

class WorldGeneratorTest extends FunSuite {
given CylinderSize = CylinderSize(8)

test("block interpolator works") {
val fn: (Int, Int, Int) => Double = (x, y, z) => x + 3 * y + 5 * z
val sampler = WorldGenerator.makeBlockInterpolator(ChunkRelWorld(1, 2, 3), fn)
for
x <- 0 until 16
y <- 0 until 16
z <- 0 until 16
do assertEqualsDouble(sampler(x, y, z), fn(16 + x, 32 + y, 48 + z), 1e-6, clue = (x, y, z))
}

test("heightmap interpolator works") {
val fn: (Int, Int) => Double = (x, z) => x + 5 * z
val sampler = WorldGenerator.makeHeightmapInterpolator(ColumnRelWorld(1, 3), fn)
for
x <- 0 until 16
y <- 0 until 16
z <- 0 until 16
do assertEqualsDouble(sampler(x, z), fn(16 + x, 48 + z), 1e-6, clue = (x, y, z))
}
}

0 comments on commit 40e2447

Please sign in to comment.