TerraForged/TerraForgedCore/src/main/java/com/terraforged/core/filter/Erosion.java

292 lines
13 KiB
Java

/*
*
* MIT License
*
* Copyright (c) 2020 TerraForged
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.terraforged.core.filter;
import com.terraforged.core.cell.Cell;
import com.terraforged.core.region.Size;
import com.terraforged.core.settings.Settings;
import com.terraforged.core.world.heightmap.Levels;
import me.dags.noise.util.NoiseUtil;
import java.util.Random;
/*
* This class in an adaption of the work by Sebastian Lague which is also licensed under MIT.
* Reference:
* https://github.com/SebLague/Hydraulic-Erosion/blob/Coding-Adventure-E01/Assets/Scripts/Erosion.cs
* https://github.com/SebLague/Hydraulic-Erosion/blob/Coding-Adventure-E01/LICENSE
*
* License In Full:
* MIT License
*
* Copyright (c) 2019 Sebastian Lague
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
public class Erosion implements Filter {
private int erosionRadius = 3;
private float inertia = 0.05f;
private float sedimentCapacityFactor = 4;
private float minSedimentCapacity = 0.01f;
private float erodeSpeed = 0.3f;
private float depositSpeed = 0.3f;
private float evaporateSpeed = 0.01f;
private float gravity = 8;
private int maxDropletLifetime = 30;
private float initialWaterVolume = 1;
private float initialSpeed = 1;
private final TerrainPos gradient = new TerrainPos();
private int[][] erosionBrushIndices = new int[0][];
private float[][] erosionBrushWeights = new float[0][];
private final Modifier modifier;
private final Random random = new Random();
public Erosion(Settings settings, Levels levels) {
erodeSpeed = settings.filters.erosion.erosionRate;
depositSpeed = settings.filters.erosion.depositeRate;
modifier = Modifier.range(levels.ground, levels.ground(15));
}
@Override
public void apply(Filterable<?> map, int seedX, int seedZ, int iterations) {
if (erosionBrushIndices.length != map.getSize().total) {
init(map.getSize().total, erosionRadius);
}
applyMain(map, seedX, seedZ, iterations, random);
// applyNeighbours(map, seedX, seedZ, iterations, random);
}
private int nextCoord(Size size, Random random) {
return random.nextInt(size.total - 1);
}
private void applyMain(Filterable<?> map, int seedX, int seedZ, int iterations, Random random) {
random.setSeed(NoiseUtil.seed(seedX, seedZ));
while (iterations-- > 0) {
int posX = nextCoord(map.getSize(), random);
int posZ = nextCoord(map.getSize(), random);
apply(map.getBacking(), posX, posZ, map.getSize().total);
}
}
private void apply(Cell<?>[] cells, float posX, float posY, int size) {
float dirX = 0;
float dirY = 0;
float speed = initialSpeed;
float water = initialWaterVolume;
float sediment = 0;
for (int lifetime = 0; lifetime < maxDropletLifetime; lifetime++) {
int nodeX = (int) posX;
int nodeY = (int) posY;
int dropletIndex = nodeY * size + nodeX;
// Calculate droplet's offset inside the cell (0,0) = at NW node, (1,1) = at SE node
float cellOffsetX = posX - nodeX;
float cellOffsetY = posY - nodeY;
// Calculate droplet's height and direction of flow with bilinear interpolation of surrounding heights
gradient.update(cells, size, posX, posY);
// Update the droplet's direction and position (move position 1 unit regardless of speed)
dirX = (dirX * inertia - gradient.gradientX * (1 - inertia));
dirY = (dirY * inertia - gradient.gradientY * (1 - inertia));
// Normalize direction
float len = (float) Math.sqrt(dirX * dirX + dirY * dirY);
if (Float.isNaN(len)) {
len = 0;
}
if (len != 0) {
dirX /= len;
dirY /= len;
}
posX += dirX;
posY += dirY;
// Stop simulating droplet if it's not moving or has flowed over edge of map
if ((dirX == 0 && dirY == 0) || posX < 0 || posX >= size - 1 || posY < 0 || posY >= size - 1) {
break;
}
// Find the droplet's new height and calculate the deltaHeight
float oldHeight = gradient.height;
float newHeight = gradient.update(cells, size, posX, posY).height;
float deltaHeight = newHeight - oldHeight;
// Calculate the droplet's sediment capacity (higher when moving fast down a slope and contains lots of water)
float sedimentCapacity = Math.max(-deltaHeight * speed * water * sedimentCapacityFactor, minSedimentCapacity);
// If carrying more sediment than capacity, or if flowing uphill:
if (sediment > sedimentCapacity || deltaHeight > 0) {
// If moving uphill (deltaHeight > 0) try fill up to the current height, otherwise deposit a fraction of the excess sediment
float amountToDeposit = (deltaHeight > 0) ? Math.min(deltaHeight, sediment) : (sediment - sedimentCapacity) * depositSpeed;
sediment -= amountToDeposit;
// Add the sediment to the four nodes of the current cell using bilinear interpolation
// Deposition is not distributed over a radius (like erosion) so that it can fill small pits
deposit(cells[dropletIndex], amountToDeposit * (1 - cellOffsetX) * (1 - cellOffsetY));
deposit(cells[dropletIndex + 1], amountToDeposit * cellOffsetX * (1 - cellOffsetY));
deposit(cells[dropletIndex + size], amountToDeposit * (1 - cellOffsetX) * cellOffsetY);
deposit(cells[dropletIndex + size + 1], amountToDeposit * cellOffsetX * cellOffsetY);
} else {
// Erode a fraction of the droplet's current carry capacity.
// Clamp the erosion to the change in height so that it doesn't dig a hole in the terrain behind the droplet
float amountToErode = Math.min((sedimentCapacity - sediment) * erodeSpeed, -deltaHeight);
// Use erosion brush to erode from all nodes inside the droplet's erosion radius
for (int brushPointIndex = 0; brushPointIndex < erosionBrushIndices[dropletIndex].length; brushPointIndex++) {
int nodeIndex = erosionBrushIndices[dropletIndex][brushPointIndex];
Cell<?> cell = cells[nodeIndex];
float brushWeight = erosionBrushWeights[dropletIndex][brushPointIndex];
float weighedErodeAmount = amountToErode * brushWeight;
float deltaSediment = Math.min(cell.value, weighedErodeAmount);//cell.value < weighedErodeAmount) ? cell.value : weighedErodeAmount;
erode(cell, deltaSediment);
sediment += deltaSediment;
}
}
// Update droplet's speed and water content
speed = (float) Math.sqrt(speed * speed + deltaHeight * gravity);
water *= (1 - evaporateSpeed);
if (Float.isNaN(speed)) {
speed = 0;
}
}
}
private void init(int size, int radius) {
erosionBrushIndices = new int[size * size][];
erosionBrushWeights = new float[size * size][];
int[] xOffsets = new int[radius * radius * 4];
int[] yOffsets = new int[radius * radius * 4];
float[] weights = new float[radius * radius * 4];
float weightSum = 0;
int addIndex = 0;
for (int i = 0; i < erosionBrushIndices.length; i++) {
int centreX = i % size;
int centreY = i / size;
if (centreY <= radius || centreY >= size - radius || centreX <= radius + 1 || centreX >= size - radius) {
weightSum = 0;
addIndex = 0;
for (int y = -radius; y <= radius; y++) {
for (int x = -radius; x <= radius; x++) {
float sqrDst = x * x + y * y;
if (sqrDst < radius * radius) {
int coordX = centreX + x;
int coordY = centreY + y;
if (coordX >= 0 && coordX < size && coordY >= 0 && coordY < size) {
float weight = 1 - (float) Math.sqrt(sqrDst) / radius;
weightSum += weight;
weights[addIndex] = weight;
xOffsets[addIndex] = x;
yOffsets[addIndex] = y;
addIndex++;
}
}
}
}
}
int numEntries = addIndex;
erosionBrushIndices[i] = new int[numEntries];
erosionBrushWeights[i] = new float[numEntries];
for (int j = 0; j < numEntries; j++) {
erosionBrushIndices[i][j] = (yOffsets[j] + centreY) * size + xOffsets[j] + centreX;
erosionBrushWeights[i][j] = weights[j] / weightSum;
}
}
}
private void deposit(Cell<?> cell, float amount) {
float change = modifier.modify(cell, amount);
cell.value += change;
cell.sediment += change;
}
private void erode(Cell<?> cell, float amount) {
float change = modifier.modify(cell, amount);
cell.value -= change;
cell.erosion -= change;
}
private static class TerrainPos {
private float height;
private float gradientX;
private float gradientY;
private TerrainPos update(Cell<?>[] nodes, int mapSize, float posX, float posY) {
int coordX = (int) posX;
int coordY = (int) posY;
// Calculate droplet's offset inside the cell (0,0) = at NW node, (1,1) = at SE node
float x = posX - coordX;
float y = posY - coordY;
// Calculate heights of the four nodes of the droplet's cell
int nodeIndexNW = coordY * mapSize + coordX;
float heightNW = nodes[nodeIndexNW].value;
float heightNE = nodes[nodeIndexNW + 1].value;
float heightSW = nodes[nodeIndexNW + mapSize].value;
float heightSE = nodes[nodeIndexNW + mapSize + 1].value;
// Calculate droplet's direction of flow with bilinear interpolation of height difference along the edges
this.gradientX = (heightNE - heightNW) * (1 - y) + (heightSE - heightSW) * y;
this.gradientY = (heightSW - heightNW) * (1 - x) + (heightSE - heightNE) * x;
// Calculate height with bilinear interpolation of the heights of the nodes of the cell
this.height = heightNW * (1 - x) * (1 - y) + heightNE * x * (1 - y) + heightSW * (1 - x) * y + heightSE * x * y;
return this;
}
}
}