diff --git a/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/NoiseAddon.java b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/NoiseAddon.java index 8b6ff17d0..59fc635a6 100644 --- a/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/NoiseAddon.java +++ b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/NoiseAddon.java @@ -36,6 +36,7 @@ import com.dfsek.terra.api.addon.BaseAddon; import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent; import com.dfsek.terra.api.event.functional.FunctionalEventHandler; import com.dfsek.terra.api.inject.annotations.Inject; +import com.dfsek.terra.api.noise.DerivativeNoiseSampler; import com.dfsek.terra.api.noise.NoiseSampler; import com.dfsek.terra.api.registry.CheckedRegistry; import com.dfsek.terra.api.util.reflection.TypeKey; @@ -71,7 +72,8 @@ public class NoiseAddon implements AddonInitializer { (type, o, loader, depthTracker) -> DistanceSampler.DistanceFunction.valueOf((String) o)) .applyLoader(DimensionApplicableNoiseSampler.class, DimensionApplicableNoiseSampler::new) .applyLoader(FunctionTemplate.class, FunctionTemplate::new) - .applyLoader(CubicSpline.Point.class, CubicSplinePointTemplate::new); + .applyLoader(CubicSpline.Point.class, CubicSplinePointTemplate::new) + .applyLoader(DerivativeNoiseSampler.class, DerivativeNoiseSamplerTemplate::new); noiseRegistry.register(addon.key("LINEAR"), LinearNormalizerTemplate::new); noiseRegistry.register(addon.key("NORMAL"), NormalNormalizerTemplate::new); @@ -94,7 +96,8 @@ public class NoiseAddon implements AddonInitializer { noiseRegistry.register(addon.key("PERLIN"), () -> new SimpleNoiseTemplate(PerlinSampler::new)); noiseRegistry.register(addon.key("SIMPLEX"), () -> new SimpleNoiseTemplate(SimplexSampler::new)); noiseRegistry.register(addon.key("GABOR"), GaborNoiseTemplate::new); - + noiseRegistry.register(addon.key("PSEUDOEROSION"), PseudoErosionTemplate::new); + noiseRegistry.register(addon.key("DERIVATIVE"), DerivativeFractalTemplate::new); noiseRegistry.register(addon.key("VALUE"), () -> new SimpleNoiseTemplate(ValueSampler::new)); noiseRegistry.register(addon.key("VALUE_CUBIC"), () -> new SimpleNoiseTemplate(ValueCubicSampler::new)); diff --git a/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/config/templates/DerivativeNoiseSamplerTemplate.java b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/config/templates/DerivativeNoiseSamplerTemplate.java new file mode 100644 index 000000000..d93054ac9 --- /dev/null +++ b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/config/templates/DerivativeNoiseSamplerTemplate.java @@ -0,0 +1,25 @@ +package com.dfsek.terra.addons.noise.config.templates; + +import com.dfsek.tectonic.api.config.template.annotations.Value; +import com.dfsek.tectonic.api.exception.ValidationException; + +import com.dfsek.terra.api.noise.DerivativeNoiseSampler; +import com.dfsek.terra.api.noise.NoiseSampler; + + +public class DerivativeNoiseSamplerTemplate extends SamplerTemplate { + + @Value(".") + private NoiseSampler sampler; + + @Override + public boolean validate() throws ValidationException { + if (!DerivativeNoiseSampler.isDifferentiable(sampler)) throw new ValidationException("Provided sampler does not support calculating a derivative"); + return super.validate(); + } + + @Override + public DerivativeNoiseSampler get() { + return (DerivativeNoiseSampler) sampler; + } +} diff --git a/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/config/templates/noise/DerivativeFractalTemplate.java b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/config/templates/noise/DerivativeFractalTemplate.java new file mode 100644 index 000000000..9387651a2 --- /dev/null +++ b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/config/templates/noise/DerivativeFractalTemplate.java @@ -0,0 +1,32 @@ +package com.dfsek.terra.addons.noise.config.templates.noise; + +import com.dfsek.tectonic.api.config.template.annotations.Default; +import com.dfsek.tectonic.api.config.template.annotations.Value; + +import com.dfsek.terra.addons.noise.config.templates.SamplerTemplate; +import com.dfsek.terra.addons.noise.samplers.noise.simplex.DerivativeFractal; + + +public class DerivativeFractalTemplate extends SamplerTemplate { + + @Value("octaves") + @Default + private int octaves = 3; + + @Value("gain") + @Default + private double gain = 0.5; + + @Value("lacunarity") + @Default + private double lacunarity = 2.0; + + @Value("frequency") + @Default + private double frequency = 0.02; + + @Override + public DerivativeFractal get() { + return new DerivativeFractal(octaves, gain, lacunarity, frequency); + } +} diff --git a/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/config/templates/noise/PseudoErosionTemplate.java b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/config/templates/noise/PseudoErosionTemplate.java new file mode 100644 index 000000000..f5e1ac46c --- /dev/null +++ b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/config/templates/noise/PseudoErosionTemplate.java @@ -0,0 +1,70 @@ +package com.dfsek.terra.addons.noise.config.templates.noise; + +import com.dfsek.tectonic.api.config.template.annotations.Default; +import com.dfsek.tectonic.api.config.template.annotations.Value; + +import com.dfsek.terra.addons.noise.config.templates.SamplerTemplate; +import com.dfsek.terra.addons.noise.samplers.noise.simplex.PseudoErosion; +import com.dfsek.terra.api.noise.DerivativeNoiseSampler; + + +public class PseudoErosionTemplate extends SamplerTemplate { + + @Value("octaves") + @Default + private int octaves = 4; + + @Value("lacunarity") + @Default + private double lacunarity = 2.0; + + @Value("gain") + @Default + private double gain = 0.5; + + @Value("slope-strength") + @Default + private double slopeStrength = 1.0; + + @Value("branch-strength") + @Default + private double branchStrength = 1.0; + + @Value("strength") + @Default + private double strength = 0.04; + + @Value("erosion-frequency") + @Default + private double erosionFrequency = 0.02; + + @Value("sampler") + private DerivativeNoiseSampler heightSampler; + + @Value("slope-mask.enable") + @Default + private boolean slopeMask = true; + + @Value("slope-mask.none") + @Default + private double slopeMaskNone = -0.5; + + @Value("slope-mask.full") + @Default + private double slopeMaskFull = 1; + + @Value("jitter") + @Default + private double jitterModifier = 1; + + @Value("average-impulses") + @Default + private boolean averageErosionImpulses = true; + + @Override + public PseudoErosion get() { + return new PseudoErosion(octaves, gain, lacunarity, + slopeStrength, branchStrength, strength, + erosionFrequency, heightSampler, slopeMask, slopeMaskFull, slopeMaskNone, jitterModifier, averageErosionImpulses); + } +} diff --git a/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/samplers/noise/simplex/DerivativeFractal.java b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/samplers/noise/simplex/DerivativeFractal.java new file mode 100644 index 000000000..91fa69e0e --- /dev/null +++ b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/samplers/noise/simplex/DerivativeFractal.java @@ -0,0 +1,109 @@ +package com.dfsek.terra.addons.noise.samplers.noise.simplex; + +import com.dfsek.terra.api.noise.DerivativeNoiseSampler; + +import static com.dfsek.terra.addons.noise.samplers.noise.simplex.PseudoErosion.dot; +import static com.dfsek.terra.addons.noise.samplers.noise.simplex.PseudoErosion.hash; +import static com.dfsek.terra.addons.noise.samplers.noise.simplex.PseudoErosion.hashX; +import static com.dfsek.terra.addons.noise.samplers.noise.simplex.PseudoErosion.hashY; + + +/** + * Temporary sampler that provides derivatives to test pseudoerosion, should be replaced with + * derivative versions of existing samplers + */ +public class DerivativeFractal implements DerivativeNoiseSampler { + + private final int heightOctaves; + private final double heightGain; + private final double heightLacunarity; + private final double frequency; + + public DerivativeFractal(int octaves, double gain, double lacunarity, double frequency) { + this.heightOctaves = octaves; + this.heightGain = gain; + this.heightLacunarity = lacunarity; + this.frequency = frequency; + } + + private static float[] baseNoise(float px, float py) { + float ix = (float)Math.floor(px); + float iy = (float)Math.floor(py); + float fx = px - ix; + float fy = py - iy; + + float ux = fx * fx * fx * (fx * (fx * 6.0f - 15.0f) + 10.0f); + float uy = fy * fy * fy * (fy * (fy * 6.0f - 15.0f) + 10.0f); + float dux = fx * fx * 30.0f * (fx * (fx - 2.0f) + 1.0f); + float duy = fy * fy * 30.0f * (fy * (fy - 2.0f) + 1.0f); + + float gan = hash(ix, iy); + float gax = hashX(gan); + float gay = hashY(gan); + + float gbn = hash(ix + 1, iy); + float gbx = hashX(gbn); + float gby = hashY(gbn); + + float gcn = hash(ix, iy + 1); + float gcx = hashX(gcn); + float gcy = hashY(gcn); + + float gdn = hash(ix + 1, iy + 1); + float gdx = hashX(gdn); + float gdy = hashY(gdn); + + float va = dot(gax, gay, fx, fy); + float vb = dot(gbx, gby, fx - 1, fy); + float vc = dot(gcx, gcy, fx, fy - 1); + float vd = dot(gdx, gdy, fx - 1, fy - 1); + + float u2x = gax + (gbx - gax) * ux + (gcx - gax) * uy + (gax - gbx - gcx + gdx) * ux * uy + dux * (uy * (va - vb - vc + vd) + vb - va); + float u2y = gay + (gby - gay) * ux + (gcy - gay) * uy + (gay - gby - gcy + gdy) * ux * uy + duy * (ux * (va - vb - vc + vd) + vc - va); + + return new float[] { va + ux * (vb - va) + uy * (vc - va) + ux * uy * (va - vb - vc + vd), u2x, u2y }; + } + + @Override + public boolean isDifferentiable() { + return true; + } + + @Override + public double[] noised(long seed, double x, double y) { + x *= frequency; + y *= frequency; + double[] out = { 0.0f, 0.0f, 0.0f }; + float heightFreq = 1.0f; + float heightAmp = 1f; + float cumAmp = 0.0f; + for (int i = 0; i < heightOctaves; i++) { + float[] noise = baseNoise((float) (x * heightFreq), (float) (y * heightFreq)); + out[0] += noise[0] * heightAmp; + out[1] += noise[1] * heightAmp * heightFreq; + out[2] += noise[2] * heightAmp * heightFreq; + cumAmp += heightAmp; + heightAmp *= heightGain; + heightFreq *= heightLacunarity; + } + out[0] /= cumAmp; + out[1] /= cumAmp; + out[2] /= cumAmp; + return out; + } + + @Override + public double[] noised(long seed, double x, double y, double z) { + return noised(seed, x, z); + } + + @Override + public double noise(long seed, double x, double y) { + return noised(seed, x, y)[0]; + } + + @Override + public double noise(long seed, double x, double y, double z) { + return noised(seed, x, y, z)[0]; + } +} diff --git a/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/samplers/noise/simplex/PseudoErosion.java b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/samplers/noise/simplex/PseudoErosion.java new file mode 100644 index 000000000..41b1e2db8 --- /dev/null +++ b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/samplers/noise/simplex/PseudoErosion.java @@ -0,0 +1,187 @@ +package com.dfsek.terra.addons.noise.samplers.noise.simplex; + + +import com.dfsek.terra.api.noise.DerivativeNoiseSampler; +import com.dfsek.terra.api.noise.NoiseSampler; +import com.dfsek.terra.api.util.MathUtil; + + +public class PseudoErosion implements NoiseSampler { + public static final float TAU = (float) (2.0 * Math.PI); + private static final float HASH_X = 0.3183099f; + private static final float HASH_Y = 0.3678794f; + private final int octaves; + public final double gain; + public final double lacunarity; + public final double slopeStrength; + public final double branchStrength; + public final double erosionStrength; + private final double erosionFrequency; + private final DerivativeNoiseSampler sampler; + private final boolean slopeMask; + private final double slopeMaskFullSq; + private final double slopeMaskNoneSq; + private final double jitter; + private final double maxCellDistSq; + private final double maxCellDistSqRecip; + private final boolean averageErosionImpulses; + + public PseudoErosion(int octaves, double gain, double lacunarity, double slopeStrength, double branchStrength, double erosionStrength, double erosionFrequency, DerivativeNoiseSampler sampler, + boolean slopeMask, double slopeMaskFull, double slopeMaskNone, double jitterModifier, + boolean averageErosionImpulses) { + this.octaves = octaves; + this.gain = gain; + this.lacunarity = lacunarity; + this.slopeStrength = slopeStrength; + this.branchStrength = branchStrength; + this.erosionStrength = erosionStrength; + this.erosionFrequency = erosionFrequency; + this.sampler = sampler; + this.slopeMask = slopeMask; + // Square these values and maintain sign since they're compared to a + // squared value, otherwise a sqrt would need to be used + this.slopeMaskFullSq = slopeMaskFull * slopeMaskFull * Math.signum(slopeMaskFull); + this.slopeMaskNoneSq = slopeMaskNone * slopeMaskNone * Math.signum((slopeMaskNone)); + this.jitter = 0.43701595 * jitterModifier; + this.averageErosionImpulses = averageErosionImpulses; + this.maxCellDistSq = 1 + jitter * jitter; + this.maxCellDistSqRecip = 1 / maxCellDistSq; + } + + public static float hash(float x, float y) { + float xx = x * HASH_X + HASH_Y; + float yy = y * HASH_Y + HASH_X; + + // Swapped the components here + return 16 * (xx * yy * (xx + yy)); + } + + public static float hashX(float n) { + // Swapped the components here + float nx = HASH_X * n; + return -1.0f + 2.0f * fract(nx); + } + + public static float hashY(float n) { + float ny = HASH_Y * n; + return -1.0f + 2.0f * fract(ny); + } + + public static float fract(float x) { + return (x - (float)Math.floor(x)); + } + + public float[] erosion(float x, float y, float dirX, float dirY) { + int gridX = Math.round(x); + int gridY = Math.round(y); + float noise = 0.0f; + float dirOutX = 0.0f; + float dirOutY = 0.0f; + float cumAmp = 0.0f; + + for (int cellX = gridX - 1; cellX <= gridX + 1; cellX++) { + for (int cellY = gridY - 1; cellY <= gridY + 1; cellY++) { + // TODO - Make seed affect hashing + float cellHash = hash(cellX, cellY); + float cellOffsetX = (float) (hashX(cellHash) * jitter); + float cellOffsetY = (float) (hashY(cellHash) * jitter); + float cellOriginDeltaX = (x - cellX) + cellOffsetX; + float cellOriginDeltaY = (y - cellY) + cellOffsetY; + float cellOriginDistSq = cellOriginDeltaX * cellOriginDeltaX + cellOriginDeltaY * cellOriginDeltaY; + if (cellOriginDistSq > maxCellDistSq) continue; // Skip calculating cells too far away + float ampTmp = (float) ((cellOriginDistSq * maxCellDistSqRecip) - 1); float amp = ampTmp * ampTmp; // Decrease cell amplitude further away + cumAmp += amp; + float directionalStrength = dot(cellOriginDeltaX, cellOriginDeltaY, dirX, dirY) * TAU; + noise += (float) (MathUtil.cos(directionalStrength) * amp); + float sinAngle = (float) MathUtil.sin(directionalStrength) * amp; + dirOutX -= sinAngle * (cellOriginDeltaX + dirX); + dirOutY -= sinAngle * (cellOriginDeltaY + dirY); + } + } + if (averageErosionImpulses && cumAmp != 0) { + noise /= cumAmp; + dirOutX /= cumAmp; + dirOutY /= cumAmp; + } + return new float[] {noise, dirOutX, dirOutY}; + } + + public static double exp(double val) { + final long tmp = (long) (1512775 * val + 1072632447); + return Double.longBitsToDouble(tmp << 32); + } + + public static float smoothstep(float edge0, float edge1, float x) { + // Scale, bias and saturate x to 0..1 range + x = clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + // Evaluate polynomial + return x * x * (3 - 2 * x); + } + + public static float clamp(float x, float minVal, float maxVal) { + return Math.max(minVal, Math.min(maxVal, x)); + } + + public float heightMap(long seed, float x, float y) { + double[] sample = sampler.noised(seed, x, y); + float height = (float) sample[0]; + float heightDirX = (float) sample[1]; + float heightDirY = (float) sample[2]; + + // Take the curl of the normal to get the gradient facing down the slope + float baseDirX = heightDirY * (float) slopeStrength; + float baseDirY = -heightDirX * (float) slopeStrength; + + float erosion = 0.0f; + float dirX = 0.0f; + float dirY = 0.0f; + float amp = 1.0f; + float cumAmp = 0.0f; + float freq = 1.0f; + + // Stack erosion octaves + for (int i = 0; i < octaves; i++) { + float[] erosionResult = erosion( + x * freq * (float) erosionFrequency, + y * freq * (float) erosionFrequency, + baseDirX + dirY * (float) branchStrength, + baseDirY - dirX * (float) branchStrength); + erosion += erosionResult[0] * amp; + dirX += erosionResult[1] * amp * freq; + dirY += erosionResult[2] * amp * freq; + cumAmp += amp; + amp *= gain; + freq *= lacunarity; + } + + // TODO - Test different output ranges, see how they affect visuals + // Normalize erosion noise + erosion /= cumAmp; + // [-1, 1] -> [0, 1] + erosion = erosion * 0.5F + 0.5F; + + // Without masking, erosion noise in areas with small gradients tend to produce mounds, + // this reduces erosion amplitude towards smaller gradients to avoid this + if (slopeMask) { + float dirMagSq = dot(baseDirX, baseDirY, baseDirX, baseDirY); + float flatness = smoothstep((float) slopeMaskNoneSq, (float) slopeMaskFullSq, dirMagSq); + erosion *= flatness; + } + + return (float) (height + erosion * erosionStrength); + } + + public static float dot(float x1, float y1, float x2, float y2) { + return x1 * x2 + y1 * y2; + } + + @Override + public double noise(long seed, double x, double y) { + return heightMap(seed, (float) x, (float) y); + } + + @Override + public double noise(long seed, double x, double y, double z) { + return noise(seed, x, z); + } +} \ No newline at end of file diff --git a/common/api/src/main/java/com/dfsek/terra/api/noise/DerivativeNoiseSampler.java b/common/api/src/main/java/com/dfsek/terra/api/noise/DerivativeNoiseSampler.java new file mode 100644 index 000000000..0e178130b --- /dev/null +++ b/common/api/src/main/java/com/dfsek/terra/api/noise/DerivativeNoiseSampler.java @@ -0,0 +1,41 @@ +package com.dfsek.terra.api.noise; + +/** + * A NoiseSampler which additionally may provide a 1st directional derivative + */ +public interface DerivativeNoiseSampler extends NoiseSampler { + + static boolean isDifferentiable(NoiseSampler sampler) { + if (sampler instanceof DerivativeNoiseSampler dSampler) { + return dSampler.isDifferentiable(); + } + return false; + } + + /** + * Samplers may or may not be able to provide a derivative depending on what + * inputs they take, this method signals whether this is the case. + * + * @return If the noise sampler provides a derivative or not + */ + boolean isDifferentiable(); + + /** + * Derivative return version of standard 2D noise evaluation + * @param seed + * @param x + * @param y + * @return 3 element array, in index order: noise value, partial x derivative, partial y derivative + */ + double[] noised(long seed, double x, double y); + + /** + * Derivative return version of standard 3D noise evaluation + * @param seed + * @param x + * @param y + * @param z + * @return 4 element array, in index order: noise value, partial x derivative, partial y derivative, partial z derivative + */ + double[] noised(long seed, double x, double y, double z); +}