Files
EVEngine/test/procgen_mesh_modifier_graph.cpp

586 lines
26 KiB
C++

#include "procgen/mesh/MeshModifierGraph.h"
#include "zeroerr/unittest.h"
#include <cmath>
using namespace eve;
using namespace eve::procgen;
namespace {
MeshBuild triangle(float offsetX = 0.f) {
MeshBuild mesh;
mesh.addVertex(offsetX + 0.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f);
mesh.addVertex(offsetX + 1.f, 0.f, 0.f, 0.f, 0.f, 1.f, 1.f, 0.f);
mesh.addVertex(offsetX + 0.f, 1.f, 0.f, 0.f, 0.f, 1.f, 0.f, 1.f);
mesh.addTriangle(0, 1, 2);
return mesh;
}
MeshBuild tetrahedron(float tipOffset = 0.f) {
MeshBuild mesh;
mesh.addVertex(0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f);
mesh.addVertex(1.f, 0.f, 0.f, 0.f, 1.f, 0.f, 1.f, 0.f);
mesh.addVertex(0.f, 1.f, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f);
mesh.addVertex(tipOffset, 0.f, 1.f, 0.f, 1.f, 0.f, 1.f, 1.f);
mesh.addTriangle(0, 2, 1);
mesh.addTriangle(0, 1, 3);
mesh.addTriangle(1, 2, 3);
mesh.addTriangle(2, 0, 3);
return mesh;
}
MeshBuild fitSurface() {
MeshBuild mesh;
mesh.addVertex(-2.f, -2.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f);
mesh.addVertex(2.f, -2.f, 0.f, 0.f, 0.f, 1.f, 1.f, 0.f);
mesh.addVertex(2.f, 2.f, 0.f, 0.f, 0.f, 1.f, 1.f, 1.f);
mesh.addVertex(-2.f, 2.f, 0.f, 0.f, 0.f, 1.f, 0.f, 1.f);
mesh.addTriangle(0, 1, 2);
mesh.addTriangle(0, 2, 3);
return mesh;
}
void requireOk(Result<void> result) { REQUIRE(result.ok()); }
} // namespace
TEST_CASE("procgen.meshModifierGraph.fusesConsecutiveVertexOperations") {
MeshModifierGraph graph;
requireOk(graph.addNode("source", "mesh.input"));
requireOk(graph.addNode("move", "deform.transform"));
requireOk(graph.addNode("twist", "deform.twist"));
requireOk(graph.addNode("out", "mesh.output"));
requireOk(graph.connect("source", "move"));
requireOk(graph.connect("move", "twist"));
requireOk(graph.connect("twist", "out"));
requireOk(graph.setNodeFloat("move", "x", 2.f));
requireOk(graph.setNodeFloat("move", "maskRadius", 0.25f));
requireOk(graph.setNodeFloat("twist", "angle", 0.f));
requireOk(graph.setNodeMesh("source", triangle()));
auto result = graph.executeResult("out");
REQUIRE(result.ok());
const auto output = std::move(result).takeValue();
CHECK_EQ(output.getVertexCount(), 3);
CHECK(std::abs(output.getPositionX(0) - 2.f) < 1e-6f);
CHECK(std::abs(output.getPositionX(1) - 1.f) < 1e-6f);
CHECK_EQ(graph.compiledSegmentCount(), 3);
CHECK_EQ(graph.fusedOperationCount(), 2);
auto cached = graph.executeResult("out");
REQUIRE(cached.ok());
CHECK_EQ(graph.metrics().size(), std::size_t(1));
CHECK(graph.metrics().front().cacheHit);
}
TEST_CASE("procgen.meshModifierGraph.appendAndWeldAreFusionBarriers") {
MeshModifierGraph graph;
for (const auto* node : {"left", "right"}) requireOk(graph.addNode(node, "mesh.input"));
requireOk(graph.addNode("append", "mesh.append"));
requireOk(graph.addNode("weld", "mesh.weld"));
requireOk(graph.addNode("out", "mesh.output"));
requireOk(graph.connect("left", "append", 0));
requireOk(graph.connect("right", "append", 1));
requireOk(graph.connect("append", "weld"));
requireOk(graph.connect("weld", "out"));
requireOk(graph.setNodeFloat("weld", "tolerance", 0.001f));
requireOk(graph.setNodeMesh("left", triangle()));
requireOk(graph.setNodeMesh("right", triangle()));
auto result = graph.executeResult("out");
REQUIRE(result.ok());
const auto output = std::move(result).takeValue();
CHECK_EQ(output.getVertexCount(), 3);
CHECK_EQ(output.getIndexCount(), 6);
CHECK_EQ(graph.fusedOperationCount(), 0);
}
TEST_CASE("procgen.meshModifierGraph.rejectsCyclesAndInvalidParameters") {
MeshModifierGraph graph;
requireOk(graph.addNode("source", "mesh.input"));
requireOk(graph.addNode("a", "deform.bend"));
requireOk(graph.addNode("b", "deform.twist"));
requireOk(graph.connect("b", "a"));
requireOk(graph.connect("a", "b"));
auto cycle = graph.executeResult("b");
REQUIRE(!cycle.ok());
const auto* diagnostic = cycle.status().primaryDiagnostic();
REQUIRE(diagnostic != nullptr);
CHECK(diagnostic->code() == DiagnosticCode::Conflict);
auto invalid = graph.setNodeString("a", "axis", "diagonal");
REQUIRE(invalid.ok());
requireOk(graph.disconnect("a"));
requireOk(graph.connect("source", "a"));
requireOk(graph.setNodeMesh("source", triangle()));
auto badAxis = graph.executeResult("a");
REQUIRE(!badAxis.ok());
diagnostic = badAxis.status().primaryDiagnostic();
REQUIRE(diagnostic != nullptr);
CHECK(diagnostic->code() == DiagnosticCode::InvalidArgument);
}
TEST_CASE("procgen.meshModifierGraph.supportsFfdMorphAndSpherify") {
MeshModifierGraph graph;
requireOk(graph.addNode("source", "mesh.input"));
requireOk(graph.addNode("ffd", "deform.ffd"));
requireOk(graph.addNode("sphere", "deform.spherify"));
requireOk(graph.connect("source", "ffd"));
requireOk(graph.connect("ffd", "sphere"));
requireOk(graph.setNodeFloat("ffd", "p001x", 0.5f));
requireOk(graph.setNodeFloat("sphere", "weight", 0.f));
requireOk(graph.setNodeMesh("source", tetrahedron()));
auto deformed = graph.executeResult("sphere");
REQUIRE(deformed.ok());
CHECK(std::abs(deformed.value().getPositionX(3) - 0.5f) < 1e-6f);
MeshModifierGraph morph;
requireOk(morph.addNode("a", "mesh.input"));
requireOk(morph.addNode("b", "mesh.input"));
requireOk(morph.addNode("blend", "deform.morph"));
requireOk(morph.connect("a", "blend", 0));
requireOk(morph.connect("b", "blend", 1));
requireOk(morph.setNodeFloat("blend", "weight", 0.5f));
requireOk(morph.setNodeMesh("a", tetrahedron()));
requireOk(morph.setNodeMesh("b", tetrahedron(1.f)));
auto blended = morph.executeResult("blend");
REQUIRE(blended.ok());
CHECK(std::abs(blended.value().getPositionX(3) - 0.5f) < 1e-6f);
}
TEST_CASE("procgen.meshModifierGraph.meshEffectorSupportsOneToFourWeightedNodesAndFusion") {
MeshModifierGraph graph;
requireOk(graph.addNode("source", "mesh.input"));
requireOk(graph.addNode("effector", "deform.effector"));
requireOk(graph.addNode("move", "deform.transform"));
requireOk(graph.addNode("out", "mesh.output"));
requireOk(graph.connect("source", "effector"));
requireOk(graph.connect("effector", "move"));
requireOk(graph.connect("move", "out"));
requireOk(graph.setNodeMesh("source", triangle()));
requireOk(graph.setNodeInt("effector", "pointCount", 2));
requireOk(graph.setNodeFloat("effector", "p0radius", 0.4f));
requireOk(graph.setNodeFloat("effector", "p0dy", 2.f));
requireOk(graph.setNodeFloat("effector", "p1x", 1.f));
requireOk(graph.setNodeFloat("effector", "p1radius", 0.4f));
requireOk(graph.setNodeFloat("effector", "p1dx", 1.f));
requireOk(graph.setNodeFloat("effector", "p1dy", 0.f));
auto result = graph.executeResult("out");
REQUIRE(result.ok());
CHECK(std::abs(result.value().getPositionY(0) - 2.f) < 1e-6f);
CHECK(std::abs(result.value().getPositionX(1) - 2.f) < 1e-6f);
CHECK(std::abs(result.value().getPositionY(2) - 1.f) < 1e-6f);
CHECK_EQ(graph.fusedOperationCount(), 2);
MeshModifierGraph invalid;
requireOk(invalid.addNode("source", "mesh.input"));
requireOk(invalid.addNode("effector", "deform.effector"));
requireOk(invalid.connect("source", "effector"));
requireOk(invalid.setNodeMesh("source", triangle()));
requireOk(invalid.setNodeInt("effector", "pointCount", 5));
CHECK(!invalid.executeResult("effector").ok());
}
TEST_CASE("procgen.meshModifierGraph.meshFitProjectsOntoSecondInputWithoutPhysicsDependency") {
MeshBuild source = triangle();
for (std::size_t index = 2; index < source.positions().size(); index += 3u) source.positions()[index] = 1.f;
MeshModifierGraph graph;
requireOk(graph.addNode("source", "mesh.input"));
requireOk(graph.addNode("surface", "mesh.input"));
requireOk(graph.addNode("fit", "deform.meshFit"));
requireOk(graph.connect("source", "fit", 0));
requireOk(graph.connect("surface", "fit", 1));
requireOk(graph.setNodeMesh("source", source));
requireOk(graph.setNodeMesh("surface", fitSurface()));
requireOk(graph.setNodeFloat("fit", "directionY", 0.f));
requireOk(graph.setNodeFloat("fit", "directionZ", -1.f));
requireOk(graph.setNodeFloat("fit", "surfaceOffset", 0.2f));
auto fitted = graph.executeResult("fit");
REQUIRE(fitted.ok());
for (int vertex = 0; vertex < fitted.value().getVertexCount(); ++vertex)
CHECK(std::abs(fitted.value().getPositionZ(vertex) - 0.2f) < 1e-6f);
CHECK_EQ(fitted.value().getMeta("deformer", ""), "deform.meshFit");
const auto revision = graph.revision();
requireOk(graph.setNodeFloat("fit", "directionX", 0.f));
requireOk(graph.setNodeFloat("fit", "directionZ", 0.f));
CHECK(!graph.executeResult("fit").ok());
CHECK(graph.revision() > revision);
}
TEST_CASE("procgen.meshModifierGraph.supportsSubdivisionAndPlaneCut") {
MeshModifierGraph graph;
requireOk(graph.addNode("source", "mesh.input"));
requireOk(graph.addNode("subdivide", "mesh.subdivide"));
requireOk(graph.addNode("cut", "mesh.cutPlane"));
requireOk(graph.connect("source", "subdivide"));
requireOk(graph.connect("subdivide", "cut"));
requireOk(graph.setNodeInt("subdivide", "levels", 1));
requireOk(graph.setNodeFloat("cut", "normalX", 1.f));
requireOk(graph.setNodeFloat("cut", "normalY", 0.f));
requireOk(graph.setNodeFloat("cut", "distance", 0.25f));
requireOk(graph.setNodeInt("cut", "cap", 1));
requireOk(graph.setNodeMesh("source", tetrahedron()));
auto result = graph.executeResult("cut");
REQUIRE(result.ok());
CHECK(result.value().getIndexCount() > 0);
CHECK_EQ(result.value().getGroupName(result.value().getGroupCount() - 1), "__cut_cap");
for (int i = 0; i < result.value().getVertexCount(); ++i) CHECK(result.value().getPositionX(i) >= 0.25f - 1e-6f);
MeshModifierGraph subdivision;
requireOk(subdivision.addNode("source", "mesh.input"));
requireOk(subdivision.addNode("subdivide", "mesh.subdivide"));
requireOk(subdivision.connect("source", "subdivide"));
requireOk(subdivision.setNodeMesh("source", triangle()));
auto subdivided = subdivision.executeResult("subdivide");
REQUIRE(subdivided.ok());
CHECK_EQ(subdivided.value().getIndexCount(), 12);
}
TEST_CASE("procgen.meshModifierGraph.deformsAlongCubicSplineOffsets") {
MeshModifierGraph graph;
requireOk(graph.addNode("source", "mesh.input"));
requireOk(graph.addNode("spline", "deform.spline"));
requireOk(graph.connect("source", "spline"));
requireOk(graph.setNodeFloat("spline", "c3x", 2.f));
requireOk(graph.setNodeMesh("source", triangle()));
auto result = graph.executeResult("spline");
REQUIRE(result.ok());
CHECK(std::abs(result.value().getPositionX(0)) < 1e-6f);
CHECK(std::abs(result.value().getPositionX(2) - 2.f) < 1e-6f);
}
TEST_CASE("procgen.meshModifierGraph.deformsAlongOwningSplinePath") {
SplinePath path;
requireOk(path.setKindResult("linear"));
requireOk(path.addPointResult({0.f, 0.f, 0.f}));
requireOk(path.addPointResult({0.f, 1.f, 0.f}));
requireOk(path.addPointResult({2.f, 2.f, 0.f}));
MeshModifierGraph graph;
requireOk(graph.addNode("source", "mesh.input"));
requireOk(graph.addNode("path", "deform.splinePath"));
requireOk(graph.connect("source", "path"));
requireOk(graph.setNodeMesh("source", triangle()));
requireOk(graph.setNodeSplinePath("path", path));
auto result = graph.executeResult("path");
REQUIRE(result.ok());
CHECK(std::abs(result.value().getPositionX(0)) < 1e-6f);
CHECK(std::abs(result.value().getPositionY(0)) < 1e-6f);
CHECK(std::abs(result.value().getPositionX(2) - 2.f) < 1e-5f);
CHECK(std::abs(result.value().getPositionY(2) - 2.f) < 1e-5f);
}
TEST_CASE("procgen.meshModifierGraph.requiresReadySplinePathBinding") {
MeshModifierGraph graph;
requireOk(graph.addNode("source", "mesh.input"));
requireOk(graph.addNode("path", "deform.splinePath"));
requireOk(graph.connect("source", "path"));
requireOk(graph.setNodeMesh("source", triangle()));
auto missing = graph.executeResult("path");
REQUIRE(!missing.ok());
REQUIRE(missing.status().primaryDiagnostic() != nullptr);
CHECK(missing.status().primaryDiagnostic()->code() == DiagnosticCode::PreconditionViolation);
SplinePath incomplete;
requireOk(incomplete.addPointResult({0.f, 0.f, 0.f}));
auto rejected = graph.setNodeSplinePath("path", incomplete);
REQUIRE(!rejected.ok());
REQUIRE(rejected.status().primaryDiagnostic() != nullptr);
CHECK(rejected.status().primaryDiagnostic()->code() == DiagnosticCode::PreconditionViolation);
}
TEST_CASE("procgen.meshModifierGraph.splineFrameStaysContinuousNearVerticalTangents") {
SplinePath path;
requireOk(path.setKindResult("bezier"));
requireOk(path.addPointResult({0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 1.2f, 1.f, 0.f}));
requireOk(path.addPointResult({0.f, 2.f, 0.f, -0.2f, -1.f, 0.f, 0.f, 0.f, 0.f}));
MeshBuild strip;
for (float y : {0.f, 0.49f, 0.51f, 1.f}) strip.addVertex(1.f, y, 0.f, 0.f, 0.f, 1.f, 0.f, y);
strip.addTriangle(0, 1, 2);
strip.addTriangle(1, 3, 2);
MeshModifierGraph graph;
requireOk(graph.addNode("source", "mesh.input"));
requireOk(graph.addNode("path", "deform.splinePath"));
requireOk(graph.connect("source", "path"));
requireOk(graph.setNodeMesh("source", strip));
requireOk(graph.setNodeSplinePath("path", path));
auto result = graph.executeResult("path");
REQUIRE(result.ok());
const float dx = result.value().getPositionX(2) - result.value().getPositionX(1);
const float dy = result.value().getPositionY(2) - result.value().getPositionY(1);
const float dz = result.value().getPositionZ(2) - result.value().getPositionZ(1);
CHECK(std::sqrt(dx * dx + dy * dy + dz * dz) < 0.2f);
}
TEST_CASE("procgen.meshModifierGraph.generatesCappedSplineTubeWithoutMeshInput") {
SplinePath path;
requireOk(path.setKindResult("linear"));
requireOk(path.addPointResult({0.f, 0.f, 0.f}));
requireOk(path.addPointResult({0.f, 2.f, 0.f}));
MeshModifierGraph graph;
requireOk(graph.addNode("tube", "mesh.splineTube"));
requireOk(graph.setNodeSplinePath("tube", path));
requireOk(graph.setNodeFloat("tube", "radius", 0.5f));
requireOk(graph.setNodeInt("tube", "pathSegments", 4));
requireOk(graph.setNodeInt("tube", "radialSegments", 8));
requireOk(graph.setNodeInt("tube", "cap", 1));
auto result = graph.executeResult("tube");
REQUIRE(result.ok());
const auto& mesh = result.value();
CHECK_EQ(mesh.getVertexCount(), 65);
CHECK_EQ(mesh.getIndexCount(), 240);
CHECK_EQ(mesh.getGroupCount(), 2);
CHECK_EQ(mesh.getGroupName(0), "tube");
CHECK_EQ(mesh.getGroupName(1), "caps");
CHECK_EQ(mesh.getMeta("generator", ""), "mesh.splineTube");
for (int vertex = 45; vertex < 55; ++vertex) CHECK(std::abs(mesh.getNormalY(vertex) + 1.f) < 1e-6f);
CHECK(std::abs(mesh.getPositionY(0)) < 1e-6f);
CHECK(
std::abs(std::sqrt(mesh.getPositionX(0) * mesh.getPositionX(0) + mesh.getPositionZ(0) * mesh.getPositionZ(0)) -
0.5f) < 1e-6f);
}
TEST_CASE("procgen.meshModifierGraph.closesSplineTubeSeamAndOmitsCaps") {
SplinePath path;
requireOk(path.setKindResult("catmullRom"));
path.setClosed(true);
requireOk(path.addPointResult({-1.f, 0.f, -1.f}));
requireOk(path.addPointResult({1.f, 0.f, -1.f}));
requireOk(path.addPointResult({1.f, 0.f, 1.f}));
requireOk(path.addPointResult({-1.f, 0.f, 1.f}));
MeshModifierGraph graph;
requireOk(graph.addNode("tube", "mesh.splineTube"));
requireOk(graph.setNodeSplinePath("tube", path));
requireOk(graph.setNodeInt("tube", "pathSegments", 8));
requireOk(graph.setNodeInt("tube", "radialSegments", 6));
auto result = graph.executeResult("tube");
REQUIRE(result.ok());
const auto& mesh = result.value();
CHECK_EQ(mesh.getVertexCount(), 63);
CHECK_EQ(mesh.getIndexCount(), 288);
CHECK_EQ(mesh.getGroupCount(), 1);
for (int radial = 0; radial <= 6; ++radial) {
const int seam = 8 * 7 + radial;
CHECK(std::abs(mesh.getPositionX(radial) - mesh.getPositionX(seam)) < 1e-6f);
CHECK(std::abs(mesh.getPositionY(radial) - mesh.getPositionY(seam)) < 1e-6f);
CHECK(std::abs(mesh.getPositionZ(radial) - mesh.getPositionZ(seam)) < 1e-6f);
CHECK(std::abs(mesh.getNormalX(radial) - mesh.getNormalX(seam)) < 1e-6f);
CHECK(std::abs(mesh.getUvU(seam) - 1.f) < 1e-6f);
}
}
TEST_CASE("procgen.meshModifierGraph.rejectsInvalidSplineTubeBudgets") {
MeshModifierGraph graph;
requireOk(graph.addNode("tube", "mesh.splineTube"));
auto missing = graph.executeResult("tube");
REQUIRE(!missing.ok());
CHECK(missing.status().primaryDiagnostic()->code() == DiagnosticCode::PreconditionViolation);
SplinePath path;
requireOk(path.addPointResult({0.f, 0.f, 0.f}));
requireOk(path.addPointResult({0.f, 1.f, 0.f}));
requireOk(graph.setNodeSplinePath("tube", path));
requireOk(graph.setNodeInt("tube", "radialSegments", 2));
auto invalid = graph.executeResult("tube");
REQUIRE(!invalid.ok());
CHECK(invalid.status().primaryDiagnostic()->code() == DiagnosticCode::InvalidArgument);
}
TEST_CASE("procgen.meshModifierGraph.generatesSplineRibbonAndSolidRoadGroups") {
SplinePath path;
requireOk(path.setKindResult("linear"));
requireOk(path.addPointResult({0.f, 0.f, 0.f}));
requireOk(path.addPointResult({0.f, 0.f, 4.f}));
MeshModifierGraph ribbon;
requireOk(ribbon.addNode("road", "mesh.splineRibbon"));
requireOk(ribbon.setNodeSplinePath("road", path));
requireOk(ribbon.setNodeFloat("road", "width", 2.f));
requireOk(ribbon.setNodeInt("road", "pathSegments", 4));
auto flat = ribbon.executeResult("road");
REQUIRE(flat.ok());
CHECK_EQ(flat.value().getVertexCount(), 10);
CHECK_EQ(flat.value().getIndexCount(), 24);
CHECK_EQ(flat.value().getGroupCount(), 1);
CHECK_EQ(flat.value().getGroupName(0), "surface");
CHECK(std::abs(flat.value().getPositionX(0) + 1.f) < 1e-6f);
CHECK(std::abs(flat.value().getPositionX(1) - 1.f) < 1e-6f);
MeshModifierGraph solid;
requireOk(solid.addNode("road", "mesh.splineRibbon"));
requireOk(solid.setNodeSplinePath("road", path));
requireOk(solid.setNodeFloat("road", "width", 2.f));
requireOk(solid.setNodeFloat("road", "thickness", 0.25f));
requireOk(solid.setNodeInt("road", "pathSegments", 4));
auto road = solid.executeResult("road");
REQUIRE(road.ok());
CHECK_EQ(road.value().getVertexCount(), 48);
CHECK_EQ(road.value().getIndexCount(), 108);
CHECK_EQ(road.value().getGroupCount(), 4);
CHECK_EQ(road.value().getGroupName(0), "surface");
CHECK_EQ(road.value().getGroupName(1), "bottom");
CHECK_EQ(road.value().getGroupName(2), "sides");
CHECK_EQ(road.value().getGroupName(3), "caps");
CHECK_EQ(road.value().getMeta("generator", ""), "mesh.splineRibbon");
}
TEST_CASE("procgen.meshModifierGraph.splineGeneratorsConsumePointProfile") {
SplinePath path;
requireOk(path.setKindResult("linear"));
requireOk(path.addPointResult({0.f, 0.f, 0.f}));
requireOk(path.addPointResult({0.f, 0.f, 2.f}));
requireOk(path.setPointProfileResult(1, 90.f, 2.f, 0.5f));
MeshModifierGraph graph;
requireOk(graph.addNode("ribbon", "mesh.splineRibbon"));
requireOk(graph.setNodeSplinePath("ribbon", path));
requireOk(graph.setNodeInt("ribbon", "pathSegments", 2));
auto result = graph.executeResult("ribbon");
REQUIRE(result.ok());
const int lastLeft = 4;
const int lastRight = 5;
const float dx = result.value().getPositionX(lastRight) - result.value().getPositionX(lastLeft);
const float dy = result.value().getPositionY(lastRight) - result.value().getPositionY(lastLeft);
const float dz = result.value().getPositionZ(lastRight) - result.value().getPositionZ(lastLeft);
CHECK(std::abs(std::sqrt(dx * dx + dy * dy + dz * dz) - 4.f) < 1e-5f);
CHECK(std::abs(dy) > 3.9f);
}
TEST_CASE("procgen.meshModifierGraph.extrudesOwningClosedProfileWithIndependentCaps") {
SplinePath path;
requireOk(path.setKindResult("linear"));
requireOk(path.addPointResult({0.f, 0.f, 0.f}));
requireOk(path.addPointResult({0.f, 0.f, 2.f}));
requireOk(path.setPointProfileResult(1, 45.f, 2.f, 0.5f));
SplineProfile square{{{-0.5f, -0.5f}, {0.5f, -0.5f}, {0.5f, 0.5f}, {-0.5f, 0.5f}}, true};
MeshModifierGraph graph;
requireOk(graph.addNode("extrude", "mesh.splineExtrude"));
requireOk(graph.setNodeSplinePath("extrude", path));
requireOk(graph.setNodeSplineProfile("extrude", square));
requireOk(graph.setNodeInt("extrude", "pathSegments", 2));
auto result = graph.executeResult("extrude");
REQUIRE(result.ok());
CHECK_EQ(result.value().getVertexCount(), 23);
CHECK_EQ(result.value().getIndexCount(), 60);
CHECK_EQ(result.value().getGroupCount(), 2);
CHECK_EQ(result.value().getGroupName(0), "profile");
CHECK_EQ(result.value().getGroupName(1), "caps");
CHECK_EQ(result.value().getMeta("generator", ""), "mesh.splineExtrude");
for (int vertex = 15; vertex < 19; ++vertex) CHECK(result.value().getNormalZ(vertex) < -0.999f);
for (int vertex = 19; vertex < 23; ++vertex) CHECK(result.value().getNormalZ(vertex) > 0.999f);
}
TEST_CASE("procgen.meshModifierGraph.extrudesOpenUProfileWithoutImplicitCaps") {
SplinePath path;
requireOk(path.addPointResult({0.f, 0.f, 0.f}));
requireOk(path.addPointResult({0.f, 1.f, 0.f}));
SplineProfile channel{{{-1.f, 1.f}, {-1.f, 0.f}, {1.f, 0.f}, {1.f, 1.f}}, false};
MeshModifierGraph graph;
requireOk(graph.addNode("channel", "mesh.splineExtrude"));
requireOk(graph.setNodeSplinePath("channel", path));
requireOk(graph.setNodeSplineProfile("channel", channel));
requireOk(graph.setNodeInt("channel", "pathSegments", 1));
auto result = graph.executeResult("channel");
REQUIRE(result.ok());
CHECK_EQ(result.value().getVertexCount(), 8);
CHECK_EQ(result.value().getIndexCount(), 18);
CHECK_EQ(result.value().getGroupCount(), 1);
CHECK_EQ(result.value().getMeta("profileClosed", ""), "0");
}
TEST_CASE("procgen.meshModifierGraph.rejectsInvalidProfileAtomically") {
MeshModifierGraph graph;
requireOk(graph.addNode("extrude", "mesh.splineExtrude"));
const auto revision = graph.revision();
SplineProfile invalid{{{0.f, 0.f}, {1.f, 0.f}, {1.f, 0.f}}, true};
auto rejected = graph.setNodeSplineProfile("extrude", invalid);
REQUIRE(!rejected.ok());
CHECK_EQ(graph.revision(), revision);
CHECK(rejected.status().primaryDiagnostic()->code() == DiagnosticCode::InvalidArgument);
}
TEST_CASE("procgen.meshModifierGraph.generatesDisconnectedSplineChunksWithoutBridgeTriangles") {
SplinePath path;
requireOk(path.setKindResult("linear"));
requireOk(path.addPointResult({0.f, 0.f, 0.f}));
requireOk(path.addPointResult({0.f, 1.f, 0.f}));
requireOk(path.addPointResult({8.f, 0.f, 0.f}));
requireOk(path.addPointResult({8.f, 1.f, 0.f}));
requireOk(path.setPointChunkBreakResult(2, true));
MeshModifierGraph graph;
requireOk(graph.addNode("tube", "mesh.splineTube"));
requireOk(graph.setNodeSplinePath("tube", path));
requireOk(graph.setNodeInt("tube", "pathSegments", 2));
requireOk(graph.setNodeInt("tube", "radialSegments", 4));
auto result = graph.executeResult("tube");
REQUIRE(result.ok());
CHECK_EQ(result.value().getVertexCount(), 54);
CHECK_EQ(result.value().getIndexCount(), 144);
CHECK_EQ(result.value().getGroupCount(), 2);
CHECK_EQ(result.value().getMeta("chunks", ""), "2");
for (int index = 0; index < result.value().getIndexCount(); index += 3) {
const bool firstChunk = result.value().getIndex(index) < 27;
CHECK_EQ(result.value().getIndex(index + 1) < 27, firstChunk);
CHECK_EQ(result.value().getIndex(index + 2) < 27, firstChunk);
}
}
TEST_CASE("procgen.meshModifierGraph.noiseKeepsHardEdgeSeamsClosed") {
MeshBuild seam;
seam.addVertex(0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 0.f);
seam.addVertex(1.f, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f, 0.f);
seam.addVertex(0.f, 1.f, 0.f, 1.f, 0.f, 0.f, 0.f, 1.f);
seam.addVertex(0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f);
seam.addVertex(0.f, 1.f, 0.f, 0.f, 1.f, 0.f, 1.f, 0.f);
seam.addVertex(0.f, 0.f, 1.f, 0.f, 1.f, 0.f, 0.f, 1.f);
seam.addTriangle(0, 1, 2);
seam.addTriangle(3, 4, 5);
MeshModifierGraph graph;
requireOk(graph.addNode("source", "mesh.input"));
requireOk(graph.addNode("noise", "deform.noise"));
requireOk(graph.connect("source", "noise"));
requireOk(graph.setNodeFloat("noise", "amplitude", 0.4f));
requireOk(graph.setNodeMesh("source", seam));
auto result = graph.executeResult("noise");
REQUIRE(result.ok());
CHECK(std::abs(result.value().getPositionX(0) - result.value().getPositionX(3)) < 1e-6f);
CHECK(std::abs(result.value().getPositionY(0) - result.value().getPositionY(3)) < 1e-6f);
CHECK(std::abs(result.value().getPositionZ(0) - result.value().getPositionZ(3)) < 1e-6f);
}
TEST_CASE("procgen.meshModifierGraph.soundReactConsumesAnalyzedLevelAndFuses") {
MeshModifierGraph graph;
requireOk(graph.addNode("source", "mesh.input"));
requireOk(graph.addNode("sound", "deform.soundReact"));
requireOk(graph.addNode("move", "deform.transform"));
requireOk(graph.connect("source", "sound"));
requireOk(graph.connect("sound", "move"));
requireOk(graph.setNodeMesh("source", triangle()));
requireOk(graph.setNodeFloat("sound", "level", 0.75f));
requireOk(graph.setNodeFloat("sound", "threshold", 0.25f));
requireOk(graph.setNodeFloat("sound", "strength", 2.f));
requireOk(graph.setNodeFloat("sound", "frequency", 0.f));
requireOk(graph.setNodeFloat("sound", "phase", 1.5707963268f));
requireOk(graph.setNodeString("sound", "axis", "y"));
auto result = graph.executeResult("move");
REQUIRE(result.ok());
CHECK(std::abs(result.value().getPositionX(0)) < 1e-5f);
CHECK(std::abs(result.value().getPositionX(1) - 2.f) < 1e-5f);
CHECK_EQ(graph.fusedOperationCount(), 2);
requireOk(graph.setNodeFloat("sound", "level", 1.1f));
auto invalid = graph.executeResult("sound");
REQUIRE(!invalid.ok());
CHECK(invalid.status().primaryDiagnostic()->code() == DiagnosticCode::InvalidArgument);
}