Files
EVEngine/test/prototype_kit.cpp
T

269 lines
11 KiB
C++

#include "zeroerr/unittest.h"
#include "image/ImageData.h"
#include "procgen/Params.h"
#include "procgen/algorithms/MarchingCubes.h"
#include "procgen/algorithms/PrototypeKit.h"
#include "procgen/texture/PrototypeTextures.h"
#include "procgen/texture/TextureRecipe.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <limits>
#include <string>
using namespace eve::procgen;
namespace {
uint64_t imageChecksum(const eve::image::ImageData& image) {
const auto* bytes = static_cast<const uint8_t*>(image.getData());
uint64_t hash = 1469598103934665603ull;
for (size_t i = 0; i < image.getSize(); ++i) {
hash ^= bytes[i];
hash *= 1099511628211ull;
}
return hash;
}
bool windingNeverOpposesNormals(const MeshBuild& mesh) {
for (int triangle = 0; triangle + 2 < mesh.getIndexCount(); triangle += 3) {
const int i0 = mesh.getIndex(triangle);
const int i1 = mesh.getIndex(triangle + 1);
const int i2 = mesh.getIndex(triangle + 2);
const float ax = mesh.getPositionX(i1) - mesh.getPositionX(i0);
const float ay = mesh.getPositionY(i1) - mesh.getPositionY(i0);
const float az = mesh.getPositionZ(i1) - mesh.getPositionZ(i0);
const float bx = mesh.getPositionX(i2) - mesh.getPositionX(i0);
const float by = mesh.getPositionY(i2) - mesh.getPositionY(i0);
const float bz = mesh.getPositionZ(i2) - mesh.getPositionZ(i0);
const float cx = ay * bz - az * by;
const float cy = az * bx - ax * bz;
const float cz = ax * by - ay * bx;
const float nx = mesh.getNormalX(i0) + mesh.getNormalX(i1) + mesh.getNormalX(i2);
const float ny = mesh.getNormalY(i0) + mesh.getNormalY(i1) + mesh.getNormalY(i2);
const float nz = mesh.getNormalZ(i0) + mesh.getNormalZ(i1) + mesh.getNormalZ(i2);
if (cx * nx + cy * ny + cz * nz < -1.0e-6f) return false;
}
return true;
}
bool windingFacesAwayFromPoint(const MeshBuild& mesh, float px, float py, float pz) {
for (int triangle = 0; triangle + 2 < mesh.getIndexCount(); triangle += 3) {
const int i0 = mesh.getIndex(triangle);
const int i1 = mesh.getIndex(triangle + 1);
const int i2 = mesh.getIndex(triangle + 2);
const float ax = mesh.getPositionX(i1) - mesh.getPositionX(i0);
const float ay = mesh.getPositionY(i1) - mesh.getPositionY(i0);
const float az = mesh.getPositionZ(i1) - mesh.getPositionZ(i0);
const float bx = mesh.getPositionX(i2) - mesh.getPositionX(i0);
const float by = mesh.getPositionY(i2) - mesh.getPositionY(i0);
const float bz = mesh.getPositionZ(i2) - mesh.getPositionZ(i0);
const float nx = ay * bz - az * by;
const float ny = az * bx - ax * bz;
const float nz = ax * by - ay * bx;
const float cx =
(mesh.getPositionX(i0) + mesh.getPositionX(i1) + mesh.getPositionX(i2)) / 3.0f;
const float cy =
(mesh.getPositionY(i0) + mesh.getPositionY(i1) + mesh.getPositionY(i2)) / 3.0f;
const float cz =
(mesh.getPositionZ(i0) + mesh.getPositionZ(i1) + mesh.getPositionZ(i2)) / 3.0f;
if (nx * (cx - px) + ny * (cy - py) + nz * (cz - pz) < -1.0e-6f) return false;
}
return true;
}
} // namespace
TEST_CASE("procgen.prototype.mesh.catalogueBuilds75GroundedPieces") {
const auto pieces = prototypePieceDescriptors();
REQUIRE_EQ(pieces.size(), size_t(75));
Params params;
for (const auto& piece : pieces) {
auto result = generatePrototypePiece(piece.id, params);
REQUIRE(result.ok());
MeshBuild mesh = std::move(result).takeValue();
REQUIRE(!mesh.empty());
REQUIRE(mesh.getVertexCount() > 0);
REQUIRE(mesh.getIndexCount() > 0);
float minimumY = mesh.getPositionY(0);
for (int vertex = 1; vertex < mesh.getVertexCount(); ++vertex)
minimumY = std::min(minimumY, mesh.getPositionY(vertex));
CHECK(minimumY >= -1.0e-5f);
for (int index = 0; index < mesh.getIndexCount(); ++index) {
CHECK(mesh.getIndex(index) >= 0);
CHECK(mesh.getIndex(index) < mesh.getVertexCount());
}
CHECK(windingNeverOpposesNormals(mesh));
CHECK_EQ(mesh.getMeta("source", ""), std::string("procedural"));
}
}
TEST_CASE("procgen.prototype.mesh.parametersChangeTopologyAndDimensions") {
Params coarse;
coarse.setInt("steps", 3);
coarse.setFloat("width", 2.0f);
Params detailed;
detailed.setInt("steps", 9);
detailed.setFloat("width", 4.0f);
auto aResult = generatePrototypePiece("stairs", coarse);
auto bResult = generatePrototypePiece("stairs", detailed);
REQUIRE(aResult.ok());
REQUIRE(bResult.ok());
MeshBuild a = std::move(aResult).takeValue();
MeshBuild b = std::move(bResult).takeValue();
CHECK(b.getVertexCount() > a.getVertexCount());
float width = 0.0f;
for (int vertex = 0; vertex < b.getVertexCount(); ++vertex)
width = std::max(width, std::abs(b.getPositionX(vertex)) * 2.0f);
CHECK(width > 3.9f);
}
TEST_CASE("procgen.prototype.mesh.rampsHaveOutwardFaces") {
Params params;
for (const char* id : {"ramp", "ramp1"}) {
auto result = generatePrototypePiece(id, params);
REQUIRE(result.ok());
MeshBuild mesh = std::move(result).takeValue();
float minX = mesh.getPositionX(0), maxX = minX;
float minY = mesh.getPositionY(0), maxY = minY;
float minZ = mesh.getPositionZ(0), maxZ = minZ;
for (int vertex = 1; vertex < mesh.getVertexCount(); ++vertex) {
minX = std::min(minX, mesh.getPositionX(vertex));
maxX = std::max(maxX, mesh.getPositionX(vertex));
minY = std::min(minY, mesh.getPositionY(vertex));
maxY = std::max(maxY, mesh.getPositionY(vertex));
minZ = std::min(minZ, mesh.getPositionZ(vertex));
maxZ = std::max(maxZ, mesh.getPositionZ(vertex));
}
const float interiorX = (minX + maxX) * 0.5f;
const float interiorY = minY + (maxY - minY) / 3.0f;
const float interiorZ = minZ + (maxZ - minZ) * 2.0f / 3.0f;
CHECK(windingFacesAwayFromPoint(mesh, interiorX, interiorY, interiorZ));
}
}
TEST_CASE("procgen.prototype.mesh.uvScaleChangesDensityWithoutChangingGeometry") {
Params base;
base.setFloat("uvScale", 0.5f);
Params dense;
dense.setFloat("uvScale", 2.0f);
auto baseResult = generatePrototypePiece("wall", base);
auto denseResult = generatePrototypePiece("wall", dense);
REQUIRE(baseResult.ok());
REQUIRE(denseResult.ok());
MeshBuild a = std::move(baseResult).takeValue();
MeshBuild b = std::move(denseResult).takeValue();
REQUIRE_EQ(a.positions().size(), b.positions().size());
REQUIRE_EQ(a.uvs().size(), b.uvs().size());
CHECK(std::equal(a.positions().begin(), a.positions().end(), b.positions().begin()));
bool changed = false;
for (size_t i = 0; i < a.uvs().size(); ++i) {
if (std::abs(a.uvs()[i] - b.uvs()[i]) > 1.0e-5f) changed = true;
}
CHECK(changed);
}
TEST_CASE("procgen.prototype.mesh.registryExposesEveryPiece") {
auto& registry = MeshRecipeRegistry::instance();
registry.registerBuiltins();
for (const auto& piece : prototypePieceDescriptors()) {
const std::string recipe = "prototype." + std::string(piece.id);
CHECK(registry.has(recipe));
const RecipeDescriptor* schema = registry.descriptor(recipe);
REQUIRE(schema != nullptr);
CHECK(schema->find("width") != nullptr);
CHECK(schema->find("detail") != nullptr);
CHECK(schema->find("uvScale") != nullptr);
}
}
TEST_CASE("procgen.prototype.mesh.unknownIdIsStructuredFailure") {
Params params;
auto result = generatePrototypePiece("not-a-piece", params);
CHECK(!result.ok());
CHECK_EQ(result.status().code(), eve::StatusCode::NotFound);
}
TEST_CASE("procgen.prototype.texture.catalogueBuilds13PatternsIn6Palettes") {
constexpr std::array<const char*, 6> palettes{{"dark", "light", "purple", "orange", "green", "red"}};
const auto patterns = prototypeTextureDescriptors();
REQUIRE_EQ(patterns.size(), size_t(13));
for (const auto& pattern : patterns) {
uint64_t previous = 0;
for (const char* palette : palettes) {
Params params;
params.setSize(96, 80);
params.setString("palette", palette);
params.setInt("cellSize", 16);
auto result = generatePrototypeTexture(pattern.id, params);
REQUIRE(result.ok());
auto image = std::move(result).takeValue();
REQUIRE(static_cast<bool>(image));
CHECK_EQ(image->getWidth(), 96);
CHECK_EQ(image->getHeight(), 80);
CHECK_EQ(image->getFormat(), std::string("RGBA8"));
const uint64_t checksum = imageChecksum(*image);
if (previous != 0) CHECK(checksum != previous);
previous = checksum;
}
}
}
TEST_CASE("procgen.prototype.texture.isDeterministicAndParameterized") {
Params base;
base.setSize(128, 128);
base.setString("palette", "custom");
base.setInt("backgroundR", 12);
base.setInt("backgroundG", 34);
base.setInt("backgroundB", 56);
base.setInt("cellSize", 16);
auto firstResult = generatePrototypeTexture("diagonal-grid", base);
auto secondResult = generatePrototypeTexture("diagonal-grid", base);
REQUIRE(firstResult.ok());
REQUIRE(secondResult.ok());
auto first = std::move(firstResult).takeValue();
auto second = std::move(secondResult).takeValue();
CHECK_EQ(imageChecksum(*first), imageChecksum(*second));
base.setInt("cellSize", 32);
auto changedResult = generatePrototypeTexture("diagonal-grid", base);
REQUIRE(changedResult.ok());
auto changed = std::move(changedResult).takeValue();
CHECK(imageChecksum(*first) != imageChecksum(*changed));
}
TEST_CASE("procgen.prototype.texture.rejectsInvalidParameters") {
Params invalidPalette;
invalidPalette.setSize(64, 64);
invalidPalette.setString("palette", "blue");
auto paletteResult = generatePrototypeTexture("fine-grid", invalidPalette);
CHECK(!paletteResult.ok());
REQUIRE(paletteResult.error() != nullptr);
CHECK_EQ(paletteResult.error()->code(), eve::DiagnosticCode::InvalidArgument);
Params invalidOpacity;
invalidOpacity.setSize(64, 64);
invalidOpacity.setFloat("minorAlpha", std::numeric_limits<float>::quiet_NaN());
auto opacityResult = generatePrototypeTexture("fine-grid", invalidOpacity);
CHECK(!opacityResult.ok());
REQUIRE(opacityResult.error() != nullptr);
CHECK_EQ(opacityResult.error()->code(), eve::DiagnosticCode::InvalidArgument);
}
TEST_CASE("procgen.prototype.texture.registryExposesEveryPattern") {
auto& registry = TextureRecipeRegistry::instance();
registry.registerBuiltins();
for (const auto& pattern : prototypeTextureDescriptors()) {
const std::string recipe = "tex.prototype." + std::string(pattern.id);
CHECK(registry.has(recipe));
const RecipeDescriptor* schema = registry.descriptor(recipe);
REQUIRE(schema != nullptr);
CHECK(schema->find("palette") != nullptr);
CHECK(schema->find("cellSize") != nullptr);
}
}