more nodes

This commit is contained in:
Connor
2026-02-22 15:40:25 +09:00
parent 3d4453b9ea
commit 376442e95a
5 changed files with 532 additions and 1192 deletions

View File

@@ -51,6 +51,14 @@ FetchContent_Declare(
GIT_SHALLOW TRUE GIT_SHALLOW TRUE
) )
# FastNoiseLite single-header noise generation (used by WorldGraph noise nodes)
FetchContent_Declare(
FastNoiseLite
GIT_REPOSITORY https://github.com/Auburn/FastNoiseLite.git
GIT_TAG master
GIT_SHALLOW TRUE
)
# ---- Frontend dependencies (used by tools/) ---------------------------------- # ---- Frontend dependencies (used by tools/) ----------------------------------
# GLFW windowing for the node-editor tool # GLFW windowing for the node-editor tool
@@ -88,7 +96,7 @@ FetchContent_Declare(
GIT_SHALLOW TRUE GIT_SHALLOW TRUE
) )
FetchContent_MakeAvailable(flecs doctest glm nlohmann_json glfw imgui imnodes) FetchContent_MakeAvailable(flecs doctest glm nlohmann_json glfw imgui imnodes FastNoiseLite)
# ---- Core library ------------------------------------------------------------ # ---- Core library ------------------------------------------------------------
@@ -105,6 +113,7 @@ add_library(factory-hole-core ${SOURCES})
target_include_directories(factory-hole-core PUBLIC target_include_directories(factory-hole-core PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/include
${fastnoiselite_SOURCE_DIR}/Cpp
) )
target_link_libraries(factory-hole-core PUBLIC target_link_libraries(factory-hole-core PUBLIC
@@ -129,6 +138,7 @@ add_executable(factory-hole-tests ${TEST_SOURCES})
target_include_directories(factory-hole-tests PRIVATE target_include_directories(factory-hole-tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/include
${fastnoiselite_SOURCE_DIR}/Cpp
) )
target_link_libraries(factory-hole-tests PRIVATE target_link_libraries(factory-hole-tests PRIVATE

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
#include "WorldGraph/WorldGraphTypes.h" #include "WorldGraph/WorldGraphTypes.h"
#include "config.h" #include "config.h"
#include <FastNoiseLite.h>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstdlib> #include <cstdlib>
@@ -380,4 +381,254 @@ public:
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override { return Value::MakeInt(ctx.worldY); }; Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override { return Value::MakeInt(ctx.worldY); };
}; };
// ─────────────────────────────── Min / Max / Clamp ───────────────────────────
/// min(a, b) (Float)
class MinNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Min"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeFloat(std::min(in[0].AsFloat(), in[1].AsFloat()));
}
};
/// max(a, b) (Float)
class MaxNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Max"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeFloat(std::max(in[0].AsFloat(), in[1].AsFloat()));
}
};
/// clamp(value, min, max) (Float)
class ClampNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float, Type::Float }; }
std::string GetName() const override { return "Clamp"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 3);
return Value::MakeFloat(std::max(in[1].AsFloat(), std::min(in[0].AsFloat(), in[2].AsFloat())));
}
};
// ─────────────────────────────── Int control flow ────────────────────────────
/// Selects between two integer inputs based on a boolean condition.
///
/// inputs[0] condition (Bool)
/// inputs[1] value if true (Int)
/// inputs[2] value if false (Int)
///
/// Unlike BranchNode, inputs are typed Int so the visual editor can flag
/// float connections at authoring time.
class IntBranchNode : public Node {
public:
Type GetOutputType() const override { return Type::Int; }
std::vector<Type> GetInputTypes() const override {
return { Type::Bool, Type::Int, Type::Int };
}
std::string GetName() const override { return "IntBranch"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 3);
return Value::MakeInt(in[0].AsBool() ? in[1].AsInt() : in[2].AsInt());
}
};
// ─────────────────────────────── Extended math nodes ─────────────────────────
/// sqrt(a), clamped to 0 for negative inputs (Float)
class SqrtNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Sqrt"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(std::sqrt(std::max(0.0f, in[0].AsFloat())));
}
};
/// pow(a, b) (Float)
class PowNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Pow"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeFloat(std::pow(in[0].AsFloat(), in[1].AsFloat()));
}
};
/// a * a (Float)
class SquareNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Square"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
float a = in[0].AsFloat();
return Value::MakeFloat(a * a);
}
};
/// 1 a (Float)
class OneMinusNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "OneMinus"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(1.0f - in[0].AsFloat());
}
};
// ─────────────────────────────── Boolean logic (extended) ────────────────────
/// Outputs a XOR b (Bool)
class XorNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Bool, Type::Bool }; }
std::string GetName() const override { return "Xor"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(in[0].AsBool() != in[1].AsBool());
}
};
/// Outputs !(a && b) (Bool)
class NandNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Bool, Type::Bool }; }
std::string GetName() const override { return "Nand"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(!(in[0].AsBool() && in[1].AsBool()));
}
};
/// Outputs !(a || b) (Bool)
class NorNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Bool, Type::Bool }; }
std::string GetName() const override { return "Nor"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(!(in[0].AsBool() || in[1].AsBool()));
}
};
/// Outputs !(a XOR b) — true when both inputs are equal (Bool)
class XnorNode : public Node {
public:
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return { Type::Bool, Type::Bool }; }
std::string GetName() const override { return "Xnor"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
return Value::MakeBool(in[0].AsBool() == in[1].AsBool());
}
};
// ─────────────────────────────── Noise nodes ─────────────────────────────────
//
// Each noise node reads worldX/Y and seed from EvalContext; no graph inputs.
// Output is a Float in [-1, 1].
//
// A new FastNoiseLite object is constructed per evaluation so that the seed
// from the context is applied correctly across every cell.
/// Perlin noise — output in [-1, 1] (Float)
class PerlinNoiseNode : public Node {
public:
float frequency { 0.01f };
explicit PerlinNoiseNode(float freq = 0.01f) : frequency(freq) {}
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "PerlinNoise"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
FastNoiseLite fn;
fn.SetSeed(static_cast<int>(ctx.seed));
fn.SetNoiseType(FastNoiseLite::NoiseType_Perlin);
fn.SetFrequency(frequency);
return Value::MakeFloat(fn.GetNoise(static_cast<float>(ctx.worldX),
static_cast<float>(ctx.worldY)));
}
};
/// OpenSimplex2 noise — output in [-1, 1] (Float)
class SimplexNoiseNode : public Node {
public:
float frequency { 0.01f };
explicit SimplexNoiseNode(float freq = 0.01f) : frequency(freq) {}
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "SimplexNoise"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
FastNoiseLite fn;
fn.SetSeed(static_cast<int>(ctx.seed));
fn.SetNoiseType(FastNoiseLite::NoiseType_OpenSimplex2);
fn.SetFrequency(frequency);
return Value::MakeFloat(fn.GetNoise(static_cast<float>(ctx.worldX),
static_cast<float>(ctx.worldY)));
}
};
/// Cellular (Worley) noise — output in [-1, 1] (Float)
class CellularNoiseNode : public Node {
public:
float frequency { 0.01f };
explicit CellularNoiseNode(float freq = 0.01f) : frequency(freq) {}
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "CellularNoise"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
FastNoiseLite fn;
fn.SetSeed(static_cast<int>(ctx.seed));
fn.SetNoiseType(FastNoiseLite::NoiseType_Cellular);
fn.SetFrequency(frequency);
return Value::MakeFloat(fn.GetNoise(static_cast<float>(ctx.worldX),
static_cast<float>(ctx.worldY)));
}
};
/// Value noise — output in [-1, 1] (Float)
class ValueNoiseNode : public Node {
public:
float frequency { 0.01f };
explicit ValueNoiseNode(float freq = 0.01f) : frequency(freq) {}
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "ValueNoise"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
FastNoiseLite fn;
fn.SetSeed(static_cast<int>(ctx.seed));
fn.SetNoiseType(FastNoiseLite::NoiseType_Value);
fn.SetFrequency(frequency);
return Value::MakeFloat(fn.GetNoise(static_cast<float>(ctx.worldX),
static_cast<float>(ctx.worldY)));
}
};
} // namespace WorldGraph } // namespace WorldGraph

View File

@@ -23,12 +23,30 @@ std::unique_ptr<Node> GraphSerializer::CreateNode(const std::string& type,
if (type == "And") return std::make_unique<AndNode>(); if (type == "And") return std::make_unique<AndNode>();
if (type == "Or") return std::make_unique<OrNode>(); if (type == "Or") return std::make_unique<OrNode>();
if (type == "Not") return std::make_unique<NotNode>(); if (type == "Not") return std::make_unique<NotNode>();
if (type == "Xor") return std::make_unique<XorNode>();
if (type == "Nand") return std::make_unique<NandNode>();
if (type == "Nor") return std::make_unique<NorNode>();
if (type == "Xnor") return std::make_unique<XnorNode>();
if (type == "Branch") return std::make_unique<BranchNode>(); if (type == "Branch") return std::make_unique<BranchNode>();
if (type == "IntBranch") return std::make_unique<IntBranchNode>();
if (type == "PositionX") return std::make_unique<PositionXNode>(); if (type == "PositionX") return std::make_unique<PositionXNode>();
if (type == "PositionY") return std::make_unique<PositionYNode>(); if (type == "PositionY") return std::make_unique<PositionYNode>();
if (type == "Sin") return std::make_unique<SinNode>(); if (type == "Sin") return std::make_unique<SinNode>();
if (type == "Cos") return std::make_unique<CosNode>(); if (type == "Cos") return std::make_unique<CosNode>();
if (type == "Modulo") return std::make_unique<ModuloNode>(); if (type == "Modulo") return std::make_unique<ModuloNode>();
if (type == "Sqrt") return std::make_unique<SqrtNode>();
if (type == "Pow") return std::make_unique<PowNode>();
if (type == "Square") return std::make_unique<SquareNode>();
if (type == "OneMinus") return std::make_unique<OneMinusNode>();
if (type == "Min") return std::make_unique<MinNode>();
if (type == "Max") return std::make_unique<MaxNode>();
if (type == "Clamp") return std::make_unique<ClampNode>();
// Noise nodes (frequency baked in)
if (type == "PerlinNoise") return std::make_unique<PerlinNoiseNode>(j.value("frequency", 0.01f));
if (type == "SimplexNoise") return std::make_unique<SimplexNoiseNode>(j.value("frequency", 0.01f));
if (type == "CellularNoise") return std::make_unique<CellularNoiseNode>(j.value("frequency",0.01f));
if (type == "ValueNoise") return std::make_unique<ValueNoiseNode>(j.value("frequency", 0.01f));
// Nodes with config data // Nodes with config data
if (type == "Constant") { if (type == "Constant") {
@@ -99,6 +117,14 @@ nlohmann::json GraphSerializer::ToJson(const Graph& g)
} else if (const auto* qd = dynamic_cast<const QueryDistanceNode*>(node)) { } else if (const auto* qd = dynamic_cast<const QueryDistanceNode*>(node)) {
jNode["tileId"] = qd->tileID; jNode["tileId"] = qd->tileID;
jNode["maxDistance"] = qd->maxDistance; jNode["maxDistance"] = qd->maxDistance;
} else if (const auto* pn = dynamic_cast<const PerlinNoiseNode*>(node)) {
jNode["frequency"] = pn->frequency;
} else if (const auto* sn = dynamic_cast<const SimplexNoiseNode*>(node)) {
jNode["frequency"] = sn->frequency;
} else if (const auto* cn = dynamic_cast<const CellularNoiseNode*>(node)) {
jNode["frequency"] = cn->frequency;
} else if (const auto* vn = dynamic_cast<const ValueNoiseNode*>(node)) {
jNode["frequency"] = vn->frequency;
} }
jNodes.push_back(std::move(jNode)); jNodes.push_back(std::move(jNode));

View File

@@ -167,4 +167,248 @@ TEST_SUITE("WorldGraph::Nodes") {
PositionYNode n; PositionYNode n;
CHECK(n.Evaluate(c, {}).AsInt() == -3); CHECK(n.Evaluate(c, {}).AsInt() == -3);
} }
// ── Min / Max / Clamp ─────────────────────────────────────────────────────
TEST_CASE("MinNode: returns smaller value") {
MinNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(3.0f), Value::MakeFloat(7.0f) }).AsFloat()
== doctest::Approx(3.0f));
CHECK(n.Evaluate(ctx, { Value::MakeFloat(7.0f), Value::MakeFloat(3.0f) }).AsFloat()
== doctest::Approx(3.0f));
}
TEST_CASE("MinNode: equal inputs") {
MinNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(5.0f), Value::MakeFloat(5.0f) }).AsFloat()
== doctest::Approx(5.0f));
}
TEST_CASE("MaxNode: returns larger value") {
MaxNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(3.0f), Value::MakeFloat(7.0f) }).AsFloat()
== doctest::Approx(7.0f));
CHECK(n.Evaluate(ctx, { Value::MakeFloat(7.0f), Value::MakeFloat(3.0f) }).AsFloat()
== doctest::Approx(7.0f));
}
TEST_CASE("MaxNode: equal inputs") {
MaxNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(5.0f), Value::MakeFloat(5.0f) }).AsFloat()
== doctest::Approx(5.0f));
}
TEST_CASE("ClampNode: value within range passes through") {
ClampNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(5.0f), Value::MakeFloat(0.0f), Value::MakeFloat(10.0f) }).AsFloat()
== doctest::Approx(5.0f));
}
TEST_CASE("ClampNode: value below min clamped to min") {
ClampNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-5.0f), Value::MakeFloat(0.0f), Value::MakeFloat(10.0f) }).AsFloat()
== doctest::Approx(0.0f));
}
TEST_CASE("ClampNode: value above max clamped to max") {
ClampNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(15.0f), Value::MakeFloat(0.0f), Value::MakeFloat(10.0f) }).AsFloat()
== doctest::Approx(10.0f));
}
// ── Int control flow ──────────────────────────────────────────────────────
TEST_CASE("IntBranchNode: selects true branch") {
IntBranchNode n;
auto r = n.Evaluate(ctx, { Value::MakeBool(true),
Value::MakeInt(7),
Value::MakeInt(99) });
CHECK(r.type == Type::Int);
CHECK(r.AsInt() == 7);
}
TEST_CASE("IntBranchNode: selects false branch") {
IntBranchNode n;
auto r = n.Evaluate(ctx, { Value::MakeBool(false),
Value::MakeInt(7),
Value::MakeInt(99) });
CHECK(r.type == Type::Int);
CHECK(r.AsInt() == 99);
}
TEST_CASE("IntBranchNode: output type is Int") {
IntBranchNode n;
CHECK(n.GetOutputType() == Type::Int);
CHECK(n.GetInputTypes() == std::vector<Type>{ Type::Bool, Type::Int, Type::Int });
}
// ── Extended math ─────────────────────────────────────────────────────────
TEST_CASE("SqrtNode: normal value") {
SqrtNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(9.0f) }).AsFloat() == doctest::Approx(3.0f));
}
TEST_CASE("SqrtNode: zero") {
SqrtNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f) }).AsFloat() == doctest::Approx(0.0f));
}
TEST_CASE("SqrtNode: negative input clamped to zero") {
SqrtNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-4.0f) }).AsFloat() == doctest::Approx(0.0f));
}
TEST_CASE("PowNode: a^b") {
PowNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(2.0f), Value::MakeFloat(10.0f) }).AsFloat()
== doctest::Approx(1024.0f));
}
TEST_CASE("PowNode: anything to the power 0 is 1") {
PowNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(99.0f), Value::MakeFloat(0.0f) }).AsFloat()
== doctest::Approx(1.0f));
}
TEST_CASE("SquareNode: positive") {
SquareNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(5.0f) }).AsFloat() == doctest::Approx(25.0f));
}
TEST_CASE("SquareNode: negative input gives positive result") {
SquareNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(-3.0f) }).AsFloat() == doctest::Approx(9.0f));
}
TEST_CASE("OneMinusNode: 1 - 0.25 == 0.75") {
OneMinusNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.25f) }).AsFloat() == doctest::Approx(0.75f));
}
TEST_CASE("OneMinusNode: 1 - 0 == 1") {
OneMinusNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(0.0f) }).AsFloat() == doctest::Approx(1.0f));
}
TEST_CASE("OneMinusNode: 1 - 1 == 0") {
OneMinusNode n;
CHECK(n.Evaluate(ctx, { Value::MakeFloat(1.0f) }).AsFloat() == doctest::Approx(0.0f));
}
// ── Extended boolean logic ────────────────────────────────────────────────
TEST_CASE("XorNode: truth table") {
XorNode n;
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(false) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(false) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(true) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(true) }).AsBool() == false);
}
TEST_CASE("NandNode: truth table") {
NandNode n;
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(false) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(false) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(true) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(true) }).AsBool() == false);
}
TEST_CASE("NorNode: truth table") {
NorNode n;
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(false) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(false) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(true) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(true) }).AsBool() == false);
}
TEST_CASE("XnorNode: truth table") {
XnorNode n;
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(false) }).AsBool() == true);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(false) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeBool(false), Value::MakeBool(true) }).AsBool() == false);
CHECK(n.Evaluate(ctx, { Value::MakeBool(true), Value::MakeBool(true) }).AsBool() == true);
}
// ── Noise nodes ───────────────────────────────────────────────────────────
TEST_CASE("PerlinNoiseNode: output is float in [-1, 1]") {
EvalContext c; c.worldX = 10; c.worldY = 20; c.seed = 42;
PerlinNoiseNode n(0.1f);
float v = n.Evaluate(c, {}).AsFloat();
CHECK(v >= -1.0f);
CHECK(v <= 1.0f);
}
TEST_CASE("PerlinNoiseNode: deterministic for same context") {
EvalContext c; c.worldX = 5; c.worldY = -3; c.seed = 123;
PerlinNoiseNode n(0.05f);
float v1 = n.Evaluate(c, {}).AsFloat();
float v2 = n.Evaluate(c, {}).AsFloat();
CHECK(v1 == doctest::Approx(v2));
}
TEST_CASE("PerlinNoiseNode: different positions give different values") {
EvalContext c1; c1.worldX = 0; c1.worldY = 0; c1.seed = 99;
EvalContext c2; c2.worldX = 50; c2.worldY = 50; c2.seed = 99;
PerlinNoiseNode n(0.1f);
CHECK(n.Evaluate(c1, {}).AsFloat() != doctest::Approx(n.Evaluate(c2, {}).AsFloat()));
}
TEST_CASE("SimplexNoiseNode: output is float in [-1, 1]") {
EvalContext c; c.worldX = 7; c.worldY = 13; c.seed = 1;
SimplexNoiseNode n(0.1f);
float v = n.Evaluate(c, {}).AsFloat();
CHECK(v >= -1.0f);
CHECK(v <= 1.0f);
}
TEST_CASE("SimplexNoiseNode: deterministic for same context") {
EvalContext c; c.worldX = 3; c.worldY = 8; c.seed = 77;
SimplexNoiseNode n(0.05f);
CHECK(n.Evaluate(c, {}).AsFloat() == doctest::Approx(n.Evaluate(c, {}).AsFloat()));
}
TEST_CASE("CellularNoiseNode: output is float in [-1, 1]") {
EvalContext c; c.worldX = 2; c.worldY = 9; c.seed = 500;
CellularNoiseNode n(0.1f);
float v = n.Evaluate(c, {}).AsFloat();
CHECK(v >= -1.0f);
CHECK(v <= 1.0f);
}
TEST_CASE("CellularNoiseNode: deterministic for same context") {
EvalContext c; c.worldX = 15; c.worldY = -7; c.seed = 256;
CellularNoiseNode n(0.08f);
CHECK(n.Evaluate(c, {}).AsFloat() == doctest::Approx(n.Evaluate(c, {}).AsFloat()));
}
TEST_CASE("ValueNoiseNode: output is float in [-1, 1]") {
EvalContext c; c.worldX = 4; c.worldY = 6; c.seed = 11;
ValueNoiseNode n(0.1f);
float v = n.Evaluate(c, {}).AsFloat();
CHECK(v >= -1.0f);
CHECK(v <= 1.0f);
}
TEST_CASE("ValueNoiseNode: deterministic for same context") {
EvalContext c; c.worldX = -2; c.worldY = 3; c.seed = 404;
ValueNoiseNode n(0.05f);
CHECK(n.Evaluate(c, {}).AsFloat() == doctest::Approx(n.Evaluate(c, {}).AsFloat()));
}
TEST_CASE("noise nodes: frequency affects output") {
EvalContext c; c.worldX = 100; c.worldY = 100; c.seed = 1;
PerlinNoiseNode lowFreq(0.001f);
PerlinNoiseNode highFreq(0.5f);
// Different frequencies must produce different values at the same position
CHECK(lowFreq.Evaluate(c, {}).AsFloat() != doctest::Approx(highFreq.Evaluate(c, {}).AsFloat()));
}
TEST_CASE("noise nodes: seed affects output") {
EvalContext c1; c1.worldX = 10; c1.worldY = 10; c1.seed = 1;
EvalContext c2; c2.worldX = 10; c2.worldY = 10; c2.seed = 2;
PerlinNoiseNode n(0.1f);
CHECK(n.Evaluate(c1, {}).AsFloat() != doctest::Approx(n.Evaluate(c2, {}).AsFloat()));
}
} }