642 lines
28 KiB
C++
642 lines
28 KiB
C++
#include "procgen/heightmap/Heightmap.h"
|
|
#include "procgen/heightmap/TerrainEffect.h"
|
|
#include "procgen/heightmap/TerrainImageMask.h"
|
|
#include "procgen/heightmap/TerrainCollisionMask.h"
|
|
#include "procgen/heightmap/TerrainBakedMaskCache.h"
|
|
#include "procgen/heightmap/TerrainPolygonMask.h"
|
|
#include "procgen/heightmap/TerrainNoiseMask.h"
|
|
#include "procgen/heightmap/TerrainMask.h"
|
|
#include "procgen/heightmap/TerrainStamp.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <numbers>
|
|
|
|
using namespace eve::procgen;
|
|
|
|
TEST_CASE("procgen.mask.polygonOpenClosedBrushAndAtomicFailure") {
|
|
TerrainPolygonMask polygon;
|
|
REQUIRE(polygon.addNode(1, 1, 0.5F, 0.25F).ok());
|
|
REQUIRE(polygon.addNode(3, 1, 0.5F, 0.5F).ok());
|
|
REQUIRE(polygon.addNode(3, 3, 0.5F, 0.75F).ok());
|
|
REQUIRE(polygon.addNode(1, 3, 0.5F, 1.0F).ok());
|
|
Heightmap output(5, 5), zeroBrush(1, 1);
|
|
TerrainSampleGrid grid;
|
|
grid.width = grid.height = 5;
|
|
REQUIRE(polygon.rasterize(output, zeroBrush, grid, TerrainPolygonMaskType::Open).ok());
|
|
CHECK(output.data()[12] == 0.0F);
|
|
REQUIRE(polygon.rasterize(output, zeroBrush, grid, TerrainPolygonMaskType::Closed).ok());
|
|
CHECK(output.data()[12] == 1.0F);
|
|
|
|
polygon.clear();
|
|
REQUIRE(polygon.addNode(2, 2, 1.0F, 0.0F).ok());
|
|
Heightmap brush(1, 1);
|
|
brush.data()[0] = 1.0F;
|
|
REQUIRE(polygon.rasterize(output, brush, grid, TerrainPolygonMaskType::Open).ok());
|
|
CHECK(std::count(output.data().begin(), output.data().end(), 1.0F) == 9);
|
|
auto before = output.data();
|
|
grid.spacingX = 0;
|
|
CHECK(!polygon.rasterize(output, brush, grid, TerrainPolygonMaskType::Open).ok());
|
|
CHECK(output.data() == before);
|
|
CHECK(polygon.getNodeCount() == 1);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.worldBiomeCacheIdentityDirtyRebuildAndResample") {
|
|
TerrainBakedMaskCache cache;
|
|
Heightmap west(2, 2), east(1, 1), output(1, 1);
|
|
west.data() = {0, 1, 1, 0};
|
|
east.data()[0] = 0.75F;
|
|
output.data()[0] = 9.0F;
|
|
REQUIRE(cache.store("west", "biome-guid", west).value() == 1);
|
|
REQUIRE(cache.store("east", "biome-guid", east).value() == 2);
|
|
REQUIRE(cache.copyMask("west", "biome-guid", output).ok());
|
|
CHECK(output.data()[0] == 0.5F);
|
|
REQUIRE(cache.markDirty("biome-guid").value() == 2);
|
|
output.data()[0] = 7.0F;
|
|
CHECK(!cache.copyMask("west", "biome-guid", output).ok());
|
|
CHECK(output.data()[0] == 7.0F);
|
|
REQUIRE(cache.store("west", "biome-guid", west).value() == 2);
|
|
REQUIRE(cache.copyMask("west", "biome-guid", output).ok());
|
|
CHECK(!cache.copyMask("east", "biome-guid", output).ok());
|
|
REQUIRE(cache.erase("east", "biome-guid").value() == 1);
|
|
CHECK(cache.getEntryCount() == 1);
|
|
CHECK(!cache.erase("east", "biome-guid").ok());
|
|
cache.clear();
|
|
CHECK(cache.getEntryCount() == 0);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.worldContextAlignsHeightSampleCenters") {
|
|
TerrainStampSettings world;
|
|
world.originX = 0.5;
|
|
world.spacingX = 0.5;
|
|
world.width = 100;
|
|
world.depth = 100;
|
|
TerrainSampleGrid grid;
|
|
grid.width = 5;
|
|
TerrainBrushBlendSettings mapping;
|
|
REQUIRE(configureTerrainBrushWorld(mapping, world, 3, 1, grid).ok());
|
|
Heightmap oldHeights(5, 1), newHeights(1, 1), brush(1, 1), output(3, 1);
|
|
oldHeights.data() = {0, 10, 20, 30, 40};
|
|
REQUIRE(blendTerrainBrush(output, oldHeights, newHeights, brush, mapping).ok());
|
|
CHECK(std::abs(output.data()[0] - 5) < 1e-5F);
|
|
CHECK(std::abs(output.data()[1] - 10) < 1e-5F);
|
|
CHECK(std::abs(output.data()[2] - 15) < 1e-5F);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.worldContextRebasedRotationAndAtomicFailure") {
|
|
TerrainStampSettings world;
|
|
world.centerX = 1;
|
|
world.centerZ = 1;
|
|
world.width = 4;
|
|
world.depth = 4;
|
|
world.rotation = std::numbers::pi / 2;
|
|
TerrainSampleGrid grid;
|
|
grid.width = 3;
|
|
grid.height = 3;
|
|
TerrainBrushBlendSettings local, translated;
|
|
local.strength = 0.25F;
|
|
translated.strength = 0.25F;
|
|
REQUIRE(configureTerrainBrushWorld(local, world, 3, 3, grid).ok());
|
|
const double u = 1.0 / 6, v = 5.0 / 6;
|
|
CHECK(std::abs(local.brushXX * u + local.brushXZ * v + local.brushOffsetX - 0.75) < 1e-6);
|
|
CHECK(std::abs(local.brushZX * u + local.brushZZ * v + local.brushOffsetZ - 0.75) < 1e-6);
|
|
world.originX += 1e12;
|
|
world.centerX += 1e12;
|
|
grid.originX += 1e12;
|
|
world.originZ -= 1e12;
|
|
world.centerZ -= 1e12;
|
|
grid.originZ -= 1e12;
|
|
REQUIRE(configureTerrainBrushWorld(translated, world, 3, 3, grid).ok());
|
|
CHECK(translated.brushOffsetX == local.brushOffsetX);
|
|
CHECK(translated.brushOffsetZ == local.brushOffsetZ);
|
|
CHECK(translated.heightOffsetX == local.heightOffsetX);
|
|
CHECK(translated.strength == 0.25F);
|
|
world.width = 0;
|
|
CHECK(!configureTerrainBrushWorld(translated, world, 3, 3, grid).ok());
|
|
CHECK(translated.brushOffsetX == local.brushOffsetX);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.imageChannelsFootprintCurveAndAlias") {
|
|
Heightmap input(1, 1), r(1, 1), g(1, 1), b(1, 1), a(1, 1), curve(8, 1), output(1, 1);
|
|
input.data() = {1};
|
|
r.data() = {0.25F};
|
|
g.data() = {0.5F};
|
|
b.data() = {0.75F};
|
|
a.data() = {0.375F};
|
|
for (int i = 0; i < 8; ++i) curve.data()[i] = (i + 0.5F) / 8;
|
|
TerrainImageMaskSettings s;
|
|
const int filters[] = {0, 2, 3, 4, 5};
|
|
const float expected[] = {0.75F, 0.25F, 0.5F, 0.75F, 0.375F};
|
|
for (int i = 0; i < 5; ++i) {
|
|
s.filter = static_cast<TerrainImageFilter>(filters[i]);
|
|
REQUIRE(generateTerrainImageMask(output, input, r, g, b, a, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(output.data()[0] == expected[i]);
|
|
}
|
|
s.offsetX = 2;
|
|
REQUIRE(generateTerrainImageMask(output, input, r, g, b, a, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(output.data()[0] == 0.0625F);
|
|
s.tiling = true;
|
|
s.filter = TerrainImageFilter::Red;
|
|
REQUIRE(generateTerrainImageMask(r, input, r, g, b, a, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(r.data()[0] == 0.25F);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.imageColorAccuracyStrictBoundaryAndFailure") {
|
|
Heightmap input(1, 1), r(1, 1), g(1, 1), b(1, 1), a(1, 1), curve(2, 1), output(1, 1);
|
|
input.data() = {1};
|
|
r.data() = {1};
|
|
g.data() = {1};
|
|
b.data() = {1};
|
|
a.data() = {1};
|
|
curve.data() = {0, 1};
|
|
TerrainImageMaskSettings s;
|
|
s.filter = TerrainImageFilter::ColorSelection;
|
|
s.accuracy = 0.5F;
|
|
REQUIRE(generateTerrainImageMask(output, input, r, g, b, a, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(output.data()[0] == 1);
|
|
s.accuracy = 1;
|
|
REQUIRE(generateTerrainImageMask(output, input, r, g, b, a, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(output.data()[0] == 0);
|
|
s.accuracy = 0.5F;
|
|
r.data()[0] = 0;
|
|
g.data()[0] = 0;
|
|
b.data()[0] = 0;
|
|
REQUIRE(generateTerrainImageMask(output, input, r, g, b, a, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(output.data()[0] == 0);
|
|
s.scaleX = 0;
|
|
CHECK(!generateTerrainImageMask(output, input, r, g, b, a, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(output.data()[0] == 0);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.imageLabGrayDistanceAndFiveBlendModes") {
|
|
Heightmap input(1, 1), gray(1, 1), curve(16, 1), output(1, 1);
|
|
input.data() = {1};
|
|
gray.data() = {0.5F};
|
|
for (int i = 0; i < 16; ++i) curve.data()[i] = (i + 0.5F) / 16;
|
|
TerrainImageMaskSettings s;
|
|
s.filter = TerrainImageFilter::ColorSelection;
|
|
s.accuracy = 0.5F;
|
|
REQUIRE(generateTerrainImageMask(output, input, gray, gray, gray, gray, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(std::abs(output.data()[0] - 0.766945F) < 0.00002F);
|
|
s.filter = TerrainImageFilter::Red;
|
|
input.data()[0] = 0.25F;
|
|
const float expected[] = {0.125F, 0.5F, 0.25F, 0.75F, -0.25F};
|
|
for (int mode = 0; mode < 5; ++mode) {
|
|
REQUIRE(generateTerrainImageMask(output, input, gray, gray, gray, gray, curve, s,
|
|
static_cast<TerrainMaskBlend>(mode))
|
|
.ok());
|
|
CHECK(output.data()[0] == expected[mode]);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.concavityBowlSignBorderAndIndexedCurve") {
|
|
Heightmap heights(9, 9), input(9, 9), curve(5, 1), output(9, 9);
|
|
for (int z = 0; z < 9; ++z)
|
|
for (int x = 0; x < 9; ++x) heights.data()[z * 9 + x] = float((x - 4) * (x - 4) + (z - 4) * (z - 4));
|
|
std::fill(input.data().begin(), input.data().end(), 1.0F);
|
|
curve.data() = {0.1F, 0.2F, 0.3F, 0.4F, 0.5F};
|
|
TerrainConcavitySettings s;
|
|
s.featureSize = 1;
|
|
s.concavity = 0.25F;
|
|
REQUIRE(generateTerrainConcavityMask(output, input, heights, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(output.data()[40] == 0.2F);
|
|
CHECK(output.data()[0] == 0.1F);
|
|
s.concavity = -0.25F;
|
|
REQUIRE(generateTerrainConcavityMask(output, input, heights, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(output.data()[40] == 0.1F);
|
|
for (auto& h : heights.data()) h = -h;
|
|
REQUIRE(generateTerrainConcavityMask(heights, input, heights, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(heights.data()[40] == 0.2F);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.concavityFlatModesAndInvalidAreAtomic") {
|
|
Heightmap heights(2, 2), input(2, 2), curve(1, 1), output(2, 2);
|
|
std::fill(input.data().begin(), input.data().end(), 0.25F);
|
|
curve.data() = {0.5F};
|
|
TerrainConcavitySettings s;
|
|
const float expected[] = {0.125F, 0.5F, 0.25F, 0.75F, -0.25F};
|
|
for (int mode = 0; mode < 5; ++mode) {
|
|
REQUIRE(
|
|
generateTerrainConcavityMask(output, input, heights, curve, s, static_cast<TerrainMaskBlend>(mode)).ok());
|
|
CHECK(output.data()[0] == expected[mode]);
|
|
}
|
|
const auto original = output.data();
|
|
s.featureSize = 0;
|
|
CHECK(!generateTerrainConcavityMask(output, input, heights, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(output.data() == original);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.concavityResolutionMappingUsesWidthRatio") {
|
|
Heightmap heights(18, 18), fullInput(18, 18), smallInput(9, 6), full(18, 18), small(9, 6), curve(8, 1);
|
|
for (int z = 0; z < 18; ++z)
|
|
for (int x = 0; x < 18; ++x) heights.data()[z * 18 + x] = float((x - 8) * (x - 8) + (z - 8) * (z - 8));
|
|
std::fill(fullInput.data().begin(), fullInput.data().end(), 1.0F);
|
|
std::fill(smallInput.data().begin(), smallInput.data().end(), 1.0F);
|
|
for (int i = 0; i < 8; ++i) curve.data()[i] = float(i) / 7;
|
|
TerrainConcavitySettings s;
|
|
s.featureSize = 1;
|
|
s.concavity = 0.25F;
|
|
REQUIRE(generateTerrainConcavityMask(full, fullInput, heights, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
REQUIRE(generateTerrainConcavityMask(small, smallInput, heights, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
for (int z = 0; z < 6; ++z)
|
|
for (int x = 0; x < 9; ++x) CHECK(small.height(x, z) == full.height(x * 2, z * 2));
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.curvatureRadialImpulseAndAtomicDomainFailure") {
|
|
Heightmap heights(3, 3), input(3, 3), curve(8, 1), output(3, 3);
|
|
heights.data()[4] = 1;
|
|
std::fill(input.data().begin(), input.data().end(), 1.0F);
|
|
for (int i = 0; i < 8; ++i) curve.data()[i] = (i + 0.5F) / 8;
|
|
TerrainCurvatureSettings s;
|
|
s.radius = 1.0F / 3;
|
|
s.worldUnits = 1;
|
|
s.intensity = 1;
|
|
s.steps = 1;
|
|
s.directions = 4;
|
|
REQUIRE(generateTerrainCurvatureMask(output, input, heights, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(std::abs(output.data()[4] - 0.8F) < 1e-6F);
|
|
CHECK(std::abs(output.data()[1] - 0.2F) < 1e-6F);
|
|
CHECK(std::abs(output.data()[3] - 0.2F) < 1e-6F);
|
|
CHECK(output.data()[0] == 0.0625F);
|
|
const auto original = output.data();
|
|
s.intensity = 0.7F;
|
|
CHECK(!generateTerrainCurvatureMask(output, input, heights, curve, s, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(output.data() == original);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.curvatureFiveSourceModesAndAlias") {
|
|
Heightmap heights(1, 1), input(1, 1), curve(1, 1), output(1, 1);
|
|
input.data() = {0.25F};
|
|
curve.data() = {0.6F};
|
|
TerrainCurvatureSettings s;
|
|
s.steps = 1;
|
|
s.directions = 1;
|
|
const float expected[] = {0.15F, 0.6F, 0.6F, 0.85F, -0.35F};
|
|
for (int mode = 0; mode < 5; ++mode) {
|
|
REQUIRE(
|
|
generateTerrainCurvatureMask(output, input, heights, curve, s, static_cast<TerrainMaskBlend>(mode)).ok());
|
|
CHECK(std::abs(output.data()[0] - expected[mode]) < 1e-6F);
|
|
}
|
|
input.data()[0] = 0.8F;
|
|
curve.data()[0] = 0.3F;
|
|
REQUIRE(generateTerrainCurvatureMask(input, input, heights, curve, s, TerrainMaskBlend::Minimum).ok());
|
|
CHECK(input.data()[0] == 0.8F);
|
|
s.steps = std::numeric_limits<int>::max();
|
|
s.directions = 2;
|
|
CHECK(!generateTerrainCurvatureMask(input, input, heights, curve, s, TerrainMaskBlend::Minimum).ok());
|
|
CHECK(input.data()[0] == 0.8F);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.growShrinkRadialGoldenAndZeroCurve") {
|
|
Heightmap source(4, 1), curve(8, 1), output(4, 1);
|
|
source.data() = {0, 1, 1, 0};
|
|
for (int i = 0; i < 8; ++i) curve.data()[i] = (i + 0.5F) / 8;
|
|
REQUIRE(growShrinkTerrainMask(output, source, curve, 0.25F).ok());
|
|
CHECK(std::abs(output.data()[0] - 0.20710678F) < 1e-6F);
|
|
CHECK(output.data()[1] == 0.9375F);
|
|
CHECK(output.data()[2] == 0.9375F);
|
|
CHECK(std::abs(output.data()[3] - 0.20710678F) < 1e-6F);
|
|
REQUIRE(growShrinkTerrainMask(output, source, curve, -0.25F).ok());
|
|
CHECK(output.data()[0] == 0.0625F);
|
|
CHECK(std::abs(output.data()[1] - 0.79289322F) < 1e-6F);
|
|
CHECK(std::abs(output.data()[2] - 0.79289322F) < 1e-6F);
|
|
CHECK(output.data()[3] == 0.0625F);
|
|
REQUIRE(growShrinkTerrainMask(source, source, curve, 0).ok());
|
|
CHECK(source.data() == std::vector<float>({0.0625F, 0.9375F, 0.9375F, 0.0625F}));
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.growShrinkAliasesAndRejectedRadius") {
|
|
Heightmap source(2, 2), curve(2, 1), expected(2, 2);
|
|
source.data() = {0, 0.25F, 0.75F, 1};
|
|
curve.data() = {0, 1};
|
|
REQUIRE(growShrinkTerrainMask(expected, source, curve, 0.3F).ok());
|
|
REQUIRE(growShrinkTerrainMask(source, source, curve, 0.3F).ok());
|
|
CHECK(source.data() == expected.data());
|
|
const auto previous = source.data();
|
|
CHECK(!growShrinkTerrainMask(source, source, curve, std::numeric_limits<float>::max()).ok());
|
|
CHECK(source.data() == previous);
|
|
CHECK(!growShrinkTerrainMask(source, source, curve, std::numeric_limits<float>::quiet_NaN()).ok());
|
|
CHECK(source.data() == previous);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.strengthFiveSourcePasses") {
|
|
Heightmap source(1, 1), curve(1, 1), output(1, 1);
|
|
source.data() = {0.25F};
|
|
curve.data() = {0.75F};
|
|
const float expected[] = {0.5F, 0.5F, 0.25F, 0.625F, -0.125F};
|
|
for (int mode = 0; mode < 5; ++mode) {
|
|
REQUIRE(applyTerrainStrength(output, source, curve, static_cast<TerrainStrengthMode>(mode), 0.5F, false).ok());
|
|
CHECK(output.data()[0] == expected[mode]);
|
|
}
|
|
REQUIRE(applyTerrainStrength(output, source, curve, TerrainStrengthMode::Replace, 1, true).ok());
|
|
CHECK(output.data()[0] == 0.25F);
|
|
CHECK(!applyTerrainStrength(output, source, curve, static_cast<TerrainStrengthMode>(9), 1, false).ok());
|
|
CHECK(output.data()[0] == 0.25F);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.erosionCompositionInvertsAfterCurveAndRollsBack") {
|
|
Heightmap oldHeights(1, 1), erosion(1, 1), brush(1, 1), curve(2, 1), output(1, 1);
|
|
oldHeights.data() = {0.2F};
|
|
erosion.data() = {0.8F};
|
|
brush.data() = {0.5F};
|
|
curve.data() = {0.2F, 0.4F};
|
|
output.data() = {42};
|
|
TerrainBrushBlendSettings spatial;
|
|
REQUIRE(generateTerrainErosionMask(output, oldHeights, erosion, brush, spatial, curve, TerrainStrengthMode::Replace,
|
|
1, false)
|
|
.ok());
|
|
CHECK(std::abs(output.data()[0] - 0.7F) < 1e-6F);
|
|
REQUIRE(generateTerrainErosionMask(output, oldHeights, erosion, brush, spatial, curve, TerrainStrengthMode::Replace,
|
|
1, true)
|
|
.ok());
|
|
CHECK(std::abs(output.data()[0] - 0.3F) < 1e-6F);
|
|
const auto previous = output.data();
|
|
curve.data()[0] = std::numeric_limits<float>::quiet_NaN();
|
|
CHECK(!generateTerrainErosionMask(output, oldHeights, erosion, brush, spatial, curve, TerrainStrengthMode::Replace,
|
|
1, false)
|
|
.ok());
|
|
CHECK(output.data() == previous);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.brushAffineSamplingAndAliasing") {
|
|
Heightmap oldHeights(2, 1), newHeights(1, 1), brush(1, 1), output(4, 1);
|
|
oldHeights.data() = {0, 8};
|
|
newHeights.data() = {10};
|
|
brush.data() = {0.5F};
|
|
TerrainBrushBlendSettings settings;
|
|
settings.brushXX = 2;
|
|
settings.brushOffsetX = -0.5F;
|
|
REQUIRE(blendTerrainBrush(output, oldHeights, newHeights, brush, settings).ok());
|
|
CHECK(output.data() == std::vector<float>({0, 6, 8, 8}));
|
|
Heightmap original = output, expected = output;
|
|
settings.heightXX = -1;
|
|
settings.heightOffsetX = 1;
|
|
REQUIRE(blendTerrainBrush(expected, original, newHeights, brush, settings).ok());
|
|
REQUIRE(blendTerrainBrush(output, output, newHeights, brush, settings).ok());
|
|
CHECK(output.data() == expected.data());
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.brushUnclampedStrengthAndAtomicFailure") {
|
|
Heightmap oldHeights(1, 1), newHeights(1, 1), brush(1, 1), output(1, 1);
|
|
oldHeights.data() = {2};
|
|
newHeights.data() = {4};
|
|
brush.data() = {1};
|
|
TerrainBrushBlendSettings settings;
|
|
settings.strength = 2;
|
|
REQUIRE(blendTerrainBrush(output, oldHeights, newHeights, brush, settings).ok());
|
|
CHECK(output.data()[0] == 6);
|
|
newHeights.data()[0] = std::numeric_limits<float>::max();
|
|
CHECK(!blendTerrainBrush(output, oldHeights, newHeights, brush, settings).ok());
|
|
CHECK(output.data()[0] == 6);
|
|
settings.brushXX = std::numeric_limits<float>::quiet_NaN();
|
|
CHECK(!blendTerrainBrush(output, oldHeights, newHeights, brush, settings).ok());
|
|
CHECK(output.data()[0] == 6);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.brushRotationAndInclusiveEdges") {
|
|
Heightmap oldHeights(1, 1), newHeights(1, 1), brush(2, 2), output(2, 2);
|
|
newHeights.data() = {8};
|
|
brush.data() = {0, 0.25F, 0.5F, 1};
|
|
TerrainBrushBlendSettings settings;
|
|
settings.brushXX = 0;
|
|
settings.brushXZ = 1;
|
|
settings.brushZX = -1;
|
|
settings.brushZZ = 0;
|
|
settings.brushOffsetZ = 1;
|
|
REQUIRE(blendTerrainBrush(output, oldHeights, newHeights, brush, settings).ok());
|
|
CHECK(output.data() == std::vector<float>({4, 0, 8, 2}));
|
|
settings.brushXZ = 0;
|
|
settings.brushZX = 0;
|
|
settings.brushOffsetX = 1;
|
|
settings.brushOffsetZ = 1;
|
|
REQUIRE(blendTerrainBrush(output, oldHeights, newHeights, brush, settings).ok());
|
|
CHECK(output.data() == std::vector<float>({8, 8, 8, 8}));
|
|
settings.brushOffsetX = 1.01F;
|
|
REQUIRE(blendTerrainBrush(output, oldHeights, newHeights, brush, settings).ok());
|
|
CHECK(output.data() == std::vector<float>({0, 0, 0, 0}));
|
|
}
|
|
namespace {
|
|
Heightmap row(std::initializer_list<float> values) {
|
|
Heightmap map(int(values.size()), 1);
|
|
map.data() = values;
|
|
return map;
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("procgen.mask.curveTextureUsesTexelCenters") {
|
|
auto values = row({-1, 0, 0.25F, 0.5F, 0.75F, 1, 2});
|
|
const auto curve = row({0, 1});
|
|
auto r = transformTerrainMask(values, values, curve);
|
|
REQUIRE(r.ok());
|
|
CHECK(values.data() == row({0, 0, 0, 0.5F, 1, 1, 1}).data());
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.twoStageRangeMatchesShaderEquations") {
|
|
auto values = row({0, 25, 50, 75, 100});
|
|
const auto filter = row({0, 1}), strength = row({1, 0});
|
|
auto r = generateTerrainRangeMask(values, values, 0, 100, filter, strength);
|
|
REQUIRE(r.ok());
|
|
CHECK(values.data() == row({1, 1, 0.5F, 0, 0}).data());
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.slopeUsesWorldSpacingAndNormalMetric") {
|
|
Heightmap plane(5, 3), slope(5, 3);
|
|
for (int y = 0; y < 3; ++y)
|
|
for (int x = 0; x < 5; ++x) plane.setHeight(x, y, float(x));
|
|
auto r = deriveTerrainSlope(slope, plane, 2, 3, 2);
|
|
REQUIRE(r.ok());
|
|
for (float value : slope.data()) CHECK(std::abs(value - float(1 - 1 / std::sqrt(2.0))) < 1e-6F);
|
|
r = deriveTerrainSlope(plane, plane, 2, 3, 2);
|
|
REQUIRE(r.ok());
|
|
CHECK(plane.data() == slope.data());
|
|
auto singleton = row({9});
|
|
r = deriveTerrainSlope(singleton, singleton, 1, 1);
|
|
REQUIRE(r.ok());
|
|
CHECK(singleton.height(0, 0) == 0);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.distanceAxesRotationAndRepeat") {
|
|
Heightmap mask(3, 3);
|
|
const auto curve = row({0, 1});
|
|
TerrainDistanceMaskSettings s;
|
|
s.axis = TerrainDistanceAxis::X;
|
|
auto r = generateTerrainDistanceMask(mask, s, curve, curve);
|
|
REQUIRE(r.ok());
|
|
for (int y = 0; y < 3; ++y) {
|
|
CHECK(mask.height(0, y) == 0);
|
|
CHECK(mask.height(1, y) == 0.5F);
|
|
CHECK(mask.height(2, y) == 1);
|
|
}
|
|
s.rotation = float(std::numbers::pi / 2);
|
|
r = generateTerrainDistanceMask(mask, s, curve, curve);
|
|
REQUIRE(r.ok());
|
|
CHECK(mask.height(1, 0) == 0);
|
|
CHECK(mask.height(1, 2) == 1);
|
|
s.rotation = 0;
|
|
s.offsetX = 1;
|
|
s.tiling = true;
|
|
r = generateTerrainDistanceMask(mask, s, curve, curve);
|
|
REQUIRE(r.ok());
|
|
CHECK(mask.height(0, 1) == 0);
|
|
CHECK(mask.height(1, 1) == 0.5F);
|
|
CHECK(mask.height(2, 1) == 1);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.distanceCircleAndSquareOutsideRules") {
|
|
Heightmap mask(3, 3);
|
|
const auto curve = row({0, 1}), reverse = row({1, 0});
|
|
TerrainDistanceMaskSettings s;
|
|
auto r = generateTerrainDistanceMask(mask, s, curve, curve);
|
|
REQUIRE(r.ok());
|
|
CHECK(mask.height(1, 1) == 0);
|
|
CHECK(mask.height(0, 0) == 1);
|
|
s.axis = TerrainDistanceAxis::RoundedSquare;
|
|
r = generateTerrainDistanceMask(mask, s, curve, curve);
|
|
REQUIRE(r.ok());
|
|
CHECK(mask.height(1, 1) == 0.5F);
|
|
s.offsetX = 2;
|
|
r = generateTerrainDistanceMask(mask, s, curve, reverse);
|
|
REQUIRE(r.ok());
|
|
for (float value : mask.data()) CHECK(value == 1); // Outside is transformed, not forcibly black.
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.rejectedInputsDoNotPublish") {
|
|
auto target = row({4, 5}), source = row({0, 1}), curve = row({0, 1});
|
|
const auto before = target;
|
|
auto r = generateTerrainRangeMask(target, source, 1, 1, curve, curve);
|
|
CHECK(!r.ok());
|
|
CHECK(target.data() == before.data());
|
|
r = deriveTerrainSlope(target, source, 0, 1);
|
|
CHECK(!r.ok());
|
|
CHECK(target.data() == before.data());
|
|
TerrainDistanceMaskSettings s;
|
|
s.scaleX = std::numeric_limits<float>::quiet_NaN();
|
|
r = generateTerrainDistanceMask(target, s, curve, curve);
|
|
CHECK(!r.ok());
|
|
CHECK(target.data() == before.data());
|
|
curve.setHeight(1, 0, std::numeric_limits<float>::infinity());
|
|
r = transformTerrainMask(target, source, curve);
|
|
CHECK(!r.ok());
|
|
CHECK(target.data() == before.data());
|
|
}
|
|
|
|
TEST_CASE("procgen.effect.contrastUsesSnapshotAndNineTapWeights") {
|
|
Heightmap target(3, 3), mask(3, 3);
|
|
std::fill(mask.data().begin(), mask.data().end(), 1.0F);
|
|
target.setHeight(1, 1, 1);
|
|
auto r = applyTerrainContrast(target, mask, 1, 1);
|
|
REQUIRE(r.ok());
|
|
CHECK(target.height(1, 1) == 1.4375F);
|
|
CHECK(target.height(1, 0) == -0.0625F);
|
|
CHECK(target.height(0, 0) == -0.046875F);
|
|
auto before = target.data();
|
|
r = applyTerrainContrast(target, mask, 1, 0);
|
|
REQUIRE(r.ok());
|
|
CHECK(target.data() == before);
|
|
mask.setHeight(2, 2, 2);
|
|
r = applyTerrainContrast(target, mask, 1, 1);
|
|
CHECK(!r.ok());
|
|
CHECK(target.data() == before);
|
|
}
|
|
TEST_CASE("procgen.effect.contrastFractionalOffsetAndAlias") {
|
|
auto target = row({0, 1, 0});
|
|
auto mask = row({1, 1, 1});
|
|
auto r = applyTerrainContrast(target, mask, 1, 0.5F);
|
|
REQUIRE(r.ok());
|
|
CHECK(target.height(1, 0) == 1.15625F);
|
|
auto alias = row({0, 1, 0});
|
|
r = applyTerrainContrast(alias, alias, 1, 1);
|
|
REQUIRE(r.ok());
|
|
CHECK(alias.data() == row({0, 1.3125F, 0}).data());
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.globalSpawnerScalarOutputUsesImageTransformAndBlend") {
|
|
Heightmap input(2, 1), source(1, 1), curve(1, 1), output(2, 1);
|
|
input.data() = {0.8F, 0.4F};
|
|
source.data() = {0.5F};
|
|
curve.data() = {0.5F};
|
|
TerrainImageMaskSettings settings;
|
|
settings.filter = static_cast<TerrainImageFilter>(99);
|
|
REQUIRE(applyTerrainGlobalSpawnerMask(output, input, source, curve, settings,
|
|
TerrainMaskBlend::Multiply).ok());
|
|
CHECK(output.data()[0] == 0.4F);
|
|
CHECK(output.data()[1] == 0.2F);
|
|
auto before = output.data();
|
|
settings.scaleX = 0;
|
|
CHECK(!applyTerrainGlobalSpawnerMask(output, input, source, curve, settings,
|
|
TerrainMaskBlend::Multiply).ok());
|
|
CHECK(output.data() == before);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.noiseFamiliesFractionalFractalWarpAndDeterminism") {
|
|
Heightmap input(7, 5), curve(4, 1), first(7, 5), second(7, 5);
|
|
std::fill(input.data().begin(), input.data().end(), 1.0F);
|
|
curve.data() = {0.0F, 0.33333334F, 0.6666667F, 1.0F};
|
|
TerrainNoiseMaskSettings settings;
|
|
settings.octaves = 3.5F;
|
|
settings.warpIterations = 1.5F;
|
|
settings.warpStrength = 0.2F;
|
|
settings.rotation = 0.3F;
|
|
settings.seed = 42;
|
|
std::vector<std::vector<float>> families;
|
|
for (int type = 0; type <= 4; ++type) {
|
|
settings.type = static_cast<TerrainNoiseType>(type);
|
|
REQUIRE(generateTerrainNoiseMask(first, input, curve, settings, TerrainMaskBlend::Multiply).ok());
|
|
REQUIRE(generateTerrainNoiseMask(second, input, curve, settings, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(first.data() == second.data());
|
|
CHECK(std::all_of(first.data().begin(), first.data().end(), [](float v) { return std::isfinite(v); }));
|
|
families.push_back(first.data());
|
|
}
|
|
CHECK(families[0] != families[1]);
|
|
auto before = first.data();
|
|
settings.octaves = 17;
|
|
CHECK(!generateTerrainNoiseMask(first, input, curve, settings, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(first.data() == before);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.smoothTwoPassVerticalityRectangularAndAtomic") {
|
|
Heightmap input(15, 15), output(15, 15);
|
|
input.setHeight(7, 7, 1);
|
|
REQUIRE(smoothTerrainMask(output, input, 0, 1).ok());
|
|
CHECK(std::abs(output.height(7, 7) - 1.0 / 57.76) < 1e-7);
|
|
CHECK(std::abs(output.height(8, 9) - 0.95 * 0.85 / 57.76) < 1e-7);
|
|
REQUIRE(smoothTerrainMask(output, input, -1, 1).ok());
|
|
for (size_t i = 0; i < output.data().size(); ++i) CHECK(output.data()[i] <= input.data()[i]);
|
|
REQUIRE(smoothTerrainMask(output, input, 1, 1).ok());
|
|
for (size_t i = 0; i < output.data().size(); ++i) CHECK(output.data()[i] >= input.data()[i]);
|
|
|
|
Heightmap rectangular(2, 4), rectangularOutput(2, 4);
|
|
rectangular.setHeight(0, 1, 1);
|
|
rectangular.setHeight(1, 1, 1);
|
|
REQUIRE(smoothTerrainMask(rectangularOutput, rectangular, 0, 0.5F).ok());
|
|
CHECK(std::abs(rectangularOutput.height(0, 1) - 1.0 / 7.6) < 1e-7);
|
|
CHECK(std::abs(rectangularOutput.height(0, 2) - 0.95 / 7.6) < 1e-7);
|
|
const auto before = rectangularOutput.data();
|
|
CHECK(!smoothTerrainMask(rectangularOutput, rectangular, 2, 1).ok());
|
|
CHECK(rectangularOutput.data() == before);
|
|
}
|
|
|
|
TEST_CASE("procgen.mask.collisionStackMatchesRadiusAndLayerInvertConventions") {
|
|
auto input = row({0.5F, 0.5F});
|
|
auto target = row({9, 9});
|
|
auto curve = row({0, 1});
|
|
auto radius = row({1, 1});
|
|
auto layer = row({0, 1});
|
|
auto inactive = row({0, 0});
|
|
TerrainCollisionMaskStack stack;
|
|
REQUIRE(stack.addLayer(radius, TerrainCollisionMaskType::RadiusTree, true, false).ok());
|
|
REQUIRE(stack.addLayer(layer, TerrainCollisionMaskType::LayerGameObject, true, false).ok());
|
|
REQUIRE(stack.addLayer(inactive, TerrainCollisionMaskType::RadiusTag, false, false).ok());
|
|
CHECK(stack.getLayerCount() == 3);
|
|
REQUIRE(stack.apply(target, input, curve, TerrainMaskBlend::Multiply).ok());
|
|
CHECK(target.data() == row({0.5F, 0}).data());
|
|
const auto before = target.data();
|
|
CHECK(!stack.addLayer(radius, static_cast<TerrainCollisionMaskType>(99), true, false).ok());
|
|
CHECK(!stack.apply(target, input, curve, static_cast<TerrainMaskBlend>(99)).ok());
|
|
CHECK(target.data() == before);
|
|
stack.clear();
|
|
CHECK(stack.getLayerCount() == 0);
|
|
}
|