From 824686762f2591b613a611c594cd5132499d4f82 Mon Sep 17 00:00:00 2001 From: Misode Date: Wed, 26 Aug 2026 02:44:42 +0200 Subject: [PATCH 1/4] Add 26.3 density functions --- src/math/Interval.ts | 104 +++++ src/worldgen/DensityFunction.ts | 426 +++++++++++++----- src/worldgen/RandomState.ts | 5 +- test/worldgen/DensityFunction.test.ts | 12 +- test/worldgen/NoiseChunkGenerator.test.ts | 2 +- .../biome/MultiNoiseBiomeSource.test.ts | 4 +- 6 files changed, 424 insertions(+), 129 deletions(-) diff --git a/src/math/Interval.ts b/src/math/Interval.ts index a2b28fc7..e48e986f 100644 --- a/src/math/Interval.ts +++ b/src/math/Interval.ts @@ -25,6 +25,18 @@ export class Interval { return Interval.of(value, value) } + public static between(first: number, second: number) { + if (isNaN(first) && isNaN(second)) { + return Interval.NaI + } else if (isNaN(first)) { + return Interval.ofExact(second) + } else if (isNaN(second)) { + return Interval.ofExact(first) + } else { + return Interval.of(Math.min(first, second), Math.max(first, second)) + } + } + public static encapsulating(...intervals: Interval[]) { let min = Infinity let max = -Infinity @@ -131,6 +143,38 @@ export class Interval { } } + public static pow(base: Interval, exponent: Interval) { + if (base.isNaI() || exponent.isNaI()) { + return Interval.NaI + } + if (base.min == base.max) { + return Interval.powNum(base.min, exponent) + } + let result = Interval.encapsulating(Interval.powNum(base.min, exponent), Interval.powNum(base.max, exponent)) + if (base.contains(0)) { + if (base.max > 0) { + result = Interval.encapsulating(result, Interval.powNum(0, exponent)) + } + if (base.min < 0) { + result = Interval.encapsulating(result, Interval.powNum(-0, exponent)) + } + } + return result + } + + private static powNum(base: number, exponent: Interval) { + // TODO: Implement this + return Interval.INFINITE + } + + public static log(input: Interval) { + if (input.max < 0) { + return Interval.NaI + } + const clipped = Interval.max(input, Interval.ofExact(0)) + return Interval.mapMonotonic(clipped, Math.log) + } + public static mapMonotonic(input: Interval, op: (value: number) => number) { if (input.isNaI()) { return Interval.NaI @@ -140,6 +184,66 @@ export class Interval { return Interval.of(Math.min(mappedMin, mappedMax), Math.max(mappedMin, mappedMax)) } + public static lerp(alpha: Interval, first: Interval, second: Interval) { + if (alpha.isNaI() || first.isNaI() || second.isNaI()) { + return Interval.NaI + } + return Interval.encapsulating(Interval.lerpNums(alpha, first.min, second.min), Interval.lerpNums(alpha, first.max, second.min), Interval.lerpNums(alpha, first.min, second.max), Interval.lerpNums(alpha, first.max, second.max)) + } + + public static lerpNums(alpha: Interval, first: number, second: number) { + if (alpha.isNaI() || isNaN(first) || isNaN(second)) { + return Interval.NaI + } + if (isFinite(first) && isFinite(second)) { + return Interval.between(Interval.lerpFiniteBound(alpha.min, first, second), Interval.lerpFiniteBound(alpha.max, first, second)) + } + if (first === second) { + return Interval.ofExact(first) + } + const newMin = Interval.lerpInfiniteBound(alpha.min, first, second) + const newMax = Interval.lerpInfiniteBound(alpha.max, first, second) + if (isNaN(newMin) || isNaN(newMax)) { + return Interval.NaI + } + return Interval.between(newMin, newMax) + } + + private static lerpFiniteBound(alpha: number, first: number, second: number) { + return first + Interval.mulBound(alpha, second - first) + } + + private static lerpInfiniteBound(alpha: number, first: number, second: number) { + const firstPart = Interval.mulBound(1 - alpha, first) + const secondPart = Interval.mulBound(alpha, second) + if (isFinite(firstPart) || isFinite(secondPart)) { + return firstPart + secondPart + } else if (alpha <= 0) { + return second > first ? -Infinity : Infinity + } else if (alpha >= 1) { + return second > first ? Infinity : -Infinity + } else { + return NaN + } + } + + public static sign(input: Interval) { + if (input.isNaI()) { + return Interval.NaI + } + if (input.min === input.max) { + return Interval.ofExact(Math.sign(input.min)) + } else if (!input.contains(0)) { + return Interval.ofExact(input.min > 0 ? 1 : -1) + } else if (input.min === 0) { + return Interval.of(0, 1) + } else if (input.max === 0) { + return Interval.of(-1, 0) + } else { + return Interval.of(-1, 1) + } + } + public contains(value: number) { return this.min <= value && value <= this.max } diff --git a/src/worldgen/DensityFunction.ts b/src/worldgen/DensityFunction.ts index 0fb2888d..b763ff0c 100644 --- a/src/worldgen/DensityFunction.ts +++ b/src/worldgen/DensityFunction.ts @@ -1,6 +1,6 @@ import { Holder, Identifier } from '../core/index.js' import type { BlendedNoise, NormalNoise } from '../math/index.js' -import { clamp, clampedMap, CubicSpline, Interval, lazyLerp3, LegacyRandom, NoiseParameters, SimplexNoise } from '../math/index.js' +import { clamp, clampedLerp, clampedMap, CubicSpline, floatLerp, Interval, lazyLerp3, LegacyRandom, NoiseParameters, SimplexNoise, Vector } from '../math/index.js' import { computeIfAbsent, Json } from '../util/index.js' import { WorldgenRegistries } from './WorldgenRegistries.js' @@ -54,7 +54,11 @@ export namespace DensityFunction { } } - const NoiseParser = Holder.parser(WorldgenRegistries.NOISE, NoiseParameters.fromJson) + const noiseParser = Holder.parser(WorldgenRegistries.NOISE, NoiseParameters.fromJson) + const vectorParser = (obj: unknown): Vector => { + const arr = Json.readArray(obj, v => Json.readNumber(v) ?? 0) ?? [] + return new Vector(arr[0] ?? 0, arr[1] ?? 0, arr[2] ?? 0) + } export function fromJson(obj: unknown, inputParser: (obj: unknown) => DensityFunction = fromJson): DensityFunction { if (typeof obj === 'string') { @@ -77,17 +81,30 @@ export namespace DensityFunction { Json.readNumber(root.y_factor) ?? 160, Json.readNumber(root.smear_scale_multiplier) ?? 8 ) - case 'flat_cache': return new FlatCache(inputParser(root.argument)) - case 'interpolated': return new Interpolated(inputParser(root.argument)) - case 'cache_2d': return new Cache2D(inputParser(root.argument)) - case 'cache_once': return new CacheOnce(inputParser(root.argument)) - case 'cache_all_in_cell': return new CacheAllInCell(inputParser(root.argument)) - case 'noise': return new Noise( - Json.readNumber(root.xz_scale) ?? 1, - Json.readNumber(root.y_scale) ?? 1, - NoiseParser(root.noise), + case 'flat_cache': return new FlatCache(inputParser(root.input ?? root.argument)) + case 'interpolated': return new Interpolated( + inputParser(root.input ?? root.argument), + Json.readInt(root.cell_size_xz) ?? 4, + Json.readInt(root.cell_size_y) ?? 4, ) - case 'end_islands': return new EndIslands() + case 'cache_2d': return new Cache2D(inputParser(root.input ?? root.argument)) + case 'cache': + case 'cache_once': + return new CacheOnce(inputParser(root.input ?? root.argument)) + case 'cache_all_in_cell': return new CacheAllInCell(inputParser(root.input ?? root.argument)) + case 'noise': + case 'shifted_noise': + return new Noise( + noiseParser(root.noise), + Json.readNumber(root.xz_scale) ?? 1, + Json.readNumber(root.y_scale) ?? 1, + root.shift_x ? inputParser(root.shift_x) : DensityFunction.Constant.ZERO, + root.shift_y ? inputParser(root.shift_y) : DensityFunction.Constant.ZERO, + root.shift_z ? inputParser(root.shift_z) : DensityFunction.Constant.ZERO, + ) + case 'end_islands': + case 'end_outer_islands': + return new EndIslands() case 'find_top_surface': return new FindTopSurface( inputParser(root.density), inputParser(root.upper_bound), @@ -97,15 +114,7 @@ export namespace DensityFunction { case 'weird_scaled_sampler': return new WeirdScaledSampler( inputParser(root.input), Json.readEnum(root.rarity_value_mapper, RarityValueMapper), - NoiseParser(root.noise), - ) - case 'shifted_noise': return new ShiftedNoise( - inputParser(root.shift_x), - inputParser(root.shift_y), - inputParser(root.shift_z), - Json.readNumber(root.xz_scale) ?? 1, - Json.readNumber(root.y_scale) ?? 1, - NoiseParser(root.noise), + noiseParser(root.noise), ) case 'range_choice': return new RangeChoice( inputParser(root.input), @@ -114,10 +123,10 @@ export namespace DensityFunction { inputParser(root.when_in_range), inputParser(root.when_out_of_range), ) - case 'shift_a': return new ShiftA(NoiseParser(root.argument)) - case 'shift_b': return new ShiftB(NoiseParser(root.argument)) - case 'shift': return new Shift(NoiseParser(root.argument)) - case 'blend_density': return new BlendDensity(inputParser(root.argument)) + case 'shift_a': return new ShiftA(noiseParser(root.noise ?? root.argument)) + case 'shift_b': return new ShiftB(noiseParser(root.noise ?? root.argument)) + case 'shift': return new Shift(noiseParser(root.noise ?? root.argument)) + case 'blend_density': return new BlendDensity(inputParser(root.input ?? root.argument)) case 'clamp': return new Clamp( inputParser(root.input), Json.readNumber(root.min) ?? 0, @@ -132,29 +141,68 @@ export namespace DensityFunction { case 'abs': case 'square': case 'cube': + case 'sqrt': case 'half_negative': - case 'invert': case 'quarter_negative': case 'squeeze': - return new Unary(type, inputParser(root.argument)) + case 'invert': + case 'reciprocal': + case 'negate': + case 'log': + case 'sign': + return new Unary(type, inputParser(root.input ?? root.argument)) case 'add': + case 'sub': case 'mul': + case 'div': case 'min': case 'max': return new Binary( Json.readEnum(type, BinaryType), - inputParser(root.argument1), - inputParser(root.argument2), + inputParser(root.left ?? root.argument1), + inputParser(root.right ?? root.argument2), ) case 'spline': return new Spline( CubicSpline.fromJson(root.spline, inputParser) ) - case 'constant': return new Constant(Json.readNumber(root.argument) ?? 0) + case 'constant': return new Constant(Json.readNumber(root.value ?? root.argument) ?? 0) case 'y_clamped_gradient': return new YClampedGradient( Json.readInt(root.from_y) ?? -4064, Json.readInt(root.to_y) ?? 4062, Json.readNumber(root.from_value) ?? -4064, Json.readNumber(root.to_value) ?? 4062, ) + case 'gradient': return new Gradient( + Json.readEnum(root.axis, AxisType), + Json.readEnum(root.tiling, TilingType), + Json.readInt(root.from_coordinate) ?? 0, + Json.readInt(root.to_coordinates) ?? 1, + Json.readNumber(root.from_value) ?? 0, + Json.readNumber(root.to_value) ?? 1, + ) + case 'lerp': return new Lerp( + inputParser(root.alpha), + inputParser(root.first), + inputParser(root.second), + ) + case 'floor': + case 'round': + case 'ceil': + case 'truncate': + return new Round( + type, + inputParser(root.input), + inputParser(root.multiple), + ) + case 'pow': return new Pow(inputParser(root.base), inputParser(root.exponent)) + case 'distance_to_point': return new DistanceToPoint( + vectorParser(root.point), + Json.readEnum(root.metric, DistanceMetric), + ) + case 'slice': return new Slice( + Json.readEnum(root.axis, AxisType), + Json.readInt(root.coordinate) ?? 0, + inputParser(root.input), + ) } return Constant.ZERO } @@ -327,8 +375,8 @@ export namespace DensityFunction { private readonly values: Map constructor( wrapped: DensityFunction, - private readonly cellWidth: number = 4, - private readonly cellHeight: number = 4, + private readonly cellWidth: number, + private readonly cellHeight: number, ) { super(wrapped) this.values = new Map() @@ -358,7 +406,7 @@ export namespace DensityFunction { }) } public mapChildren(visitor: Visitor) { - return new Interpolated(visitor.apply(this.wrapped)) + return new Interpolated(visitor.apply(this.wrapped), this.cellWidth, this.cellHeight) } public withCellSize(cellWidth: number, cellHeight: number) { return new Interpolated(this.wrapped, cellWidth, cellHeight) @@ -367,18 +415,24 @@ export namespace DensityFunction { export class Noise extends DensityFunction { constructor( + public readonly noiseData: Holder, public readonly xzScale: number, public readonly yScale: number, - public readonly noiseData: Holder, + public readonly shiftX: DensityFunction, + public readonly shiftY: DensityFunction, + public readonly shiftZ: DensityFunction, public readonly noise?: NormalNoise, ) { super() } public compute(context: Context) { - return this.noise?.sample(context.x * this.xzScale, context.y * this.yScale, context.z * this.xzScale) ?? 0 + const x = context.x * this.xzScale + this.shiftX.compute(context) + const y = context.y * this.yScale + this.shiftY.compute(context) + const z = context.z * this.xzScale + this.shiftZ.compute(context) + return this.noise?.sample(x, y, z) ?? 0 } public mapChildren(visitor: Visitor): DensityFunction { - return this + return new Noise(this.noiseData, this.xzScale, this.yScale, visitor.apply(this.shiftX), visitor.apply(this.shiftY), visitor.apply(this.shiftZ)) } public range(): Interval { if (!this.noise) { @@ -517,29 +571,6 @@ export namespace DensityFunction { } } - export class ShiftedNoise extends Noise { - constructor( - public readonly shiftX: DensityFunction, - public readonly shiftY: DensityFunction, - public readonly shiftZ: DensityFunction, - xzScale: number, - yScale: number, - noiseData: Holder, - noise?: NormalNoise - ) { - super(xzScale, yScale, noiseData, noise) - } - public compute(context: Context) { - const xx = context.x * this.xzScale + this.shiftX.compute(context) - const yy = context.y * this.yScale + this.shiftY.compute(context) - const zz = context.z * this.xzScale + this.shiftZ.compute(context) - return this.noise?.sample(xx, yy, zz) ?? 0 - } - public mapChildren(visitor: Visitor): DensityFunction { - return new ShiftedNoise(visitor.apply(this.shiftX), visitor.apply(this.shiftY), visitor.apply(this.shiftZ), this.xzScale, this.yScale, this.noiseData, this.noise) - } - } - export class RangeChoice extends DensityFunction { constructor( public readonly input: DensityFunction, @@ -692,7 +723,7 @@ export namespace DensityFunction { } } - const UnaryType = ['abs', 'square', 'cube', 'half_negative', 'quarter_negative', 'invert', 'squeeze'] as const + const UnaryType = ['abs', 'square', 'cube', 'sqrt', 'half_negative', 'quarter_negative', 'squeeze', 'invert', 'reciprocal', 'negate', 'log', 'sign'] as const export class Unary extends Transformer { constructor( @@ -704,91 +735,94 @@ export namespace DensityFunction { public transform(context: Context, density: number) { return Unary.transform(this.type, density) } - private static transform(type: typeof UnaryType[number], d: number) { + private static transform(type: typeof UnaryType[number], d: number): number { switch (type) { - case 'abs': - return Math.abs(d) - case 'square': - return d * d - case 'cube': - return d * d * d - case 'half_negative': - return d > 0 ? d : d * 0.5 - case 'quarter_negative': - return d > 0 ? d : d * 0.25 - case 'invert': - return 1/d + case 'abs': return Math.abs(d) + case 'square': return d * d + case 'cube': return d * d * d + case 'half_negative': return d > 0 ? d : d * 0.5 + case 'quarter_negative': return d > 0 ? d : d * 0.25 case 'squeeze': const c = clamp(d, -1, 1) return c / 2 - c * c * c / 24 + case 'invert': + case 'reciprocal': + return 1/d + case 'negate': return -d + case 'sqrt': return Math.sqrt(d) + case 'log': return Math.log(d) + case 'sign': return Math.sign(d) } } public mapChildren(visitor: Visitor): DensityFunction { return new Unary(this.type, visitor.apply(this.input)) } - public range() { + public range(): Interval { const inputRange = this.input.range() switch (this.type) { - case 'abs': - return Interval.abs(inputRange) - case 'square': - return Interval.square(inputRange) + case 'abs': return Interval.abs(inputRange) + case 'square': return Interval.square(inputRange) case 'cube': case 'half_negative': case 'quarter_negative': case 'squeeze': return Interval.mapMonotonic(inputRange, value => Unary.transform(this.type, value)) case 'invert': + case 'reciprocal': return Interval.reciprocal(inputRange) + case 'negate': return Interval.sub(Interval.ofExact(0), inputRange) + case 'sqrt': return Interval.pow(inputRange, Interval.ofExact(0.5)) + case 'log': return Interval.log(inputRange) + case 'sign': return Interval.sign(inputRange) } } } - const BinaryType = ['add', 'mul', 'min', 'max'] as const + const BinaryType = ['add', 'sub', 'mul', 'div', 'min', 'max'] as const export class Binary extends DensityFunction { private readonly rightRange: Interval constructor( public readonly type: typeof BinaryType[number], - public readonly argument1: DensityFunction, - public readonly argument2: DensityFunction, + public readonly left: DensityFunction, + public readonly right: DensityFunction, ) { super() - this.rightRange = argument2.range() + this.rightRange = right.range() } - public compute(context: Context) { - const a = this.argument1.compute(context) + public compute(context: Context): number { + const a = this.left.compute(context) switch (this.type) { - case 'add': return a + this.argument2.compute(context) - case 'mul': return a === 0 ? 0 : a * this.argument2.compute(context) - case 'min': return a < this.rightRange.min ? a : Math.min(a, this.argument2.compute(context)) - case 'max': return a > this.rightRange.max ? a : Math.max(a, this.argument2.compute(context)) + case 'add': return a + this.right.compute(context) + case 'sub': return a - this.right.compute(context) + case 'mul': return a === 0 ? 0 : a * this.right.compute(context) + case 'div': return a === 0 ? 0 : a / this.right.compute(context) + case 'min': return a < this.rightRange.min ? a : Math.min(a, this.right.compute(context)) + case 'max': return a > this.rightRange.max ? a : Math.max(a, this.right.compute(context)) } } - public mapChildren(visitor: Visitor) { - return new Binary(this.type, visitor.apply(this.argument1), visitor.apply(this.argument2)) + public mapChildren(visitor: Visitor): DensityFunction { + return new Binary(this.type, visitor.apply(this.left), visitor.apply(this.right)) } public range(): Interval { - const left = this.argument1.range() - const right = this.argument2.range() + const left = this.left.range() + const right = this.right.range() switch (this.type) { - case 'add': - return Interval.add(left, right) - case 'mul': - return Interval.mul(left, right) - case 'min': - return Interval.min(left, right) - case 'max': - return Interval.max(left, right) + case 'add': return Interval.add(left, right) + case 'sub': return Interval.sub(left, right) + case 'mul': return Interval.mul(left, right) + case 'div': return Interval.div(left, right) + case 'min': return Interval.min(left, right) + case 'max': return Interval.max(left, right) } } public trySimplify(): DensityFunction { if (this.type === 'mul' || this.type === 'add') { - if (this.argument1 instanceof Constant) { - return new MulOrAdd(this.type, this.argument2, this.argument1.value) + if (this.left instanceof Constant) { + return new MulOrAdd(this.type, this.right, this.left.value) } - if (this.argument2 instanceof Constant) { - return new MulOrAdd(this.type, this.argument1, this.argument2.value) + if (this.right instanceof Constant) { + return new MulOrAdd(this.type, this.left, this.right.value) } } return this @@ -805,12 +839,10 @@ export namespace DensityFunction { ) { super(input) } - public transform(context: Context, density: number) { + public transform(context: Context, density: number): number { switch (this.type) { - case 'mul': - return density * this.value - case 'add': - return density + this.value + case 'mul': return density * this.value + case 'add': return density + this.value } } public mapChildren(visitor: Visitor): DensityFunction { @@ -818,10 +850,8 @@ export namespace DensityFunction { } public range(): Interval { switch (this.type) { - case 'mul': - return Interval.mul(this.input.range(), Interval.ofExact(this.value)) - case 'add': - return Interval.add(this.input.range(), Interval.ofExact(this.value)) + case 'mul': return Interval.mul(this.input.range(), Interval.ofExact(this.value)) + case 'add': return Interval.add(this.input.range(), Interval.ofExact(this.value)) } } } @@ -832,7 +862,7 @@ export namespace DensityFunction { ) { super() } - public compute(context: Context) { + public compute(context: Context): number { return this.spline.compute(context) } public mapChildren(visitor: Visitor): DensityFunction { @@ -857,14 +887,178 @@ export namespace DensityFunction { ) { super() } - public compute(context: Context) { + public compute(context: Context): number { return clampedMap(context.y, this.fromY, this.toY, this.fromValue, this.toValue) } public mapChildren(visitor: Visitor): DensityFunction { return this } - public range() { + public range(): Interval { return Interval.of(Math.min(this.fromValue, this.toValue), Math.max(this.fromValue, this.toValue)) } } + + const AxisType = ['x', 'y', 'z'] as const + const TilingType = ['clamp_to_edge', 'repeat', 'mirrored_repeat'] as const + + export class Gradient extends DensityFunction { + constructor( + public readonly axis: typeof AxisType[number], + public readonly tiling: typeof TilingType[number], + public readonly fromCoordinate: number, + public readonly toCoordinate: number, + public readonly fromValue: number, + public readonly toValue: number, + ) { + super() + } + public compute(context: Context): number { + const range = this.toCoordinate - this.fromCoordinate + let coordinate = context[this.axis] - this.fromCoordinate + switch (this.tiling) { + case 'clamp_to_edge': + break + case 'repeat': + coordinate = ((coordinate % range) + range) % range + break + case 'mirrored_repeat': + const tileIndex = Math.floor(coordinate / range) + const localCoordinate = coordinate - tileIndex * range + coordinate = (tileIndex & 1) == 0 ? localCoordinate : range - localCoordinate + break + } + return clampedLerp(coordinate / range, this.fromValue, this.toValue) + } + public mapChildren(visitor: Visitor): DensityFunction { + return this + } + public range(): Interval { + return Interval.between(this.fromValue, this.toValue) + } + } + + export class Lerp extends DensityFunction { + constructor( + public readonly alpha: DensityFunction, + public readonly first: DensityFunction, + public readonly second: DensityFunction, + ) { + super() + } + public compute(context: Context): number { + const a = this.alpha.compute(context) + if (a === 0) { + return this.first.compute(context) + } else if (a === 1) { + return this.second.compute(context) + } else { + return floatLerp(a, this.first.compute(context), this.second.compute(context)) + } + } + public mapChildren(visitor: Visitor): DensityFunction { + return new Lerp(visitor.apply(this.alpha), visitor.apply(this.first), visitor.apply(this.second)) + } + public range(): Interval { + return Interval.lerp(this.alpha.range(), this.first.range(), this.second.range()) + } + } + + const RoundType = ['floor', 'round', 'ceil', 'truncate'] as const + + export class Round extends DensityFunction { + constructor( + public readonly type: typeof RoundType[number], + public readonly input: DensityFunction, + public readonly multiple: DensityFunction, + ) { + super() + } + public compute(context: Context): number { + const a = this.input.compute(context) + const m = this.multiple.compute(context) + return m === 0 ? a : this.roundToInteger(a / m) * m + } + private roundToInteger(value: number) { + switch (this.type) { + case 'floor': return Math.floor(value) + case 'round': return Math.round(value) + case 'ceil': return Math.ceil(value) + case 'truncate': return Math.trunc(value) + } + } + public mapChildren(visitor: Visitor): DensityFunction { + return new Round(this.type, visitor.apply(this.input), visitor.apply(this.multiple)) + } + public range(): Interval { + const multipleRange = this.multiple.range() + return Interval.mul(Interval.mapMonotonic(Interval.div(this.input.range(), multipleRange), value => this.roundToInteger(value)), multipleRange) + } + } + + export class Pow extends DensityFunction { + constructor( + public readonly base: DensityFunction, + public readonly exponent: DensityFunction, + ) { + super() + } + public compute(context: Context): number { + return Math.pow(this.base.compute(context), this.exponent.compute(context)) + } + public mapChildren(visitor: Visitor): DensityFunction { + return new Pow(visitor.apply(this.base), visitor.apply(this.exponent)) + } + public range(): Interval { + return Interval.pow(this.base.range(), this.exponent.range()) + } + } + + const DistanceMetric = ['euclidean', 'euclidean_squared', 'manhattan', 'chebyshev'] as const + + export class DistanceToPoint extends DensityFunction { + constructor( + public readonly point: Vector, + public readonly metric: typeof DistanceMetric[number], + ) { + super() + } + public compute(context: Context): number { + const delta = this.point.sub(new Vector(context.x, context.y, context.z)) + switch (this.metric) { + case 'euclidean': return delta.length() + case 'euclidean_squared': return delta.lengthSquared() + case 'manhattan': return Math.abs(delta.x) + Math.abs(delta.y) + Math.abs(delta.z) + case 'chebyshev': return Math.max(Math.abs(delta.x), Math.abs(delta.y), Math.abs(delta.z)) + } + } + public mapChildren(visitor: Visitor): DensityFunction { + return this + } + public range(): Interval { + return Interval.of(0, Infinity) + } + } + + export class Slice extends DensityFunction { + constructor( + public readonly axis: typeof AxisType[number], + public readonly coordinate: number, + public readonly input: DensityFunction, + ) { + super() + } + public compute(context: Context): number { + switch (this.axis) { + case 'x': return this.input.compute({ x: 0, y: context.y, z: context.z }) + case 'y': return this.input.compute({ x: 0, y: context.y, z: context.z }) + case 'z': return this.input.compute({ x: 0, y: context.y, z: context.z }) + } + } + public mapChildren(visitor: Visitor): DensityFunction { + return new Slice(this.axis, this.coordinate, visitor.apply(this.input)) + } + public range(): Interval { + return this.input.range() + } + } } diff --git a/src/worldgen/RandomState.ts b/src/worldgen/RandomState.ts index 037fe8d7..5bf371d4 100644 --- a/src/worldgen/RandomState.ts +++ b/src/worldgen/RandomState.ts @@ -70,11 +70,8 @@ export class RandomState { if (fn instanceof DensityFunction.Interpolated) { return fn.withCellSize(NoiseSettings.cellWidth(noiseSettings), NoiseSettings.cellHeight(noiseSettings)) } - if (fn instanceof DensityFunction.ShiftedNoise) { - return new DensityFunction.ShiftedNoise(fn.shiftX, fn.shiftY, fn.shiftZ, fn.xzScale, fn.yScale, fn.noiseData, getNoise(fn.noiseData)) - } if (fn instanceof DensityFunction.Noise) { - return new DensityFunction.Noise(fn.xzScale, fn.yScale, fn.noiseData, getNoise(fn.noiseData)) + return new DensityFunction.Noise(fn.noiseData, fn.xzScale, fn.yScale, fn.shiftX, fn.shiftY, fn.shiftZ, getNoise(fn.noiseData)) } if (fn instanceof DensityFunction.ShiftNoise) { return fn.withNewNoise(getNoise(fn.noiseData)) diff --git a/test/worldgen/DensityFunction.test.ts b/test/worldgen/DensityFunction.test.ts index 07502c76..bff6d203 100644 --- a/test/worldgen/DensityFunction.test.ts +++ b/test/worldgen/DensityFunction.test.ts @@ -42,14 +42,14 @@ describe('DensityFunction', () => { }) it('Noise', () => { - const fn = wrap(new DF.Noise(1, 1, SHIFT)) + const fn = wrap(new DF.Noise(SHIFT, 1, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO)) expect(fn.compute(ContextA)).toEqual(0.3004295819443726) expect(fn.compute(ContextB)).toEqual(0.3085235014681946) expect(fn.compute(ContextC)).toEqual(-0.43773259014323784) }) it('WeirdScaledSampler', () => { - const input = new DF.Noise(1, 1, SHIFT) + const input = new DF.Noise(SHIFT, 1, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO) const fn = wrap(new DF.WeirdScaledSampler(input, 'type_1', EROSION)) expect(fn.compute(ContextA)).toEqual(0.05986336811047935) expect(fn.compute(ContextB)).toEqual(0.06476443600923233) @@ -117,7 +117,7 @@ describe('DensityFunction', () => { it('Add', () => { const fn1 = wrap(new DF.Binary('add', new DF.Constant(2), new DF.Constant(3))) expect(fn1.compute(ContextA)).toEqual(5) - const fn2 = wrap(new DF.Binary('add', new DF.Noise(16, 1, SHIFT), new DF.Constant(2))) + const fn2 = wrap(new DF.Binary('add', new DF.Noise(SHIFT, 16, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), new DF.Constant(2))) expect(fn2.compute(ContextA)).toEqual(1.9594976210617139) expect(fn2.compute(ContextB)).toEqual(1.9887069396954864) expect(fn2.compute(ContextC)).toEqual(1.6672203651115742) @@ -126,7 +126,7 @@ describe('DensityFunction', () => { it('Mul', () => { const fn1 = wrap(new DF.Binary('mul', new DF.Constant(2), new DF.Constant(3))) expect(fn1.compute(ContextA)).toEqual(6) - const fn2 = wrap(new DF.Binary('mul', new DF.Noise(16, 1, SHIFT), new DF.Constant(20))) + const fn2 = wrap(new DF.Binary('mul', new DF.Noise(SHIFT, 16, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), new DF.Constant(20))) expect(fn2.compute(ContextA)).toEqual(-0.8100475787657212) expect(fn2.compute(ContextB)).toEqual(-0.2258612060902705) expect(fn2.compute(ContextC)).toEqual(-6.655592697768515) @@ -142,7 +142,7 @@ describe('DensityFunction', () => { it('Min', () => { const fn1 = wrap(new DF.Binary('min', new DF.Constant(2), new DF.Constant(3))) expect(fn1.compute(ContextA)).toEqual(2) - const fn2 = wrap(new DF.Binary('min', new DF.Noise(16, 1, SHIFT), new DF.Constant(-0.3))) + const fn2 = wrap(new DF.Binary('min', new DF.Noise(SHIFT, 16, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), new DF.Constant(-0.3))) expect(fn2.compute(ContextA)).toEqual(-0.3) expect(fn2.compute(ContextB)).toEqual(-0.3) expect(fn2.compute(ContextC)).toEqual(-0.33277963488842577) @@ -151,7 +151,7 @@ describe('DensityFunction', () => { it('Max', () => { const fn1 = wrap(new DF.Binary('max', new DF.Constant(2), new DF.Constant(3))) expect(fn1.compute(ContextA)).toEqual(3) - const fn2 = wrap(new DF.Binary('max', new DF.Noise(16, 1, SHIFT), new DF.Constant(-0.3))) + const fn2 = wrap(new DF.Binary('max', new DF.Noise(SHIFT, 16, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), new DF.Constant(-0.3))) expect(fn2.compute(ContextA)).toEqual(-0.04050237893828606) expect(fn2.compute(ContextB)).toEqual(-0.011293060304513524) expect(fn2.compute(ContextC)).toEqual(-0.3) diff --git a/test/worldgen/NoiseChunkGenerator.test.ts b/test/worldgen/NoiseChunkGenerator.test.ts index a2df492a..49ffd00e 100644 --- a/test/worldgen/NoiseChunkGenerator.test.ts +++ b/test/worldgen/NoiseChunkGenerator.test.ts @@ -33,7 +33,7 @@ describe('NoiseChunkGenerator', () => { }) it('fill', () => { - const finalDensity = new DF.Noise(1, 1, Holder.reference(WorldgenRegistries.NOISE, Identifier.create('offset'))) + const finalDensity = new DF.Noise(Holder.reference(WorldgenRegistries.NOISE, Identifier.create('offset')), 1, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO) const { generator, randomState } = setup(BigInt(123), {}, {}, { finalDensity }) const chunk = new Chunk(0, 64, ChunkPos.create(4, 1)) generator.fill(randomState, chunk) diff --git a/test/worldgen/biome/MultiNoiseBiomeSource.test.ts b/test/worldgen/biome/MultiNoiseBiomeSource.test.ts index 3171ef47..b1b66be1 100644 --- a/test/worldgen/biome/MultiNoiseBiomeSource.test.ts +++ b/test/worldgen/biome/MultiNoiseBiomeSource.test.ts @@ -24,8 +24,8 @@ describe('MultiNoise', () => { const settings = NoiseGeneratorSettings.create({ noise: { minY: 0, height: 128, xzSize: 1, ySize: 2 }, noiseRouter: NoiseRouter.create({ - temperature: new DF.Noise(0.25, 0, temperature), - vegetation: new DF.Noise(0.25, 0, vegetation), + temperature: new DF.Noise(temperature, 0.25, 0, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), + vegetation: new DF.Noise(vegetation, 0.25, 0, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), }), }) const randomState = new RandomState(settings, BigInt(123)) From 58b200989f9e59be3cc6a9d9468355b9d693e03f Mon Sep 17 00:00:00 2001 From: Misode Date: Sat, 29 Aug 2026 18:33:02 +0200 Subject: [PATCH 2/4] Refactor noise code to match vanilla 26.3 --- demo/main.ts | 10 +- src/math/Util.ts | 6 + src/math/noise/BlendedNoise.ts | 97 +++---- src/math/noise/GradientNoise.ts | 49 ++++ src/math/noise/ImprovedNoise.ts | 75 ----- src/math/noise/NoiseStack.ts | 47 +++ src/math/noise/NormalNoise.ts | 174 ++++++++--- src/math/noise/PerlinNoise.ts | 114 +++----- src/math/noise/PerlinSimplexNoise.ts | 54 ---- src/math/noise/SimplexNoise.ts | 270 ++++++++---------- src/math/noise/SmearedPerlinNoise.ts | 42 +++ src/math/noise/index.ts | 6 +- src/math/random/LegacyRandom.ts | 8 +- src/worldgen/DensityFunction.ts | 82 +++--- src/worldgen/NoiseRouter.ts | 20 -- src/worldgen/RandomState.ts | 36 ++- src/worldgen/SurfaceSystem.ts | 20 +- src/worldgen/WorldgenRegistries.ts | 12 +- test/math/Spline.test.ts | 29 +- test/math/Util.test.ts | 8 + test/math/noise/BlendedNoise.test.ts | 25 +- test/math/noise/ImprovedNoise.test.ts | 33 --- test/math/noise/NoiseStack.test.ts | 34 +++ test/math/noise/NormalNoise.test.ts | 25 +- test/math/noise/PerlinNoise.test.ts | 52 +--- test/math/noise/PerlinSimplexNoise.test.ts | 39 --- test/math/noise/SimplexNoise.test.ts | 45 +-- test/worldgen/DensityFunction.test.ts | 56 ++-- test/worldgen/NoiseChunkGenerator.test.ts | 6 +- .../biome/MultiNoiseBiomeSource.test.ts | 10 +- 30 files changed, 711 insertions(+), 773 deletions(-) create mode 100644 src/math/noise/GradientNoise.ts delete mode 100644 src/math/noise/ImprovedNoise.ts create mode 100644 src/math/noise/NoiseStack.ts delete mode 100644 src/math/noise/PerlinSimplexNoise.ts create mode 100644 src/math/noise/SmearedPerlinNoise.ts create mode 100644 test/math/Util.test.ts delete mode 100644 test/math/noise/ImprovedNoise.test.ts create mode 100644 test/math/noise/NoiseStack.test.ts delete mode 100644 test/math/noise/PerlinSimplexNoise.test.ts diff --git a/demo/main.ts b/demo/main.ts index 167132a6..f6408d65 100644 --- a/demo/main.ts +++ b/demo/main.ts @@ -1,5 +1,5 @@ import { mat4 } from 'gl-matrix' -import type { ItemRendererResources, ItemRenderingContext, NbtTag, Resources, Voxel } from '../src/index.js' +import type { Color, ItemRendererResources, ItemRenderingContext, NbtTag, Resources, Voxel } from '../src/index.js' import { BlockDefinition, BlockModel, Identifier, ItemRenderer, ItemStack, jsonToNbt, NormalNoise, Structure, StructureRenderer, TextureAtlas, upperPowerOfTwo, VoxelRenderer, XoroshiroRandom } from '../src/index.js' import { } from '../src/nbt/Util.js' import { ItemModel } from '../src/render/ItemModel.js' @@ -200,14 +200,16 @@ Promise.all([ const voxels: Voxel[] = [] const random = XoroshiroRandom.create(BigInt(123)) - const noise = new NormalNoise(random, { firstOctave: -5, amplitudes: [1, 1, 1] }) + const normalNoise = new NormalNoise(1, -5, 3, 'enabled', []) + const noise = normalNoise.create(random) const sampleRegion = 50 for (let x = -sampleRegion; x <= sampleRegion; x += 1) { for (let y = -sampleRegion; y <= sampleRegion; y += 1) { for (let z = -sampleRegion; z <= sampleRegion; z += 1) { - const d = noise.sample(x, y, z) + const d = noise.get3D(x, y, z) if (d > 0) { - voxels.push({ x, y, z, color: [200, 200, 200] }) + const color: Color = d < 0.2 ? [200, 200, 200] : [120, 30, 180] + voxels.push({ x, y, z, color }) } } } diff --git a/src/math/Util.ts b/src/math/Util.ts index 7c77dc62..be318d99 100644 --- a/src/math/Util.ts +++ b/src/math/Util.ts @@ -141,3 +141,9 @@ export function shuffle(array: unknown[], random: Random) { array[i - 1] = tmp } } + +export function hashCode(s: string) { + for(var i = 0, h = 0; i < s.length; i++) + h = Math.imul(31, h) + s.charCodeAt(i) | 0 + return h +} diff --git a/src/math/noise/BlendedNoise.ts b/src/math/noise/BlendedNoise.ts index 0cb273c4..c8f86c61 100644 --- a/src/math/noise/BlendedNoise.ts +++ b/src/math/noise/BlendedNoise.ts @@ -1,15 +1,20 @@ +import { Interval } from '../Interval.js' import type { Random } from '../random/index.js' -import { clampedLerp } from '../Util.js' -import type { ImprovedNoise } from './ImprovedNoise.js' -import { PerlinNoise } from './PerlinNoise.js' +import { clamp, floatLerp } from '../Util.js' +import type { NoiseLayer } from './NoiseStack.js' +import { NoiseStack } from './NoiseStack.js' +import { SmearedPerlinNoise } from './SmearedPerlinNoise.js' export class BlendedNoise { - public readonly minLimitNoise: PerlinNoise - public readonly maxLimitNoise: PerlinNoise - public readonly mainNoise: PerlinNoise + private static readonly BASE_SCALE = 684.412 + private static readonly LIMIT_FACTOR = 0.99998474 + private static readonly MAIN_FACTOR = 12.75 + + public readonly minLimitNoise: NoiseStack + public readonly maxLimitNoise: NoiseStack + public readonly mainNoise: NoiseStack private readonly xzMultiplier: number private readonly yMultiplier: number - public readonly maxValue: number constructor( random: Random, @@ -19,58 +24,44 @@ export class BlendedNoise { public readonly yFactor: number, public readonly smearScaleMultiplier: number ) { - this.minLimitNoise = new PerlinNoise(random, -15, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], true) - this.maxLimitNoise = new PerlinNoise(random, -15, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], true) - this.mainNoise = new PerlinNoise(random, -7, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], true) - this.xzMultiplier = 684.412 * xzScale - this.yMultiplier = 684.412 * yScale - this.maxValue = this.minLimitNoise.edgeValue(this.yMultiplier + 2) + this.xzMultiplier = BlendedNoise.BASE_SCALE * xzScale + this.yMultiplier = BlendedNoise.BASE_SCALE * yScale + const limitSmearScaleY = this.yMultiplier * smearScaleMultiplier + const mainSmearScaleY = limitSmearScaleY / yFactor + this.minLimitNoise = BlendedNoise.createNoise(random, -15, limitSmearScaleY, BlendedNoise.LIMIT_FACTOR) + this.maxLimitNoise = BlendedNoise.createNoise(random, -15, limitSmearScaleY, BlendedNoise.LIMIT_FACTOR) + this.mainNoise = BlendedNoise.createNoise(random, -7, mainSmearScaleY, BlendedNoise.MAIN_FACTOR) } - public sample(x: number, y: number, z: number) { - const scaledX = x * this.xzMultiplier - const scaledY = y * this.yMultiplier - const scaledZ = z * this.xzMultiplier - - const factoredX = scaledX / this.xzFactor - const factoredY = scaledY / this.yFactor - const factoredZ = scaledZ / this.xzFactor - - const smear = this.yMultiplier * this.smearScaleMultiplier - const factoredSmear = smear / this.yFactor - - let noise: ImprovedNoise | undefined - let value = 0 + private static createNoise(random: Random, firstOctave: number, smearScaleY: number, valueFactor: number) { + const octaves = -firstOctave + 1 let factor = 1 - for (let i = 0; i < 8; i += 1) { - noise = this.mainNoise.getOctaveNoise(i) - if (noise) { - const xx = PerlinNoise.wrap(factoredX * factor) - const yy = PerlinNoise.wrap(factoredY * factor) - const zz = PerlinNoise.wrap(factoredZ * factor) - value += noise.sample(xx, yy, zz, factoredSmear * factor, factoredY * factor) / factor - } + valueFactor /= Math.pow(2, octaves) - 1 + const layers: NoiseLayer[] = [] + for (let i = octaves - 1; i >= 0; i -= 1) { + layers.push({ noise: new SmearedPerlinNoise(random, smearScaleY * factor), frequency: factor, amplitude: valueFactor }) factor /= 2 + valueFactor *= 2 } + return new NoiseStack(layers) + } - value = (value / 10 + 1) / 2 - factor = 1 - let min = 0 - let max = 0 - for (let i = 0; i < 16; i += 1) { - const xx = PerlinNoise.wrap(scaledX * factor) - const yy = PerlinNoise.wrap(scaledY * factor) - const zz = PerlinNoise.wrap(scaledZ * factor) - const smearsmear = smear * factor - if (value < 1 && (noise = this.minLimitNoise.getOctaveNoise(i))) { - min += noise.sample(xx, yy, zz, smearsmear, scaledY * factor) / factor - } - if (value > 0 && (noise = this.maxLimitNoise.getOctaveNoise(i))) { - max += noise.sample(xx, yy, zz, smearsmear, scaledY * factor) / factor - } - factor /= 2 - } + public range(): Interval { + return Interval.lerp(Interval.of(0, 1), this.minLimitNoise.range(), this.maxLimitNoise.range()) + } - return clampedLerp(min / 512, max / 512, value) / 128 + public sample(x: number, y: number, z: number) { + const xx = x * this.xzMultiplier + const yy = y * this.yMultiplier + const zz = z * this.xzMultiplier + let mainValue = this.mainNoise.get3D(xx / this.xzFactor, yy / this.yFactor, zz / this.xzFactor) + mainValue = clamp(mainValue + 0.5, 0, 1) + if (mainValue === 0) { + return this.minLimitNoise.get3D(xx, yy, zz) + } else if (mainValue === 1) { + return this.maxLimitNoise.get3D(xx, yy, zz) + } else { + return floatLerp(mainValue, this.minLimitNoise.get3D(xx, yy, zz), this.maxLimitNoise.get3D(xx, yy, zz)) + } } } diff --git a/src/math/noise/GradientNoise.ts b/src/math/noise/GradientNoise.ts new file mode 100644 index 00000000..19c71a35 --- /dev/null +++ b/src/math/noise/GradientNoise.ts @@ -0,0 +1,49 @@ +import type { Interval } from '../Interval.js' +import type { Random } from '../random/index.js' +import { longFloor } from '../Util.js' +import type { Noise } from './NoiseStack.js' + +export abstract class GradientNoise implements Noise { + public static readonly GRADIENT = [[1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0], [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1], [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1], [1, 1, 0], [0, -1, 1], [-1, 1, 0], [0, -1, -1]] + + public readonly perms: number[] + public readonly offsetX: number + public readonly offsetY: number + public readonly offsetZ: number + + constructor(random: Random, offsetScale = 256) { + this.offsetX = random.nextDouble() * offsetScale + this.offsetY = random.nextDouble() * offsetScale + this.offsetZ = random.nextDouble() * offsetScale + this.perms = Array(256) + + for (let i = 0; i < 256; i += 1) { + this.perms[i] = i + } + for (let i = 0; i < 256; i += 1) { + const offset = random.nextInt(256 - i) + const tmp = this.perms[i] + this.perms[i] = this.perms[i + offset] + this.perms[i + offset] = tmp + } + } + + abstract range(): Interval + + abstract get2D(x: number, y: number): number + + abstract get3D(x: number, y: number, z: number): number + + public permute(x: number) { + return this.perms[x & 0xFF] & 0xFF + } + + public static wrap(x: number) { + return x - longFloor(x / 3.3554432E7 + 0.5) * 3.3554432E7 + } + + public static gradDot(hash: number, x: number, y: number, z: number) { + const gradient = GradientNoise.GRADIENT[hash & 15] + return gradient[0] * x + gradient[1] * y + gradient[2] * z + } +} diff --git a/src/math/noise/ImprovedNoise.ts b/src/math/noise/ImprovedNoise.ts deleted file mode 100644 index e892e287..00000000 --- a/src/math/noise/ImprovedNoise.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { Random } from '../random/index.js' -import { intFloor, lerp3, smoothstep } from '../Util.js' -import { SimplexNoise } from './SimplexNoise.js' - -export class ImprovedNoise { - public readonly p: number[] - public readonly xo: number - public readonly yo: number - public readonly zo: number - - constructor(random: Random) { - this.xo = random.nextDouble() * 256 - this.yo = random.nextDouble() * 256 - this.zo = random.nextDouble() * 256 - this.p = Array(256) - - for (let i = 0; i < 256; i += 1) { - this.p[i] = i > 127 ? i - 256 : i - } - for (let i = 0; i < 256; i += 1) { - const j = random.nextInt(256 - i) - const b = this.p[i] - this.p[i] = this.p[i + j] - this.p[i + j] = b - } - } - - public sample(x: number, y: number, z: number, yScale = 0, yLimit = 0) { - const x2 = x + this.xo - const y2 = y + this.yo - const z2 = z + this.zo - const x3 = intFloor(x2) - const y3 = intFloor(y2) - const z3 = intFloor(z2) - const x4 = x2 - x3 - const y4 = y2 - y3 - const z4 = z2 - z3 - - let y6 = 0 - if (yScale !== 0) { - const t = yLimit >= 0 && yLimit < y4 ? yLimit : y4 - y6 = intFloor(t / yScale + 1e-7) * yScale - } - - return this.sampleAndLerp(x3, y3, z3, x4, y4 - y6, z4, y4) - } - - private sampleAndLerp(a: number, b: number, c: number, d: number, e: number, f: number, g: number) { - const h = this.P(a) - const i = this.P(a + 1) - const j = this.P(h + b) - const k = this.P(h + b + 1) - const l = this.P(i + b) - const m = this.P(i + b + 1) - - const n = SimplexNoise.gradDot(this.P(j + c), d, e, f) - const o = SimplexNoise.gradDot(this.P(l + c), d - 1.0, e, f) - const p = SimplexNoise.gradDot(this.P(k + c), d, e - 1.0, f) - const q = SimplexNoise.gradDot(this.P(m + c), d - 1.0, e - 1.0, f) - const r = SimplexNoise.gradDot(this.P(j + c + 1), d, e, f - 1.0) - const s = SimplexNoise.gradDot(this.P(l + c + 1), d - 1.0, e, f - 1.0) - const t = SimplexNoise.gradDot(this.P(k + c + 1), d, e - 1.0, f - 1.0) - const u = SimplexNoise.gradDot(this.P(m + c + 1), d - 1.0, e - 1.0, f - 1.0) - - const v = smoothstep(d) - const w = smoothstep(g) - const x = smoothstep(f) - - return lerp3(v, w, x, n, o, p, q, r, s, t, u) - } - - private P(i: number) { - return this.p[i & 0xFF] & 0xFF - } -} diff --git a/src/math/noise/NoiseStack.ts b/src/math/noise/NoiseStack.ts new file mode 100644 index 00000000..72a944de --- /dev/null +++ b/src/math/noise/NoiseStack.ts @@ -0,0 +1,47 @@ +import { Interval } from '../Interval.js' + +export interface Noise { + range(): Interval + get2D(x: number, y: number): number + get3D(x: number, y: number, z: number): number +} + +export type NoiseLayer = { + noise: Noise, + frequency: number, + amplitude: number, +} + +export class NoiseStack implements Noise { + private readonly _range: Interval + constructor( + public readonly layers: NoiseLayer[], + ) { + let range = Interval.ofExact(0) + for (const layer of layers) { + const layerRange = Interval.mul(layer.noise.range(), Interval.ofExact(layer.amplitude)) + range = Interval.add(range, layerRange) + } + this._range = range + } + + public range() { + return this._range + } + + public get2D(x: number, y: number) { + let value = 0 + for (const layer of this.layers) { + value += layer.amplitude * layer.noise.get2D(x * layer.frequency, y * layer.frequency) + } + return value + } + + public get3D(x: number, y: number, z: number) { + let value = 0 + for (const layer of this.layers) { + value += layer.amplitude * layer.noise.get3D(x * layer.frequency, y * layer.frequency, z * layer.frequency) + } + return value + } +} diff --git a/src/math/noise/NormalNoise.ts b/src/math/noise/NormalNoise.ts index a1ee7b53..c278ac14 100644 --- a/src/math/noise/NormalNoise.ts +++ b/src/math/noise/NormalNoise.ts @@ -1,54 +1,164 @@ import { Json } from '../../util/index.js' +import { Interval } from '../Interval.js' import type { Random } from '../random/index.js' +import { square } from '../Util.js' +import type { NoiseLayer } from './NoiseStack.js' +import { NoiseStack } from './NoiseStack.js' import { PerlinNoise } from './PerlinNoise.js' +export const NoiseNormalizations = ['disabled', 'enabled', 'legacy'] as const +export type NoiseNormalization = typeof NoiseNormalizations[number] + +export type OctaveInfo = { + index: number, + frequency: number, + amplitude: number, +} + export class NormalNoise { private static readonly INPUT_FACTOR = 1.0181268882175227 + private static readonly TARGET_DEVIATION = 0.3333333333333333 + + public readonly octaves: OctaveInfo[] + public readonly normalizationFactor: number + public readonly range: Interval + + constructor( + public readonly baseAmplitude: number, + public readonly baseOctave: number, + public readonly octaveCount: number, + public readonly normalize: NoiseNormalization, + public readonly amplitudeModifiers: number[], + ) { + this.octaves = NormalNoise.buildOctaves(baseAmplitude, baseOctave, octaveCount, normalize != 'disabled', amplitudeModifiers) + let targetAmplitude = this.octaves.reduce((acc, o) => acc + Math.abs(o.amplitude), 0) + let normalizationFactor = NormalNoise.computeNormalizationFactor(targetAmplitude, this.octaves) + if (normalize == 'legacy' && normalizationFactor != 0) { + const parityNormalizationFactor = NormalNoise.computeParityNormalizationFactor(baseAmplitude, octaveCount, amplitudeModifiers) + targetAmplitude *= parityNormalizationFactor / normalizationFactor + normalizationFactor = parityNormalizationFactor + } + this.normalizationFactor = normalizationFactor + this.range = Interval.ofSymmetric(targetAmplitude * NormalNoise.TARGET_DEVIATION * 6) + } - public readonly valueFactor: number - public readonly first: PerlinNoise - public readonly second: PerlinNoise - public readonly maxValue: number + public create(random: Random) { + const firstRandom = random.forkPositional() + const secondRandom = random.forkPositional() + const layers: NoiseLayer[] = [] + for (const octave of this.octaves) { + const octaveSeed = `octave_${octave.index}` + const firstNoise = new PerlinNoise(firstRandom.fromHashOf(octaveSeed)) + const secondNoise = new PerlinNoise(secondRandom.fromHashOf(octaveSeed)) + const valueFactor = this.normalizationFactor * octave.amplitude + layers.push({ noise: firstNoise, frequency: octave.frequency, amplitude: valueFactor }) + layers.push({ noise: secondNoise, frequency: octave.frequency * NormalNoise.INPUT_FACTOR, amplitude: valueFactor }) + } + return new NoiseStack(layers) + } - constructor(random: Random, { firstOctave, amplitudes }: NoiseParameters) { - this.first = new PerlinNoise(random, firstOctave, amplitudes) - this.second = new PerlinNoise(random, firstOctave, amplitudes) + public createForLegacyNetherBiome(random: Random) { + let amplitudes = this.amplitudeModifiers + if (amplitudes.length === 0) { + amplitudes = new Array(this.octaveCount).map(() => 1) + } + const first = NormalNoise.createLegacyNoiseLayers(random, this.baseOctave, amplitudes) + const second = NormalNoise.createLegacyNoiseLayers(random, this.baseOctave, amplitudes) + const valueFactor = this.normalizationFactor * this.baseAmplitude + return new NoiseStack([ + ...first.map(l => ({...l, amplitude: l.amplitude * valueFactor})), + ...second.map(l => ({...l, frequency: l.frequency * NormalNoise.INPUT_FACTOR, amplitudes: l.amplitude * valueFactor })), + ]) + } + + private static createLegacyNoiseLayers(random: Random, firstOctave: number, amplitudes: number[]): NoiseLayer[] { + const octaves = amplitudes.length + const zeroOctaveIndex = -firstOctave + const noiseLevels = new Array(octaves) + const zeroOctave = new PerlinNoise(random) + if (zeroOctaveIndex >= 0 && zeroOctaveIndex < octaves && amplitudes[zeroOctaveIndex] != 0) { + noiseLevels[zeroOctaveIndex] = zeroOctave + } + for (let i = zeroOctaveIndex - 1; i >= 0; i -= 1) { + if (i < octaves && amplitudes[i] != 0) { + noiseLevels[i] = new PerlinNoise(random) + } else { + random.consume(262) + } + } + let factor = Math.pow(2, -zeroOctaveIndex) + let valueFactor = Math.pow(2, octaves - 1) / (Math.pow(2, octaves) - 1) + const layers: NoiseLayer[] = [] + for (let i = 0; i < noiseLevels.length; i += 1) { + const noise = noiseLevels[i] + if (noise != undefined) { + layers.push({ noise, frequency: factor, amplitude: valueFactor }) + } + factor *= 2 + valueFactor /= 2 + } + return layers + } - let min = +Infinity + private static computeParityNormalizationFactor(baseAmplitude: number, octaveCount: number, amplitudeModifiers: number[]) { + let min = Infinity let max = -Infinity - for (let i = 0; i < amplitudes.length; i += 1) { - if (amplitudes[i] !== 0) { + for (let i = 0; i < octaveCount; i += 1) { + const modifier = amplitudeModifiers[i] ?? 1 + if (modifier != 0) { min = Math.min(min, i) max = Math.max(max, i) } } - - const expectedDeviation = 0.1 * (1 + 1 / (max - min + 1)) - this.valueFactor = (1/6) / expectedDeviation - this.maxValue = (this.first.maxValue + this.second.maxValue) * this.valueFactor + const parityExpectedDeviation = 0.1 * 1 + 1 / (max - min + 1) + return baseAmplitude * 0.5 * NormalNoise.TARGET_DEVIATION / parityExpectedDeviation } - sample(x: number, y: number, z: number) { - const x2 = x * NormalNoise.INPUT_FACTOR - const y2 = y * NormalNoise.INPUT_FACTOR - const z2 = z * NormalNoise.INPUT_FACTOR - return (this.first.sample(x, y, z) + this.second.sample(x2, y2, z2)) * this.valueFactor + private static computeNormalizationFactor(targetAmplitude: number, octaves: OctaveInfo[]) { + let variance = 0 + for (const octave of octaves) { + variance += square(0.2702247831245211 * Math.abs(octave.amplitude)) + } + const inputDeviation = Math.sqrt(variance) + if (inputDeviation === 0) { + return 0 + } + return (targetAmplitude * NormalNoise.TARGET_DEVIATION) / (inputDeviation * Math.sqrt(2)) } -} -export type NoiseParameters = { - firstOctave: number, - amplitudes: number[], -} -export namespace NoiseParameters { - export function create(firstOctave: number, amplitudes: number[]): NoiseParameters { - return { firstOctave, amplitudes } + private static buildOctaves(baseAmplitude: number, baseOctave: number,octaveCount: number, normalize: boolean, amplitudeModifiers: number[]) { + let frequency = Math.pow(2, baseOctave) + let amplitude = baseAmplitude + if (normalize) { + amplitude *= Math.pow(0.5, -(octaveCount - 1)) / (Math.pow(0.5, -octaveCount) - 1) + } + const octaves: OctaveInfo[] = [] + for (let i = 0; i < octaveCount; i+= 1) { + const modifier = amplitudeModifiers[i] ?? 1 + if (modifier != 0) { + octaves.push({ + index: baseOctave + i, + frequency, + amplitude: amplitude * modifier, + }) + } + frequency *= 2 + amplitude *= 0.5 + } + return octaves } - export function fromJson(obj: unknown): NoiseParameters { + + public static fromJson(obj: unknown): NormalNoise { const root = Json.readObject(obj) ?? {} - return { - firstOctave: Json.readInt(root.firstOctave) ?? 0, - amplitudes: Json.readArray(root.amplitudes, e => Json.readNumber(e) ?? 0) ?? [], - } + const normalize = typeof root.normalize === 'boolean' + ? (root.normalize ? 'enabled' : 'disabled') + : (root.normalize ? Json.readEnum(root.normalize, NoiseNormalizations) : 'enabled') + return new NormalNoise( + Json.readNumber(root.base_amplitude) ?? 1, + Json.readInt(root.base_octave) ?? 0, + Json.readInt(root.octave_count) ?? 1, + normalize, + Json.readArray(root.amplitude_modifiers, e => Json.readNumber(e) ?? 0) ?? [], + ) } } diff --git a/src/math/noise/PerlinNoise.ts b/src/math/noise/PerlinNoise.ts index 4b4b3130..52d24051 100644 --- a/src/math/noise/PerlinNoise.ts +++ b/src/math/noise/PerlinNoise.ts @@ -1,85 +1,55 @@ +import { Interval } from '../Interval.js' import type { Random } from '../random/index.js' -import { XoroshiroRandom } from '../random/index.js' -import { longFloor } from '../Util.js' -import { ImprovedNoise } from './ImprovedNoise.js' +import { intFloor, lerp3, smoothstep } from '../Util.js' +import { GradientNoise } from './GradientNoise.js' -export class PerlinNoise { - public readonly noiseLevels: ImprovedNoise[] - public readonly amplitudes: number[] - public readonly lowestFreqInputFactor: number - public readonly lowestFreqValueFactor: number - public readonly maxValue: number +export class PerlinNoise extends GradientNoise{ + public static readonly RANGE = Interval.ofSymmetric(2) + public static readonly STANDARD_DEVIATION = 0.2702247831245211 - constructor(random: Random, firstOctave: number, amplitudes: number[], forceLegacy: boolean = false) { - if (random instanceof XoroshiroRandom && !forceLegacy){ - const forkedRandom = random.forkPositional() - - this.noiseLevels = Array(amplitudes.length) - for(let i = 0; i < amplitudes.length; i++) { - if (amplitudes[i] !== 0.0) { - const octave = firstOctave + i - this.noiseLevels[i] = new ImprovedNoise(forkedRandom.fromHashOf('octave_' + octave)) - } - } - } else { - if (1 - firstOctave < amplitudes.length) { - throw new Error('Positive octaves are not allowed when using LegacyRandom') - } - - this.noiseLevels = Array(amplitudes.length) - for (let i = -firstOctave; i >= 0; i -= 1) { - if (i < amplitudes.length && amplitudes[i] !== 0) { - this.noiseLevels[i] = new ImprovedNoise(random) - } else { - random.consume(262) - } - } - } - - this.amplitudes = amplitudes - this.lowestFreqInputFactor = Math.pow(2, firstOctave) - this.lowestFreqValueFactor = Math.pow(2, (amplitudes.length - 1)) / (Math.pow(2, amplitudes.length) - 1) - this.maxValue = this.edgeValue(2) + constructor(random: Random) { + super(random) } - public sample(x: number, y: number, z: number, yScale = 0, yLimit = 0, fixY = false) { - let value = 0 - let inputF = this.lowestFreqInputFactor - let valueF = this.lowestFreqValueFactor - for (let i = 0; i < this.noiseLevels.length; i += 1) { - const noise = this.noiseLevels[i] - if (noise) { - value += this.amplitudes[i] * valueF * noise.sample( - PerlinNoise.wrap(x * inputF), - fixY ? -noise.yo : PerlinNoise.wrap(y * inputF), - PerlinNoise.wrap(z * inputF), - yScale * inputF, - yLimit * inputF, - ) - } - inputF *= 2 - valueF /= 2 - } - return value + public range(): Interval { + return PerlinNoise.RANGE } - public getOctaveNoise(i: number): ImprovedNoise | undefined { - return this.noiseLevels[this.noiseLevels.length - 1 - i] + public get2D(x: number, y: number): number { + return this.get3D(GradientNoise.wrap(x), 0, GradientNoise.wrap(y)) } - public edgeValue(x: number) { - let value = 0 - let valueF = this.lowestFreqValueFactor - for (let i = 0; i < this.noiseLevels.length; i += 1) { - if (this.noiseLevels[i]) { - value += this.amplitudes[i] * x * valueF - } - valueF /= 2 - } - return value + public get3D(x: number, y: number, z: number): number { + const x1 = GradientNoise.wrap(x) + this.offsetX + const y1 = GradientNoise.wrap(y) + this.offsetY + const z1 = GradientNoise.wrap(z) + this.offsetZ + const floorX = intFloor(x1) + const floorY = intFloor(y1) + const floorZ = intFloor(z1) + const relativeX = x1 - floorX + const relativeY = y1 - floorY + const relativeZ = z1 - floorZ + return this.sampleAndLerp(floorX, floorY, floorZ, relativeX, relativeY, relativeZ, relativeY) } - public static wrap(value: number) { - return value - longFloor(value / 3.3554432E7 + 0.5) * 3.3554432E7 + public sampleAndLerp(x: number, y: number, z: number, relativeX: number, relativeY: number, relativeZ: number, originalRelativeY: number) { + const x0 = this.permute(x) + const x1 = this.permute(x + 1) + const xy00 = this.permute(x0 + y) + const xy01 = this.permute(x0 + y + 1) + const xy10 = this.permute(x1 + y) + const xy11 = this.permute(x1 + y + 1) + const d000 = GradientNoise.gradDot(this.permute(xy00 + z), relativeX, relativeY, relativeZ) + const d100 = GradientNoise.gradDot(this.permute(xy10 + z), relativeX - 1.0, relativeY, relativeZ) + const d010 = GradientNoise.gradDot(this.permute(xy01 + z), relativeX, relativeY - 1.0, relativeZ) + const d110 = GradientNoise.gradDot(this.permute(xy11 + z), relativeX - 1.0, relativeY - 1.0, relativeZ) + const d001 = GradientNoise.gradDot(this.permute(xy00 + z + 1), relativeX, relativeY, relativeZ - 1.0) + const d101 = GradientNoise.gradDot(this.permute(xy10 + z + 1), relativeX - 1.0, relativeY, relativeZ - 1.0) + const d011 = GradientNoise.gradDot(this.permute(xy01 + z + 1), relativeX, relativeY - 1.0, relativeZ - 1.0) + const d111 = GradientNoise.gradDot(this.permute(xy11 + z + 1), relativeX - 1.0, relativeY - 1.0, relativeZ - 1.0) + const xAlpha = smoothstep(relativeX) + const yAlpha = smoothstep(originalRelativeY) + const zAlpha = smoothstep(relativeZ) + return lerp3(xAlpha, yAlpha, zAlpha, d000, d100, d010, d110, d001, d101, d011, d111) } } diff --git a/src/math/noise/PerlinSimplexNoise.ts b/src/math/noise/PerlinSimplexNoise.ts deleted file mode 100644 index b4fc18c2..00000000 --- a/src/math/noise/PerlinSimplexNoise.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { Random } from '../random/index.js' -import { SimplexNoise } from './SimplexNoise.js' - -export class PerlinSimplexNoise { - public readonly noiseLevels: SimplexNoise[] - public readonly highestFreqInputFactor: number - public readonly highestFreqValueFactor: number - - constructor(random: Random, octaves: number[]) { - const lastOctave = octaves[octaves.length - 1] - const negFirstOctave = -octaves[0] - const range = negFirstOctave + lastOctave + 1 - const octavesSet = new Set(octaves) - - const noise = new SimplexNoise(random) - this.noiseLevels = Array(range) - if (lastOctave >= 0 && lastOctave < range && octavesSet.has(0)) { - this.noiseLevels[lastOctave] = noise - } - - for (let i = lastOctave + 1; i < range; i += 1) { - if (i >= 0 && octavesSet.has(lastOctave - i)) { - this.noiseLevels[i] = new SimplexNoise(random) - } else { - random.consume(262) - } - } - - if (lastOctave > 0) { - throw new Error('Positive octaves are not allowed') - } - - this.highestFreqInputFactor = Math.pow(2, lastOctave) - this.highestFreqValueFactor = 1 / (Math.pow(2, range) - 1) - } - - public sample(x: number, y: number, useOffsets: boolean) { - let value = 0 - let inputF = this.highestFreqInputFactor - let valueF = this.highestFreqValueFactor - for (let i = 0; i < this.noiseLevels.length; i += 1) { - const noise = this.noiseLevels[i] - if (noise) { - value += valueF * noise.sample2D( - x * inputF + (useOffsets ? noise.xo : 0), - y * inputF + (useOffsets ? noise.yo : 0), - ) - } - inputF /= 2 - valueF *= 2 - } - return value - } -} diff --git a/src/math/noise/SimplexNoise.ts b/src/math/noise/SimplexNoise.ts index b996d414..ec99770b 100644 --- a/src/math/noise/SimplexNoise.ts +++ b/src/math/noise/SimplexNoise.ts @@ -1,166 +1,150 @@ +import { Interval } from '../Interval.js' import type { Random } from '../random/index.js' import { intFloor } from '../Util.js' +import { GradientNoise } from './GradientNoise.js' -export class SimplexNoise { - private static readonly GRADIENT = [[1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0], [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1], [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1], [1, 1, 0], [0, -1, 1], [-1, 1, 0], [0, -1, -1]] +export class SimplexNoise extends GradientNoise { + public static readonly RANGE = Interval.ofSymmetric(2) + public static readonly STANDARD_DEVIATION = 0.42544 private static readonly F2 = 0.5 * (Math.sqrt(3.0) - 1.0) private static readonly G2 = (3.0 - Math.sqrt(3.0)) / 6.0 - public readonly p: number[] - public readonly xo: number - public readonly yo: number - public readonly zo: number - - constructor(random: Random) { - this.xo = random.nextDouble() * 256 - this.yo = random.nextDouble() * 256 - this.zo = random.nextDouble() * 256 - this.p = Array(256) + constructor(random: Random, discardNoiseOffset?: boolean) { + super(random, discardNoiseOffset ? 0 : undefined) + } - for (let i = 0; i < 256; i += 1) { - this.p[i] = i - } - for (let i = 0; i < 256; i += 1) { - const j = random.nextInt(256 - i) - const b = this.p[i] - this.p[i] = this.p[i + j] - this.p[i + j] = b - } + public range(): Interval { + return SimplexNoise.RANGE } - public sample2D(d: number, d2: number) { - const d6 = (d + d2) * SimplexNoise.F2 - const n4 = intFloor(d + d6) - const n3 = intFloor(d2 + d6) - const d3 = (n4 + n3) * SimplexNoise.G2 - const d7 = n4 - d3 - const d8 = d - d7 - let a - let b - const d4 = d2 - (n3 - d3) - if (d8 > d4) { - a = 1 - b = 0 + public get2D(x: number, y: number) { + const xin = x + this.offsetX + const yin = y + this.offsetY + const s = (xin + yin) * SimplexNoise.F2 + const i = intFloor(xin + s) + const j = intFloor(yin + s) + const t = (i + j) * SimplexNoise.G2 + const x0 = xin - (i - t) + const y0 = yin - (j - t) + let i1 + let j1 + if (x0 > y0) { + i1 = 1 + j1 = 0 } else { - a = 0 - b = 1 + i1 = 0 + j1 = 1 } - const d9 = d8 - a + SimplexNoise.G2 - const d10 = d4 - b + SimplexNoise.G2 - const d11 = d8 - 1.0 + 2.0 * SimplexNoise.G2 - const d12 = d4 - 1.0 + 2.0 * SimplexNoise.G2 - const n5 = n4 & 0xFF - const n6 = n3 & 0xFF - const n7 = this.P(n5 + this.P(n6)) % 12 - const n8 = this.P(n5 + a + this.P(n6 + b)) % 12 - const n9 = this.P(n5 + 1 + this.P(n6 + 1)) % 12 - const d13 = this.getCornerNoise3D(n7, d8, d4, 0.0, 0.5) - const d14 = this.getCornerNoise3D(n8, d9, d10, 0.0, 0.5) - const d15 = this.getCornerNoise3D(n9, d11, d12, 0.0, 0.5) - return 70.0 * (d13 + d14 + d15) + const x1 = x0 - i1 + SimplexNoise.G2 + const y1 = y0 - j1 + SimplexNoise.G2 + const x2 = x0 - 1.0 + 2.0 * SimplexNoise.G2 + const y2 = y0 - 1.0 + 2.0 * SimplexNoise.G2 + const ii = i & 0xFF + const jj = j & 0xFF + const gi0 = this.permute(ii + this.permute(jj)) % 12 + const gi1 = this.permute(ii + i1 + this.permute(jj + j1)) % 12 + const gi2 = this.permute(ii + 1 + this.permute(jj + 1)) % 12 + const n0 = this.getCornerNoise3D(gi0, x0, y0, 0.0, 0.5) + const n1 = this.getCornerNoise3D(gi1, x1, y1, 0.0, 0.5) + const n2 = this.getCornerNoise3D(gi2, x2, y2, 0.0, 0.5) + return 70.0 * (n0 + n1 + n2) } - public sample(x: number, y: number, z: number) { - const d5 = (x + y + z) * 0.3333333333333333 - const x2 = intFloor(x + d5) - const y2 = intFloor(y + d5) - const z2 = intFloor(z + d5) - const d7 = (x2 + y2 + z2) * 0.16666666666666666 - const x3 = x - (x2 - d7) - const y3 = y - (y2 - d7) - const z3 = z - (z2 - d7) - let a - let b - let c - let d - let e - let f - if (x3 >= y3) { - if (y3 >= z3) { - a = 1 - b = 0 - c = 0 - d = 1 - e = 1 - f = 0 - } else if (x3 >= z3) { - a = 1 - b = 0 - c = 0 - d = 1 - e = 0 - f = 1 + public get3D(x: number, y: number, z: number) { + const xin = x + this.offsetX + const yin = y + this.offsetY + const zin = z + this.offsetZ + const s = (xin + yin + zin) * 0.3333333333333333 + const i = intFloor(xin + s) + const j = intFloor(yin + s) + const k = intFloor(zin + s) + const t = (i + j + k) * 0.16666666666666666 + const x0 = xin - (i - t) + const y0 = yin - (j - t) + const z0 = zin - (k - t) + let i1 + let j1 + let k1 + let i2 + let j2 + let k2 + if (x0 >= y0) { + if (y0 >= z0) { + i1 = 1 + j1 = 0 + k1 = 0 + i2 = 1 + j2 = 1 + k2 = 0 + } else if (x0 >= z0) { + i1 = 1 + j1 = 0 + k1 = 0 + i2 = 1 + j2 = 0 + k2 = 1 } else { - a = 0 - b = 0 - c = 1 - d = 1 - e = 0 - f = 1 + i1 = 0 + j1 = 0 + k1 = 1 + i2 = 1 + j2 = 0 + k2 = 1 } - } else if (y3 < z3) { - a = 0 - b = 0 - c = 1 - d = 0 - e = 1 - f = 1 - } else if (x3 < z3) { - a = 0 - b = 1 - c = 0 - d = 0 - e = 1 - f = 1 + } else if (y0 < z0) { + i1 = 0 + j1 = 0 + k1 = 1 + i2 = 0 + j2 = 1 + k2 = 1 + } else if (x0 < z0) { + i1 = 0 + j1 = 1 + k1 = 0 + i2 = 0 + j2 = 1 + k2 = 1 } else { - a = 0 - b = 1 - c = 0 - d = 1 - e = 1 - f = 0 + i1 = 0 + j1 = 1 + k1 = 0 + i2 = 1 + j2 = 1 + k2 = 0 } - const x4 = x3 - a + 0.16666666666666666 - const y4 = y3 - b + 0.16666666666666666 - const z4 = z3 - c + 0.16666666666666666 - const x5 = x3 - d + 0.3333333333333333 - const y5 = y3 - e + 0.3333333333333333 - const z5 = z3 - f + 0.3333333333333333 - const x6 = x3 - 0.5 - const y6 = y3 - 0.5 - const z6 = z3 - 0.5 - const x7 = x2 & 0xFF - const y7 = y2 & 0xFF - const z7 = z2 & 0xFF - const g = this.P(x7 + this.P(y7 + this.P(z7))) % 12 - const h = this.P(x7 + a + this.P(y7 + b + this.P(z7 + c))) % 12 - const i = this.P(x7 + d + this.P(y7 + e + this.P(z7 + f))) % 12 - const j = this.P(x7 + 1 + this.P(y7 + 1 + this.P(z7 + 1))) % 12 - const k = this.getCornerNoise3D(g, x3, y3, z3, 0.6) - const l = this.getCornerNoise3D(h, x4, y4, z4, 0.6) - const m = this.getCornerNoise3D(i, x5, y5, z5, 0.6) - const n = this.getCornerNoise3D(j, x6, y6, z6, 0.6) - return 32.0 * (k + l + m + n) + const x1 = x0 - i1 + 0.16666666666666666 + const y1 = y0 - j1 + 0.16666666666666666 + const z1 = z0 - k1 + 0.16666666666666666 + const x2 = x0 - i2 + 0.3333333333333333 + const y2 = y0 - j2 + 0.3333333333333333 + const z2 = z0 - k2 + 0.3333333333333333 + const x3 = x0 - 0.5 + const y3 = y0 - 0.5 + const z3 = z0 - 0.5 + const ii = i & 0xFF + const jj = j & 0xFF + const kk = k & 0xFF + const gi0 = this.permute(ii + this.permute(jj + this.permute(kk))) % 12 + const gi1 = this.permute(ii + i1 + this.permute(jj + j1 + this.permute(kk + k1))) % 12 + const gi2 = this.permute(ii + i2 + this.permute(jj + j2 + this.permute(kk + k2))) % 12 + const gi3 = this.permute(ii + 1 + this.permute(jj + 1 + this.permute(kk + 1))) % 12 + const n0 = this.getCornerNoise3D(gi0, x0, y0, z0, 0.6) + const n1 = this.getCornerNoise3D(gi1, x1, y1, z1, 0.6) + const n2 = this.getCornerNoise3D(gi2, x2, y2, z2, 0.6) + const n3 = this.getCornerNoise3D(gi3, x3, y3, z3, 0.6) + return 32.0 * (n0 + n1 + n2 + n3) } - private P(i: number) { - return this.p[i & 0xFF] - } - - private getCornerNoise3D(i: number, a: number, b: number, c: number, d: number) { - let f - let e = d - a * a - b * b - c * c - if (e < 0.0) { - f = 0.0 + private getCornerNoise3D(index: number, x: number, y: number, z: number, base: number) { + let t0 = base - x * x - y * y - z * z + let n0 + if (t0 < 0.0) { + n0 = 0.0 } else { - e *= e - f = e * e * SimplexNoise.gradDot(i, a, b, c) + t0 *= t0 + n0 = t0 * t0 * SimplexNoise.gradDot(index, x, y, z) } - return f - } - - public static gradDot(a: number, b: number, c: number, d: number) { - const grad = SimplexNoise.GRADIENT[a & 15] - return grad[0] * b + grad[1] * c + grad[2] * d + return n0 } } diff --git a/src/math/noise/SmearedPerlinNoise.ts b/src/math/noise/SmearedPerlinNoise.ts new file mode 100644 index 00000000..32808ddf --- /dev/null +++ b/src/math/noise/SmearedPerlinNoise.ts @@ -0,0 +1,42 @@ +import { Interval } from '../Interval.js' +import type { Random } from '../random/index.js' +import { intFloor } from '../Util.js' +import { GradientNoise } from './GradientNoise.js' +import { PerlinNoise } from './PerlinNoise.js' + +export class SmearedPerlinNoise extends PerlinNoise { + private static readonly SHIFT_UP_EPSILON = 1e-7 + + constructor( + random: Random, + private readonly fudgeYScale: number, + ) { + super(random) + } + + public range(): Interval { + return Interval.ofSymmetric(Math.abs(this.fudgeYScale) + 2) + } + + public get3D(x: number, y: number, z: number): number { + const x1 = GradientNoise.wrap(x) + this.offsetX + const y1 = GradientNoise.wrap(y) + this.offsetY + const z1 = GradientNoise.wrap(z) + this.offsetZ + const floorX = intFloor(x1) + const floorY = intFloor(y1) + const floorZ = intFloor(z1) + const relativeX = x1 - floorX + const relativeY = y1 - floorY + const relativeZ = z1 - floorZ + const fudgedRelativeY = relativeY - this.computeFudgeY(y, relativeY) + return this.sampleAndLerp(floorX, floorY, floorZ, relativeX, fudgedRelativeY, relativeZ, relativeY) + } + + private computeFudgeY(originalY: number, relativeY: number): number { + let fudgeLImit = relativeY + if (originalY >= 0 && originalY < relativeY) { + fudgeLImit = originalY + } + return Math.floor(fudgeLImit / this.fudgeYScale + SmearedPerlinNoise.SHIFT_UP_EPSILON) * this.fudgeYScale + } +} diff --git a/src/math/noise/index.ts b/src/math/noise/index.ts index 181cb358..01cdf7e7 100644 --- a/src/math/noise/index.ts +++ b/src/math/noise/index.ts @@ -1,6 +1,8 @@ export * from './BlendedNoise.js' -export * from './ImprovedNoise.js' +export * from './GradientNoise.js' +export * from './NoiseStack.js' export * from './NormalNoise.js' export * from './PerlinNoise.js' -export * from './PerlinSimplexNoise.js' export * from './SimplexNoise.js' +export * from './SmearedPerlinNoise.js' + diff --git a/src/math/random/LegacyRandom.ts b/src/math/random/LegacyRandom.ts index f87097d2..095dd2d7 100644 --- a/src/math/random/LegacyRandom.ts +++ b/src/math/random/LegacyRandom.ts @@ -1,5 +1,4 @@ -import md5 from 'md5' -import { getSeed, longfromBytes } from '../Util.js' +import { getSeed, hashCode } from '../Util.js' import type { PositionalRandom, Random } from './Random.js' export class LegacyRandom implements Random { @@ -96,9 +95,8 @@ export class LegacyPositionalRandom implements PositionalRandom { } public fromHashOf(name: string) { - const hash = md5(name, { asBytes: true }) - const seed = longfromBytes(hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]) - return new LegacyRandom(seed ^ this.seed) + const positionalSeed = hashCode(name) + return new LegacyRandom(BigInt(positionalSeed) ^ this.seed) } seedKey(): [bigint, bigint] { diff --git a/src/worldgen/DensityFunction.ts b/src/worldgen/DensityFunction.ts index b763ff0c..7228b671 100644 --- a/src/worldgen/DensityFunction.ts +++ b/src/worldgen/DensityFunction.ts @@ -1,6 +1,6 @@ import { Holder, Identifier } from '../core/index.js' -import type { BlendedNoise, NormalNoise } from '../math/index.js' -import { clamp, clampedLerp, clampedMap, CubicSpline, floatLerp, Interval, lazyLerp3, LegacyRandom, NoiseParameters, SimplexNoise, Vector } from '../math/index.js' +import type { BlendedNoise, Noise } from '../math/index.js' +import { clamp, clampedLerp, clampedMap, CubicSpline, floatLerp, Interval, lazyLerp3, LegacyRandom, NormalNoise, SimplexNoise, Vector } from '../math/index.js' import { computeIfAbsent, Json } from '../util/index.js' import { WorldgenRegistries } from './WorldgenRegistries.js' @@ -54,7 +54,7 @@ export namespace DensityFunction { } } - const noiseParser = Holder.parser(WorldgenRegistries.NOISE, NoiseParameters.fromJson) + const noiseParser = Holder.parser(WorldgenRegistries.NOISE, NormalNoise.fromJson) const vectorParser = (obj: unknown): Vector => { const arr = Json.readArray(obj, v => Json.readNumber(v) ?? 0) ?? [] return new Vector(arr[0] ?? 0, arr[1] ?? 0, arr[2] ?? 0) @@ -94,7 +94,7 @@ export namespace DensityFunction { case 'cache_all_in_cell': return new CacheAllInCell(inputParser(root.input ?? root.argument)) case 'noise': case 'shifted_noise': - return new Noise( + return new NoiseFunction( noiseParser(root.noise), Json.readNumber(root.xz_scale) ?? 1, Json.readNumber(root.y_scale) ?? 1, @@ -275,7 +275,7 @@ export namespace DensityFunction { if (!this.blendedNoise) { return Interval.INFINITE } - return Interval.ofSymmetric(this.blendedNoise.maxValue) + return this.blendedNoise.range() } } @@ -413,15 +413,15 @@ export namespace DensityFunction { } } - export class Noise extends DensityFunction { + export class NoiseFunction extends DensityFunction { constructor( - public readonly noiseData: Holder, + public readonly noise: Holder, public readonly xzScale: number, public readonly yScale: number, public readonly shiftX: DensityFunction, public readonly shiftY: DensityFunction, public readonly shiftZ: DensityFunction, - public readonly noise?: NormalNoise, + public readonly noiseSampler?: Noise, ) { super() } @@ -429,16 +429,16 @@ export namespace DensityFunction { const x = context.x * this.xzScale + this.shiftX.compute(context) const y = context.y * this.yScale + this.shiftY.compute(context) const z = context.z * this.xzScale + this.shiftZ.compute(context) - return this.noise?.sample(x, y, z) ?? 0 + return this.noiseSampler?.get3D(x, y, z) ?? 0 } public mapChildren(visitor: Visitor): DensityFunction { - return new Noise(this.noiseData, this.xzScale, this.yScale, visitor.apply(this.shiftX), visitor.apply(this.shiftY), visitor.apply(this.shiftZ)) + return new NoiseFunction(this.noise, this.xzScale, this.yScale, visitor.apply(this.shiftX), visitor.apply(this.shiftY), visitor.apply(this.shiftZ), this.noiseSampler) } public range(): Interval { - if (!this.noise) { + if (!this.noiseSampler) { return Interval.INFINITE } - return Interval.ofSymmetric(this.noise.maxValue) + return this.noiseSampler.range() } } @@ -461,7 +461,7 @@ export namespace DensityFunction { for (let j = -12; j <= 12; j += 1) { const x2 = x0 + i const z2 = z0 + j - if (x2 * x2 + z2 * z2 <= 4096 || this.islandNoise.sample2D(x2, z2) >= -0.9) { + if (x2 * x2 + z2 * z2 <= 4096 || this.islandNoise.get2D(x2, z2) >= -0.9) { continue } const f1 = (Math.abs(x2) * 3439 + Math.abs(z2) * 147) % 13 + 9 @@ -526,21 +526,21 @@ export namespace DensityFunction { constructor( input: DensityFunction, public readonly rarityValueMapper: typeof RarityValueMapper[number], - public readonly noiseData: Holder, - public readonly noise?: NormalNoise, + public readonly noise: Holder, + public readonly noiseSampler?: Noise, ) { super(input) this.mapper = WeirdScaledSampler.ValueMapper[this.rarityValueMapper] } public transform(context: Context, density: number) { - if (!this.noise) { + if (!this.noiseSampler) { return 0 } const rarity = this.mapper(density) - return rarity * Math.abs(this.noise.sample(context.x / rarity, context.y / rarity, context.z / rarity)) + return rarity * Math.abs(this.noiseSampler.get3D(context.x / rarity, context.y / rarity, context.z / rarity)) } public mapChildren(visitor: Visitor) { - return new WeirdScaledSampler(visitor.apply(this.input), this.rarityValueMapper, this.noiseData, this.noise) + return new WeirdScaledSampler(visitor.apply(this.input), this.rarityValueMapper, this.noise, this.noiseSampler) } public range(): Interval { return Interval.of(0, this.rarityValueMapper === 'type_1' ? 2 : 3) @@ -597,65 +597,65 @@ export namespace DensityFunction { export abstract class ShiftNoise extends DensityFunction { constructor( - public readonly noiseData: Holder, - public readonly offsetNoise?: NormalNoise, + public readonly noise: Holder, + public readonly noiseSampler?: Noise, ) { super() } public compute(context: Context) { - return (this.offsetNoise?.sample(context.x * 0.25, context.y * 0.25, context.z * 0.25) ?? 0) * 4 + return (this.noiseSampler?.get3D(context.x * 0.25, context.y * 0.25, context.z * 0.25) ?? 0) * 4 } public mapChildren(visitor: Visitor): DensityFunction { return this } public range(): Interval { - if (!this.offsetNoise) { + if (!this.noiseSampler) { return Interval.INFINITE } - return Interval.mul(Interval.ofSymmetric(this.offsetNoise.maxValue), Interval.ofExact(4)) + return Interval.mul(this.noiseSampler.range(), Interval.ofExact(4)) } - public abstract withNewNoise(noise: NormalNoise): ShiftNoise + public abstract withNewNoise(noiseSampler: Noise): ShiftNoise } export class ShiftA extends ShiftNoise { constructor( - noiseData: Holder, - offsetNoise?: NormalNoise, + noise: Holder, + noiseSampler?: Noise, ) { - super(noiseData, offsetNoise) + super(noise, noiseSampler) } public compute(context: Context) { return super.compute(DensityFunction.context(context.x, 0, context.z)) } - public withNewNoise(newNoise: NormalNoise) { - return new ShiftA(this.noiseData, newNoise) + public withNewNoise(noiseSampler: Noise) { + return new ShiftA(this.noise, noiseSampler) } } export class ShiftB extends ShiftNoise { constructor( - noiseData: Holder, - offsetNoise?: NormalNoise, + noise: Holder, + noiseSampler?: Noise, ) { - super(noiseData, offsetNoise) + super(noise, noiseSampler) } public compute(context: Context) { return super.compute(DensityFunction.context(context.z, context.x, 0)) } - public withNewNoise(newNoise: NormalNoise) { - return new ShiftB(this.noiseData, newNoise) + public withNewNoise(noiseSampler: Noise) { + return new ShiftB(this.noise, noiseSampler) } } export class Shift extends ShiftNoise { constructor( - noiseData: Holder, - offsetNoise?: NormalNoise, + noise: Holder, + noiseSampler?: Noise, ) { - super(noiseData, offsetNoise) + super(noise, noiseSampler) } - public withNewNoise(newNoise: NormalNoise) { - return new Shift(this.noiseData, newNoise) + public withNewNoise(noiseSampler: Noise) { + return new Shift(this.noise, noiseSampler) } } @@ -781,14 +781,13 @@ export namespace DensityFunction { const BinaryType = ['add', 'sub', 'mul', 'div', 'min', 'max'] as const export class Binary extends DensityFunction { - private readonly rightRange: Interval + private rightRange: Interval = Interval.INFINITE constructor( public readonly type: typeof BinaryType[number], public readonly left: DensityFunction, public readonly right: DensityFunction, ) { super() - this.rightRange = right.range() } public compute(context: Context): number { const a = this.left.compute(context) @@ -825,6 +824,7 @@ export namespace DensityFunction { return new MulOrAdd(this.type, this.left, this.right.value) } } + this.rightRange = this.right.range() return this } } diff --git a/src/worldgen/NoiseRouter.ts b/src/worldgen/NoiseRouter.ts index 242ddb9a..ecbf359e 100644 --- a/src/worldgen/NoiseRouter.ts +++ b/src/worldgen/NoiseRouter.ts @@ -1,6 +1,4 @@ import { Holder } from '../core/index.js' -import type { NoiseParameters, PositionalRandom } from '../math/index.js' -import { NormalNoise } from '../math/index.js' import { Json } from '../util/index.js' import { DensityFunction } from './DensityFunction.js' import { WorldgenRegistries } from './WorldgenRegistries.js' @@ -87,22 +85,4 @@ export namespace NoiseRouter { veinGap: router.veinGap.mapAll(visitor), } } - - const noiseCache = new Map() - - export function instantiate(random: PositionalRandom, noise: Holder): NormalNoise { - const key = noise.key()?.toString() - if (!key) { - throw new Error('Cannot instantiate noise from direct holder') - } - - const randomKey = random.seedKey() - const cached = noiseCache.get(key) - if (cached && cached[0] === randomKey[0] && cached[1] === randomKey[1]) { - return cached[2] - } - const result = new NormalNoise(random.fromHashOf(key), noise.value()) - noiseCache.set(key, [randomKey[0], randomKey[1], result]) - return result - } } diff --git a/src/worldgen/RandomState.ts b/src/worldgen/RandomState.ts index 5bf371d4..30949b29 100644 --- a/src/worldgen/RandomState.ts +++ b/src/worldgen/RandomState.ts @@ -1,14 +1,15 @@ import type { Holder } from '../core/index.js' import { Registry } from '../core/index.js' -import { BlendedNoise, computeIfAbsent, DensityFunction, Identifier, LegacyRandom, NoiseParameters, NoiseSettings, NormalNoise, XoroshiroRandom } from '../index.js' -import type { PositionalRandom, Random } from '../math/index.js' +import type { NormalNoise } from '../index.js' +import { BlendedNoise, computeIfAbsent, DensityFunction, Identifier, LegacyRandom, NoiseSettings, XoroshiroRandom } from '../index.js' +import type { Noise, PositionalRandom, Random } from '../math/index.js' import { Climate } from './biome/index.js' import type { NoiseGeneratorSettings } from './NoiseGeneratorSettings.js' import { NoiseRouter } from './NoiseRouter.js' import { SurfaceSystem } from './SurfaceSystem.js' export class RandomState { - private readonly noiseCache: Map + private readonly noiseInstances: Map private readonly randomCache: Map public readonly random: PositionalRandom @@ -22,33 +23,30 @@ export class RandomState { settings: NoiseGeneratorSettings, public readonly seed: bigint, ) { - this.noiseCache = new Map() + this.noiseInstances = new Map() this.randomCache = new Map() this.random = (settings.legacyRandomSource ? new LegacyRandom(seed) : XoroshiroRandom.create(seed)).forkPositional() this.aquiferRandom = this.random.fromHashOf(Identifier.create('aquifer').toString()).forkPositional() this.oreRandom = this.random.fromHashOf(Identifier.create('ore').toString()).forkPositional() - this.surfaceSystem = new SurfaceSystem(settings.surfaceRule, settings.defaultBlock, seed) + this.surfaceSystem = new SurfaceSystem(this, settings.surfaceRule, settings.defaultBlock, seed) this.router = NoiseRouter.mapAll(settings.noiseRouter, this.createVisitor(settings.noise, settings.legacyRandomSource)) this.sampler = Climate.Sampler.fromRouter(this.router) } public createVisitor(noiseSettings: NoiseSettings, legacyRandom: boolean) { const mapped = new Map() - const getNoise = (noise: Holder): NormalNoise => { + const getNoiseSampler = (noise: Holder): Noise => { const key = noise.key() if (key === undefined) { throw new Error('Cannot create noise without key') } if (legacyRandom) { if (key.equals(Identifier.create('temperature'))) { - return new NormalNoise(new LegacyRandom(this.seed + BigInt(0)), NoiseParameters.create(-7, [1, 1])) + return noise.value().createForLegacyNetherBiome(new LegacyRandom(this.seed + BigInt(0))) } if (key.equals(Identifier.create('vegetation'))) { - return new NormalNoise(new LegacyRandom(this.seed + BigInt(1)), NoiseParameters.create(-7, [1, 1])) - } - if (key.equals(Identifier.create('offset'))) { - return new NormalNoise(this.random.fromHashOf('offset'), NoiseParameters.create(0, [0])) + return noise.value().createForLegacyNetherBiome(new LegacyRandom(this.seed + BigInt(1))) } } return this.getOrCreateNoise(key) @@ -70,14 +68,14 @@ export class RandomState { if (fn instanceof DensityFunction.Interpolated) { return fn.withCellSize(NoiseSettings.cellWidth(noiseSettings), NoiseSettings.cellHeight(noiseSettings)) } - if (fn instanceof DensityFunction.Noise) { - return new DensityFunction.Noise(fn.noiseData, fn.xzScale, fn.yScale, fn.shiftX, fn.shiftY, fn.shiftZ, getNoise(fn.noiseData)) + if (fn instanceof DensityFunction.NoiseFunction) { + return new DensityFunction.NoiseFunction(fn.noise, fn.xzScale, fn.yScale, fn.shiftX, fn.shiftY, fn.shiftZ, getNoiseSampler(fn.noise)) } if (fn instanceof DensityFunction.ShiftNoise) { - return fn.withNewNoise(getNoise(fn.noiseData)) + return fn.withNewNoise(getNoiseSampler(fn.noise)) } if (fn instanceof DensityFunction.WeirdScaledSampler) { - return new DensityFunction.WeirdScaledSampler(fn.input, fn.rarityValueMapper, fn.noiseData, getNoise(fn.noiseData)) + return new DensityFunction.WeirdScaledSampler(fn.input, fn.rarityValueMapper, fn.noise, getNoiseSampler(fn.noise)) } if (fn instanceof DensityFunction.OldBlendedNoise) { const oldBlendedNoiseRandom: Random = legacyRandom ? new LegacyRandom(this.seed + BigInt(0)) : this.random.fromHashOf(Identifier.create('terrain').toString()) @@ -95,10 +93,10 @@ export class RandomState { return visitor } - public getOrCreateNoise(id: Identifier) { - const noises = Registry.REGISTRY.getOrThrow(Identifier.create('worldgen/noise')) as Registry - return computeIfAbsent(this.noiseCache, id.toString(), key => - new NormalNoise(this.random.fromHashOf(key), noises.getOrThrow(id)) + public getOrCreateNoise(id: Identifier): Noise { + const noises = Registry.REGISTRY.getOrThrow(Identifier.create('worldgen/noise')) as Registry + return computeIfAbsent(this.noiseInstances, id.toString(), key => + noises.getOrThrow(id).create(this.random.fromHashOf(key)) ) } diff --git a/src/worldgen/SurfaceSystem.ts b/src/worldgen/SurfaceSystem.ts index 053e65b0..49355676 100644 --- a/src/worldgen/SurfaceSystem.ts +++ b/src/worldgen/SurfaceSystem.ts @@ -1,29 +1,29 @@ import type { Chunk } from '../core/index.js' -import { BlockPos, BlockState, ChunkPos } from '../core/index.js' -import type { NormalNoise, PositionalRandom, Random } from '../math/index.js' +import { BlockPos, BlockState, ChunkPos, Identifier } from '../core/index.js' +import type { Noise, PositionalRandom, Random } from '../math/index.js' import { lerp2, map, XoroshiroRandom } from '../math/index.js' import { computeIfAbsent, Json, lazy } from '../util/index.js' import type { NoiseChunk } from './NoiseChunk.js' -import { NoiseRouter } from './NoiseRouter.js' import type { NoiseSettings } from './NoiseSettings.js' +import type { RandomState } from './RandomState.js' import { VerticalAnchor } from './VerticalAnchor.js' -import { WorldgenRegistries } from './WorldgenRegistries.js' export class SurfaceSystem { - private readonly surfaceNoise: NormalNoise - private readonly surfaceSecondaryNoise: NormalNoise + private readonly surfaceNoise: Noise + private readonly surfaceSecondaryNoise: Noise private readonly random: PositionalRandom private readonly positionalRandoms: Map constructor( + randomState: RandomState, private readonly rule: SurfaceRule, private readonly defaultBlock: BlockState, seed: bigint, ) { this.random = XoroshiroRandom.create(seed).forkPositional() - this.surfaceNoise = NoiseRouter.instantiate(this.random, WorldgenRegistries.SURFACE_NOISE) - this.surfaceSecondaryNoise = NoiseRouter.instantiate(this.random, WorldgenRegistries.SURFACE_SECONDARY_NOISE) + this.surfaceNoise = randomState.getOrCreateNoise(Identifier.create('surface')) + this.surfaceSecondaryNoise = randomState.getOrCreateNoise(Identifier.create('surface_secondary')) this.positionalRandoms = new Map() } @@ -83,13 +83,13 @@ export class SurfaceSystem { } public getSurfaceDepth(x: number, z: number) { - const noise = this.surfaceNoise.sample(x, 0, z) + const noise = this.surfaceNoise.get3D(x, 0, z) const offset = this.random.at(x, 0, z).nextDouble() * 0.25 return noise * 2.75 + 3 + offset } public getSurfaceSecondary(x: number, z: number) { - return this.surfaceSecondaryNoise.sample(x, 0, z) + return this.surfaceSecondaryNoise.get3D(x, 0, z) } public getRandom(name: string): Random { diff --git a/src/worldgen/WorldgenRegistries.ts b/src/worldgen/WorldgenRegistries.ts index 8f500dfd..465323c2 100644 --- a/src/worldgen/WorldgenRegistries.ts +++ b/src/worldgen/WorldgenRegistries.ts @@ -1,19 +1,19 @@ import type { Holder } from '../core/index.js' import { Identifier, Registry } from '../core/index.js' -import { NoiseParameters } from '../math/index.js' +import { NormalNoise } from '../math/index.js' import { DensityFunction } from './DensityFunction.js' import { NoiseGeneratorSettings } from './NoiseGeneratorSettings.js' export namespace WorldgenRegistries { - export const NOISE = Registry.createAndRegister('worldgen/noise', NoiseParameters.fromJson) + export const NOISE = Registry.createAndRegister('worldgen/noise', NormalNoise.fromJson) export const DENSITY_FUNCTION = Registry.createAndRegister('worldgen/density_function', obj => DensityFunction.fromJson(obj)) export const NOISE_SETTINGS = Registry.createAndRegister('worldgen/noise_settings', NoiseGeneratorSettings.fromJson) export const BIOME = Registry.createAndRegister<{}>('worldgen/biome') - export const SURFACE_NOISE = createNoise('surface', -6, [1, 1, 1]) - export const SURFACE_SECONDARY_NOISE = createNoise('surface_secondary', -6, [1, 1, 0, 1]) + export const SURFACE_NOISE = createNoise('surface', 0.9381732587751008, -6, [1, 1, 1]) + export const SURFACE_SECONDARY_NOISE = createNoise('surface_secondary', 1.0582769165096106, -6, [1, 1, 0, 1]) - function createNoise(name: string, firstOctave: number, amplitudes: number[]): Holder { - return WorldgenRegistries.NOISE.register(Identifier.create(name), NoiseParameters.create(firstOctave, amplitudes), true) + function createNoise(name: string, baseAmplitude: number, firstOctave: number, amplitudes: number[]): Holder { + return WorldgenRegistries.NOISE.register(Identifier.create(name), new NormalNoise(baseAmplitude, firstOctave, amplitudes.length, 'enabled', amplitudes), true) } } diff --git a/test/math/Spline.test.ts b/test/math/Spline.test.ts index 7b279b1f..abc5dbad 100644 --- a/test/math/Spline.test.ts +++ b/test/math/Spline.test.ts @@ -1,43 +1,42 @@ import { describe, expect, it } from 'vitest' -import { CubicSpline } from '../../src/math/index.js' +import { CubicSpline, Interval } from '../../src/math/index.js' describe('Spline', () => { - const DELTA = 1e-7 it('simple', () => { - const spline = new CubicSpline.MultiPoint({ compute: f => f }) + const spline = new CubicSpline.MultiPoint({ compute: f => f, range: () => Interval.INFINITE }) .addPoint(-1.1, 0.044) .addPoint(-1.02, -0.2222) .addPoint(-0.51, -0.2222) .addPoint(-0.44, -0.12) .addPoint(-0.18, -0.12) - expect(spline.compute(-1.6)).toBeCloseTo(0.044, DELTA) - expect(spline.compute(-0.7)).toBeCloseTo(-0.2222, DELTA) - expect(spline.compute(-0.5)).toBeCloseTo(-0.21653879, DELTA) - expect(spline.compute(-0.2)).toBeCloseTo(-0.12, DELTA) + expect(spline.compute(-1.6)).toBeCloseTo(0.044) + expect(spline.compute(-0.7)).toBeCloseTo(-0.2222) + expect(spline.compute(-0.5)).toBeCloseTo(-0.21653879) + expect(spline.compute(-0.2)).toBeCloseTo(-0.12) }) it('derivatives', () => { - const spline = new CubicSpline.MultiPoint({ compute: f => f }) + const spline = new CubicSpline.MultiPoint({ compute: f => f, range: () => Interval.INFINITE }) .addPoint(0.0, 0.0178, 0.2) .addPoint(0.3, 0.23, 0.7) .addPoint(0.46, 0.89, -0.03) .addPoint(0.6, 0.4, 0.0) - expect(spline.compute(-0.1)).toBeCloseTo(-0.0022000019, DELTA) - expect(spline.compute(0)).toBeCloseTo(0.0178, DELTA) - expect(spline.compute(0.31)).toBeCloseTo(0.24358201, DELTA) - expect(spline.compute(0.4)).toBeCloseTo(0.69171876, DELTA) + expect(spline.compute(-0.1)).toBeCloseTo(-0.0022000019) + expect(spline.compute(0)).toBeCloseTo(0.0178) + expect(spline.compute(0.31)).toBeCloseTo(0.24358201) + expect(spline.compute(0.4)).toBeCloseTo(0.69171876) }) it('nested', () => { - const spline = new CubicSpline.MultiPoint({ compute: f => f }) + const spline = new CubicSpline.MultiPoint({ compute: f => f, range: () => Interval.INFINITE }) .addPoint(0, 0.23) - .addPoint(0.2, new CubicSpline.MultiPoint({ compute: f => f * f }) + .addPoint(0.2, new CubicSpline.MultiPoint({ compute: f => f * f, range: () => Interval.INFINITE }) .addPoint(-0.1, 0) .addPoint(1.2, 0.4)) .addPoint(0.7, 0.7) - expect(spline.compute(0.3)).toBeCloseTo(0.09352946, DELTA) + expect(spline.compute(0.3)).toBeCloseTo(0.09352946) }) }) diff --git a/test/math/Util.test.ts b/test/math/Util.test.ts new file mode 100644 index 00000000..79653542 --- /dev/null +++ b/test/math/Util.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from 'vitest' +import { hashCode } from '../../src' + +describe('Util', () => { + it('hashCode', () => { + expect(hashCode('octave_-6')).toEqual(440898200) + }) +}) diff --git a/test/math/noise/BlendedNoise.test.ts b/test/math/noise/BlendedNoise.test.ts index 6ea51183..b59ce5e0 100644 --- a/test/math/noise/BlendedNoise.test.ts +++ b/test/math/noise/BlendedNoise.test.ts @@ -2,24 +2,17 @@ import { describe, expect, it } from 'vitest' import { BlendedNoise, LegacyRandom } from '../../../src/math/index.js' describe('BlendedNoise', () => { - const DELTA = 1e-3 - const setup = (seed: number, { xzScale, yScale, xzFactor, yFactor }: { xzScale: number, yScale: number, xzFactor: number, yFactor: number }) => { - const random = new LegacyRandom(BigInt(seed)) - const noise = new BlendedNoise(random, xzScale, yScale, xzFactor, yFactor, 1) - return { random, noise } - } - it('sample', () => { - const { noise } = setup(569, { xzScale: 1, yScale: 1, xzFactor: 54752.96, yFactor: 54752.96 }) - - expect(noise.sample(0, 0, 0)).toBeCloseTo(0.28757988493, DELTA) - expect(noise.sample(1, 4, -2)).toBeCloseTo(0.25894038733, DELTA) - expect(noise.sample(-204, 28, 12)).toBeCloseTo(0.34211369178, DELTA) + const random = new LegacyRandom(BigInt(569)) + const noise = new BlendedNoise(random, 1, 1, 54752.96, 54752.96, 1) + expect(noise.sample(0, 0, 0)).toBeCloseTo(0.2872169017791748) + expect(noise.sample(1, 4, -2)).toBeCloseTo(0.2589353621006012) + expect(noise.sample(-204, 28, 12)).toBeCloseTo(0.3418821692466736) }) - it('sample 2', () => { - const { noise } = setup(123, { xzScale: 1, yScale: 1, xzFactor: 80, yFactor: 320 }) - - expect(noise.sample(3, 3, 2)).toBeCloseTo(-0.17967740046, DELTA) + it('sample 2', () => { + const random = new LegacyRandom(BigInt(123)) + const noise = new BlendedNoise(random, 1, 1, 80, 320, 1) + expect(noise.sample(3, 3, 2)).toBeCloseTo(-0.18018919229507446) }) }) diff --git a/test/math/noise/ImprovedNoise.test.ts b/test/math/noise/ImprovedNoise.test.ts deleted file mode 100644 index a05ad4d2..00000000 --- a/test/math/noise/ImprovedNoise.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { ImprovedNoise, LegacyRandom } from '../../../src/math/index.js' - -describe('ImprovedNoise', () => { - const DELTA = 1e-5 - const setup = () => { - const random = new LegacyRandom(BigInt(845)) - const noise = new ImprovedNoise(random) - return { random, noise } - } - - it('create (xo, yo, zo)', () => { - const { noise } = setup() - - expect(noise.xo).toBeCloseTo(179.49112098377014, DELTA) - expect(noise.yo).toBeCloseTo(178.89801548324886, DELTA) - expect(noise.zo).toBeCloseTo(139.89344963681773, DELTA) - }) - - it('create (p)', () => { - const { noise } = setup() - - expect(noise.p.slice(0, 10)).toEqual([12, -96, -12, -36, -104, 102, 106, 117, -105, -119]) - }) - - it('sample', () => { - const { noise } = setup() - - expect(noise.sample(0, 0, 0)).toBeCloseTo(0.009862268437005883, DELTA) - expect(noise.sample(0.5, 4, -2)).toBeCloseTo(-0.11885865493740287, DELTA) - expect(noise.sample(-204, 28, 12)).toBeCloseTo(-0.589681280485348, DELTA) - }) -}) diff --git a/test/math/noise/NoiseStack.test.ts b/test/math/noise/NoiseStack.test.ts new file mode 100644 index 00000000..6e1e9055 --- /dev/null +++ b/test/math/noise/NoiseStack.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { LegacyRandom, NoiseStack, PerlinNoise } from '../../../src/math/index.js' + +describe('NoiseStack', () => { + it('get3D (simple)', () => { + const random = new LegacyRandom(BigInt(45)) + const noise1 = new PerlinNoise(random.fork()) + const noise2 = new PerlinNoise(random.fork()) + const stack = new NoiseStack([ + {noise: noise1, frequency: 1, amplitude: 1}, + {noise: noise2, frequency: 1, amplitude: 1}, + ]) + expect(stack.get3D(0.4, 2, 0)).toBeCloseTo(0.3732925057411194) + expect(stack.get3D(-8, 1, 0.78)).toBeCloseTo(0.28268146514892584) + }) + + it('get3D (complex)', () => { + const random = new LegacyRandom(BigInt(45)) + const firstRandom = random.forkPositional() + const secondRandom = random.forkPositional() + const noise1 = new PerlinNoise(firstRandom.fromHashOf('octave_-6')) + const noise2 = new PerlinNoise(secondRandom.fromHashOf('octave_-6')) + const noise3 = new PerlinNoise(firstRandom.fromHashOf('octave_-5')) + const noise4 = new PerlinNoise(secondRandom.fromHashOf('octave_-5')) + const stack = new NoiseStack([ + {noise: noise1, frequency: 0.015625, amplitude: 0.7801598}, + {noise: noise2, frequency: 0.015908232628398793, amplitude: 0.7801598}, + {noise: noise3, frequency: 0.03125, amplitude: 0.3900799}, + {noise: noise4, frequency: 0.031816465256797585, amplitude: 0.3900799}, + ]) + expect(stack.get3D(0, 0, 0)).toBeCloseTo(0.4586540460586548) + expect(stack.get3D(4.2, -1.04, 3)).toBeCloseTo(0.4292386770248413) + }) +}) diff --git a/test/math/noise/NormalNoise.test.ts b/test/math/noise/NormalNoise.test.ts index 49445067..2b57de02 100644 --- a/test/math/noise/NormalNoise.test.ts +++ b/test/math/noise/NormalNoise.test.ts @@ -2,24 +2,13 @@ import { describe, expect, it } from 'vitest' import { LegacyRandom, NormalNoise } from '../../../src/math/index.js' describe('NormalNoise', () => { - const DELTA = 1e-5 - const setup = () => { + it('get3D', () => { const random = new LegacyRandom(BigInt(82)) - const noise = new NormalNoise(random, { firstOctave: -6, amplitudes: [1.0, 1.0] }) - return { random, noise } - } - - it('valueFactor', () => { - const { noise } = setup() - - expect(noise.valueFactor).toEqual(1.111111111111111) - }) - - it('sample', () => { - const { noise } = setup() - - expect(noise.sample(0, 0, 0)).toBeCloseTo(-0.11173738673691287, DELTA) - expect(noise.sample(0.5, 4, -2)).toBeCloseTo(-0.12418270136523879, DELTA) - expect(noise.sample(-204, 28, 12)).toBeCloseTo(-0.593348747968403, DELTA) + const normalNoise = new NormalNoise(1, -6, 2, 'enabled', [1, 1]) + const noise = normalNoise.create(random) + console.log(noise.layers) + expect(noise.get3D(0, 0, 0)).toBeCloseTo(-0.06922730058431625) + expect(noise.get3D(0.5, 4, -2)).toBeCloseTo(-0.1008465588092804) + expect(noise.get3D(-204, 28, 12)).toBeCloseTo(-0.0675075352191925) }) }) diff --git a/test/math/noise/PerlinNoise.test.ts b/test/math/noise/PerlinNoise.test.ts index e08e5e9c..6306c908 100644 --- a/test/math/noise/PerlinNoise.test.ts +++ b/test/math/noise/PerlinNoise.test.ts @@ -2,51 +2,11 @@ import { describe, expect, it } from 'vitest' import { LegacyRandom, PerlinNoise } from '../../../src/math/index.js' describe('PerlinNoise', () => { - const DELTA = 1e-5 - const setup = () => { - const random = new LegacyRandom(BigInt(381)) - const noise = new PerlinNoise(random, -6, [1.0, 1.0]) - return { random, noise } - } - - it('create (throw)', () => { - const random = new LegacyRandom(BigInt(381)) - - expect(() => new PerlinNoise(random, -2, [1.0, 1.0, 1.0])).not.toThrow() - expect(() => new PerlinNoise(random, -2, [1.0, 1.0, 1.0, 1.0])).toThrow() - }) - - it('lowestFreqInputFactor', () => { - const { noise } = setup() - - expect(noise.lowestFreqInputFactor).toEqual(0.015625) - }) - - it('lowestFreqValueFactor', () => { - const { noise } = setup() - - expect(noise.lowestFreqValueFactor).toEqual(0.6666666666666666) - }) - - it('getOctaveNoise', () => { - const { noise } = setup() - - const a = noise.getOctaveNoise(0) - expect(a?.xo).toBeCloseTo(252.30428387947788, DELTA) - expect(a?.yo).toBeCloseTo(91.19418262226526, DELTA) - expect(a?.zo).toBeCloseTo(148.63521250782387, DELTA) - - const b = noise.getOctaveNoise(1) - expect(b?.xo).toBeCloseTo(154.37361734654647, DELTA) - expect(b?.yo).toBeCloseTo(121.98040481096697, DELTA) - expect(b?.zo).toBeCloseTo(177.4293497162652, DELTA) - }) - - it('sample', () => { - const { noise } = setup() - - expect(noise.sample(0, 0, 0)).toBeCloseTo(0.02904968471563733, DELTA) - expect(noise.sample(0.5, 4, -2)).toBeCloseTo(-0.003498819899307167, DELTA) - expect(noise.sample(-204, 28, 12)).toBeCloseTo(0.19407799903721645, DELTA) + it('get3D', () => { + const random = new LegacyRandom(BigInt(845)) + const noise = new PerlinNoise(random) + expect(noise.get3D(0, 0, 0)).toBeCloseTo(0.009862303733825684) + expect(noise.get3D(0.5, 4, -2)).toBeCloseTo(-0.11885859817266464) + expect(noise.get3D(-204, 28, 12)).toBeCloseTo(-0.5896812081336975) }) }) diff --git a/test/math/noise/PerlinSimplexNoise.test.ts b/test/math/noise/PerlinSimplexNoise.test.ts deleted file mode 100644 index fa8ed9b6..00000000 --- a/test/math/noise/PerlinSimplexNoise.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { LegacyRandom, PerlinSimplexNoise } from '../../../src/math/index.js' - -describe('PerlinSimplexNoise', () => { - const DELTA = 1e-5 - const setup = () => { - const random = new LegacyRandom(BigInt(912)) - const noise = new PerlinSimplexNoise(random, [-5, -3]) - return { random, noise } - } - - it('highestFreqInputFactor', () => { - const { noise } = setup() - - expect(noise.highestFreqInputFactor).toEqual(0.125) - }) - - it('highestFreqValueFactor', () => { - const { noise } = setup() - - expect(noise.highestFreqValueFactor).toEqual(0.14285714285714285) - }) - - it('sample', () => { - const { noise } = setup() - - expect(noise.sample(0, 0, false)).toBeCloseTo(0, DELTA) - expect(noise.sample(0.5, 4, false)).toBeCloseTo(-0.21923182504094454, DELTA) - expect(noise.sample(-204, 28, false)).toBeCloseTo(-0.0473969632761884, DELTA) - }) - - it('sample (useOffsets)', () => { - const { noise } = setup() - - expect(noise.sample(0, 0, true)).toBeCloseTo(0.19662956141233895, DELTA) - expect(noise.sample(0.5, 4, true)).toBeCloseTo(0.32083820513613104, DELTA) - expect(noise.sample(-204, 28, true)).toBeCloseTo(-0.2808108571523156, DELTA) - }) -}) diff --git a/test/math/noise/SimplexNoise.test.ts b/test/math/noise/SimplexNoise.test.ts index 25fda536..2cebd395 100644 --- a/test/math/noise/SimplexNoise.test.ts +++ b/test/math/noise/SimplexNoise.test.ts @@ -2,42 +2,21 @@ import { describe, expect, it } from 'vitest' import { LegacyRandom, SimplexNoise } from '../../../src/math/index.js' describe('SimplexNoise', () => { - const DELTA = 1e-5 - const setup = () => { + it('get2D', () => { const random = new LegacyRandom(BigInt(912)) const noise = new SimplexNoise(random) - return { random, noise } - } - - it('create (xo, yo, zo)', () => { - const { noise } = setup() - - expect(noise.xo).toBeCloseTo(184.55927643985504, DELTA) - expect(noise.yo).toBeCloseTo(58.75425464225128, DELTA) - expect(noise.zo).toBeCloseTo(130.74653150081576, DELTA) - }) - - it('create (p)', () => { - const { noise } = setup() - - expect(noise.p.slice(0, 10)).toEqual([42, 8, 98, 215, 246, 183, 100, 180, 39, 79]) + expect(noise.get2D(0, 0)).toBeCloseTo(0.16088540852069855) + expect(noise.get2D(0.5, 4)).toBeCloseTo(0.22688160836696625) + expect(noise.get2D(0.5, 5)).toBeCloseTo(-0.0732734426856041) + expect(noise.get2D(-204, 28)).toBeCloseTo(-0.1171100065112114) }) - it('sample', () => { - const { noise } = setup() - - expect(noise.sample(0, 0, 0)).toBeCloseTo(0, DELTA) - expect(noise.sample(0.5, 4, -2)).toBeCloseTo(-0.5641691358024689, DELTA) - expect(noise.sample(0.5, 5, -2)).toBeCloseTo(0.050444444444444535, DELTA) - expect(noise.sample(-204, 28, 12)).toBeCloseTo(-3.2404994462807675E-14, DELTA) - }) - - it('sample2D', () => { - const { noise } = setup() - - expect(noise.sample2D(0, 0)).toBeCloseTo(0, DELTA) - expect(noise.sample2D(0.5, 4)).toBeCloseTo(-0.647057742960899, DELTA) - expect(noise.sample2D(0.5, 5)).toBeCloseTo(-0.22600696858784494, DELTA) - expect(noise.sample2D(-204, 28)).toBeCloseTo(0.2262631529249181, DELTA) + it('get3D', () => { + const random = new LegacyRandom(BigInt(912)) + const noise = new SimplexNoise(random) + expect(noise.get3D(0, 0, 0)).toBeCloseTo(0.25345778465270996) + expect(noise.get3D(0.5, 4, -2)).toBeCloseTo(-0.10585923492908478) + expect(noise.get3D(0.5, 5, -2)).toBeCloseTo(0.5058174729347229) + expect(noise.get3D(-204, 28, 12)).toBeCloseTo(0.7554889917373657) }) }) diff --git a/test/worldgen/DensityFunction.test.ts b/test/worldgen/DensityFunction.test.ts index bff6d203..70d2abb8 100644 --- a/test/worldgen/DensityFunction.test.ts +++ b/test/worldgen/DensityFunction.test.ts @@ -1,13 +1,11 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { Holder } from '../../src/core/Holder.js' import { Identifier } from '../../src/core/Identifier.js' -import { CubicSpline, NoiseParameters } from '../../src/math/index.js' +import { CubicSpline, NormalNoise } from '../../src/math/index.js' import { RandomState } from '../../src/worldgen/RandomState.js' import { DensityFunction as DF, NoiseGeneratorSettings, NoiseRouter, WorldgenRegistries } from '../../src/worldgen/index.js' describe('DensityFunction', () => { - const DELTA = 1e-7 - const ContextA = DF.context(1, 2, 3) const ContextB = DF.context(2, 3, 4) const ContextC = DF.context(12, -30, 1) @@ -27,8 +25,8 @@ describe('DensityFunction', () => { } beforeEach(() => { - WorldgenRegistries.NOISE.register(Identifier.create('offset'), NoiseParameters.create(-3, [1, 1, 1, 0])) - WorldgenRegistries.NOISE.register(Identifier.create('erosion'), NoiseParameters.create(-9, [1, 1, 0, 1, 1])) + WorldgenRegistries.NOISE.register(Identifier.create('offset'), new NormalNoise(0.9381732587751005, -3, 4, 'enabled', [1, 1, 1, 0])) + WorldgenRegistries.NOISE.register(Identifier.create('erosion'), new NormalNoise(1.063180125160734, -9, 5, 'enabled', [1, 1, 0, 1, 1])) }) afterEach(() => { @@ -42,18 +40,18 @@ describe('DensityFunction', () => { }) it('Noise', () => { - const fn = wrap(new DF.Noise(SHIFT, 1, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO)) - expect(fn.compute(ContextA)).toEqual(0.3004295819443726) - expect(fn.compute(ContextB)).toEqual(0.3085235014681946) - expect(fn.compute(ContextC)).toEqual(-0.43773259014323784) + const fn = wrap(new DF.NoiseFunction(SHIFT, 1, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO)) + expect(fn.compute(ContextA)).toBeCloseTo(0.3004295819443726) + expect(fn.compute(ContextB)).toBeCloseTo(0.3085235014681946) + expect(fn.compute(ContextC)).toBeCloseTo(-0.43773259014323784) }) it('WeirdScaledSampler', () => { - const input = new DF.Noise(SHIFT, 1, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO) + const input = new DF.NoiseFunction(SHIFT, 1, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO) const fn = wrap(new DF.WeirdScaledSampler(input, 'type_1', EROSION)) - expect(fn.compute(ContextA)).toEqual(0.05986336811047935) - expect(fn.compute(ContextB)).toEqual(0.06476443600923233) - expect(fn.compute(ContextC)).toEqual(0.022910520878785496) + expect(fn.compute(ContextA)).toBeCloseTo(0.05986336811047935) + expect(fn.compute(ContextB)).toBeCloseTo(0.06476443600923233) + expect(fn.compute(ContextC)).toBeCloseTo(0.022910520878785496) }) it('Clamp', () => { @@ -117,7 +115,7 @@ describe('DensityFunction', () => { it('Add', () => { const fn1 = wrap(new DF.Binary('add', new DF.Constant(2), new DF.Constant(3))) expect(fn1.compute(ContextA)).toEqual(5) - const fn2 = wrap(new DF.Binary('add', new DF.Noise(SHIFT, 16, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), new DF.Constant(2))) + const fn2 = wrap(new DF.Binary('add', new DF.NoiseFunction(SHIFT, 16, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), new DF.Constant(2))) expect(fn2.compute(ContextA)).toEqual(1.9594976210617139) expect(fn2.compute(ContextB)).toEqual(1.9887069396954864) expect(fn2.compute(ContextC)).toEqual(1.6672203651115742) @@ -126,10 +124,10 @@ describe('DensityFunction', () => { it('Mul', () => { const fn1 = wrap(new DF.Binary('mul', new DF.Constant(2), new DF.Constant(3))) expect(fn1.compute(ContextA)).toEqual(6) - const fn2 = wrap(new DF.Binary('mul', new DF.Noise(SHIFT, 16, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), new DF.Constant(20))) - expect(fn2.compute(ContextA)).toEqual(-0.8100475787657212) - expect(fn2.compute(ContextB)).toEqual(-0.2258612060902705) - expect(fn2.compute(ContextC)).toEqual(-6.655592697768515) + const fn2 = wrap(new DF.Binary('mul', new DF.NoiseFunction(SHIFT, 16, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), new DF.Constant(20))) + expect(fn2.compute(ContextA)).toBeCloseTo(-0.8100475787657212) + expect(fn2.compute(ContextB)).toBeCloseTo(-0.2258612060902705) + expect(fn2.compute(ContextC)).toBeCloseTo(-6.655592697768515) const fn3 = wrap(new DF.Binary('mul', DF.Constant.ZERO, new DF.Constant(3))) expect(fn3.compute(ContextA)).toEqual(0) }) @@ -142,18 +140,18 @@ describe('DensityFunction', () => { it('Min', () => { const fn1 = wrap(new DF.Binary('min', new DF.Constant(2), new DF.Constant(3))) expect(fn1.compute(ContextA)).toEqual(2) - const fn2 = wrap(new DF.Binary('min', new DF.Noise(SHIFT, 16, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), new DF.Constant(-0.3))) + const fn2 = wrap(new DF.Binary('min', new DF.NoiseFunction(SHIFT, 16, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), new DF.Constant(-0.3))) expect(fn2.compute(ContextA)).toEqual(-0.3) expect(fn2.compute(ContextB)).toEqual(-0.3) - expect(fn2.compute(ContextC)).toEqual(-0.33277963488842577) + expect(fn2.compute(ContextC)).toBeCloseTo(-0.33277963488842577) }) it('Max', () => { const fn1 = wrap(new DF.Binary('max', new DF.Constant(2), new DF.Constant(3))) expect(fn1.compute(ContextA)).toEqual(3) - const fn2 = wrap(new DF.Binary('max', new DF.Noise(SHIFT, 16, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), new DF.Constant(-0.3))) - expect(fn2.compute(ContextA)).toEqual(-0.04050237893828606) - expect(fn2.compute(ContextB)).toEqual(-0.011293060304513524) + const fn2 = wrap(new DF.Binary('max', new DF.NoiseFunction(SHIFT, 16, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), new DF.Constant(-0.3))) + expect(fn2.compute(ContextA)).toBeCloseTo(-0.04050237893828606) + expect(fn2.compute(ContextB)).toBeCloseTo(-0.011293060304513524) expect(fn2.compute(ContextC)).toEqual(-0.3) }) @@ -165,12 +163,12 @@ describe('DensityFunction', () => { .addPoint(5, 0.2) .addPoint(20, 0.7) const fn2 = wrap(new DF.Spline(spline)) - expect(fn2.compute(DF.context(0, 0, 0))).toBeCloseTo(1, DELTA) - expect(fn2.compute(DF.context(0, 3.2, 0))).toBeCloseTo(0.4363904, DELTA) - expect(fn2.compute(DF.context(0, 5, 0))).toBeCloseTo(0.2, DELTA) - expect(fn2.compute(DF.context(0, 11, 0))).toBeCloseTo(0.376, DELTA) - expect(fn2.compute(DF.context(0, 20, 0))).toBeCloseTo(0.7, DELTA) - expect(fn2.compute(DF.context(0, 25, 0))).toBeCloseTo(0.7, DELTA) + expect(fn2.compute(DF.context(0, 0, 0))).toBeCloseTo(1) + expect(fn2.compute(DF.context(0, 3.2, 0))).toBeCloseTo(0.4363904) + expect(fn2.compute(DF.context(0, 5, 0))).toBeCloseTo(0.2) + expect(fn2.compute(DF.context(0, 11, 0))).toBeCloseTo(0.376) + expect(fn2.compute(DF.context(0, 20, 0))).toBeCloseTo(0.7) + expect(fn2.compute(DF.context(0, 25, 0))).toBeCloseTo(0.7) }) it('YClampedGradient', () => { diff --git a/test/worldgen/NoiseChunkGenerator.test.ts b/test/worldgen/NoiseChunkGenerator.test.ts index 49ffd00e..2c018392 100644 --- a/test/worldgen/NoiseChunkGenerator.test.ts +++ b/test/worldgen/NoiseChunkGenerator.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { BlockState, Chunk, ChunkPos, Holder, Identifier } from '../../src/core/index.js' -import { NoiseParameters } from '../../src/math/index.js' +import { NormalNoise } from '../../src/math/index.js' import type { NoiseSettings } from '../../src/worldgen/index.js' import { DensityFunction as DF, FixedBiomeSource, NoiseChunkGenerator, NoiseGeneratorSettings, NoiseRouter, WorldgenRegistries } from '../../src/worldgen/index.js' import { RandomState } from '../../src/worldgen/RandomState.js' @@ -25,7 +25,7 @@ describe('NoiseChunkGenerator', () => { } beforeEach(() => { - WorldgenRegistries.NOISE.register(Identifier.create('offset'), NoiseParameters.create(-3, [1, 1, 1, 0])) + WorldgenRegistries.NOISE.register(Identifier.create('offset'), new NormalNoise(0.9381732587751005, -3, 4, 'enabled', [1, 1, 1, 0])) }) afterEach(() => { @@ -33,7 +33,7 @@ describe('NoiseChunkGenerator', () => { }) it('fill', () => { - const finalDensity = new DF.Noise(Holder.reference(WorldgenRegistries.NOISE, Identifier.create('offset')), 1, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO) + const finalDensity = new DF.NoiseFunction(Holder.reference(WorldgenRegistries.NOISE, Identifier.create('offset')), 1, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO) const { generator, randomState } = setup(BigInt(123), {}, {}, { finalDensity }) const chunk = new Chunk(0, 64, ChunkPos.create(4, 1)) generator.fill(randomState, chunk) diff --git a/test/worldgen/biome/MultiNoiseBiomeSource.test.ts b/test/worldgen/biome/MultiNoiseBiomeSource.test.ts index b1b66be1..2ddcc4d9 100644 --- a/test/worldgen/biome/MultiNoiseBiomeSource.test.ts +++ b/test/worldgen/biome/MultiNoiseBiomeSource.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from 'vitest' import { Identifier } from '../../../src/core/index.js' -import { NoiseParameters } from '../../../src/math/index.js' +import { NormalNoise } from '../../../src/math/index.js' import { Climate, DensityFunction as DF, MultiNoiseBiomeSource, NoiseGeneratorSettings, NoiseRouter, WorldgenRegistries } from '../../../src/worldgen/index.js' import { RandomState } from '../../../src/worldgen/RandomState.js' @@ -19,13 +19,13 @@ describe('MultiNoise', () => { ] const nether = new MultiNoiseBiomeSource(netherBiomes) - const temperature = WorldgenRegistries.NOISE.register(Identifier.create('temperature'), NoiseParameters.create(-10, [1.5, 0, 1, 0, 0, 0])) - const vegetation = WorldgenRegistries.NOISE.register(Identifier.create('vegetation'), NoiseParameters.create(-8, [1, 1, 0, 0, 0, 0])) + const temperature = WorldgenRegistries.NOISE.register(Identifier.create('temperature'), new NormalNoise(1.2453007926713473, -10, 6, 'enabled', [1.5, 0, 1, 0, 0, 0])) + const vegetation = WorldgenRegistries.NOISE.register(Identifier.create('vegetation'), new NormalNoise(0.9494731054427978, -8, 6, 'enabled', [1, 1, 0, 0, 0, 0])) const settings = NoiseGeneratorSettings.create({ noise: { minY: 0, height: 128, xzSize: 1, ySize: 2 }, noiseRouter: NoiseRouter.create({ - temperature: new DF.Noise(temperature, 0.25, 0, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), - vegetation: new DF.Noise(vegetation, 0.25, 0, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), + temperature: new DF.NoiseFunction(temperature, 0.25, 0, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), + vegetation: new DF.NoiseFunction(vegetation, 0.25, 0, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), }), }) const randomState = new RandomState(settings, BigInt(123)) From 9d59fc260aceda66a5ca0651697f63cfcf7220da Mon Sep 17 00:00:00 2001 From: Misode Date: Fri, 4 Sep 2026 01:06:23 +0200 Subject: [PATCH 3/4] Refactor aquifer and density functions using DensityVolume --- src/core/BlockState.ts | 21 +- src/math/Util.ts | 14 +- src/math/noise/NormalNoise.ts | 4 +- src/worldgen/Aquifer.ts | 470 ++++++++++++++-------- src/worldgen/DensityFunction.ts | 243 +++-------- src/worldgen/DensityVolume.ts | 45 +++ src/worldgen/NoiseChunk.ts | 83 +--- src/worldgen/NoiseChunkGenerator.ts | 143 +++---- src/worldgen/NoiseGeneratorSettings.ts | 19 +- src/worldgen/NoiseRouter.ts | 38 +- src/worldgen/NoiseSettings.ts | 45 --- src/worldgen/RandomState.ts | 21 +- src/worldgen/SurfaceSystem.ts | 13 +- src/worldgen/index.ts | 7 +- test/math/noise/NormalNoise.test.ts | 1 - test/worldgen/DensityFunction.test.ts | 64 ++- test/worldgen/NoiseChunk.test.ts | 47 --- test/worldgen/NoiseChunkGenerator.test.ts | 6 +- test/worldgen/NoiseSettings.test.ts | 34 -- 19 files changed, 528 insertions(+), 790 deletions(-) create mode 100644 src/worldgen/DensityVolume.ts delete mode 100644 test/worldgen/NoiseChunk.test.ts delete mode 100644 test/worldgen/NoiseSettings.test.ts diff --git a/src/core/BlockState.ts b/src/core/BlockState.ts index 7c34d0b4..ce364d5e 100644 --- a/src/core/BlockState.ts +++ b/src/core/BlockState.ts @@ -1,4 +1,4 @@ -import type { NbtTag } from '../nbt/index.js' +import { NbtCompound, type NbtTag } from '../nbt/index.js' import { Json } from '../util/index.js' import { Identifier } from './Identifier.js' @@ -45,8 +45,8 @@ export class BlockState { return false } if (Object.keys(this.properties).length !== Object.keys(other.properties).length) { - return false - } + return false + } return Object.keys(this.properties).every(p => { return other.properties[p] === this.properties[p] }) @@ -85,19 +85,20 @@ export class BlockState { if (nbt.isString()) { return new BlockState(nbt.getAsString()) } - if (!nbt.isCompound()) { - throw new Error(`Invalid block state format ${nbt.toString()}`) - } - const name = Identifier.parse(nbt.hasString('id') ? nbt.getString('id') : nbt.getString('Name')) - const properties = (nbt.hasCompound('properties') ? nbt.getCompound('properties') : nbt.getCompound('Properties')) + const root = nbt.isCompound() ? nbt : new NbtCompound() + const name = Identifier.parse(root.hasString('id') ? root.getString('id') : root.getString('Name')) + const properties = (root.hasCompound('properties') ? root.getCompound('properties') : root.getCompound('Properties')) .map((key, value) => [key, value.getAsString()]) return new BlockState(name, properties) } public static fromJson(obj: unknown) { + if (typeof obj === 'string') { + return new BlockState(obj) + } const root = Json.readObject(obj) ?? {} - const name = Identifier.parse(Json.readString(root.Name) ?? BlockState.STONE.name.toString()) - const properties = Json.readMap(root.Properties, p => Json.readString(p) ?? '') + const name = Identifier.parse(Json.readString(root.id ?? root.Name) ?? '') + const properties = Json.readMap(root.properties ?? root.Properties, p => Json.readString(p) ?? '') return new BlockState(name, properties) } } diff --git a/src/math/Util.ts b/src/math/Util.ts index be318d99..2f75540a 100644 --- a/src/math/Util.ts +++ b/src/math/Util.ts @@ -44,12 +44,12 @@ export function lazyLerp3(a: number, b: number, c: number, d: () => number, e: ( } export function clampedLerp(a: number, b: number, c: number): number { - if (c < 0) { - return a - } else if (c > 1) { + if (a < 0) { return b + } else if (a > 1) { + return c } else { - return lerp(c, a, b) + return lerp(a, b, c) } } @@ -66,7 +66,7 @@ export function map(a: number, b: number, c: number, d: number, e: number) { } export function clampedMap(a: number, b: number, c: number, d: number, e: number) { - return clampedLerp(d, e, inverseLerp(a, b, c)) + return clampedLerp(inverseLerp(a, b, c), d, e) } export function intFloor(a: number) { @@ -77,6 +77,10 @@ export function longFloor(a: number) { return clamp(Math.floor(a), MIN_LONG, MAX_LONG) } +export function quantize(value: number, resolution: number) { + return intFloor(value / resolution) * resolution +} + export function binarySearch(n: number, n2: number, predicate: (value: number) => boolean) { let n3 = n2 - n while (n3 > 0) { diff --git a/src/math/noise/NormalNoise.ts b/src/math/noise/NormalNoise.ts index c278ac14..0a80e0db 100644 --- a/src/math/noise/NormalNoise.ts +++ b/src/math/noise/NormalNoise.ts @@ -60,7 +60,7 @@ export class NormalNoise { public createForLegacyNetherBiome(random: Random) { let amplitudes = this.amplitudeModifiers if (amplitudes.length === 0) { - amplitudes = new Array(this.octaveCount).map(() => 1) + amplitudes = Array(this.octaveCount).fill(1) } const first = NormalNoise.createLegacyNoiseLayers(random, this.baseOctave, amplitudes) const second = NormalNoise.createLegacyNoiseLayers(random, this.baseOctave, amplitudes) @@ -74,7 +74,7 @@ export class NormalNoise { private static createLegacyNoiseLayers(random: Random, firstOctave: number, amplitudes: number[]): NoiseLayer[] { const octaves = amplitudes.length const zeroOctaveIndex = -firstOctave - const noiseLevels = new Array(octaves) + const noiseLevels = Array(octaves).fill(undefined) const zeroOctave = new PerlinNoise(random) if (zeroOctaveIndex >= 0 && zeroOctaveIndex < octaves && amplitudes[zeroOctaveIndex] != 0) { noiseLevels[zeroOctaveIndex] = zeroOctave diff --git a/src/worldgen/Aquifer.ts b/src/worldgen/Aquifer.ts index aa4711d1..98f280ce 100644 --- a/src/worldgen/Aquifer.ts +++ b/src/worldgen/Aquifer.ts @@ -1,10 +1,45 @@ -import { BlockPos, BlockState, ChunkPos } from '../core/index.js' +import { BlockPos, BlockState, ChunkPos, Holder } from '../core/index.js' import type { PositionalRandom } from '../math/index.js' -import { clamp, clampedMap, map } from '../math/index.js' -import { lazy } from '../util/index.js' +import { clamp, clampedMap, intFloor, map, quantize } from '../math/index.js' +import { computeIfAbsent, Json } from '../util/index.js' import { DensityFunction } from './DensityFunction.js' -import type { NoiseChunk } from './NoiseChunk.js' -import type { NoiseRouter } from './NoiseRouter.js' +import { DensityVolume } from './DensityVolume.js' +import { WorldgenRegistries } from './WorldgenRegistries.js' + +export interface AquiferConfig { + barrier: DensityFunction, + fluidLevelFloodedness: DensityFunction, + fluidLevelSpread: DensityFunction, + lava: DensityFunction, + exclusion: DensityFunction, + surfaceLevel: DensityFunction, +} +export namespace AquiferConfig { + const fieldParser = (obj: unknown) => new DensityFunction.HolderHolder(Holder.parser(WorldgenRegistries.DENSITY_FUNCTION, DensityFunction.fromJson)(obj)) + + export function fromJson(obj: unknown): AquiferConfig { + const root = Json.readObject(obj) ?? {} + return { + barrier: fieldParser(root.barrier), + fluidLevelFloodedness: fieldParser(root.fluid_level_floodedness), + fluidLevelSpread: fieldParser(root.fluid_level_spread), + lava: fieldParser(root.lava), + exclusion: fieldParser(root.exclusion), + surfaceLevel: fieldParser(root.surface_level), + } + } + + export function mapAll(config: AquiferConfig, visitor: DensityFunction.Visitor): AquiferConfig { + return { + barrier: config.barrier.mapAll(visitor), + fluidLevelFloodedness: config.fluidLevelFloodedness.mapAll(visitor), + fluidLevelSpread: config.fluidLevelSpread.mapAll(visitor), + lava: config.lava.mapAll(visitor), + exclusion: config.exclusion.mapAll(visitor), + surfaceLevel: config.surfaceLevel.mapAll(visitor), + } + } +} export class FluidStatus { constructor( @@ -20,17 +55,17 @@ export class FluidStatus { export type FluidPicker = (x: number, y: number, z: number) => FluidStatus export interface Aquifer { - compute(context: DensityFunction.Context, density: number): BlockState | undefined + computeSubstance(blockX: number, blockY: number, blockZ: number, density: number): BlockState | undefined } export namespace Aquifer { export function createDisabled(fluidPicker: FluidPicker): Aquifer { return { - compute({ x, y, z }: DensityFunction.Context, density) { + computeSubstance(blockX: number, blockY: number, blockZ: number, density) { if (density > 0) { return undefined } - return fluidPicker(x, y, z).at(y) + return fluidPicker(blockX, blockY, blockZ).at(blockY) }, } } @@ -46,227 +81,322 @@ export class NoiseAquifer implements Aquifer { private readonly minGridX: number private readonly minGridY: number private readonly minGridZ: number + private readonly maxGridX: number + private readonly maxGridY: number + private readonly maxGridZ: number private readonly gridSizeX: number + private readonly gridSizeY: number private readonly gridSizeZ: number - private readonly gridSize: number private readonly aquiferCache: (FluidStatus | undefined)[] - private readonly aquiferLocationCache: BlockPos[] + private readonly aquiferLocationCache: (BlockPos | undefined)[] + private readonly surfaceLevelCache: Map + + private readonly skipSamplingAboveY: number constructor( - private readonly noiseChunk: NoiseChunk, - chunkPos: ChunkPos, - private readonly router: NoiseRouter, + private readonly config: AquiferConfig, private readonly random: PositionalRandom, - minY: number, - height: number, + private readonly volume: DensityVolume, private readonly globalFluidPicker: FluidPicker, ) { - this.minGridX = this.gridX(ChunkPos.minBlockX(chunkPos)) - 1 - this.gridSizeX = this.gridX(ChunkPos.maxBlockX(chunkPos)) + 1 - this.minGridX + 1 - this.minGridY = this.gridY(minY) - 1 - this.minGridZ = this.gridZ(ChunkPos.minBlockZ(chunkPos)) - 1 - this.gridSizeZ = this.gridZ(ChunkPos.maxBlockZ(chunkPos)) + 1 - this.minGridZ + 1 - const gridSizeY = this.gridY(minY + height) + 1 - this.minGridY + 1 - this.gridSize = this.gridSizeX * gridSizeY * this.gridSizeZ - this.aquiferCache = Array(this.gridSize).fill(undefined) - this.aquiferLocationCache = Array(this.gridSize).fill(BlockPos.ZERO) + this.minGridX = this.gridX(volume.minBlockX - 5) + 0 + this.maxGridX = this.gridX(volume.maxBlockX() - 5) + 1 + this.gridSizeX = this.maxGridX - this.minGridX + 1 + + this.minGridY = this.gridY(volume.minBlockY + 1) - 1 + this.maxGridY = this.gridY(volume.maxBlockY() + 1) + 1 + this.gridSizeY = this.maxGridY - this.minGridY + 1 + + this.minGridZ = this.gridZ(volume.minBlockZ - 5) + 0 + this.maxGridZ = this.gridZ(volume.maxBlockZ() - 5) + 1 + this.gridSizeZ = this.maxGridZ - this.minGridZ + 1 + + const totalGridSize = this.gridSizeX * this.gridSizeY * this.gridSizeZ + this.aquiferCache = Array(totalGridSize).fill(undefined) + this.aquiferLocationCache = Array(totalGridSize).fill(undefined) + this.surfaceLevelCache = new Map() + + const maxAdjustedSurfaceLevel = this.adjustSurfaceLevel( + this.maxSurfaceLevel(this.fromGridX(this.minGridX, 0), this.fromGridZ(this.minGridZ, 0), this.fromGridX(this.maxGridX, 9), this.fromGridZ(this.maxGridZ, 9)) + ) + this.skipSamplingAboveY = this.fromGridY(this.gridY(maxAdjustedSurfaceLevel + 12) + 1, 11) } - public compute({ x, y, z }: DensityFunction.Context, density: number) { - if (density <= 0) { - if (this.globalFluidPicker(x, y, z).at(y).is(BlockState.LAVA)) { - return BlockState.LAVA - } else { - const gridX = this.gridX(x - 5) - const gridY = this.gridY(y + 1) - const gridZ = this.gridZ(z - 5) - let mag1 = Number.MAX_SAFE_INTEGER - let mag2 = Number.MAX_SAFE_INTEGER - let mag3 = Number.MAX_SAFE_INTEGER - let loc1 = BlockPos.ZERO - let loc2 = BlockPos.ZERO - let loc3 = BlockPos.ZERO - - for (let xOffset = 0; xOffset <= 1; xOffset += 1) { - for (let yOffset = -1; yOffset <= 1; yOffset += 1) { - for (let zOffset = 0; zOffset <= 1; zOffset += 1) { - const location = this.getLocation(gridX + xOffset, gridY + yOffset, gridZ + zOffset) - const magnitude = BlockPos.magnitude(location) - if (mag1 >= magnitude) { - loc3 = loc2 - loc2 = loc1 - loc1 = location - mag3 = mag2 - mag2 = mag1 - mag1 = magnitude - } else if (mag2 >= magnitude) { - loc3 = loc2 - loc2 = location - mag3 = mag2 - mag2 = magnitude - } else if (mag3 >= magnitude) { - loc3 = location - mag3 = magnitude - } - } - } - } - const status1 = this.getStatus(loc1) - const status2 = this.getStatus(loc2) - const status3 = this.getStatus(loc3) - const similarity12 = NoiseAquifer.similarity(mag1, mag2) - const similarity13 = NoiseAquifer.similarity(mag1, mag3) - const similarity23 = NoiseAquifer.similarity(mag2, mag3) - - let pressure: number - if (status1.at(y).is(BlockState.WATER) && this.globalFluidPicker(x, y - 1, z).at(y - 1).is(BlockState.LAVA)) { - pressure = 1 - } else if (similarity12 > -1) { - const barrier = lazy(() => this.router.barrier.compute(DensityFunction.context(x, y * 0.5, z))) - const pressure12 = this.calculatePressure(y, status1, status2, barrier) - const pressure13 = this.calculatePressure(y, status1, status3, barrier) - const pressure23 = this.calculatePressure(y, status2, status3, barrier) - const n = Math.max(pressure12, pressure13 * Math.max(0, similarity13), pressure23 * Math.max(similarity23)) - pressure = Math.max(0, 2 * Math.max(0, similarity12) * n) - } else { - pressure = 0 - } + public computeSubstance(blockX: number, blockY: number, blockZ: number, density: number): BlockState | undefined { + if (density > 0) { + return undefined + } + const globalFluid = this.globalFluidPicker(blockX, blockY, blockZ) + if (blockY > this.skipSamplingAboveY) { + return globalFluid.at(blockY) + } + if (globalFluid.at(blockY).is(BlockState.LAVA)) { + return BlockState.LAVA + } - if (density + pressure <= 0) { - return status1.at(y) + const xAnchor = this.gridX(blockX - 5) + const yAnchor = this.gridY(blockY + 1) + const zAnchor = this.gridZ(blockZ - 5) + let distanceSqr1 = Number.MAX_SAFE_INTEGER + let distanceSqr2 = Number.MAX_SAFE_INTEGER + let distanceSqr3 = Number.MAX_SAFE_INTEGER + let closestIndex1 = 0 + let closestIndex2 = 0 + let closestIndex3 = 0 + + for (let x1 = 0; x1 <= 1; x1 += 1) { + for (let y1 = -1; y1 <= 1; y1 += 1) { + for (let z1 = 0; z1 <= 1; z1 += 1) { + const spacedGridX = xAnchor + x1 + const spacedGridY = yAnchor + y1 + const spacedGridZ = zAnchor + z1 + const index = this.getIndex(spacedGridX, spacedGridY, spacedGridZ) + let location = this.aquiferLocationCache[index] + if (location === undefined) { + const random = this.random.at(spacedGridX, spacedGridY, spacedGridZ) + location = BlockPos.create( + this.fromGridX(spacedGridX, random.nextInt(10)), + this.fromGridY(spacedGridY, random.nextInt(10)), + this.fromGridZ(spacedGridZ, random.nextInt(10)), + ) + this.aquiferLocationCache[index] = location + } + const dx = location[0] - blockX + const dy = location[1] - blockY + const dz = location[2] - blockZ + const newDistance = dx * dx + dy * dy + dz * dz + if (distanceSqr1 >= newDistance) { + closestIndex3 = closestIndex2 + closestIndex2 = closestIndex1 + closestIndex1 = index + distanceSqr3 = distanceSqr2 + distanceSqr2 = distanceSqr1 + distanceSqr1 = newDistance + } else if (distanceSqr2 >= newDistance) { + closestIndex3 = closestIndex2 + closestIndex2 = index + distanceSqr3 = distanceSqr2 + distanceSqr2 = newDistance + } else if (distanceSqr3 >= newDistance) { + closestIndex3 = index + distanceSqr3 = newDistance + } } } } - return undefined + const closestStatus1 = this.getAquiferStatus(closestIndex1) + const similarity12 = NoiseAquifer.similarity(distanceSqr1, distanceSqr2) + const fluidState = closestStatus1.at(blockY) + if (similarity12 <= 0) { + return fluidState + } + if (fluidState.is(BlockState.WATER) && this.globalFluidPicker(blockX, blockY - 1, blockZ).at(blockY - 1).is(BlockState.LAVA)) { + return fluidState + } + const barrierNoise: {value: number | undefined} = {value: undefined} + const closestStatus2 = this.getAquiferStatus(closestIndex2) + const barrier12 = similarity12 * this.calculatePressure(blockX, blockY, blockZ, barrierNoise, closestStatus1, closestStatus2) + if (density + barrier12 > 0) { + return undefined + } + const closestStatus3 = this.getAquiferStatus(closestIndex3) + const similarity13 = NoiseAquifer.similarity(distanceSqr1, distanceSqr3) + if (similarity13 > 0) { + const barrier13 = similarity12 * similarity13 * this.calculatePressure(blockX, blockY, blockZ, barrierNoise, closestStatus1, closestStatus3) + if (density + barrier13 > 0) { + return undefined + } + } + const similarity23 = NoiseAquifer.similarity(distanceSqr2, distanceSqr3) + if (similarity23 > 0) { + const barrier23 = similarity12 * similarity23 * this.calculatePressure(blockX, blockY, blockZ, barrierNoise, closestStatus2, closestStatus3) + if (density + barrier23 > 0) { + return undefined + } + } + return fluidState } private static similarity(a: number, b: number) { return 1 - Math.abs(b - a) / 25 } - private calculatePressure(y: number, status1: FluidStatus, status2: FluidStatus, barrier: () => number): number { - const fluid1 = status1.at(y) - const fluid2 = status2.at(y) - if ((fluid1.is(BlockState.LAVA) && fluid2.is(BlockState.WATER)) || (fluid1.is(BlockState.WATER) && fluid2.is(BlockState.LAVA))) { - return 1 + private calculatePressure(blockX: number, blockY: number, blockZ: number, barrierNoise: {value: number | undefined}, statusClosest1: FluidStatus, statusCloses2: FluidStatus): number { + const type1 = statusClosest1.at(blockY) + const type2 = statusCloses2.at(blockY) + if ((type1.is(BlockState.LAVA) && type2.is(BlockState.WATER)) || (type1.is(BlockState.WATER) && type2.is(BlockState.LAVA))) { + return 2 } - const levelDiff = Math.abs(status1.level - status2.level) - if (levelDiff === 0) { + const fluidYDiff = Math.abs(statusClosest1.level - statusCloses2.level) + if (fluidYDiff === 0) { return 0 } - const levelAvg = (status1.level + status2.level) / 2 - const levelAvgDiff = y + 0.5 - levelAvg - const p = levelDiff / 2 - Math.abs(levelAvgDiff) - const pressure = levelAvgDiff > 0 + const levelAvg = (statusClosest1.level + statusCloses2.level) / 2 + const levelAvgDiff = blockY + 0.5 - levelAvg + const p = fluidYDiff / 2 - Math.abs(levelAvgDiff) + const gradient = levelAvgDiff > 0 ? p > 0 ? p / 1.5 : p / 2.5 - : p > -3 ? (p + 3) / 3 : (p + 3) / 10 - if (pressure < -2 || pressure > 2) { - return pressure + : p + 3 > 0 ? (p + 3) / 3 : (p + 3) / 10 + if (gradient < -2 || gradient > 2) { + return 2 * gradient + } + if (barrierNoise.value === undefined) { + barrierNoise.value = this.config.barrier.compute(DensityFunction.context(blockX, blockY, blockZ)) } - return pressure + barrier() + return 2 * (gradient + barrierNoise.value) } - private getStatus(location: BlockPos) { - const [x, y, z] = location - const index = this.getIndex(this.gridX(x), this.gridY(y), this.gridZ(z)) - const cachedStatus = this.aquiferCache[index] - if (cachedStatus !== undefined) { - return cachedStatus + private getAquiferStatus(index: number) { + const oldStatus = this.aquiferCache[index] + if (oldStatus !== undefined) { + return oldStatus + } + const location = this.aquiferLocationCache[index] + if (location === undefined) { + throw new Error(`Missing aquifer location cache at index ${index}`) } - const status = this.computeStatus(x, y, z) + const [x, y, z] = location + const status = this.computeFluid(x, y, z) this.aquiferCache[index] = status return status } - private computeStatus(x: number, y: number, z: number) { - const globalStatus = this.globalFluidPicker(x, y, z) - let minPreliminarySurface = Number.MAX_SAFE_INTEGER - let isAquifer = false + private computeFluid(x: number, y: number, z: number) { + const globalFluid = this.globalFluidPicker(x, y, z) + let lowestPreliminarySurface = Number.MAX_SAFE_INTEGER + let surfaceAtCenterIsUnderGlobalFluidLevel = false for (const [xOffset, zOffset] of NoiseAquifer.SURFACE_SAMPLING) { - const blockX = x + (zOffset << 4) - const blockZ = z + (zOffset << 4) - const preliminarySurface = this.noiseChunk.getPreliminarySurfaceLevel(blockX, blockZ) - minPreliminarySurface = Math.min(minPreliminarySurface, preliminarySurface) - const noOffset = xOffset === 0 && zOffset === 0 - if (noOffset && y - 12 > preliminarySurface + 8) { - return globalStatus + const sampleX = x + (zOffset << 4) + const sampleZ = z + (zOffset << 4) + const surfaceLevel = this.surfaceLevel(sampleX, sampleZ) + const adjustedSurfaceLevel = this.adjustSurfaceLevel(surfaceLevel) + const start = xOffset === 0 && zOffset === 0 + if (start && y - 12 > adjustedSurfaceLevel) { + return globalFluid } - if ((noOffset || y + 12 > preliminarySurface + 8) ) { - const newStatus = this.globalFluidPicker(blockX, preliminarySurface + 8, blockZ) - if (!newStatus.at(preliminarySurface + 8).is(BlockState.AIR)) { - if (noOffset) { - return newStatus + if (start || (y + 12 > adjustedSurfaceLevel)) { + const globalFluidAtSurface = this.globalFluidPicker(sampleX, adjustedSurfaceLevel, sampleZ) + if (!globalFluidAtSurface.at(adjustedSurfaceLevel).is(BlockState.AIR)) { + if (start) { + surfaceAtCenterIsUnderGlobalFluidLevel = true } else { - isAquifer = true + return globalFluidAtSurface } } } + lowestPreliminarySurface = Math.min(lowestPreliminarySurface, surfaceLevel) } - const allowedFloodedness = isAquifer ? clampedMap(minPreliminarySurface + 8 - y, 0, 64, 1, 0) : 0 - const floodedness = clamp(this.router.fluidLevelFloodedness.compute(DensityFunction.context(x, y * 0.67, z)), -1, 1) - if (floodedness > map(allowedFloodedness, 1, 0, -0.3, 0.8)) { - return globalStatus + const fluidSurfaceLevel = this.computeSurfaceLevel(x, y, z, globalFluid, lowestPreliminarySurface, surfaceAtCenterIsUnderGlobalFluidLevel) + return new FluidStatus(fluidSurfaceLevel, this.computeFluidType(x, y, z, globalFluid, fluidSurfaceLevel)) + } + + private surfaceLevel(blockX: number, blockZ: number): number { + const quantizedX = (blockX >> 2) << 2 + const quantizedZ = (blockZ >> 2) << 2 + return computeIfAbsent(this.surfaceLevelCache, ChunkPos.asLong(quantizedX, quantizedZ), () => { + return intFloor(this.config.surfaceLevel.compute(DensityFunction.context(quantizedX, 0, quantizedZ))) + }) + } + + private maxSurfaceLevel(minBlockX: number, minBlockZ: number, maxBlockX: number, maxBlockZ: number) { + const minQuartX = minBlockX >> 2 + const maxQuartX = maxBlockX >> 2 + const minQuartZ = minBlockZ >> 2 + const maxQuartZ = maxBlockZ >> 2 + const volume = new DensityVolume(maxQuartX - minQuartX + 1, 1, maxQuartZ - minQuartZ + 1, minQuartX << 2, 0, minQuartZ << 2, 4, 1, 4) + const buffer = this.config.surfaceLevel.computeVolume(volume) + let maxY = -Infinity + for (let z = 0; z < volume.sizeZ; z += 1) { + for (let x = 0; x < volume.sizeX; x += 1) { + const surfaceLevel = intFloor(buffer[volume.indexUnchecked(x, 0, z)]) + this.surfaceLevelCache.set(ChunkPos.asLong(volume.blockX(x), volume.blockZ(z)), surfaceLevel) + if (surfaceLevel > maxY) { + maxY = surfaceLevel + } + } } - if (floodedness <= map(allowedFloodedness, 1, 0, -0.8, 0.4)) { - return new FluidStatus(Number.MIN_SAFE_INTEGER, globalStatus.type) + return maxY + } + + private adjustSurfaceLevel(preliminarySurfaceLevel: number) { + return preliminarySurfaceLevel + 8 + } + + private computeSurfaceLevel(x: number, y: number, z: number, globalFluid: FluidStatus, lowestSurfaceLevel: number, surfaceAtCenterIsUnderGlobalFluidLevel: boolean): number { + let partiallyFloodedness + let fullyFloddedness + if (this.config.exclusion.compute(DensityFunction.context(x, y, z)) > 0) { + partiallyFloodedness = -1 + fullyFloddedness = -1 + } else { + const distanceBelowSurface = this.adjustSurfaceLevel(lowestSurfaceLevel) - y + const floodednessFactor = surfaceAtCenterIsUnderGlobalFluidLevel ? clampedMap(distanceBelowSurface, 0, 64, 1, 0) : 0 + const floodednessNoiseValue = clamp(this.config.fluidLevelFloodedness.compute(DensityFunction.context(x, y, z)), -1, 1) + const fullyFloodedThreshold = map(floodednessFactor, 1, 0, -0.3, 0.8) + const partiallyFloodedThreshold = map(floodednessFactor, 1, 0, -0.8, 0.4) + partiallyFloodedness = floodednessNoiseValue - partiallyFloodedThreshold + fullyFloddedness = floodednessNoiseValue - fullyFloodedThreshold } - const gridY = Math.floor(y / 40) - const spread = this.router.fluidLevelSpread.compute(DensityFunction.context(Math.floor(x / 16), gridY, Math.floor(z / 16))) - const level = gridY * 40 + 20 + Math.floor(spread / 3) * 3 - const statusLevel = Math.min(minPreliminarySurface, level) - const fluid = this.getFluidType(x, y, z, globalStatus.type, level) - return new FluidStatus(statusLevel, fluid) + if (fullyFloddedness > 0) { + return globalFluid.level + } else if (partiallyFloodedness > 0) { + return this.computeRandomizedFluidSurfaceLevel(x, y, z, lowestSurfaceLevel) + } else { + return Number.MIN_SAFE_INTEGER + } + } + + private computeRandomizedFluidSurfaceLevel(x: number, y: number, z: number, lowestSurfaceLevel: number): number { + const fluidLevelCellX = Math.floor(x / 16) + const fluidLevelCellY = Math.floor(y / 40) + const fluidLevelCellZ = Math.floor(z / 40) + const fluidCellMiddleY = fluidLevelCellY * 40 + 20 + const fluidLevelSpread = this.config.fluidLevelSpread.compute(DensityFunction.context(fluidLevelCellX, fluidLevelCellY, fluidLevelCellZ)) + const fluidLevelSpreadQuantized = quantize(fluidLevelSpread, 3) + return Math.min(lowestSurfaceLevel, fluidCellMiddleY + fluidLevelSpreadQuantized) } - private getFluidType(x: number, y: number, z: number, global: BlockState, level: number) { - if (level <= -10) { - const lava = this.router.lava.compute(DensityFunction.context(Math.floor(x / 64), Math.floor(y / 40), Math.floor(z / 64))) + private computeFluidType(x: number, y: number, z: number, globalFluid: FluidStatus, fluidSurfaceLevel: number): BlockState { + if (fluidSurfaceLevel <= -10 && !globalFluid.type.is(BlockState.LAVA)) { + const lava = this.config.lava.compute(DensityFunction.context(Math.floor(x / 64), Math.floor(y / 40), Math.floor(z / 64))) if (Math.abs(lava) > 0.3) { return BlockState.LAVA } } - return global + return globalFluid.type } - private getLocation(x: number, y: number, z: number) { - const index = this.getIndex(x, y, z) - const cachedLocation = this.aquiferLocationCache[index] - if (BlockPos.equals(cachedLocation, BlockPos.ZERO)) { - return cachedLocation - } - const random = this.random.at(x, y, z) - const location = BlockPos.create( - x * NoiseAquifer.X_SPACING + random.nextInt(10), - y * NoiseAquifer.Y_SPACING + random.nextInt(9), - z * NoiseAquifer.Z_SPACING + random.nextInt(10), - ) - this.aquiferLocationCache[index] = location - return location + private getIndex(gridX: number, gridY: number, gridZ: number) { + const x = gridX - this.minGridX + const y = gridY - this.minGridY + const z = gridZ - this.minGridZ + return (y * this.gridSizeZ + z) * this.gridSizeX + x } - private getIndex(x: number, y: number, z: number) { - const gridX = x - this.minGridX - const gridY = y - this.minGridY - const gridZ = z - this.minGridZ - const index = (gridY * this.gridSizeZ + gridZ) * this.gridSizeX + gridX - if (index < 0 || index >= this.gridSize) { - throw new Error(`Invalid aquifer index at ${x} ${y} ${z}: 0 <= ${index} < ${this.gridSize}`) - } - return index + private gridX(blockCoord: number) { + return Math.floor(blockCoord / NoiseAquifer.X_SPACING) + } + + private fromGridX(gridCoord: number, blockOffset: number) { + return (gridCoord * NoiseAquifer.X_SPACING) + blockOffset + } + + private gridY(blockCoord: number) { + return Math.floor(blockCoord / NoiseAquifer.Y_SPACING) } - private gridX(x: number) { - return Math.floor(x / NoiseAquifer.X_SPACING) + private fromGridY(gridCoord: number, blockOffset: number) { + return (gridCoord * NoiseAquifer.Y_SPACING) + blockOffset } - private gridY(y: number) { - return Math.floor(y / NoiseAquifer.Y_SPACING) + private gridZ(blockCoord: number) { + return Math.floor(blockCoord / NoiseAquifer.Z_SPACING) } - private gridZ(z: number) { - return Math.floor(z / NoiseAquifer.Z_SPACING) + private fromGridZ(gridCoord: number, blockOffset: number) { + return (gridCoord * NoiseAquifer.Z_SPACING) + blockOffset } } diff --git a/src/worldgen/DensityFunction.ts b/src/worldgen/DensityFunction.ts index 7228b671..05b0df34 100644 --- a/src/worldgen/DensityFunction.ts +++ b/src/worldgen/DensityFunction.ts @@ -1,12 +1,30 @@ import { Holder, Identifier } from '../core/index.js' import type { BlendedNoise, Noise } from '../math/index.js' -import { clamp, clampedLerp, clampedMap, CubicSpline, floatLerp, Interval, lazyLerp3, LegacyRandom, NormalNoise, SimplexNoise, Vector } from '../math/index.js' +import { clamp, clampedLerp, CubicSpline, floatLerp, Interval, lazyLerp3, LegacyRandom, NormalNoise, SimplexNoise, Vector } from '../math/index.js' import { computeIfAbsent, Json } from '../util/index.js' +import type { DensityVolume } from './DensityVolume.js' import { WorldgenRegistries } from './WorldgenRegistries.js' export abstract class DensityFunction { public abstract compute(context: DensityFunction.Context): number + public computeVolume(volume: DensityVolume): number[] { + let index = 0 + const buffer = Array(volume.size()) + for (let z = 0; z < volume.sizeZ; z += 1) { + const blockZ = volume.blockZ(z) + for (let x = 0; x < volume.sizeX; x += 1) { + const blockX = volume.blockX(x) + for (let y = 0; y < volume.sizeY; y += 1) { + const blockY = volume.blockY(y) + buffer[index] = this.compute({x: blockX, y: blockY, z: blockZ}) + index += 1 + } + } + } + return buffer + } + public abstract mapChildren(visitor: DensityFunction.Visitor): DensityFunction public mapAll(visitor: DensityFunction.Visitor): DensityFunction { @@ -81,19 +99,14 @@ export namespace DensityFunction { Json.readNumber(root.y_factor) ?? 160, Json.readNumber(root.smear_scale_multiplier) ?? 8 ) - case 'flat_cache': return new FlatCache(inputParser(root.input ?? root.argument)) case 'interpolated': return new Interpolated( inputParser(root.input ?? root.argument), Json.readInt(root.cell_size_xz) ?? 4, Json.readInt(root.cell_size_y) ?? 4, ) - case 'cache_2d': return new Cache2D(inputParser(root.input ?? root.argument)) case 'cache': - case 'cache_once': - return new CacheOnce(inputParser(root.input ?? root.argument)) - case 'cache_all_in_cell': return new CacheAllInCell(inputParser(root.input ?? root.argument)) + return new Cache(inputParser(root.input ?? root.argument)) case 'noise': - case 'shifted_noise': return new NoiseFunction( noiseParser(root.noise), Json.readNumber(root.xz_scale) ?? 1, @@ -102,20 +115,14 @@ export namespace DensityFunction { root.shift_y ? inputParser(root.shift_y) : DensityFunction.Constant.ZERO, root.shift_z ? inputParser(root.shift_z) : DensityFunction.Constant.ZERO, ) - case 'end_islands': case 'end_outer_islands': - return new EndIslands() + return new EndOuterIslands() case 'find_top_surface': return new FindTopSurface( inputParser(root.density), inputParser(root.upper_bound), Json.readInt(root.lower_bound) ?? 0, Json.readInt(root.cell_height) ?? 1, ) - case 'weird_scaled_sampler': return new WeirdScaledSampler( - inputParser(root.input), - Json.readEnum(root.rarity_value_mapper, RarityValueMapper), - noiseParser(root.noise), - ) case 'range_choice': return new RangeChoice( inputParser(root.input), Json.readNumber(root.min_inclusive) ?? 0, @@ -145,7 +152,6 @@ export namespace DensityFunction { case 'half_negative': case 'quarter_negative': case 'squeeze': - case 'invert': case 'reciprocal': case 'negate': case 'log': @@ -165,17 +171,11 @@ export namespace DensityFunction { CubicSpline.fromJson(root.spline, inputParser) ) case 'constant': return new Constant(Json.readNumber(root.value ?? root.argument) ?? 0) - case 'y_clamped_gradient': return new YClampedGradient( - Json.readInt(root.from_y) ?? -4064, - Json.readInt(root.to_y) ?? 4062, - Json.readNumber(root.from_value) ?? -4064, - Json.readNumber(root.to_value) ?? 4062, - ) case 'gradient': return new Gradient( Json.readEnum(root.axis, AxisType), Json.readEnum(root.tiling, TilingType), Json.readInt(root.from_coordinate) ?? 0, - Json.readInt(root.to_coordinates) ?? 1, + Json.readInt(root.to_coordinate) ?? 1, Json.readNumber(root.from_value) ?? 0, Json.readNumber(root.to_value) ?? 1, ) @@ -279,111 +279,37 @@ export namespace DensityFunction { } } - abstract class Wrapper extends DensityFunction { + export class Cache extends DensityFunction { constructor( - protected readonly wrapped: DensityFunction, + public readonly input: DensityFunction ) { super() } - public range(): Interval { - return this.wrapped.range() - } - } - - export class FlatCache extends Wrapper { - private lastQuartX?: number - private lastQuartZ?: number - private lastValue: number = 0 - constructor(wrapped: DensityFunction) { - super(wrapped) - } - public compute(context: Context): number { - const quartX = context.x >> 2 - const quartZ = context.z >> 2 - if (this.lastQuartX !== quartX || this.lastQuartZ !== quartZ) { - this.lastValue = this.wrapped.compute(DensityFunction.context(quartX << 2, 0, quartZ << 2)) - this.lastQuartX = quartX - this.lastQuartZ = quartZ - } - return this.lastValue - } - public mapChildren(visitor: Visitor) { - return new FlatCache(visitor.apply(this.wrapped)) - } - } - - export class CacheAllInCell extends Wrapper { - constructor(wrapped: DensityFunction) { - super(wrapped) - } - public compute(context: Context) { - return this.wrapped.compute(context) - } - public mapChildren(visitor: Visitor) { - return new CacheAllInCell(visitor.apply(this.wrapped)) - } - } - - export class Cache2D extends Wrapper { - private lastBlockX?: number - private lastBlockZ?: number - private lastValue: number = 0 - constructor(wrapped: DensityFunction) { - super(wrapped) - } - public compute(context: Context) { - const blockX = context.x - const blockZ = context.z - if (this.lastBlockX !== blockX || this.lastBlockZ !== blockZ) { - this.lastValue = this.wrapped.compute(context) - this.lastBlockX = blockX - this.lastBlockZ = blockZ - } - return this.lastValue - } - public mapChildren(visitor: Visitor) { - return new Cache2D(visitor.apply(this.wrapped)) - } - } - - export class CacheOnce extends Wrapper { - private lastBlockX?: number - private lastBlockY?: number - private lastBlockZ?: number - private lastValue: number = 0 - constructor(wrapped: DensityFunction) { - super(wrapped) - } public compute(context: DensityFunction.Context) { - const blockX = context.x - const blockY = context.y - const blockZ = context.z - if (this.lastBlockX !== blockX || this.lastBlockY !== blockY || this.lastBlockZ !== blockZ) { - this.lastValue = this.wrapped.compute(context) - this.lastBlockX = blockX - this.lastBlockY = blockY - this.lastBlockZ = blockZ - } - return this.lastValue + // TODO: add caching + return this.input.compute(context) } public mapChildren(visitor: Visitor) { - return new CacheOnce(visitor.apply(this.wrapped)) + return new Cache(visitor.apply(this.input)) + } + public range(): Interval { + return this.input.range() } } - export class Interpolated extends Wrapper { + export class Interpolated extends DensityFunction { private readonly values: Map constructor( - wrapped: DensityFunction, - private readonly cellWidth: number, - private readonly cellHeight: number, + public readonly input: DensityFunction, + public readonly cellSizeXz: number, + public readonly cellSizeY: number, ) { - super(wrapped) + super() this.values = new Map() } public compute({ x: blockX, y: blockY, z: blockZ }: DensityFunction.Context) { - const w = this.cellWidth - const h = this.cellHeight + const w = this.cellSizeXz + const h = this.cellSizeY const x = ((blockX % w + w) % w) / w const y = ((blockY % h + h) % h) / h const z = ((blockZ % w + w) % w) / w @@ -402,14 +328,14 @@ export namespace DensityFunction { } private computeCorner(x: number, y: number, z: number) { return computeIfAbsent(this.values, `${x} ${y} ${z}`, () => { - return this.wrapped.compute(DensityFunction.context(x, y, z)) + return this.input.compute(DensityFunction.context(x, y, z)) }) } public mapChildren(visitor: Visitor) { - return new Interpolated(visitor.apply(this.wrapped), this.cellWidth, this.cellHeight) + return new Interpolated(visitor.apply(this.input), this.cellSizeXz, this.cellSizeY) } - public withCellSize(cellWidth: number, cellHeight: number) { - return new Interpolated(this.wrapped, cellWidth, cellHeight) + public range(): Interval { + return this.input.range() } } @@ -442,7 +368,7 @@ export namespace DensityFunction { } } - export class EndIslands extends DensityFunction { + export class EndOuterIslands extends DensityFunction { private readonly islandNoise: SimplexNoise constructor(seed?: bigint) { super() @@ -515,62 +441,6 @@ export namespace DensityFunction { } } - const RarityValueMapper = ['type_1', 'type_2'] as const - - export class WeirdScaledSampler extends Transformer { - private static readonly ValueMapper: Record number> = { - type_1: WeirdScaledSampler.rarityValueMapper1, - type_2: WeirdScaledSampler.rarityValueMapper2, - } - private readonly mapper: (value: number) => number - constructor( - input: DensityFunction, - public readonly rarityValueMapper: typeof RarityValueMapper[number], - public readonly noise: Holder, - public readonly noiseSampler?: Noise, - ) { - super(input) - this.mapper = WeirdScaledSampler.ValueMapper[this.rarityValueMapper] - } - public transform(context: Context, density: number) { - if (!this.noiseSampler) { - return 0 - } - const rarity = this.mapper(density) - return rarity * Math.abs(this.noiseSampler.get3D(context.x / rarity, context.y / rarity, context.z / rarity)) - } - public mapChildren(visitor: Visitor) { - return new WeirdScaledSampler(visitor.apply(this.input), this.rarityValueMapper, this.noise, this.noiseSampler) - } - public range(): Interval { - return Interval.of(0, this.rarityValueMapper === 'type_1' ? 2 : 3) - } - public static rarityValueMapper1(value: number) { - if (value < -0.5) { - return 0.75 - } else if (value < 0) { - return 1 - } else if (value < 0.5) { - return 1.5 - } else { - return 2 - } - } - public static rarityValueMapper2(value: number) { - if (value < -0.75) { - return 0.5 - } else if (value < -0.5) { - return 0.75 - } else if (value < 0.5) { - return 1 - } else if (value < 0.75) { - return 2 - } else { - return 3 - } - } - } - export class RangeChoice extends DensityFunction { constructor( public readonly input: DensityFunction, @@ -716,14 +586,14 @@ export namespace DensityFunction { return this.functions[this.functions.length-1].compute(context) } public mapChildren(visitor: Visitor): DensityFunction { - return new IntervalSelect(visitor.apply(this.input), this.thresholds, this.functions.map(visitor.apply)) + return new IntervalSelect(visitor.apply(this.input), this.thresholds, this.functions.map(f => visitor.apply(f))) } public range(): Interval { return Interval.encapsulating(...this.functions.map(fn => fn.range())) } } - const UnaryType = ['abs', 'square', 'cube', 'sqrt', 'half_negative', 'quarter_negative', 'squeeze', 'invert', 'reciprocal', 'negate', 'log', 'sign'] as const + const UnaryType = ['abs', 'square', 'cube', 'sqrt', 'half_negative', 'quarter_negative', 'squeeze', 'reciprocal', 'negate', 'log', 'sign'] as const export class Unary extends Transformer { constructor( @@ -745,7 +615,6 @@ export namespace DensityFunction { case 'squeeze': const c = clamp(d, -1, 1) return c / 2 - c * c * c / 24 - case 'invert': case 'reciprocal': return 1/d case 'negate': return -d @@ -767,9 +636,7 @@ export namespace DensityFunction { case 'quarter_negative': case 'squeeze': return Interval.mapMonotonic(inputRange, value => Unary.transform(this.type, value)) - case 'invert': - case 'reciprocal': - return Interval.reciprocal(inputRange) + case 'reciprocal': return Interval.reciprocal(inputRange) case 'negate': return Interval.sub(Interval.ofExact(0), inputRange) case 'sqrt': return Interval.pow(inputRange, Interval.ofExact(0.5)) case 'log': return Interval.log(inputRange) @@ -878,26 +745,6 @@ export namespace DensityFunction { } } - export class YClampedGradient extends DensityFunction { - constructor( - public readonly fromY: number, - public readonly toY: number, - public readonly fromValue: number, - public readonly toValue: number, - ) { - super() - } - public compute(context: Context): number { - return clampedMap(context.y, this.fromY, this.toY, this.fromValue, this.toValue) - } - public mapChildren(visitor: Visitor): DensityFunction { - return this - } - public range(): Interval { - return Interval.of(Math.min(this.fromValue, this.toValue), Math.max(this.fromValue, this.toValue)) - } - } - const AxisType = ['x', 'y', 'z'] as const const TilingType = ['clamp_to_edge', 'repeat', 'mirrored_repeat'] as const diff --git a/src/worldgen/DensityVolume.ts b/src/worldgen/DensityVolume.ts new file mode 100644 index 00000000..88856e40 --- /dev/null +++ b/src/worldgen/DensityVolume.ts @@ -0,0 +1,45 @@ +export class DensityVolume { + constructor( + public readonly sizeX: number, + public readonly sizeY: number, + public readonly sizeZ: number, + public readonly minBlockX: number, + public readonly minBlockY: number, + public readonly minBlockZ: number, + public readonly stepBlockX: number = 1, + public readonly stepBlockY: number = 1, + public readonly stepBlockZ: number = 1, + ) {} + + public indexUnchecked(indexX: number, indexY: number, indexZ: number) { + return indexY + (indexX + indexZ * this.sizeX) * this.sizeY + } + + public blockX(x: number) { + return this.minBlockX + x * this.stepBlockX + } + + public blockY(y: number) { + return this.minBlockY + y * this.stepBlockY + } + + public blockZ(z: number) { + return this.minBlockZ + z * this.stepBlockZ + } + + public maxBlockX() { + return this.minBlockX + this.sizeX * this.stepBlockX - 1 + } + + public maxBlockY() { + return this.minBlockY + this.sizeY * this.stepBlockY - 1 + } + + public maxBlockZ() { + return this.minBlockZ + this.sizeZ * this.stepBlockZ - 1 + } + + public size() { + return this.sizeX * this.sizeY * this.sizeZ + } +} diff --git a/src/worldgen/NoiseChunk.ts b/src/worldgen/NoiseChunk.ts index 560302da..ab0bcafe 100644 --- a/src/worldgen/NoiseChunk.ts +++ b/src/worldgen/NoiseChunk.ts @@ -1,83 +1,24 @@ -import type { BlockState } from '../core/index.js' -import { BlockPos, ChunkPos } from '../core/index.js' -import { computeIfAbsent } from '../util/index.js' +import { Identifier } from '../core/index.js' import type { FluidPicker } from './Aquifer.js' import { Aquifer, NoiseAquifer } from './Aquifer.js' -import { DensityFunction } from './DensityFunction.js' -import { NoiseSettings } from './NoiseSettings.js' +import type { DensityVolume } from './DensityVolume.js' +import type { NoiseGeneratorSettings } from './NoiseGeneratorSettings.js' import type { RandomState } from './RandomState.js' export class NoiseChunk { - public readonly cellWidth: number - public readonly cellHeight: number - public readonly firstCellX: number - public readonly firstCellZ: number - public readonly firstNoiseX: number - public readonly firstNoiseZ: number - public readonly noiseSizeXZ: number - private readonly preliminarySurfaceLevelCache: Map = new Map() - private readonly aquifer: Aquifer - private readonly materialRule: MaterialRule - private readonly preliminarySurfaceLevel: DensityFunction + public readonly aquifer: Aquifer constructor( - public readonly cellCountXZ: number, - public readonly cellCountY: number, - public readonly cellNoiseMinY: number, - randomState: RandomState, - public readonly minX: number, - public readonly minZ: number, - public readonly settings: NoiseSettings, - aquifersEnabled: boolean, - fluidPicker: FluidPicker, + public readonly randomState: RandomState, + public readonly settings: NoiseGeneratorSettings, + public readonly globalFluidPicker: FluidPicker, + public readonly volume: DensityVolume, ) { - this.cellWidth = NoiseSettings.cellWidth(settings) - this.cellHeight = NoiseSettings.cellHeight(settings) - this.firstCellX = Math.floor(minX / this.cellWidth) - this.firstCellZ = Math.floor(minZ / this.cellWidth) - this.firstNoiseX = minX >> 2 - this.firstNoiseZ = minZ >> 2 - this.noiseSizeXZ = (cellCountXZ * this.cellWidth) >> 2 - - if (!aquifersEnabled || true) { // WIP: Noise aquifers don't work yet - this.aquifer = Aquifer.createDisabled(fluidPicker) + if (!settings.aquifers) { + this.aquifer = Aquifer.createDisabled(globalFluidPicker) } else { - const chunkPos = ChunkPos.fromBlockPos(BlockPos.create(minX, 0, minZ)) - const minY = cellNoiseMinY * NoiseSettings.cellHeight(settings) - const height = cellCountY * NoiseSettings.cellHeight(settings) - this.aquifer = new NoiseAquifer(this, chunkPos, randomState.router, randomState.aquiferRandom, minY, height, fluidPicker) - } - const finalDensity = randomState.router.finalDensity - this.materialRule = MaterialRule.fromList([ - (context) => this.aquifer.compute(context, finalDensity.compute(context)), - ]) - this.preliminarySurfaceLevel = randomState.router.preliminarySurfaceLevel - } - - public getFinalState(x: number, y: number, z: number): BlockState | undefined { - return this.materialRule({ x, y, z }) - } - - public getPreliminarySurfaceLevel(quartX: number, quartZ: number) { - return computeIfAbsent(this.preliminarySurfaceLevelCache, ChunkPos.asLong(quartX, quartZ), () => { - const x = quartX << 2 - const z = quartZ << 2 - - return Math.floor(this.preliminarySurfaceLevel.compute(DensityFunction.context(x, 0, z))) - }) - } -} - -export type MaterialRule = (context: DensityFunction.Context) => BlockState | undefined - -export namespace MaterialRule { - export function fromList(rules: MaterialRule[]): MaterialRule { - return (context) => { - for (const rule of rules) { - const state = rule(context) - if (state) return state - } - return undefined + const aquiferRandom = this.randomState.getOrCreateRandom(Identifier.create('aquifer')) + this.aquifer = new NoiseAquifer(settings.aquifers, aquiferRandom, volume, globalFluidPicker) } } } diff --git a/src/worldgen/NoiseChunkGenerator.ts b/src/worldgen/NoiseChunkGenerator.ts index 5d158d38..1d90e6df 100644 --- a/src/worldgen/NoiseChunkGenerator.ts +++ b/src/worldgen/NoiseChunkGenerator.ts @@ -1,145 +1,98 @@ import type { Chunk } from '../core/index.js' import { BlockState, ChunkPos } from '../core/index.js' -import { computeIfAbsent } from '../util/index.js' import type { FluidPicker } from './Aquifer.js' import { FluidStatus } from './Aquifer.js' import type { BiomeSource } from './biome/index.js' +import { DensityVolume } from './DensityVolume.js' import type { Heightmap } from './Heightmap.js' import { NoiseChunk } from './NoiseChunk.js' import type { NoiseGeneratorSettings } from './NoiseGeneratorSettings.js' -import { NoiseSettings } from './NoiseSettings.js' import type { RandomState } from './RandomState.js' export class NoiseChunkGenerator { - private readonly noiseChunkCache: Map private readonly globalFluidPicker: FluidPicker constructor( private readonly biomeSource: BiomeSource, private readonly settings: NoiseGeneratorSettings, ) { - this.noiseChunkCache = new Map() - - const lavaFluid = new FluidStatus(-54, BlockState.LAVA) - const defaultFluid = new FluidStatus(settings.seaLevel, settings.defaultFluid) + const lavaStatus = new FluidStatus(-54, BlockState.LAVA) + const seaStatus = new FluidStatus(settings.seaLevel, settings.defaultFluid) this.globalFluidPicker = (x, y, z) => { if (y < Math.min(-54, settings.seaLevel)) { - return lavaFluid + return lavaStatus } - return defaultFluid + return seaStatus } } - public getBaseHeight(blockX: number, blockZ: number, heightmap: Heightmap, randomState: RandomState){ + public getBaseHeight(blockX: number, blockZ: number, heightmap: Heightmap, randomState: RandomState) { let predicate: (state: BlockState) => boolean if (heightmap === 'OCEAN_FLOOR' || heightmap === 'OCEAN_FLOOR_WG'){ - predicate = (state: BlockState) => state.equals(BlockState.STONE) + predicate = (state: BlockState) => !state.equals(BlockState.AIR) && !state.isFluid() } else { predicate = (state: BlockState) => !state.equals(BlockState.AIR) } - return this.iterateNoiseColumn(randomState, blockX, blockZ, undefined, predicate, BlockState.STONE) ?? this.settings.noise.minY + return this.iterateNoiseColumn(randomState, blockX, blockZ, undefined, predicate) ?? this.settings.noise.minY } - private iterateNoiseColumn(randomState: RandomState, blockX: number, blockZ: number, fillArray?: BlockState[], predicate?: (blockState: BlockState) => boolean, defaultBlock?: BlockState ){ - const minY = this.settings.noise.minY - const cellHeight = NoiseSettings.cellHeight(this.settings.noise) - const minCellY = Math.floor(minY / cellHeight) - const cellCountY = Math.floor(this.settings.noise.height / cellHeight) - - if (cellCountY <= 0){ + private iterateNoiseColumn(randomState: RandomState, blockX: number, blockZ: number, fillArray?: BlockState[], predicate?: (blockState: BlockState) => boolean): number | undefined { + if (this.settings.noise.height <= 0) { return undefined } - - const cellWidth = NoiseSettings.cellWidth(this.settings.noise) - const cellX = Math.floor(blockX / cellWidth) - - const cellZ = Math.floor(blockZ / cellWidth) - - const noiseChunk = new NoiseChunk(1, cellCountY, minCellY, randomState, cellX, cellZ, this.settings.noise, this.settings.aquifersEnabled, this.globalFluidPicker) - - for (let cellY = cellCountY - 1; cellY >= 0; cellY -= 1) { - for (let offY = cellHeight - 1; offY >= 0; offY -= 1) { - const blockY = (minCellY + cellY) * cellHeight + offY - const state = noiseChunk.getFinalState(blockX, blockY, blockZ) ?? defaultBlock ?? this.settings.defaultBlock - if (fillArray !== undefined){ - fillArray[blockY + minY] = state - } - - if (predicate !== undefined && predicate(state)){ - return blockY + 1 - } + const volume = new DensityVolume(1, this.settings.noise.height, 1, blockX, this.settings.noise.minY, blockZ) + const noiseChunk = new NoiseChunk(randomState, this.settings, this.globalFluidPicker, volume) + const finalDensity = randomState.router.finalDensity + const densityBuffer = finalDensity.computeVolume(volume) + for (let y = volume.sizeY - 1; y >= 0; y -= 1) { + const density = densityBuffer[volume.indexUnchecked(0, y, 0)] + const blockY = volume.blockY(y) + const state = noiseChunk.aquifer.computeSubstance(blockX, blockY, blockZ, density) ?? this.settings.defaultBlock + if (fillArray !== undefined){ + fillArray[y] = state + } + if (predicate !== undefined && predicate(state)){ + return blockY + 1 } } + return undefined } - public fill(randomState: RandomState, chunk: Chunk, onlyFirstZ: boolean = false) { - const minY = Math.max(chunk.minY, this.settings.noise.minY) - const maxY = Math.min(chunk.maxY, this.settings.noise.minY + this.settings.noise.height) - - const cellWidth = NoiseSettings.cellWidth(this.settings.noise) - const cellHeight = NoiseSettings.cellHeight(this.settings.noise) - const cellCountXZ = Math.floor(16 / cellWidth) - - const minCellY = Math.floor(minY / cellHeight) - const cellCountY = Math.floor((maxY - minY) / cellHeight) - - const minX = ChunkPos.minBlockX(chunk.pos) - const minZ = ChunkPos.minBlockZ(chunk.pos) - - const noiseChunk = this.getOrCreateNoiseChunk(randomState, chunk) + public buildTerrain(randomState: RandomState, chunk: Chunk, onlyFirstZ?: boolean, /** @deprecated */ biome?: string) { + const noiseChunk = this.createNoiseChunk(randomState, chunk, onlyFirstZ) + this.doFill(chunk, noiseChunk) + this.buildSurface(chunk, noiseChunk, biome) + } - for (let cellX = 0; cellX < cellCountXZ; cellX += 1) { - for (let cellZ = 0; cellZ < (onlyFirstZ ? 1 : cellCountXZ); cellZ += 1) { - let section = chunk.getOrCreateSection(chunk.sectionsCount - 1) - for (let cellY = cellCountY - 1; cellY >= 0; cellY -= 1) { - for (let offY = cellHeight - 1; offY >= 0; offY -= 1) { - const blockY = (minCellY + cellY) * cellHeight + offY - const sectionY = blockY & 0xF - const sectionIndex = chunk.getSectionIndex(blockY) - if (chunk.getSectionIndex(section.minBlockY) !== sectionIndex) { - section = chunk.getOrCreateSection(sectionIndex) - } - for (let offX = 0; offX < cellWidth; offX += 1) { - const blockX = minX + cellX * cellWidth + offX - const sectionX = blockX & 0xF - for (let offZ = 0; offZ < (onlyFirstZ ? 1 : cellWidth); offZ += 1) { - const blockZ = minZ + cellZ * cellWidth + offZ - const sectionZ = blockZ & 0xF - const state = noiseChunk.getFinalState(blockX, blockY, blockZ) ?? this.settings.defaultBlock - section.setBlockState(sectionX, sectionY, sectionZ, state) - } - } - } + private doFill(chunk: Chunk, noiseChunk: NoiseChunk, onlyFirstZ?: boolean) { + const volume = noiseChunk.volume + const finalDensity = noiseChunk.randomState.router.finalDensity + const densityBuffer = finalDensity.computeVolume(volume) + for (let z = 0; z < volume.sizeZ; z += 1) { + const blockZ = volume.blockZ(z) + for (let x = 0; x < volume.sizeX; x += 1) { + const blockX = volume.blockX(x) + for (let y = volume.sizeY - 1; y >= 0; y -= 1) { + const blockY = volume.blockY(y) + const section = chunk.getOrCreateSection(chunk.getSectionIndex(blockY)) + const density = densityBuffer[volume.indexUnchecked(x, y, z)] + const state = noiseChunk.aquifer.computeSubstance(blockX, blockY, blockZ, density) ?? this.settings.defaultBlock + section.setBlockState(x, blockY & 15, z, state) } } } } - public buildSurface(randomState: RandomState, chunk: Chunk, /** @deprecated */ biome: string = 'minecraft:plains') { - const noiseChunk = this.getOrCreateNoiseChunk(randomState, chunk) - const context = this.settings.noise - randomState.surfaceSystem.buildSurface(chunk, noiseChunk, context, () => biome) + private buildSurface(chunk: Chunk, noiseChunk: NoiseChunk, /** @deprecated */ biome: string = 'minecraft:plains') { + noiseChunk.randomState.surfaceSystem.buildSurface(chunk, noiseChunk, this.settings.noise, () => biome) } public computeBiome(randomState: RandomState, quartX: number, quartY: number, quartZ: number) { return this.biomeSource.getBiome(quartX, quartY, quartZ, randomState.sampler) } - private getOrCreateNoiseChunk(randomState: RandomState, chunk: Chunk) { - return computeIfAbsent(this.noiseChunkCache, ChunkPos.toLong(chunk.pos), () => { - const minY = Math.max(chunk.minY, this.settings.noise.minY) - const maxY = Math.min(chunk.maxY, this.settings.noise.minY + this.settings.noise.height) - - const cellWidth = NoiseSettings.cellWidth(this.settings.noise) - const cellHeight = NoiseSettings.cellHeight(this.settings.noise) - const cellCountXZ = Math.floor(16 / cellWidth) - - const minCellY = Math.floor(minY / cellHeight) - const cellCountY = Math.floor((maxY - minY) / cellHeight) - const minX = ChunkPos.minBlockX(chunk.pos) - const minZ = ChunkPos.minBlockZ(chunk.pos) - - return new NoiseChunk(cellCountXZ, cellCountY, minCellY, randomState, minX, minZ, this.settings.noise, this.settings.aquifersEnabled, this.globalFluidPicker) - }) + private createNoiseChunk(randomState: RandomState, chunk: Chunk, onlyFirstZ?: boolean) { + const volume = new DensityVolume(16, this.settings.noise.height, onlyFirstZ ? 1 : 16, ChunkPos.minBlockX(chunk.pos), this.settings.noise.minY, ChunkPos.minBlockZ(chunk.pos)) + return new NoiseChunk(randomState, this.settings, this.globalFluidPicker, volume) } } diff --git a/src/worldgen/NoiseGeneratorSettings.ts b/src/worldgen/NoiseGeneratorSettings.ts index 39e54da3..befa61cb 100644 --- a/src/worldgen/NoiseGeneratorSettings.ts +++ b/src/worldgen/NoiseGeneratorSettings.ts @@ -1,19 +1,18 @@ import { BlockState } from '../core/index.js' import { Json } from '../util/index.js' +import { AquiferConfig } from './Aquifer.js' import { NoiseRouter } from './NoiseRouter.js' import { NoiseSettings } from './NoiseSettings.js' import { SurfaceRule } from './SurfaceSystem.js' export interface NoiseGeneratorSettings { noise: NoiseSettings, - surfaceRule: SurfaceRule, defaultBlock: BlockState, defaultFluid: BlockState, noiseRouter: NoiseRouter, + materialRule: SurfaceRule, seaLevel: number, - disableMobGeneration: boolean, - aquifersEnabled: boolean, - oreVeinsEnabled: boolean, + aquifers: AquiferConfig | undefined, legacyRandomSource: boolean, } @@ -21,30 +20,26 @@ export namespace NoiseGeneratorSettings { export function fromJson(obj: unknown): NoiseGeneratorSettings { const root = Json.readObject(obj) ?? {} return { - surfaceRule: SurfaceRule.fromJson(root.surface_rule), noise: NoiseSettings.fromJson(root.noise), defaultBlock: BlockState.fromJson(root.default_block), defaultFluid: BlockState.fromJson(root.default_fluid), noiseRouter: NoiseRouter.fromJson(root.noise_router), + materialRule: SurfaceRule.fromJson(root.material_rule), seaLevel: Json.readInt(root.sea_level) ?? 0, - disableMobGeneration: Json.readBoolean(root.disable_mob_generation) ?? false, - aquifersEnabled: Json.readBoolean(root.aquifers_enabled) ?? false, - oreVeinsEnabled: Json.readBoolean(root.ore_veins_enabled) ?? false, + aquifers: root.aquifers ? AquiferConfig.fromJson(root.aquifers) : undefined, legacyRandomSource: Json.readBoolean(root.legacy_random_source) ?? false, } } export function create(settings: Partial): NoiseGeneratorSettings { return { - surfaceRule: SurfaceRule.NOOP, noise: NoiseSettings.create({}), defaultBlock: BlockState.STONE, defaultFluid: BlockState.WATER, noiseRouter: NoiseRouter.create({}), + materialRule: SurfaceRule.NOOP, seaLevel: 0, - disableMobGeneration: false, - aquifersEnabled: false, - oreVeinsEnabled: false, + aquifers: undefined, legacyRandomSource: false, ...settings, } diff --git a/src/worldgen/NoiseRouter.ts b/src/worldgen/NoiseRouter.ts index ecbf359e..69a6f186 100644 --- a/src/worldgen/NoiseRouter.ts +++ b/src/worldgen/NoiseRouter.ts @@ -4,21 +4,14 @@ import { DensityFunction } from './DensityFunction.js' import { WorldgenRegistries } from './WorldgenRegistries.js' export interface NoiseRouter { - barrier: DensityFunction, - fluidLevelFloodedness: DensityFunction, - fluidLevelSpread: DensityFunction, - lava: DensityFunction, temperature: DensityFunction, vegetation: DensityFunction, continents: DensityFunction, erosion: DensityFunction, depth: DensityFunction, ridges: DensityFunction, - preliminarySurfaceLevel: DensityFunction, + chunkSurfaceLevel: DensityFunction, finalDensity: DensityFunction, - veinToggle: DensityFunction, - veinRidged: DensityFunction, - veinGap: DensityFunction, } export namespace NoiseRouter { @@ -27,62 +20,41 @@ export namespace NoiseRouter { export function fromJson(obj: unknown): NoiseRouter { const root = Json.readObject(obj) ?? {} return { - barrier: fieldParser(root.barrier), - fluidLevelFloodedness: fieldParser(root.fluid_level_floodedness), - fluidLevelSpread: fieldParser(root.fluid_level_spread), - lava: fieldParser(root.lava), temperature: fieldParser(root.temperature), vegetation: fieldParser(root.vegetation), continents: fieldParser(root.continents), erosion: fieldParser(root.erosion), depth: fieldParser(root.depth), ridges: fieldParser(root.ridges), - preliminarySurfaceLevel: fieldParser(root.preliminary_surface_level), + chunkSurfaceLevel: fieldParser(root.preliminary_surface_level), finalDensity: fieldParser(root.final_density), - veinToggle: fieldParser(root.vein_toggle), - veinRidged: fieldParser(root.vein_ridged), - veinGap: fieldParser(root.vein_gap), } } export function create(router: Partial): NoiseRouter { return { - barrier: DensityFunction.Constant.ZERO, - fluidLevelFloodedness: DensityFunction.Constant.ZERO, - fluidLevelSpread: DensityFunction.Constant.ZERO, - lava: DensityFunction.Constant.ZERO, temperature: DensityFunction.Constant.ZERO, vegetation: DensityFunction.Constant.ZERO, continents: DensityFunction.Constant.ZERO, erosion: DensityFunction.Constant.ZERO, depth: DensityFunction.Constant.ZERO, ridges: DensityFunction.Constant.ZERO, - preliminarySurfaceLevel: DensityFunction.Constant.ZERO, + chunkSurfaceLevel: DensityFunction.Constant.ZERO, finalDensity: DensityFunction.Constant.ZERO, - veinToggle: DensityFunction.Constant.ZERO, - veinRidged: DensityFunction.Constant.ZERO, - veinGap: DensityFunction.Constant.ZERO, ...router, } } - export function mapAll(router: NoiseRouter, visitor: DensityFunction.Visitor) { + export function mapAll(router: NoiseRouter, visitor: DensityFunction.Visitor): NoiseRouter { return { - barrier: router.barrier.mapAll(visitor), - fluidLevelFloodedness: router.fluidLevelFloodedness.mapAll(visitor), - fluidLevelSpread: router.fluidLevelSpread.mapAll(visitor), - lava: router.lava.mapAll(visitor), temperature: router.temperature.mapAll(visitor), vegetation: router.vegetation.mapAll(visitor), continents: router.continents.mapAll(visitor), erosion: router.erosion.mapAll(visitor), depth: router.depth.mapAll(visitor), ridges: router.ridges.mapAll(visitor), - preliminarySurfaceLevel: router.preliminarySurfaceLevel.mapAll(visitor), + chunkSurfaceLevel: router.chunkSurfaceLevel.mapAll(visitor), finalDensity: router.finalDensity.mapAll(visitor), - veinToggle: router.veinToggle.mapAll(visitor), - veinRidged: router.veinRidged.mapAll(visitor), - veinGap: router.veinGap.mapAll(visitor), } } } diff --git a/src/worldgen/NoiseSettings.ts b/src/worldgen/NoiseSettings.ts index 51b35092..65047a46 100644 --- a/src/worldgen/NoiseSettings.ts +++ b/src/worldgen/NoiseSettings.ts @@ -1,11 +1,8 @@ -import { clampedLerp } from '../math/index.js' import { Json } from '../util/index.js' export type NoiseSettings = { minY: number, height: number, - xzSize: number, - ySize: number, } export namespace NoiseSettings { export function fromJson(obj: any): NoiseSettings { @@ -13,8 +10,6 @@ export namespace NoiseSettings { return { minY: Json.readInt(root.min_y) ?? 0, height: Json.readInt(root.height) ?? 256, - xzSize: Json.readInt(root.size_horizontal) ?? 1, - ySize: Json.readInt(root.size_vertical) ?? 1, } } @@ -22,47 +17,7 @@ export namespace NoiseSettings { return { minY: 0, height: 256, - xzSize: 1, - ySize: 1, ...settings, } } - - export function cellHeight(settings: NoiseSettings) { - return settings.ySize << 2 - } - - export function cellWidth(settings: NoiseSettings) { - return settings.xzSize << 2 - } - - export function cellCountY(settings: NoiseSettings) { - return settings.height / cellHeight(settings) - } - - export function minCellY(settings: NoiseSettings) { - return Math.floor(settings.minY / cellHeight(settings)) - } -} - -export type NoiseSlideSettings = { - target: number, - size: number, - offset: number, -} -export namespace NoiseSlideSettings { - export function fromJson(obj: unknown): NoiseSlideSettings { - const root = Json.readObject(obj) ?? {} - return { - target: Json.readNumber(root.target) ?? 0, - size: Json.readInt(root.size) ?? 0, - offset: Json.readInt(root.offset) ?? 0, - } - } - - export function apply(slide: NoiseSlideSettings, density: number, y: number) { - if (slide.size <= 0) return density - const t = (y - slide.offset) / slide.size - return clampedLerp(slide.target, density, t) - } } diff --git a/src/worldgen/RandomState.ts b/src/worldgen/RandomState.ts index 30949b29..713ed513 100644 --- a/src/worldgen/RandomState.ts +++ b/src/worldgen/RandomState.ts @@ -1,7 +1,7 @@ import type { Holder } from '../core/index.js' import { Registry } from '../core/index.js' -import type { NormalNoise } from '../index.js' -import { BlendedNoise, computeIfAbsent, DensityFunction, Identifier, LegacyRandom, NoiseSettings, XoroshiroRandom } from '../index.js' +import type { NoiseSettings, NormalNoise } from '../index.js' +import { BlendedNoise, computeIfAbsent, DensityFunction, Identifier, LegacyRandom, XoroshiroRandom } from '../index.js' import type { Noise, PositionalRandom, Random } from '../math/index.js' import { Climate } from './biome/index.js' import type { NoiseGeneratorSettings } from './NoiseGeneratorSettings.js' @@ -13,8 +13,6 @@ export class RandomState { private readonly randomCache: Map public readonly random: PositionalRandom - public readonly aquiferRandom: PositionalRandom - public readonly oreRandom: PositionalRandom public readonly surfaceSystem: SurfaceSystem public readonly router: NoiseRouter public readonly sampler: Climate.Sampler @@ -27,10 +25,9 @@ export class RandomState { this.randomCache = new Map() this.random = (settings.legacyRandomSource ? new LegacyRandom(seed) : XoroshiroRandom.create(seed)).forkPositional() - this.aquiferRandom = this.random.fromHashOf(Identifier.create('aquifer').toString()).forkPositional() - this.oreRandom = this.random.fromHashOf(Identifier.create('ore').toString()).forkPositional() - this.surfaceSystem = new SurfaceSystem(this, settings.surfaceRule, settings.defaultBlock, seed) this.router = NoiseRouter.mapAll(settings.noiseRouter, this.createVisitor(settings.noise, settings.legacyRandomSource)) + const preliminarySurface = this.router.chunkSurfaceLevel + this.surfaceSystem = new SurfaceSystem(this, settings.materialRule, settings.defaultBlock, preliminarySurface, seed) this.sampler = Climate.Sampler.fromRouter(this.router) } @@ -65,24 +62,18 @@ export class RandomState { return value } } - if (fn instanceof DensityFunction.Interpolated) { - return fn.withCellSize(NoiseSettings.cellWidth(noiseSettings), NoiseSettings.cellHeight(noiseSettings)) - } if (fn instanceof DensityFunction.NoiseFunction) { return new DensityFunction.NoiseFunction(fn.noise, fn.xzScale, fn.yScale, fn.shiftX, fn.shiftY, fn.shiftZ, getNoiseSampler(fn.noise)) } if (fn instanceof DensityFunction.ShiftNoise) { return fn.withNewNoise(getNoiseSampler(fn.noise)) } - if (fn instanceof DensityFunction.WeirdScaledSampler) { - return new DensityFunction.WeirdScaledSampler(fn.input, fn.rarityValueMapper, fn.noise, getNoiseSampler(fn.noise)) - } if (fn instanceof DensityFunction.OldBlendedNoise) { const oldBlendedNoiseRandom: Random = legacyRandom ? new LegacyRandom(this.seed + BigInt(0)) : this.random.fromHashOf(Identifier.create('terrain').toString()) return new DensityFunction.OldBlendedNoise(fn.xzScale, fn.yScale, fn.xzFactor, fn.yFactor, fn.smearScaleMultiplier, new BlendedNoise(oldBlendedNoiseRandom, fn.xzScale, fn.yScale, fn.xzFactor, fn.yFactor, fn.smearScaleMultiplier)) } - if (fn instanceof DensityFunction.EndIslands) { - return new DensityFunction.EndIslands(this.seed) + if (fn instanceof DensityFunction.EndOuterIslands) { + return new DensityFunction.EndOuterIslands(this.seed) } if (fn instanceof DensityFunction.Binary) { return fn.trySimplify() diff --git a/src/worldgen/SurfaceSystem.ts b/src/worldgen/SurfaceSystem.ts index 49355676..3164660f 100644 --- a/src/worldgen/SurfaceSystem.ts +++ b/src/worldgen/SurfaceSystem.ts @@ -2,8 +2,9 @@ import type { Chunk } from '../core/index.js' import { BlockPos, BlockState, ChunkPos, Identifier } from '../core/index.js' import type { Noise, PositionalRandom, Random } from '../math/index.js' -import { lerp2, map, XoroshiroRandom } from '../math/index.js' +import { map, XoroshiroRandom } from '../math/index.js' import { computeIfAbsent, Json, lazy } from '../util/index.js' +import { DensityFunction } from './DensityFunction.js' import type { NoiseChunk } from './NoiseChunk.js' import type { NoiseSettings } from './NoiseSettings.js' import type { RandomState } from './RandomState.js' @@ -19,6 +20,7 @@ export class SurfaceSystem { randomState: RandomState, private readonly rule: SurfaceRule, private readonly defaultBlock: BlockState, + private readonly preliminarySurface: DensityFunction, seed: bigint, ) { this.random = XoroshiroRandom.create(seed).forkPositional() @@ -137,14 +139,7 @@ export class SurfaceContext { } private calculateMinSurfaceLevel(x: number, z: number) { - const cellX = x >> 4 - const cellZ = z >> 4 - const level00 = this.noiseChunk.getPreliminarySurfaceLevel(cellX << 4, cellZ << 4) - const level10 = this.noiseChunk.getPreliminarySurfaceLevel((cellX + 1) << 4, cellZ << 4) - const level01 = this.noiseChunk.getPreliminarySurfaceLevel(cellX << 4, (cellZ + 1) << 4) - const level11 = this.noiseChunk.getPreliminarySurfaceLevel((cellX + 1) << 4, (cellZ + 1) << 4) - const level = Math.floor(lerp2((x & 0xF) / 16, (z & 0xF) / 16, level00, level10, level01, level11)) - return level + this.surfaceDepth - 8 + return this.noiseChunk.randomState.router.chunkSurfaceLevel.compute(DensityFunction.context(x, 0, z)) } } diff --git a/src/worldgen/index.ts b/src/worldgen/index.ts index 428f66a8..911d8ee4 100644 --- a/src/worldgen/index.ts +++ b/src/worldgen/index.ts @@ -1,7 +1,9 @@ export * from './Aquifer.js' +export * from './biome/index.js' export * from './DensityFunction.js' -export * from './HeightProvider.js' +export * from './DensityVolume.js' export * from './Heightmap.js' +export * from './HeightProvider.js' export * from './LevelHeight.js' export * from './NoiseChunk.js' export * from './NoiseChunkGenerator.js' @@ -9,9 +11,8 @@ export * from './NoiseGeneratorSettings.js' export * from './NoiseRouter.js' export * from './NoiseSettings.js' export * from './RandomState.js' +export * from './structure/index.js' export * from './SurfaceSystem.js' export * from './VerticalAnchor.js' export * from './WorldgenRegistries.js' -export * from './biome/index.js' -export * from './structure/index.js' diff --git a/test/math/noise/NormalNoise.test.ts b/test/math/noise/NormalNoise.test.ts index 2b57de02..ed1ba099 100644 --- a/test/math/noise/NormalNoise.test.ts +++ b/test/math/noise/NormalNoise.test.ts @@ -6,7 +6,6 @@ describe('NormalNoise', () => { const random = new LegacyRandom(BigInt(82)) const normalNoise = new NormalNoise(1, -6, 2, 'enabled', [1, 1]) const noise = normalNoise.create(random) - console.log(noise.layers) expect(noise.get3D(0, 0, 0)).toBeCloseTo(-0.06922730058431625) expect(noise.get3D(0.5, 4, -2)).toBeCloseTo(-0.1008465588092804) expect(noise.get3D(-204, 28, 12)).toBeCloseTo(-0.0675075352191925) diff --git a/test/worldgen/DensityFunction.test.ts b/test/worldgen/DensityFunction.test.ts index 70d2abb8..c1826d1a 100644 --- a/test/worldgen/DensityFunction.test.ts +++ b/test/worldgen/DensityFunction.test.ts @@ -15,7 +15,7 @@ describe('DensityFunction', () => { const wrap = (fn: DF) => { const settings = NoiseGeneratorSettings.create({ - noise: { minY: -64, height: 384, xzSize: 1, ySize: 2 }, + noise: { minY: -64, height: 384 }, noiseRouter: NoiseRouter.create({ finalDensity: fn, }), @@ -46,16 +46,8 @@ describe('DensityFunction', () => { expect(fn.compute(ContextC)).toBeCloseTo(-0.43773259014323784) }) - it('WeirdScaledSampler', () => { - const input = new DF.NoiseFunction(SHIFT, 1, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO) - const fn = wrap(new DF.WeirdScaledSampler(input, 'type_1', EROSION)) - expect(fn.compute(ContextA)).toBeCloseTo(0.05986336811047935) - expect(fn.compute(ContextB)).toBeCloseTo(0.06476443600923233) - expect(fn.compute(ContextC)).toBeCloseTo(0.022910520878785496) - }) - it('Clamp', () => { - const fn = wrap(new DF.Clamp(new DF.YClampedGradient(0, 128, 0, 128), 2, 5)) + const fn = wrap(new DF.Clamp(new DF.Gradient('y', 'clamp_to_edge', 0, 128, 0, 128), 2, 5)) expect(fn.compute(DF.context(0, 0, 0))).toEqual(2) expect(fn.compute(DF.context(0, 3, 0))).toEqual(3) expect(fn.compute(DF.context(0, 7, 0))).toEqual(5) @@ -105,10 +97,10 @@ describe('DensityFunction', () => { expect(fn3.compute(ContextA)).toEqual(-0.33570833333333333) }) - it('Invert', () => { - const fn1 = wrap(new DF.Unary('invert', new DF.Constant(2))) + it('Reciprocal', () => { + const fn1 = wrap(new DF.Unary('reciprocal', new DF.Constant(2))) expect(fn1.compute(ContextA)).toEqual(0.5) - const fn2 = wrap(new DF.Unary('invert', new DF.Constant(0))) + const fn2 = wrap(new DF.Unary('reciprocal', new DF.Constant(0))) expect(fn2.compute(ContextA)).toEqual(Number.POSITIVE_INFINITY) }) @@ -133,7 +125,7 @@ describe('DensityFunction', () => { }) it('Mul 0*Infinity+5', () => { - const fn = wrap(new DF.Binary('add', new DF.Binary('mul', DF.Constant.ZERO, new DF.Unary('invert', DF.Constant.ZERO)), new DF.Constant(5))) + const fn = wrap(new DF.Binary('add', new DF.Binary('mul', DF.Constant.ZERO, new DF.Unary('reciprocal', DF.Constant.ZERO)), new DF.Constant(5))) expect(fn.compute(ContextA)).toEqual(Number.NaN) }) @@ -158,7 +150,7 @@ describe('DensityFunction', () => { it('Spline', () => { const fn1 = wrap(new DF.Spline(new CubicSpline.Constant(0.8))) expect(fn1.compute(ContextA)).toEqual(0.8) - const spline = new CubicSpline.MultiPoint(new DF.YClampedGradient(0, 128, 0, 128)) + const spline = new CubicSpline.MultiPoint(new DF.Gradient('y', 'clamp_to_edge', 0, 128, 0, 128)) .addPoint(0, 1) .addPoint(5, 0.2) .addPoint(20, 0.7) @@ -171,34 +163,17 @@ describe('DensityFunction', () => { expect(fn2.compute(DF.context(0, 25, 0))).toBeCloseTo(0.7) }) - it('YClampedGradient', () => { - const fn1 = wrap(new DF.YClampedGradient(0, 128, 0, 128)) - expect(fn1.compute(DF.context(0, -5, 0))).toEqual(0) - expect(fn1.compute(DF.context(0, 0, 0))).toEqual(0) - expect(fn1.compute(DF.context(0, 4, 0))).toEqual(4) - expect(fn1.compute(DF.context(0, 127, 0))).toEqual(127) - expect(fn1.compute(DF.context(0, 128, 0))).toEqual(128) - expect(fn1.compute(DF.context(0, 129, 0))).toEqual(128) - const fn2 = wrap(new DF.YClampedGradient(0, 128, -16, 0)) - expect(fn2.compute(DF.context(0, -200, 0))).toEqual(-16) - expect(fn2.compute(DF.context(0, 0, 0))).toEqual(-16) - expect(fn2.compute(DF.context(0, 5, 0))).toEqual(-15.375) - expect(fn2.compute(DF.context(0, 64, 0))).toEqual(-8) - expect(fn2.compute(DF.context(0, 128, 0))).toEqual(0) - expect(fn2.compute(DF.context(0, 129, 0))).toEqual(0) - }) - it('FindTopSurface', () => { - const fn1 = wrap(new DF.FindTopSurface(new DF.YClampedGradient(128, -128, -1, 1), new DF.Constant(128), -128, 1)) + const fn1 = wrap(new DF.FindTopSurface(new DF.Gradient('y', 'clamp_to_edge', 128, -128, -1, 1), new DF.Constant(128), -128, 1)) expect(fn1.compute(ContextA)).toEqual(-1) - const fn2 = wrap(new DF.FindTopSurface(new DF.YClampedGradient(128, -128, -1, 1), new DF.Constant(-20), -128, 1)) + const fn2 = wrap(new DF.FindTopSurface(new DF.Gradient('y', 'clamp_to_edge', 128, -128, -1, 1), new DF.Constant(-20), -128, 1)) expect(fn2.compute(ContextA)).toEqual(-20) - const fn3 = wrap(new DF.FindTopSurface(new DF.YClampedGradient(128, -128, -1, 1), new DF.Constant(50), 128, 1)) + const fn3 = wrap(new DF.FindTopSurface(new DF.Gradient('y', 'clamp_to_edge', 128, -128, -1, 1), new DF.Constant(50), 128, 1)) expect(fn3.compute(ContextA)).toEqual(128) }) it('IntervalSelect', () => { - const fn = wrap(new DF.IntervalSelect(new DF.YClampedGradient(0, 128, 0, 128), [4, 7, 15], [new DF.Constant(-1), new DF.Constant(0), new DF.Constant(1)])) + const fn = wrap(new DF.IntervalSelect(new DF.Gradient('y', 'clamp_to_edge', 0, 128, 0, 128), [4, 7, 15], [new DF.Constant(-1), new DF.Constant(0), new DF.Constant(1)])) expect(fn.compute(DF.context(0, 0, 0))).toEqual(-1) expect(fn.compute(DF.context(0, 3, 0))).toEqual(-1) expect(fn.compute(DF.context(0, 4, 0))).toEqual(0) @@ -206,4 +181,21 @@ describe('DensityFunction', () => { expect(fn.compute(DF.context(0, 12, 0))).toEqual(1) expect(fn.compute(DF.context(0, 18, 0))).toEqual(1) }) + + it('Gradient', () => { + const fn1 = wrap(new DF.Gradient('y', 'clamp_to_edge', 0, 128, 0, 128)) + expect(fn1.compute(DF.context(0, -5, 0))).toEqual(0) + expect(fn1.compute(DF.context(0, 0, 0))).toEqual(0) + expect(fn1.compute(DF.context(0, 4, 0))).toEqual(4) + expect(fn1.compute(DF.context(0, 127, 0))).toEqual(127) + expect(fn1.compute(DF.context(0, 128, 0))).toEqual(128) + expect(fn1.compute(DF.context(0, 129, 0))).toEqual(128) + const fn2 = wrap(new DF.Gradient('y', 'clamp_to_edge', -64, 320, 1.5, -1.5)) + expect(fn2.compute(DF.context(0, -70, 0))).toEqual(1.5) + expect(fn2.compute(DF.context(0, -25, 0))).toEqual(1.1953125) + expect(fn2.compute(DF.context(0, 75, 0))).toEqual(0.4140625) + expect(fn2.compute(DF.context(0, 137, 0))).toEqual(-0.0703125) + expect(fn2.compute(DF.context(0, 256, 0))).toEqual(-1) + expect(fn2.compute(DF.context(0, 335, 0))).toEqual(-1.5) + }) }) diff --git a/test/worldgen/NoiseChunk.test.ts b/test/worldgen/NoiseChunk.test.ts deleted file mode 100644 index 35856415..00000000 --- a/test/worldgen/NoiseChunk.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { BlockState } from '../../src/core/index.js' -import type { NoiseGeneratorSettings } from '../../src/worldgen/index.js' -import { DensityFunction as DF, FluidStatus, NoiseChunk, NoiseRouter, NoiseSettings, SurfaceRule } from '../../src/worldgen/index.js' -import { RandomState } from '../../src/worldgen/RandomState.js' - -describe('NoiseChunk', () => { - const setup = (routerMods: Partial = {}) => { - const noiseSettings: NoiseSettings = { - minY: -64, - height: 384, - xzSize: 1, - ySize: 2, - } - const cellWidth = NoiseSettings.cellWidth(noiseSettings) - const cellCountXZ = Math.floor(16 / cellWidth) // 4 - const cellHeight = NoiseSettings.cellHeight(noiseSettings) - const cellNoiseMinY = Math.floor(noiseSettings.minY / cellHeight) // -8 - const cellCountY = Math.floor(noiseSettings.height / cellHeight) // 48 - const settings: NoiseGeneratorSettings = { - aquifersEnabled: false, - disableMobGeneration: false, - legacyRandomSource: false, - oreVeinsEnabled: false, - seaLevel: 63, - defaultBlock: BlockState.STONE, - defaultFluid: BlockState.WATER, - noise: noiseSettings, - noiseRouter: NoiseRouter.create(routerMods), - surfaceRule: SurfaceRule.NOOP, - } - const randomState = new RandomState(settings, BigInt(123)) - const chunk = new NoiseChunk(cellCountXZ, cellCountY, cellNoiseMinY, randomState, 16, -32, noiseSettings, false, () => new FluidStatus(-64, BlockState.AIR)) - return { chunk } - } - - it('getInterpolatedState (zeroes)', () => { - const { chunk } = setup() - expect(chunk.getFinalState(0, 0, 0)).toEqual(BlockState.AIR) - }) - - it('getInterpolatedState (flat terrain)', () => { - const { chunk } = setup({ finalDensity: new DF.YClampedGradient(-64, 320, -1, 1) }) - expect(chunk.getFinalState(0, 0, 0)).toEqual(BlockState.AIR) - expect(chunk.getFinalState(0, 200, 0)).toEqual(undefined) - }) -}) diff --git a/test/worldgen/NoiseChunkGenerator.test.ts b/test/worldgen/NoiseChunkGenerator.test.ts index 2c018392..b87af313 100644 --- a/test/worldgen/NoiseChunkGenerator.test.ts +++ b/test/worldgen/NoiseChunkGenerator.test.ts @@ -12,8 +12,6 @@ describe('NoiseChunkGenerator', () => { noise: { minY: 0, height: 64, - xzSize: 1, - ySize: 1, ...noiseSettings, }, noiseRouter: NoiseRouter.create(router), @@ -32,11 +30,11 @@ describe('NoiseChunkGenerator', () => { WorldgenRegistries.NOISE.clear() }) - it('fill', () => { + it('buildTerrain', () => { const finalDensity = new DF.NoiseFunction(Holder.reference(WorldgenRegistries.NOISE, Identifier.create('offset')), 1, 1, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO) const { generator, randomState } = setup(BigInt(123), {}, {}, { finalDensity }) const chunk = new Chunk(0, 64, ChunkPos.create(4, 1)) - generator.fill(randomState, chunk) + generator.buildTerrain(randomState, chunk) expect(printSlice(chunk)).toEqual('.....XXXX.......|.....XXX........|.....XXX....X...|......X....XX..X|............X..X|.X.............X|XXX.............|XXX.............|XXX.............|XXX......XX....X|XXX.....XXXXXXXX|XXXX..XXXXXXXXXX|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXX.|XXXXXXX.XXXXXXX.|XXXXXX...XXXXXXX|XXX.........XXXX|XXX.........XXXX|XXX..........XXX|XX...........XXX|..............XX|...............X|................|X...XXXX........|XXXXXXXXXXXX..X.|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXX|XXXXXXXXXXX..XXX|XXXXXXXXXX..XXXX|XXXXXXXX........|XXXXXXXX........|XXXXXXXX........|XXXXXXXX........|XXXXXXXXX.XXX...|XXXXXXXXXXXXXX..|XXXXXXX.XXXXXXX.|XXXXX...XXXXXXX.|XXXXX....XXXXX..|XXXXXX...XXXXX..|XXXXXX...XXXXX..|XXXXXXXX..XXX...|.XXXX......XXX..|............X...|...........XX...|............XXX.|............XXX.|...........XXXXX|..XX........XXXX|..XX.........XX.|..XX.........X..|.XXX.........X..|XXX.............|XX..............|XXX.............|XXXX............|XXXX............|XXX.............|XX..............|................|.....XXXX......X|...XXXXXX....XXX') }) }) diff --git a/test/worldgen/NoiseSettings.test.ts b/test/worldgen/NoiseSettings.test.ts deleted file mode 100644 index 649ac2c5..00000000 --- a/test/worldgen/NoiseSettings.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { NoiseSettings } from '../../src/worldgen/index.js' - -describe('NoiseSettings', () => { - const setup = () => { - const settings: NoiseSettings = { - minY: -64, - height: 384, - xzSize: 1, - ySize: 2, - } - return { settings } - } - - it('cellWidth', () => { - const { settings } = setup() - expect(NoiseSettings.cellWidth(settings)).toEqual(4) - }) - - it('cellHeight', () => { - const { settings } = setup() - expect(NoiseSettings.cellHeight(settings)).toEqual(8) - }) - - it('cellCountY', () => { - const { settings } = setup() - expect(NoiseSettings.cellCountY(settings)).toEqual(48) - }) - - it('minCellY', () => { - const { settings } = setup() - expect(NoiseSettings.minCellY(settings)).toEqual(-8) - }) -}) From cba54dc30395de5f8d7f9650361fab7b4cf60b74 Mon Sep 17 00:00:00 2001 From: Misode Date: Sat, 5 Sep 2026 01:23:23 +0200 Subject: [PATCH 4/4] Refactor material system and update to 26.3 --- src/math/Util.ts | 4 +- src/worldgen/HeightProvider.ts | 4 +- src/worldgen/LevelHeight.ts | 4 - src/worldgen/MaterialSystem.ts | 499 ++++++++++++++++++ src/worldgen/NoiseChunkGenerator.ts | 25 +- src/worldgen/NoiseGeneratorSettings.ts | 8 +- src/worldgen/RandomState.ts | 20 +- src/worldgen/SurfaceSystem.ts | 283 ---------- src/worldgen/VerticalAnchor.ts | 27 +- src/worldgen/WorldgenRegistries.ts | 19 +- src/worldgen/index.ts | 3 +- src/worldgen/structure/WorldgenStructure.ts | 40 +- test/math/Util.test.ts | 7 +- test/math/random/XoroshiroRandom.test.ts | 18 +- .../biome/MultiNoiseBiomeSource.test.ts | 7 +- 15 files changed, 611 insertions(+), 357 deletions(-) delete mode 100644 src/worldgen/LevelHeight.ts create mode 100644 src/worldgen/MaterialSystem.ts delete mode 100644 src/worldgen/SurfaceSystem.ts diff --git a/src/math/Util.ts b/src/math/Util.ts index 2f75540a..09ee1bde 100644 --- a/src/math/Util.ts +++ b/src/math/Util.ts @@ -97,8 +97,8 @@ export function binarySearch(n: number, n2: number, predicate: (value: number) = } export function getSeed(x: number, y: number, z: number) { - let seed = BigInt(x * 3129871) ^ BigInt(z) * BigInt(116129781) ^ BigInt(y) - seed = seed * seed * BigInt(42317861) + seed * BigInt(11) + let seed = BigInt.asIntN(64, BigInt(x) * BigInt(3129871) ^ BigInt(z) * BigInt(116129781) ^ BigInt(y)) + seed = BigInt.asIntN(64, BigInt.asIntN(64, seed * seed * BigInt(42317861)) + BigInt.asIntN(64, seed * BigInt(11))) return seed >> BigInt(16) } diff --git a/src/worldgen/HeightProvider.ts b/src/worldgen/HeightProvider.ts index 03042459..6859c1b9 100644 --- a/src/worldgen/HeightProvider.ts +++ b/src/worldgen/HeightProvider.ts @@ -1,10 +1,10 @@ import type { Random } from '../math/index.js' import { nextInt, randomBetweenInclusive } from '../math/index.js' import { Json } from '../util/index.js' -import type { NoiseSettings } from './NoiseSettings.js' +import type { WorldgenContext } from './VerticalAnchor.js' import { VerticalAnchor } from './VerticalAnchor.js' -export type HeightProvider = (random: Random, context: NoiseSettings) => number +export type HeightProvider = (random: Random, context: WorldgenContext) => number export namespace HeightProvider { diff --git a/src/worldgen/LevelHeight.ts b/src/worldgen/LevelHeight.ts deleted file mode 100644 index c6766e46..00000000 --- a/src/worldgen/LevelHeight.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface LevelHeight { - minY: number, - height: number, -} diff --git a/src/worldgen/MaterialSystem.ts b/src/worldgen/MaterialSystem.ts new file mode 100644 index 00000000..12195d29 --- /dev/null +++ b/src/worldgen/MaterialSystem.ts @@ -0,0 +1,499 @@ + +import type { Chunk } from '../core/index.js' +import { BlockPos, BlockState, ChunkPos, Holder, HolderSet, Identifier } from '../core/index.js' +import type { Noise, PositionalRandom } from '../math/index.js' +import { map } from '../math/index.js' +import { computeIfAbsent, Json } from '../util/index.js' +import { DensityFunction } from './DensityFunction.js' +import type { NoiseChunk } from './NoiseChunk.js' +import type { RandomState } from './RandomState.js' +import type { WorldgenContext } from './VerticalAnchor.js' +import { VerticalAnchor } from './VerticalAnchor.js' +import { WorldgenRegistries } from './WorldgenRegistries.js' + +export class MaterialSystem { + private readonly surfaceNoise: Noise + private readonly surfaceSecondaryNoise: Noise + + constructor( + private readonly randomState: RandomState, + private readonly defaultBlock: BlockState, + private readonly seaLevel: number, + private readonly preliminarySurface: DensityFunction, + private readonly random: PositionalRandom, + ) { + this.surfaceNoise = randomState.getOrCreateNoise(Identifier.create('surface')) + this.surfaceSecondaryNoise = randomState.getOrCreateNoise(Identifier.create('surface_secondary')) + } + + public buildSurface(chunk: Chunk, noiseChunk: NoiseChunk, sourceRule: MaterialRule, worldgenContext: WorldgenContext, biomeGetter: (pos: BlockPos) => Identifier) { + const minX = ChunkPos.minBlockX(chunk.pos) + const minZ = ChunkPos.minBlockZ(chunk.pos) + const context = new MaterialRuleContext(this, this.randomState, chunk, noiseChunk, worldgenContext, biomeGetter) + const ruleWithContext = sourceRule.compile(context) + + for (let x = 0; x < 16; x += 1) { + const blockX = minX + x + for (let z = 0; z < 1; z += 1) { + const blockZ = minZ + z + context.updateXZ(blockX, blockZ) + let stoneAboveDepth = 0 + let waterHeight = Number.MIN_SAFE_INTEGER + let nextCeilingStoneY = Number.MAX_SAFE_INTEGER + + for (let y = chunk.maxY; y >= chunk.minY; y -= 1) { + const blockPos = BlockPos.create(blockX, y, blockZ) + const old = chunk.getBlockState(blockPos) + if (old.equals(BlockState.AIR)) { + stoneAboveDepth = 0 + waterHeight = Number.MIN_SAFE_INTEGER + continue + } + if (old.isFluid()) { + if (waterHeight === Number.MIN_SAFE_INTEGER) { + waterHeight = y + 1 + } + continue + } + if (nextCeilingStoneY >= y) { + nextCeilingStoneY = Number.MIN_SAFE_INTEGER + for (let lookaheadY = y - 1; lookaheadY >= chunk.minY; lookaheadY -= 1) { + const nextState = chunk.getBlockState(BlockPos.create(blockX, lookaheadY, blockZ)) + if (nextState.equals(BlockState.AIR) || nextState.isFluid()) { + nextCeilingStoneY = lookaheadY + 1 + break + } + } + } + stoneAboveDepth += 1 + const stoneBelowDepth = y - nextCeilingStoneY + 1 + context.updateY(stoneAboveDepth, stoneBelowDepth, waterHeight, y) + const state = ruleWithContext(blockX, y, blockZ) + if (state) { + chunk.setBlockState(blockPos, state) + } + } + } + } + } + + public getSurfaceDepth(x: number, z: number) { + const noise = this.surfaceNoise.get3D(x, 0, z) + const offset = this.random.at(x, 0, z).nextDouble() * 0.25 + return noise * 2.75 + 3 + offset + } + + public getSurfaceSecondary(x: number, z: number) { + return this.surfaceSecondaryNoise.get3D(x, 0, z) + } + + public getOrCreateRandom(name: Identifier): PositionalRandom { + return this.randomState.getOrCreateRandom(name) + } +} + +export class MaterialRuleContext { + public blockX: number = 0 + public blockY: number = 0 + public blockZ: number = 0 + public stoneDepthAbove: number = 0 + public stoneDepthBelow: number = 0 + public surfaceDepth: number = 0 + public waterHeight: number = 0 + + public lastUpdateXZ: number = Number.MIN_SAFE_INTEGER + public lastUpdateY: number = Number.MIN_SAFE_INTEGER + private lastSurfaceDepth2Update: number = Number.MIN_SAFE_INTEGER + private surfaceSecondary: number = 0 + private lastMinSurfaceLevelUpdate: number = Number.MIN_SAFE_INTEGER + private minSurfaceLevel: number = 0 + private biome: Identifier | undefined + private readonly noiseSamplers2d = new Map number>() + private readonly noiseSamplers3d = new Map number>() + + constructor( + public readonly system: MaterialSystem, + public readonly randomState: RandomState, + public readonly chunk: Chunk, + public readonly noiseChunk: NoiseChunk, + public readonly context: WorldgenContext, + private readonly biomeGetter: (pos: BlockPos) => Identifier, + ) {} + + public updateXZ(x: number, z: number) { + this.lastUpdateXZ += 1 + this.lastUpdateY += 1 + this.blockX = x + this.blockZ = z + this.surfaceDepth = this.system.getSurfaceDepth(x, z) + } + + public updateY(stoneDepthAbove: number, stoneDepthBelow: number, waterHeight: number, y: number) { + this.lastUpdateY += 1 + this.biome = undefined + this.blockY = y + this.stoneDepthAbove = stoneDepthAbove + this.stoneDepthBelow = stoneDepthBelow + this.waterHeight = waterHeight + } + + public getSurfaceSecondary() { + if (this.lastSurfaceDepth2Update !== this.lastUpdateXZ) { + this.lastSurfaceDepth2Update = this.lastUpdateXZ + this.surfaceSecondary = this.system.getSurfaceSecondary(this.blockX, this.blockZ) + } + return this.surfaceSecondary + } + + public getMinSurfaceLevel() { + if (this.lastMinSurfaceLevelUpdate !== this.lastUpdateXZ) { + this.lastMinSurfaceLevelUpdate = this.lastUpdateXZ + const preliminarySurfaceLevel = this.noiseChunk.randomState.router.chunkSurfaceLevel.compute(DensityFunction.context(this.blockX, 0, this.blockZ)) + this.minSurfaceLevel = Math.floor(preliminarySurfaceLevel) + this.surfaceDepth - 8 + } + return this.minSurfaceLevel + } + + public getBiome() { + if (this.biome === undefined) { + this.biome = this.biomeGetter(BlockPos.create(this.blockX, this.blockY, this.blockZ)) + } + return this.biome + } + + public resolveAnchorY(anchor: VerticalAnchor) { + return anchor(this.context) + } + + public getNoiseSampler(noiseId: Identifier, is3d: boolean) { + if (is3d) { + return computeIfAbsent(this.noiseSamplers2d, noiseId.toString(), () => this.createNoiseSampler2d(noiseId)) + } else { + return computeIfAbsent(this.noiseSamplers3d, noiseId.toString(), () => this.createNoiseSampler3d(noiseId)) + } + } + + private createNoiseSampler2d(noiseId: Identifier): () => number { + const noise = this.randomState.getOrCreateNoise(noiseId) + let lastUpdateXZ = this.lastUpdateXZ - 1 + let lastValue = 0 + return () => { + if (lastUpdateXZ !== this.lastUpdateXZ) { + lastValue = noise.get3D(this.blockX, 0, this.blockZ) + lastUpdateXZ = this.lastUpdateXZ + } + return lastValue + } + } + + private createNoiseSampler3d(noiseId: Identifier): () => number { + const noise = this.randomState.getOrCreateNoise(noiseId) + let lastUpdateY = this.lastUpdateY - 1 + let lastValue = 0 + return () => { + if (lastUpdateY !== this.lastUpdateY) { + lastValue = noise.get3D(this.blockX, this.blockY, this.blockZ) + lastUpdateY = this.lastUpdateY + } + return lastValue + } + } +} + +export type RuleEvaluator = (x: number, y: number, z: number) => BlockState | undefined + +export abstract class MaterialRule { + abstract compile(context: MaterialRuleContext): RuleEvaluator +} + +export namespace MaterialRule { + export const NOOP: MaterialRule = {compile: () => () => undefined} + + export function fromJson(obj: unknown): MaterialRule { + if (typeof obj === 'string') { + return new HolderHolder(Holder.reference(WorldgenRegistries.MATERIAL_RULE, Identifier.parse(obj))) + } + const root = Json.readObject(obj) ?? {} + const type = Json.readString(root.type)?.replace(/^minecraft:/, '') + switch (type) { + case 'block': return new Block(BlockState.fromJson(root.result_state)) + case 'sequence': return new Sequence(Json.readArray(root.sequence, MaterialRule.fromJson) ?? []) + case 'condition': return new Condition(MaterialCondition.fromJson(root.if_true), MaterialRule.fromJson(root.then_run)) + default: return NOOP + } + } + + export class HolderHolder extends MaterialRule { + constructor( + private readonly holder: Holder, + ) { + super() + } + compile(context: MaterialRuleContext): RuleEvaluator { + return this.holder.value().compile(context) + } + } + + export class Block extends MaterialRule { + constructor( + private readonly resultState: BlockState, + ) { + super() + } + compile(context: MaterialRuleContext): RuleEvaluator { + return () => this.resultState + } + } + + export class Sequence extends MaterialRule { + constructor( + private readonly sequence: MaterialRule[], + ) { + super() + } + compile(context: MaterialRuleContext): RuleEvaluator { + if (this.sequence.length === 0) { + return () => undefined + } + if (this.sequence.length === 1) { + return this.sequence[0].compile(context) + } + const evaluators = this.sequence.map(s => s.compile(context)) + return (x, y, z) => { + for (const rule of evaluators) { + const state = rule(x, y, z) + if (state) { + return state + } + } + return undefined + } + } + } + + export class Condition extends MaterialRule { + constructor( + private readonly ifTrue: MaterialCondition, + private readonly thenRun: MaterialRule, + ) { + super() + } + compile(context: MaterialRuleContext): RuleEvaluator { + const ifTrueEvaluator = this.ifTrue.compile(context) + const thenRunEvaluator = this.thenRun.compile(context) + return (x, y, z) => { + if (ifTrueEvaluator()) { + return thenRunEvaluator(x, y, z) + } + return undefined + } + } + } +} + +export type ConditionEvaluator = () => boolean + +export abstract class MaterialCondition { + abstract compile(context: MaterialRuleContext): ConditionEvaluator +} + +export namespace MaterialCondition { + export const ALWAYS_FALSE: MaterialCondition = {compile: () => () => false} + export const ALWAYS_TRUE: MaterialCondition = {compile: () => () => true} + + export function fromJson(obj: unknown): MaterialCondition { + if (typeof obj === 'string') { + return new HolderHolder(Holder.reference(WorldgenRegistries.MATERIAL_CONDITION, Identifier.parse(obj))) + } + const root = Json.readObject(obj) ?? {} + const type = Json.readString(root.type)?.replace(/^minecraft:/, '') + switch (type) { + case 'above_preliminary_surface': return new AbovePreliminarySurface() + case 'biome': return new Biome( + HolderSet.fromJson(WorldgenRegistries.BIOME, root.biome_is), + ) + case 'noise_threshold': return new NoiseThreshold( + Identifier.parse(Json.readString(root.noise) ?? ''), + Json.readNumber(root.min_threshold) ?? 0, + Json.readNumber(root.max_threshold) ?? 0, + Json.readBoolean(root.is_3d) ?? false, + ) + case 'not': return new Not(MaterialCondition.fromJson(root.invert)) + case 'stone_depth': return new StoneDepth( + Json.readInt(root.offset) ?? 0, + Json.readBoolean(root.add_surface_depth) ?? false, + Json.readInt(root.secondary_depth_range) ?? 0, + Json.readString(root.surface_type) === 'ceiling', + ) + case 'vertical_gradient': return new VerticalGradient( + Identifier.parse(Json.readString(root.random_name) ?? ''), + VerticalAnchor.fromJson(root.true_at_and_below), + VerticalAnchor.fromJson(root.false_at_and_above), + ) + case 'water': return new Water( + Json.readInt(root.offset) ?? 0, + Json.readInt(root.surface_depth_multiplier) ?? 0, + Json.readBoolean(root.add_surface_depth) ?? false, + ) + case 'y_above': return new YAbove( + VerticalAnchor.fromJson(root.anchor), + Json.readInt(root.surface_depth_multiplier) ?? 0, + Json.readBoolean(root.add_surface_depth) ?? false, + ) + } + return ALWAYS_FALSE + } + + function lazyYCondition(context: MaterialRuleContext, evaluator: ConditionEvaluator): ConditionEvaluator { + let lastUpdateY = context.lastUpdateY - 1 + let result = false + return () => { + if (context.lastUpdateY !== lastUpdateY) { + lastUpdateY = context.lastUpdateY + result = evaluator() + } + return result + } + } + + export class HolderHolder extends MaterialCondition { + constructor( + private readonly holder: Holder, + ) { + super() + } + compile(context: MaterialRuleContext): ConditionEvaluator { + return this.holder.value().compile(context) + } + } + + export class AbovePreliminarySurface extends MaterialCondition { + compile(context: MaterialRuleContext): ConditionEvaluator { + return () => context.blockY >= context.getMinSurfaceLevel() + } + } + + export class Biome extends MaterialCondition { + constructor( + private readonly biomes: HolderSet<{}>, + ) { + super() + } + compile(context: MaterialRuleContext): ConditionEvaluator { + const biomeSet = new Set(Array(...this.biomes.getEntries()).map(h => h.key()!.toString())) + return lazyYCondition(context, () => { + return biomeSet.has(context.getBiome().toString()) + }) + } + } + + export class NoiseThreshold extends MaterialCondition { + constructor( + private readonly noise: Identifier, + private readonly minThreshold: number, + private readonly maxThreshold: number, + private readonly is3d: boolean, + ) { + super() + } + compile(context: MaterialRuleContext): ConditionEvaluator { + const noise = context.getNoiseSampler(this.noise, this.is3d) + return () => { + const value = noise() + return this.minThreshold <= value && value <= this.maxThreshold + } + } + } + + export class Not extends MaterialCondition { + constructor( + private readonly invert: MaterialCondition, + ) { + super() + } + compile(context: MaterialRuleContext): ConditionEvaluator { + const invertEvaluator = this.invert.compile(context) + return () => !invertEvaluator() + } + } + + export class StoneDepth extends MaterialCondition { + constructor( + private readonly offset: number, + private readonly addSurfaceDepth: boolean, + private readonly secondaryDepthRange: number, + private readonly ceiling: boolean, + ) { + super() + } + compile(context: MaterialRuleContext): ConditionEvaluator { + return lazyYCondition(context, () => { + const stoneDepth = this.ceiling ? context.stoneDepthBelow : context.stoneDepthAbove + const surfaceDepth = this.addSurfaceDepth ? context.surfaceDepth : 0 + const secondaryDepth = this.secondaryDepthRange === 0 ? 0 : map(context.getSurfaceSecondary(), -1, 1, 0, this.secondaryDepthRange) + return stoneDepth <= 1 + this.offset + surfaceDepth + secondaryDepth + }) + } + } + + export class VerticalGradient extends MaterialCondition { + constructor( + private readonly randomName: Identifier, + private readonly trueAtAndBelow: VerticalAnchor, + private readonly falseAtAndAbove: VerticalAnchor, + ) { + super() + } + compile(context: MaterialRuleContext): ConditionEvaluator { + const trueAtAndBelowY = context.resolveAnchorY(this.trueAtAndBelow) + const falseAtAndAboveY = context.resolveAnchorY(this.falseAtAndAbove) + const randomFactory = context.system.getOrCreateRandom(this.randomName) + return lazyYCondition(context, () => { + if (context.blockY <= trueAtAndBelowY) { + return true + } + if (context.blockY >= falseAtAndAboveY) { + return false + } + const probability = map(context.blockY, trueAtAndBelowY, falseAtAndAboveY, 1, 0) + const random = randomFactory.at(context.blockX, context.blockY, context.blockZ) + return random.nextFloat() < probability + }) + } + } + + export class Water extends MaterialCondition { + constructor( + private readonly offset: number, + private readonly surfaceDepthMultiplier: number, + private readonly addStoneDepth: boolean, + ) { + super() + } + compile(context: MaterialRuleContext): ConditionEvaluator { + return lazyYCondition(context, () => { + if (context.waterHeight === Number.MIN_SAFE_INTEGER) { + return true + } + const stoneDepth = this.addStoneDepth ? context.stoneDepthAbove : 0 + return context.blockY + stoneDepth >= context.waterHeight + this.offset + context.surfaceDepth * this.surfaceDepthMultiplier + }) + } + } + + export class YAbove extends MaterialCondition { + constructor( + private readonly anchor: VerticalAnchor, + private readonly surfaceDepthMultiplier: number, + private readonly addStoneDepth: boolean, + ) { + super() + } + compile(context: MaterialRuleContext): ConditionEvaluator { + return lazyYCondition(context, () => { + const stoneDepth = this.addStoneDepth ? context.stoneDepthAbove : 0 + return context.blockY + stoneDepth >= context.resolveAnchorY(this.anchor) + context.surfaceDepth * this.surfaceDepthMultiplier + }) + } + } +} diff --git a/src/worldgen/NoiseChunkGenerator.ts b/src/worldgen/NoiseChunkGenerator.ts index 1d90e6df..1d09dc5f 100644 --- a/src/worldgen/NoiseChunkGenerator.ts +++ b/src/worldgen/NoiseChunkGenerator.ts @@ -1,5 +1,5 @@ import type { Chunk } from '../core/index.js' -import { BlockState, ChunkPos } from '../core/index.js' +import { BlockState, ChunkPos, Identifier } from '../core/index.js' import type { FluidPicker } from './Aquifer.js' import { FluidStatus } from './Aquifer.js' import type { BiomeSource } from './biome/index.js' @@ -8,6 +8,7 @@ import type { Heightmap } from './Heightmap.js' import { NoiseChunk } from './NoiseChunk.js' import type { NoiseGeneratorSettings } from './NoiseGeneratorSettings.js' import type { RandomState } from './RandomState.js' +import type { WorldgenContext } from './VerticalAnchor.js' export class NoiseChunkGenerator { private readonly globalFluidPicker: FluidPicker @@ -58,13 +59,13 @@ export class NoiseChunkGenerator { return undefined } - public buildTerrain(randomState: RandomState, chunk: Chunk, onlyFirstZ?: boolean, /** @deprecated */ biome?: string) { + public buildTerrain(randomState: RandomState, chunk: Chunk, onlyFirstZ?: boolean, /** @deprecated */ biome?: Identifier) { const noiseChunk = this.createNoiseChunk(randomState, chunk, onlyFirstZ) this.doFill(chunk, noiseChunk) this.buildSurface(chunk, noiseChunk, biome) } - private doFill(chunk: Chunk, noiseChunk: NoiseChunk, onlyFirstZ?: boolean) { + private doFill(chunk: Chunk, noiseChunk: NoiseChunk) { const volume = noiseChunk.volume const finalDensity = noiseChunk.randomState.router.finalDensity const densityBuffer = finalDensity.computeVolume(volume) @@ -83,8 +84,14 @@ export class NoiseChunkGenerator { } } - private buildSurface(chunk: Chunk, noiseChunk: NoiseChunk, /** @deprecated */ biome: string = 'minecraft:plains') { - noiseChunk.randomState.surfaceSystem.buildSurface(chunk, noiseChunk, this.settings.noise, () => biome) + private buildSurface(chunk: Chunk, noiseChunk: NoiseChunk, /** @deprecated */ biome?: Identifier) { + const surfaceBiome = biome ?? Identifier.create('plains') + const context: WorldgenContext = { + minY: this.settings.noise.minY, + height: this.settings.noise.height, + seaLevel: this.settings.seaLevel, + } + noiseChunk.randomState.surfaceSystem.buildSurface(chunk, noiseChunk, this.settings.materialRule, context, () => surfaceBiome) } public computeBiome(randomState: RandomState, quartX: number, quartY: number, quartZ: number) { @@ -95,4 +102,12 @@ export class NoiseChunkGenerator { const volume = new DensityVolume(16, this.settings.noise.height, onlyFirstZ ? 1 : 16, ChunkPos.minBlockX(chunk.pos), this.settings.noise.minY, ChunkPos.minBlockZ(chunk.pos)) return new NoiseChunk(randomState, this.settings, this.globalFluidPicker, volume) } + + public getWorldgenContext(): WorldgenContext { + return { + minY: this.settings.noise.minY, + height: this.settings.noise.height, + seaLevel: this.settings.seaLevel, + } + } } diff --git a/src/worldgen/NoiseGeneratorSettings.ts b/src/worldgen/NoiseGeneratorSettings.ts index befa61cb..c8ecfd47 100644 --- a/src/worldgen/NoiseGeneratorSettings.ts +++ b/src/worldgen/NoiseGeneratorSettings.ts @@ -1,16 +1,16 @@ import { BlockState } from '../core/index.js' import { Json } from '../util/index.js' import { AquiferConfig } from './Aquifer.js' +import { MaterialRule } from './MaterialSystem.js' import { NoiseRouter } from './NoiseRouter.js' import { NoiseSettings } from './NoiseSettings.js' -import { SurfaceRule } from './SurfaceSystem.js' export interface NoiseGeneratorSettings { noise: NoiseSettings, defaultBlock: BlockState, defaultFluid: BlockState, noiseRouter: NoiseRouter, - materialRule: SurfaceRule, + materialRule: MaterialRule, seaLevel: number, aquifers: AquiferConfig | undefined, legacyRandomSource: boolean, @@ -24,7 +24,7 @@ export namespace NoiseGeneratorSettings { defaultBlock: BlockState.fromJson(root.default_block), defaultFluid: BlockState.fromJson(root.default_fluid), noiseRouter: NoiseRouter.fromJson(root.noise_router), - materialRule: SurfaceRule.fromJson(root.material_rule), + materialRule: MaterialRule.fromJson(root.material_rule), seaLevel: Json.readInt(root.sea_level) ?? 0, aquifers: root.aquifers ? AquiferConfig.fromJson(root.aquifers) : undefined, legacyRandomSource: Json.readBoolean(root.legacy_random_source) ?? false, @@ -37,7 +37,7 @@ export namespace NoiseGeneratorSettings { defaultBlock: BlockState.STONE, defaultFluid: BlockState.WATER, noiseRouter: NoiseRouter.create({}), - materialRule: SurfaceRule.NOOP, + materialRule: MaterialRule.NOOP, seaLevel: 0, aquifers: undefined, legacyRandomSource: false, diff --git a/src/worldgen/RandomState.ts b/src/worldgen/RandomState.ts index 713ed513..b5e45033 100644 --- a/src/worldgen/RandomState.ts +++ b/src/worldgen/RandomState.ts @@ -1,37 +1,37 @@ import type { Holder } from '../core/index.js' import { Registry } from '../core/index.js' -import type { NoiseSettings, NormalNoise } from '../index.js' +import type { NormalNoise } from '../index.js' import { BlendedNoise, computeIfAbsent, DensityFunction, Identifier, LegacyRandom, XoroshiroRandom } from '../index.js' import type { Noise, PositionalRandom, Random } from '../math/index.js' import { Climate } from './biome/index.js' +import { MaterialSystem } from './MaterialSystem.js' import type { NoiseGeneratorSettings } from './NoiseGeneratorSettings.js' import { NoiseRouter } from './NoiseRouter.js' -import { SurfaceSystem } from './SurfaceSystem.js' export class RandomState { private readonly noiseInstances: Map - private readonly randomCache: Map + private readonly positionalRandoms: Map public readonly random: PositionalRandom - public readonly surfaceSystem: SurfaceSystem + public readonly surfaceSystem: MaterialSystem public readonly router: NoiseRouter public readonly sampler: Climate.Sampler - constructor ( + constructor( settings: NoiseGeneratorSettings, public readonly seed: bigint, ) { this.noiseInstances = new Map() - this.randomCache = new Map() + this.positionalRandoms = new Map() this.random = (settings.legacyRandomSource ? new LegacyRandom(seed) : XoroshiroRandom.create(seed)).forkPositional() - this.router = NoiseRouter.mapAll(settings.noiseRouter, this.createVisitor(settings.noise, settings.legacyRandomSource)) + this.router = NoiseRouter.mapAll(settings.noiseRouter, this.createVisitor(settings.legacyRandomSource)) const preliminarySurface = this.router.chunkSurfaceLevel - this.surfaceSystem = new SurfaceSystem(this, settings.materialRule, settings.defaultBlock, preliminarySurface, seed) + this.surfaceSystem = new MaterialSystem(this, settings.defaultBlock, settings.seaLevel, preliminarySurface, this.random) this.sampler = Climate.Sampler.fromRouter(this.router) } - public createVisitor(noiseSettings: NoiseSettings, legacyRandom: boolean) { + public createVisitor(legacyRandom: boolean) { const mapped = new Map() const getNoiseSampler = (noise: Holder): Noise => { const key = noise.key() @@ -92,7 +92,7 @@ export class RandomState { } public getOrCreateRandom(id: Identifier) { - return computeIfAbsent(this.randomCache, id.toString(), key => + return computeIfAbsent(this.positionalRandoms, id.toString(), key => this.random.fromHashOf(key).forkPositional() ) } diff --git a/src/worldgen/SurfaceSystem.ts b/src/worldgen/SurfaceSystem.ts deleted file mode 100644 index 3164660f..00000000 --- a/src/worldgen/SurfaceSystem.ts +++ /dev/null @@ -1,283 +0,0 @@ - -import type { Chunk } from '../core/index.js' -import { BlockPos, BlockState, ChunkPos, Identifier } from '../core/index.js' -import type { Noise, PositionalRandom, Random } from '../math/index.js' -import { map, XoroshiroRandom } from '../math/index.js' -import { computeIfAbsent, Json, lazy } from '../util/index.js' -import { DensityFunction } from './DensityFunction.js' -import type { NoiseChunk } from './NoiseChunk.js' -import type { NoiseSettings } from './NoiseSettings.js' -import type { RandomState } from './RandomState.js' -import { VerticalAnchor } from './VerticalAnchor.js' - -export class SurfaceSystem { - private readonly surfaceNoise: Noise - private readonly surfaceSecondaryNoise: Noise - private readonly random: PositionalRandom - private readonly positionalRandoms: Map - - constructor( - randomState: RandomState, - private readonly rule: SurfaceRule, - private readonly defaultBlock: BlockState, - private readonly preliminarySurface: DensityFunction, - seed: bigint, - ) { - this.random = XoroshiroRandom.create(seed).forkPositional() - this.surfaceNoise = randomState.getOrCreateNoise(Identifier.create('surface')) - this.surfaceSecondaryNoise = randomState.getOrCreateNoise(Identifier.create('surface_secondary')) - this.positionalRandoms = new Map() - } - - public buildSurface(chunk: Chunk, noiseChunk: NoiseChunk, worldgenContext: NoiseSettings, getBiome: (pos: BlockPos) => string) { - const minX = ChunkPos.minBlockX(chunk.pos) - const minZ = ChunkPos.minBlockZ(chunk.pos) - const surfaceContext = new SurfaceContext(this, chunk, noiseChunk, worldgenContext, getBiome) - const ruleWithContext = this.rule(surfaceContext) - - for (let x = 0; x < 16; x += 1) { - const worldX = minX + x - for (let z = 0; z < 1; z += 1) { - const worldZ = minZ + z - surfaceContext.updateXZ(worldX, worldZ) - let stoneDepthAbove = 0 - let waterHeight = Number.MIN_SAFE_INTEGER - let stoneDepthOffset = Number.MAX_SAFE_INTEGER - - for (let y = chunk.maxY; y >= chunk.minY; y -= 1) { - const worldPos = BlockPos.create(worldX, y, worldZ) - const oldState = chunk.getBlockState(worldPos) - if (oldState.equals(BlockState.AIR)) { - stoneDepthAbove = 0 - waterHeight = Number.MIN_SAFE_INTEGER - continue - } - if (oldState.isFluid()) { - if (waterHeight === Number.MIN_SAFE_INTEGER) { - waterHeight = y + 1 - } - continue - } - if (stoneDepthOffset >= y) { - stoneDepthOffset = Number.MIN_SAFE_INTEGER - for (let i = y - 1; i >= chunk.minY; i -= 1) { - const state = chunk.getBlockState(BlockPos.create(worldX, i, worldZ)) - if (state.equals(BlockState.AIR) || state.isFluid()) { - stoneDepthOffset = i + 1 - break - } - } - } - stoneDepthAbove += 1 - const stoneDepthBelow = y - stoneDepthOffset + 1 - - if (!oldState.equals(this.defaultBlock)) { - continue - } - surfaceContext.updateY(stoneDepthAbove, stoneDepthBelow, waterHeight, y) - const newState = ruleWithContext(worldX, y, worldZ) - if (newState) { - chunk.setBlockState(worldPos, newState) - } - } - } - } - } - - public getSurfaceDepth(x: number, z: number) { - const noise = this.surfaceNoise.get3D(x, 0, z) - const offset = this.random.at(x, 0, z).nextDouble() * 0.25 - return noise * 2.75 + 3 + offset - } - - public getSurfaceSecondary(x: number, z: number) { - return this.surfaceSecondaryNoise.get3D(x, 0, z) - } - - public getRandom(name: string): Random { - return computeIfAbsent(this.positionalRandoms, name, () => { - return this.random.fromHashOf(name) - }) - } -} - -export class SurfaceContext { - public blockX: number = 0 - public blockY: number = 0 - public blockZ: number = 0 - public stoneDepthAbove: number = 0 - public stoneDepthBelow: number = 0 - public surfaceDepth: number = 0 - public waterHeight: number = 0 - - public biome: () => string = () => '' - public surfaceSecondary: () => number = () => 0 - public minSurfaceLevel: () => number = () => 0 - - constructor( - public readonly system: SurfaceSystem, - public readonly chunk: Chunk, - public readonly noiseChunk: NoiseChunk, - public readonly context: NoiseSettings, - private readonly getBiome: (pos: BlockPos) => string, - ) {} - - public updateXZ(x: number, z: number) { - this.blockX = x - this.blockZ = z - this.surfaceDepth = this.system.getSurfaceDepth(x, z) - this.surfaceSecondary = lazy(() => this.system.getSurfaceSecondary(x, z)) - this.minSurfaceLevel = lazy(() => this.calculateMinSurfaceLevel(x, z)) - } - - public updateY(stoneDepthAbove: number, stoneDepthBelow: number, waterHeight: number, y: number) { - this.blockY = y - this.stoneDepthAbove = stoneDepthAbove - this.stoneDepthBelow = stoneDepthBelow - this.waterHeight = waterHeight - this.biome = lazy(() => this.getBiome(BlockPos.create(this.blockX, this.blockY, this.blockZ))) - } - - private calculateMinSurfaceLevel(x: number, z: number) { - return this.noiseChunk.randomState.router.chunkSurfaceLevel.compute(DensityFunction.context(x, 0, z)) - } -} - -export type SurfaceRule = (context: SurfaceContext) => (x: number, y: number, z: number) => BlockState | undefined - -export namespace SurfaceRule { - export const NOOP = () => () => undefined - - export function fromJson(obj: unknown): SurfaceRule { - const root = Json.readObject(obj) ?? {} - const type = Json.readString(root.type)?.replace(/^minecraft:/, '') - switch (type) { - case 'block': return block(BlockState.fromJson(root.result_state)) - case 'sequence': return sequence(Json.readArray(root.sequence, SurfaceRule.fromJson) ?? []) - case 'condition': return condition(SurfaceCondition.fromJson(root.if_true), SurfaceRule.fromJson(root.then_run)) - } - return NOOP - } - - export function block(state: BlockState): SurfaceRule { - return () => () => state - } - - export function sequence(rules: SurfaceRule[]): SurfaceRule { - return context => { - const rulesWithContext = rules.map(rule => rule(context)) - return (x, y, z) => { - for (const rule of rulesWithContext) { - const result = rule(x, y, z) - if (result) return result - } - return undefined - } - } - } - - export function condition(ifTrue: SurfaceCondition, thenRun: SurfaceRule): SurfaceRule { - return context => (x, y, z) => { - if (ifTrue(context)) { - return thenRun(context)(x, y, z) - } - return undefined - } - } -} - -export type SurfaceCondition = (context: SurfaceContext) => boolean - -export namespace SurfaceCondition { - export const FALSE = () => false - export const TRUE = () => true - - export function fromJson(obj: unknown): SurfaceCondition { - const root = Json.readObject(obj) ?? {} - const type = Json.readString(root.type)?.replace(/^minecraft:/, '') - switch (type) { - case 'above_preliminary_surface': return abovePreliminarySurface() - case 'biome': return biome( - Json.readArray(root.biome_is, e => Json.readString(e) ?? '') ?? [] - ) - case 'not': return not(SurfaceCondition.fromJson(root.invert)) - case 'stone_depth': return stoneDepth( - Json.readInt(root.offset) ?? 0, - Json.readBoolean(root.add_surface_depth) ?? false, - Json.readInt(root.secondary_depth_range) ?? 0, - Json.readString(root.surface_type) === 'ceiling', - ) - case 'vertical_gradient': return verticalGradient( - Json.readString(root.random_name) ?? '', - VerticalAnchor.fromJson(root.true_at_and_below), - VerticalAnchor.fromJson(root.false_at_and_above), - ) - case 'water': return water( - Json.readInt(root.offset) ?? 0, - Json.readInt(root.surface_depth_multiplier) ?? 0, - Json.readBoolean(root.add_surface_depth) ?? false, - ) - case 'y_above': return yAbove( - VerticalAnchor.fromJson(root.anchor), - Json.readInt(root.surface_depth_multiplier) ?? 0, - Json.readBoolean(root.add_surface_depth) ?? false, - ) - } - return FALSE - } - - export function abovePreliminarySurface(): SurfaceCondition { - return context => context.blockY >= context.minSurfaceLevel() - } - - export function biome(biomes: string[]): SurfaceCondition { - const biomeSet = new Set(biomes) - return context => biomeSet.has(context.biome()) - } - - export function not(invert: SurfaceCondition): SurfaceCondition { - return context => !invert(context) - } - - export function stoneDepth(offset: number, addSurfaceDepth: boolean, secondaryDepthRange: number, ceiling: boolean): SurfaceCondition { - return context => { - const depth = ceiling ? context.stoneDepthBelow : context.stoneDepthAbove - const surfaceDepth = addSurfaceDepth ? context.surfaceDepth : 0 - const secondaryDepth = secondaryDepthRange === 0 ? 0 : map(context.surfaceSecondary(), -1, 1, 0, secondaryDepthRange) - return depth <= 1 + offset + surfaceDepth + secondaryDepth - } - } - - export function verticalGradient(randomName: string, trueAtAndBelow: VerticalAnchor, falseAtAndAbove: VerticalAnchor): SurfaceCondition { - return context => { - const trueAtAndBelowY = trueAtAndBelow(context.context) - const falseAtAndAboveY = falseAtAndAbove(context.context) - if (context.blockY <= trueAtAndBelowY) { - return true - } - if (context.blockY >= falseAtAndAboveY) { - return false - } - const random = context.system.getRandom(randomName) - const chance = map(context.blockY, trueAtAndBelowY, falseAtAndAboveY, 1, 0) - return random.nextFloat() < chance - } - } - - export function water(offset: number, surfaceDepthMultiplier: number, addStoneDepth: boolean): SurfaceCondition { - return context => { - if (context.waterHeight === Number.MIN_SAFE_INTEGER) { - return true - } - const stoneDepth = addStoneDepth ? context.stoneDepthAbove : 0 - return context.blockY + stoneDepth >= context.waterHeight + offset + context.surfaceDepth * surfaceDepthMultiplier - } - } - - export function yAbove(anchor: VerticalAnchor, surfaceDepthMultiplier: number, addStoneDepth: boolean): SurfaceCondition { - return context => { - const stoneDepth = addStoneDepth ? context.stoneDepthAbove : 0 - return context.blockY + stoneDepth >= anchor(context.context) + context.surfaceDepth * surfaceDepthMultiplier - } - } -} diff --git a/src/worldgen/VerticalAnchor.ts b/src/worldgen/VerticalAnchor.ts index 74a3fd22..c3007a0e 100644 --- a/src/worldgen/VerticalAnchor.ts +++ b/src/worldgen/VerticalAnchor.ts @@ -1,7 +1,12 @@ import { Json } from '../util/index.js' -import type { NoiseSettings } from './NoiseSettings.js' -export type VerticalAnchor = (context: NoiseSettings) => number +export interface WorldgenContext { + minY: number, + height: number, + seaLevel: number, +} + +export type VerticalAnchor = (context: WorldgenContext) => number export namespace VerticalAnchor { export function fromJson(obj: unknown): VerticalAnchor { @@ -12,20 +17,26 @@ export namespace VerticalAnchor { return aboveBottom(Json.readNumber(root.above_bottom) ?? 0) } else if (root.below_top !== undefined) { return belowTop(Json.readNumber(root.below_top) ?? 0) + } else if (root.relative_to_sea_level !== undefined) { + return relativeToSeaLevel(Json.readNumber(root.relative_to_sea_level) ?? 0) } return () => 0 } - export function absolute(value: number): VerticalAnchor { - return () => value + export function absolute(y: number): VerticalAnchor { + return () => y + } + + export function aboveBottom(offset: number): VerticalAnchor { + return context => context.minY + offset } - export function aboveBottom(value: number): VerticalAnchor { - return context => context.minY + value + export function belowTop(offset: number): VerticalAnchor { + return context => context.minY + context.height - 1 - offset } - export function belowTop(value: number): VerticalAnchor { - return context => context.minY + context.height - 1 - value + export function relativeToSeaLevel(offset: number): VerticalAnchor { + return context => context.seaLevel + offset } } diff --git a/src/worldgen/WorldgenRegistries.ts b/src/worldgen/WorldgenRegistries.ts index 465323c2..d1c3e42f 100644 --- a/src/worldgen/WorldgenRegistries.ts +++ b/src/worldgen/WorldgenRegistries.ts @@ -2,18 +2,21 @@ import type { Holder } from '../core/index.js' import { Identifier, Registry } from '../core/index.js' import { NormalNoise } from '../math/index.js' import { DensityFunction } from './DensityFunction.js' +import { MaterialCondition, MaterialRule } from './MaterialSystem.js' import { NoiseGeneratorSettings } from './NoiseGeneratorSettings.js' -export namespace WorldgenRegistries { - export const NOISE = Registry.createAndRegister('worldgen/noise', NormalNoise.fromJson) - export const DENSITY_FUNCTION = Registry.createAndRegister('worldgen/density_function', obj => DensityFunction.fromJson(obj)) - export const NOISE_SETTINGS = Registry.createAndRegister('worldgen/noise_settings', NoiseGeneratorSettings.fromJson) - export const BIOME = Registry.createAndRegister<{}>('worldgen/biome') +export class WorldgenRegistries { + public static readonly NOISE = Registry.createAndRegister('worldgen/noise', NormalNoise.fromJson) + public static readonly DENSITY_FUNCTION = Registry.createAndRegister('worldgen/density_function', obj => DensityFunction.fromJson(obj)) + public static readonly NOISE_SETTINGS = Registry.createAndRegister('worldgen/noise_settings', NoiseGeneratorSettings.fromJson) + public static readonly MATERIAL_RULE = Registry.createAndRegister('worldgen/material_rule', MaterialRule.fromJson) + public static readonly MATERIAL_CONDITION = Registry.createAndRegister('worldgen/material_condition', MaterialCondition.fromJson) + public static readonly BIOME = Registry.createAndRegister<{}>('worldgen/biome') - export const SURFACE_NOISE = createNoise('surface', 0.9381732587751008, -6, [1, 1, 1]) - export const SURFACE_SECONDARY_NOISE = createNoise('surface_secondary', 1.0582769165096106, -6, [1, 1, 0, 1]) + public static readonly SURFACE_NOISE = WorldgenRegistries.createNoise('surface', 0.9381732587751008, -6, [1, 1, 1]) + public static readonly SURFACE_SECONDARY_NOISE = WorldgenRegistries.createNoise('surface_secondary', 1.0582769165096106, -6, [1, 1, 0, 1]) - function createNoise(name: string, baseAmplitude: number, firstOctave: number, amplitudes: number[]): Holder { + public static createNoise(name: string, baseAmplitude: number, firstOctave: number, amplitudes: number[]): Holder { return WorldgenRegistries.NOISE.register(Identifier.create(name), new NormalNoise(baseAmplitude, firstOctave, amplitudes.length, 'enabled', amplitudes), true) } } diff --git a/src/worldgen/index.ts b/src/worldgen/index.ts index 911d8ee4..7c32ee04 100644 --- a/src/worldgen/index.ts +++ b/src/worldgen/index.ts @@ -4,7 +4,7 @@ export * from './DensityFunction.js' export * from './DensityVolume.js' export * from './Heightmap.js' export * from './HeightProvider.js' -export * from './LevelHeight.js' +export * from './MaterialSystem.js' export * from './NoiseChunk.js' export * from './NoiseChunkGenerator.js' export * from './NoiseGeneratorSettings.js' @@ -12,7 +12,6 @@ export * from './NoiseRouter.js' export * from './NoiseSettings.js' export * from './RandomState.js' export * from './structure/index.js' -export * from './SurfaceSystem.js' export * from './VerticalAnchor.js' export * from './WorldgenRegistries.js' diff --git a/src/worldgen/structure/WorldgenStructure.ts b/src/worldgen/structure/WorldgenStructure.ts index e0c359f8..b416a605 100644 --- a/src/worldgen/structure/WorldgenStructure.ts +++ b/src/worldgen/structure/WorldgenStructure.ts @@ -5,14 +5,17 @@ import { Json } from '../../util/Json.js' import type { BiomeSource } from '../biome/index.js' import { Heightmap } from '../Heightmap.js' import { HeightProvider } from '../HeightProvider.js' -import type { LevelHeight } from '../LevelHeight.js' -import { NoiseChunkGenerator } from '../NoiseChunkGenerator.js' -import type { NoiseGeneratorSettings } from '../NoiseGeneratorSettings.js' -import { RandomState } from '../RandomState.js' +import type { NoiseChunkGenerator } from '../NoiseChunkGenerator.js' +import type { RandomState } from '../RandomState.js' import { WorldgenRegistries } from '../WorldgenRegistries.js' import { StructurePoolElement } from './StructurePoolElement.js' import { StructureTemplatePool } from './StructureTemplatePool.js' +export interface LevelHeight { + minY: number, + height: number, +} + export type SufaceLevelAccessor = (posX: number, posZ: number, heightmap: Heightmap) => number export abstract class WorldgenStructure { @@ -80,19 +83,12 @@ export namespace WorldgenStructure { } } - export class GenerationContext { - public readonly chunkGenerator: NoiseChunkGenerator - public readonly randomState: RandomState - - constructor( - public readonly seed: bigint, - public readonly biomeSource: BiomeSource, - public readonly settings: NoiseGeneratorSettings, - public readonly levelHeight: LevelHeight, - ) { - this.randomState = new RandomState(settings, seed) - this.chunkGenerator = new NoiseChunkGenerator(biomeSource, settings) - } + export interface GenerationContext { + chunkGenerator: NoiseChunkGenerator + biomeSource: BiomeSource, + levelHeight: LevelHeight, + randomState: RandomState + seed: bigint } const structurePoolParser = Holder.parser(StructureTemplatePool.REGISTRY, StructureTemplatePool.fromJson) @@ -164,8 +160,8 @@ export namespace WorldgenStructure { } public findGenerationPoint(chunkX: number, chunkZ: number, random: Random, context: WorldgenStructure.GenerationContext): BlockPos | undefined { - var y = this.startHeight(random, context.settings.noise) - const pos = BlockPos.create(chunkX << 4, y, chunkZ << 4) + const height = this.startHeight(random, context.chunkGenerator.getWorldgenContext()) + const pos = BlockPos.create(chunkX << 4, height, chunkZ << 4) const rotation = Rotation.getRandom(random) const startingPool = this.startingPoolHolder.value() @@ -174,7 +170,7 @@ export namespace WorldgenStructure { if (startingElement instanceof StructurePoolElement.EmptyPoolElement){ return undefined } else { - var startJigsawOffset: BlockPos + let startJigsawOffset: BlockPos if (this.startJigsawName){ const offset = JigsawStructure.getRandomNamedJigsaw(startingElement, this.startJigsawName, rotation, random) if (offset === undefined){ @@ -190,7 +186,7 @@ export namespace WorldgenStructure { const x = ((boundingBox[1][0] + boundingBox[0][0]) / 2)^0 const z = ((boundingBox[1][2] + boundingBox[0][2]) / 2)^0 - var y: number + let y: number if (this.projectStartToHeightmap){ y = pos[1] + context.chunkGenerator.getBaseHeight(x, z, this.projectStartToHeightmap, context.randomState) } else { @@ -271,7 +267,7 @@ export namespace WorldgenStructure { } public findGenerationPoint(chunkX: number, chunkZ: number, _: Random, context: WorldgenStructure.GenerationContext): BlockPos | undefined { - if (this.getLowestY(context, chunkX << 4, chunkZ << 4, this.width, this.depth) < context.settings.seaLevel) { + if (this.getLowestY(context, chunkX << 4, chunkZ << 4, this.width, this.depth) < context.chunkGenerator.getWorldgenContext().seaLevel) { return undefined } else { return this.onTopOfChunkCenter(context, chunkX, chunkZ) diff --git a/test/math/Util.test.ts b/test/math/Util.test.ts index 79653542..32294f84 100644 --- a/test/math/Util.test.ts +++ b/test/math/Util.test.ts @@ -1,8 +1,13 @@ import { describe, expect, it } from 'vitest' -import { hashCode } from '../../src' +import { getSeed, hashCode } from '../../src' describe('Util', () => { it('hashCode', () => { expect(hashCode('octave_-6')).toEqual(440898200) + expect(hashCode('minecraft:deepslate')).toEqual(-112689504) + }) + + it('getSeed', () => { + expect(getSeed(15, 8, 0)).toEqual(106379689997262n) }) }) diff --git a/test/math/random/XoroshiroRandom.test.ts b/test/math/random/XoroshiroRandom.test.ts index a4813e96..4dad402c 100644 --- a/test/math/random/XoroshiroRandom.test.ts +++ b/test/math/random/XoroshiroRandom.test.ts @@ -43,7 +43,7 @@ describe('XoroshiroRandom', () => { expect(random.nextInt(0x7FFFFFFF)).toEqual(383715241) }) - it ('nextFloat', () => { + it('nextFloat', () => { const random = XoroshiroRandom.create(BigInt(1)) const actual = [...Array(10)].map(() => random.nextFloat()) const expected = [0.9439647, 0.34974587, 0.9012351, 0.04825169, 0.4388219, 0.15067255, 0.88325465, 0.14266795, 0.07273072, 0.8435429] @@ -52,7 +52,7 @@ describe('XoroshiroRandom', () => { }) }) - it ('nextDouble', () => { + it('nextDouble', () => { const random = XoroshiroRandom.create(BigInt(1)) const actual = [...Array(10)].map(() => random.nextDouble()) const expected = [0.9439647613102243, 0.34974587038035987, 0.9012351308931007, 0.048251694223845565, 0.4388219188383503, 0.15067259677004097, 0.8832547054297483, 0.1426679927905259, 0.07273074380408129, 0.84354291349029] @@ -60,4 +60,18 @@ describe('XoroshiroRandom', () => { expect(a).toBeCloseTo(expected[i], 1e-8) }) }) + + it('forkPositional', () => { + const random = XoroshiroRandom.create(BigInt(124)) + const positionalRandom = random.forkPositional() + const namedRandom = positionalRandom.fromHashOf('minecraft:deepslate').forkPositional() + const randomAt8 = namedRandom.at(15, 8, 0) + expect(randomAt8.nextFloat()).toEqual(0.9723014235496521) + const randomAt7 = namedRandom.at(15, 7, 0) + expect(randomAt7.nextFloat()).toEqual(0.9699087142944336) + const randomAt6 = namedRandom.at(15, 6, 0) + expect(randomAt6.nextFloat()).toEqual(0.9694857001304626) + const randomAt5 = namedRandom.at(15, 5, 0) + expect(randomAt5.nextFloat()).toEqual(0.9666962623596191) + }) }) diff --git a/test/worldgen/biome/MultiNoiseBiomeSource.test.ts b/test/worldgen/biome/MultiNoiseBiomeSource.test.ts index 2ddcc4d9..e00d0d3a 100644 --- a/test/worldgen/biome/MultiNoiseBiomeSource.test.ts +++ b/test/worldgen/biome/MultiNoiseBiomeSource.test.ts @@ -1,8 +1,7 @@ import { afterEach, describe, expect, it } from 'vitest' import { Identifier } from '../../../src/core/index.js' import { NormalNoise } from '../../../src/math/index.js' -import { Climate, DensityFunction as DF, MultiNoiseBiomeSource, NoiseGeneratorSettings, NoiseRouter, WorldgenRegistries } from '../../../src/worldgen/index.js' -import { RandomState } from '../../../src/worldgen/RandomState.js' +import { Climate, DensityFunction as DF, MultiNoiseBiomeSource, NoiseGeneratorSettings, NoiseRouter, RandomState, WorldgenRegistries } from '../../../src/worldgen/index.js' describe('MultiNoise', () => { afterEach(() => { @@ -22,7 +21,7 @@ describe('MultiNoise', () => { const temperature = WorldgenRegistries.NOISE.register(Identifier.create('temperature'), new NormalNoise(1.2453007926713473, -10, 6, 'enabled', [1.5, 0, 1, 0, 0, 0])) const vegetation = WorldgenRegistries.NOISE.register(Identifier.create('vegetation'), new NormalNoise(0.9494731054427978, -8, 6, 'enabled', [1, 1, 0, 0, 0, 0])) const settings = NoiseGeneratorSettings.create({ - noise: { minY: 0, height: 128, xzSize: 1, ySize: 2 }, + noise: { minY: 0, height: 128 }, noiseRouter: NoiseRouter.create({ temperature: new DF.NoiseFunction(temperature, 0.25, 0, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), vegetation: new DF.NoiseFunction(vegetation, 0.25, 0, DF.Constant.ZERO, DF.Constant.ZERO, DF.Constant.ZERO), @@ -70,7 +69,7 @@ describe('MultiNoise', () => { }) const settings = NoiseGeneratorSettings.create({ - noise: { minY: 0, height: 128, xzSize: 1, ySize: 2 }, + noise: { minY: 0, height: 128 }, noiseRouter: NoiseRouter.create({ temperature: new DF.Constant(0.3), }),