diff --git a/docs/usr/modules/procgen.md b/docs/usr/modules/procgen.md index 45453c14..7ea6bdbf 100644 --- a/docs/usr/modules/procgen.md +++ b/docs/usr/modules/procgen.md @@ -321,7 +321,13 @@ local trees = graph.execute("prune"); `input`、`spatial.sample/filter/project`、`merge`、`copy.points`、`transform`、 `density.remap`、`attribute.math.float`、 `filter.float/string`、`attribute.set.float/string`、`density.cull`、 -`self.prune`、`jitter`、`branch` 和 `subgraph`。 +`self.prune`、`jitter`、`biome.generate`、`grammar.generate`、`branch` 和 `subgraph`。 + +`biome.generate` 通过 `setNodeSpatial` 与 `setNodeBiomeRules` 绑定生成域和 Biome 规则, +反射 `spacing/seed/jitter`;`grammar.generate` 通过 `setNodeShapeGrammar` 绑定 Shape Grammar, +接收控制点输入并反射 `grammar/seed/acceptIncomplete`。setter 会复制规则资产,调用方销毁或 +热重载原对象不会悬空;这些外部槽和 PointSet/SpatialData 一样不写入图定义,加载或 +`instantiate()` 后必须重新绑定。 `copy.points` 对每个 target 实例化全部 source 点(稳定的 target-major 顺序),组合位置、 yaw、scale、density 与独立 seed;可继承 target metadata,冲突时 source metadata 胜出。 @@ -341,7 +347,8 @@ yaw、scale、density 与独立 seed;可继承 target metadata,冲突时 sou 图资产可用 `exposeParameter(name, nodeId, key)` 将节点参数公开为强类型图参数;运行时实例通过 `setParameterFloat/Int/String()` 覆写,`clearParameterOverride()` 恢复资产默认值。覆写不会写入 序列化资产,并且只失效目标节点及下游缓存,适合关卡实例、Biome 和流式 Cell 共享图定义。 -`instantiate()` 从资产定义创建独立运行实例,不复制 PointSet/SpatialData 外部输入、覆写、缓存、 +`instantiate()` 从资产定义创建独立运行实例,不复制 PointSet/SpatialData/BiomeRules/ +ShapeGrammar 外部输入、覆写、缓存、 metric 或取消状态;并行 Cell/后台任务应各自实例化并重新绑定输入,不能共享同一个可变实例。 `getNodeOutput` 可取得任意已执行节点的中间 PointSet;`getMetric*` 和 `debugReport` 提供逐节点输出数量、耗时及 cache hit。任意 node/edge/parameter/input diff --git a/src/modules/procgen/PointGraph.cpp b/src/modules/procgen/PointGraph.cpp index 34281046..cd3dd9e7 100644 --- a/src/modules/procgen/PointGraph.cpp +++ b/src/modules/procgen/PointGraph.cpp @@ -1,5 +1,8 @@ #include "procgen/PointGraph.h" +#include "procgen/Biome.h" +#include "procgen/ShapeGrammar.h" + #include #include #include @@ -24,6 +27,10 @@ const std::vector& operationSpecs() { {"spatial.sample", 0, {{"spacing", "float", "1"}, {"seed", "int", "1"}, {"jitter", "float", "0"}}}, {"spatial.filter", 1, {{"invert", "bool", "false"}}}, {"spatial.project", 1, {}}, + {"biome.generate", 0, {{"spacing", "float", "1"}, {"seed", "int", "1"}, + {"jitter", "float", "0"}}}, + {"grammar.generate", 1, {{"grammar", "string", ""}, {"seed", "int", "1"}, + {"acceptIncomplete", "bool", "true"}}}, {"merge", 2, {}}, {"copy.points", 2, {{"maxPoints", "int", "100000"}, {"inheritTargetAttributes", "bool", "true"}}}, @@ -195,6 +202,24 @@ bool PointGraph::setNodeSpatial(const std::string& id, SpatialData* spatial) { return true; } +bool PointGraph::setNodeBiomeRules(const std::string& id, BiomeRules* rules) { + const auto found = nodes_.find(id); + if (found == nodes_.end() || found->second.operation != "biome.generate" || !rules) + return false; + found->second.biomeRules = std::make_shared(*rules); + invalidateFrom(id); + return true; +} + +bool PointGraph::setNodeShapeGrammar(const std::string& id, ShapeGrammar* grammar) { + const auto found = nodes_.find(id); + if (found == nodes_.end() || found->second.operation != "grammar.generate" || !grammar) + return false; + found->second.shapeGrammar = std::make_shared(*grammar); + invalidateFrom(id); + return true; +} + bool PointGraph::setNodeFloat(const std::string& id, const std::string& key, float value) { const auto found = nodes_.find(id); const auto* spec = found == nodes_.end() ? nullptr : operationSpec(found->second.operation); @@ -509,6 +534,32 @@ const PointSet* PointGraph::evaluate(const std::string& id, } else if (node.operation == "spatial.project") { if (!first || !node.spatial) error_ = "spatial.project requires input and spatial data: " + id; else node.cache = node.spatial->project(*first); + } else if (node.operation == "biome.generate") { + if (!node.spatial || !node.biomeRules) + error_ = "biome.generate requires spatial data and biome rules: " + id; + else { + std::unique_ptr result(node.biomeRules->generate( + node.spatial.get(), floatValue(node, "spacing", 1.f), + uint32_t(intValue(node, "seed", 1)), floatValue(node, "jitter", 0.f))); + if (!result) + error_ = "biome.generate failed at " + id + ": " + node.biomeRules->getError(); + else + node.cache = *result; + } + } else if (node.operation == "grammar.generate") { + if (!first || !node.shapeGrammar) + error_ = "grammar.generate requires input and shape grammar: " + id; + else { + std::unique_ptr result(node.shapeGrammar->generate( + stringValue(node, "grammar"), const_cast(first), + uint32_t(intValue(node, "seed", 1)), + intValue(node, "acceptIncomplete", 1) != 0)); + if (!result) + error_ = "grammar.generate failed at " + id + ": " + + node.shapeGrammar->getError(); + else + node.cache = *result; + } } else if (node.operation == "merge") { if (!first || !second) error_ = "merge requires two inputs: " + id; else node.cache = mergePointSets(*first, *second); @@ -649,9 +700,13 @@ bool PointGraph::validateNode(const std::string& id, std::unordered_mapsecond; if (node.operation == "input" && !node.hasPoints) error_ = "input node has no points: " + id; else if ((node.operation == "spatial.sample" || node.operation == "spatial.filter" || - node.operation == "spatial.project") && + node.operation == "spatial.project" || node.operation == "biome.generate") && !node.spatial) error_ = "node has no spatial data: " + id; + else if (node.operation == "biome.generate" && !node.biomeRules) + error_ = "node has no biome rules: " + id; + else if (node.operation == "grammar.generate" && !node.shapeGrammar) + error_ = "node has no shape grammar: " + id; else if (node.operation == "subgraph" && (!node.subgraph || node.inputs[0].empty())) error_ = "subgraph is not configured: " + id; else if (node.operation == "subgraph") { diff --git a/src/modules/procgen/PointGraph.h b/src/modules/procgen/PointGraph.h index 701262bb..fdaf9a78 100644 --- a/src/modules/procgen/PointGraph.h +++ b/src/modules/procgen/PointGraph.h @@ -11,6 +11,9 @@ namespace eve::procgen { +class BiomeRules; +class ShapeGrammar; + /** @brief Per-node execution information from the most recent graph run. */ struct PointGraphNodeMetric { std::string id; @@ -55,6 +58,10 @@ public: bool setNodePoints(const std::string& id, PointSet* points); /** @brief Assign copied spatial data to spatial.sample or spatial.filter. */ bool setNodeSpatial(const std::string& id, SpatialData* spatial); + /** @brief Assign copied biome rules to a `biome.generate` node. */ + bool setNodeBiomeRules(const std::string& id, BiomeRules* rules); + /** @brief Assign a copied shape grammar to a `grammar.generate` node. */ + bool setNodeShapeGrammar(const std::string& id, ShapeGrammar* grammar); bool setNodeFloat(const std::string& id, const std::string& key, float value); bool setNodeInt(const std::string& id, const std::string& key, int value); bool setNodeString(const std::string& id, const std::string& key, const std::string& value); @@ -134,8 +141,8 @@ public: /** * @brief Serialize graph topology, parameters and nested graphs. * - * Runtime PointSet and SpatialData values are external slots and are not - * embedded; callers rebind them after loading. + * Runtime PointSet, SpatialData, BiomeRules and ShapeGrammar values are + * external slots and are not embedded; callers rebind them after loading. */ std::string serializeDefinition() const; /** @brief Transactionally replace this graph from serializeDefinition output. */ @@ -162,6 +169,8 @@ private: PointSet points; bool hasPoints = false; std::shared_ptr spatial; + std::shared_ptr biomeRules; + std::shared_ptr shapeGrammar; std::shared_ptr subgraph; std::string subgraphInput; std::string subgraphOutput; diff --git a/src/modules/procgen/PointGraphScript.cpp b/src/modules/procgen/PointGraphScript.cpp index 35a8e160..8141f79c 100644 --- a/src/modules/procgen/PointGraphScript.cpp +++ b/src/modules/procgen/PointGraphScript.cpp @@ -23,6 +23,8 @@ void exposePointGraph(ssq::Table& table) { graph.addFunc("getInputNode", &PointGraph::getInputNode); graph.addFunc("setNodePoints", &PointGraph::setNodePoints); graph.addFunc("setNodeSpatial", &PointGraph::setNodeSpatial); + graph.addFunc("setNodeBiomeRules", &PointGraph::setNodeBiomeRules); + graph.addFunc("setNodeShapeGrammar", &PointGraph::setNodeShapeGrammar); graph.addFunc("setNodeFloat", &PointGraph::setNodeFloat); graph.addFunc("setNodeInt", &PointGraph::setNodeInt); graph.addFunc("setNodeString", &PointGraph::setNodeString); diff --git a/test/procgen_point_graph.cpp b/test/procgen_point_graph.cpp index ca213a7d..3e15c0c4 100644 --- a/test/procgen_point_graph.cpp +++ b/test/procgen_point_graph.cpp @@ -1,4 +1,6 @@ #include "procgen/PointGraph.h" +#include "procgen/Biome.h" +#include "procgen/ShapeGrammar.h" #include #include @@ -467,3 +469,59 @@ TEST_CASE("procgen.pointGraph.instantiatesIsolatedRuntimeState") { CHECK_EQ(asset.getParameterFloat("offset", -1.f), 9.f); delete instance; } + +TEST_CASE("procgen.pointGraph.generatesBiomeThroughReflectedNode") { + SpatialData domain = SpatialData::box(0.f, 0.f, 0.f, 4.f, 0.f, 4.f); + BiomeRules rules; + CHECK(rules.addLayer("forest", &domain, 1, 1.f)); + CHECK(rules.addAsset("forest", "oak", 1.f, 1.f, 1.f, false)); + + PointGraph graph; + CHECK(graph.addNode("biome", "biome.generate")); + CHECK(graph.setNodeSpatial("biome", &domain)); + CHECK(graph.setNodeBiomeRules("biome", &rules)); + CHECK(graph.setNodeFloat("biome", "spacing", 2.f)); + CHECK(graph.setNodeInt("biome", "seed", 42)); + CHECK(graph.validate()); + PointSet* output = graph.execute("biome"); + REQUIRE(bool(output)); + CHECK_EQ(output->getCount(), 9); + CHECK_EQ(output->getStringAttribute(0, "biome", ""), std::string("forest")); + CHECK_EQ(output->getStringAttribute(0, "asset", ""), std::string("oak")); + delete output; +} + +TEST_CASE("procgen.pointGraph.expandsShapeGrammarAndRebindsExternalAssets") { + PointSet controlPoints; + controlPoints.add(0.f, 0.f, 0.f); + controlPoints.add(6.f, 0.f, 0.f); + ShapeGrammar grammar; + CHECK(grammar.addModule("A", "wall", 2.f, 1.f)); + + PointGraph asset; + CHECK(asset.addNode("path", "input")); + CHECK(asset.addNode("facade", "grammar.generate")); + CHECK(asset.connect("path", "facade")); + CHECK(asset.setNodePoints("path", &controlPoints)); + CHECK(asset.setNodeShapeGrammar("facade", &grammar)); + CHECK(asset.setNodeString("facade", "grammar", "A+")); + CHECK(asset.validate()); + PointSet* output = asset.execute("facade"); + REQUIRE(bool(output)); + CHECK_EQ(output->getCount(), 3); + CHECK_EQ(output->getStringAttribute(0, "asset", ""), std::string("wall")); + delete output; + + PointGraph* instance = asset.instantiate(); + REQUIRE(bool(instance)); + CHECK(instance->setNodePoints("path", &controlPoints)); + CHECK(!instance->validate()); + CHECK(instance->getError().find("shape grammar") != std::string::npos); + CHECK(instance->setNodeShapeGrammar("facade", &grammar)); + CHECK(instance->validate()); + output = instance->execute("facade"); + REQUIRE(bool(output)); + CHECK_EQ(output->getCount(), 3); + delete output; + delete instance; +}