diff --git a/docs/usr/modules/procgen.md b/docs/usr/modules/procgen.md index 12b933aa..53627b50 100644 --- a/docs/usr/modules/procgen.md +++ b/docs/usr/modules/procgen.md @@ -301,12 +301,16 @@ local trees = graph.execute("prune"); 支持的 operation 可用 `getOperationCount/getOperationId` 枚举,包括: `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`。 `copy.points` 对每个 target 实例化全部 source 点(稳定的 target-major 顺序),组合位置、 yaw、scale、density 与独立 seed;可继承 target metadata,冲突时 source metadata 胜出。 `maxPoints` 在笛卡尔积分配前实施硬上限,防止错误图造成编辑器或流式任务内存爆炸。 +`density.remap` 线性映射 density 范围并可钳制输出;`attribute.math.float` 对 float metadata +执行 add/subtract/multiply/divide/min/max,可写入新属性并为缺失输入提供确定性默认值。 +零输入范围、非法 operation 和除零会作为节点执行错误报告。 `execute(outputId)` 只求值该输出的祖先节点;没有配置变化时复用缓存。节点参数、输入或 空间数据变化时只失效该节点及其下游,未受影响的分支继续复用结果;`getRevision()` 提供 diff --git a/src/modules/procgen/PointGraph.cpp b/src/modules/procgen/PointGraph.cpp index 2cb4173c..c035e748 100644 --- a/src/modules/procgen/PointGraph.cpp +++ b/src/modules/procgen/PointGraph.cpp @@ -27,6 +27,14 @@ const std::vector& operationSpecs() { {"merge", 2, {}}, {"copy.points", 2, {{"maxPoints", "int", "100000"}, {"inheritTargetAttributes", "bool", "true"}}}, + {"density.remap", 1, {{"inputMin", "float", "0"}, {"inputMax", "float", "1"}, + {"outputMin", "float", "0"}, {"outputMax", "float", "1"}, + {"clamp", "bool", "true"}}}, + {"attribute.math.float", 1, {{"attribute", "string", ""}, + {"outputAttribute", "string", ""}, + {"operation", "string", "multiply"}, + {"operand", "float", "1"}, + {"defaultValue", "float", "0"}}}, {"transform", 1, {{"x", "float", "0"}, {"y", "float", "0"}, {"z", "float", "0"}, {"yaw", "float", "0"}, {"scaleX", "float", "1"}, {"scaleY", "float", "1"}, {"scaleZ", "float", "1"}}}, @@ -451,6 +459,35 @@ const PointSet* PointGraph::evaluate(const std::string& id, else node.cache = copyPointsToTargets( *first, *second, intValue(node, "inheritTargetAttributes", 1) != 0); + } else if (node.operation == "density.remap") { + const float inputMin = floatValue(node, "inputMin", 0.f); + const float inputMax = floatValue(node, "inputMax", 1.f); + if (!first) error_ = "density.remap requires input: " + id; + else if (inputMin == inputMax) + error_ = "density.remap requires a non-zero input range: " + id; + else + node.cache = remapPointDensity( + *first, inputMin, inputMax, floatValue(node, "outputMin", 0.f), + floatValue(node, "outputMax", 1.f), intValue(node, "clamp", 1) != 0); + } else if (node.operation == "attribute.math.float") { + const std::string attribute = stringValue(node, "attribute"); + std::string outputAttribute = stringValue(node, "outputAttribute"); + if (outputAttribute.empty()) outputAttribute = attribute; + const std::string operation = stringValue(node, "operation", "multiply"); + const float operand = floatValue(node, "operand", 1.f); + const bool operationKnown = operation == "add" || operation == "subtract" || + operation == "multiply" || operation == "divide" || + operation == "min" || operation == "max"; + if (!first) error_ = "attribute.math.float requires input: " + id; + else if (attribute.empty()) error_ = "attribute.math.float requires an attribute: " + id; + else if (!operationKnown) + error_ = "attribute.math.float has invalid operation at node: " + id; + else if (operation == "divide" && operand == 0.f) + error_ = "attribute.math.float cannot divide by zero at node: " + id; + else + node.cache = mathPointFloatAttribute( + *first, attribute, outputAttribute, operation, operand, + floatValue(node, "defaultValue", 0.f)); } else if (node.operation == "transform") { if (!first) error_ = "transform requires input: " + id; else diff --git a/src/modules/procgen/PointSet.cpp b/src/modules/procgen/PointSet.cpp index 3e590356..146a46a2 100644 --- a/src/modules/procgen/PointSet.cpp +++ b/src/modules/procgen/PointSet.cpp @@ -493,6 +493,38 @@ PointSet copyPointsToTargets(const PointSet& source, const PointSet& targets, return result; } +PointSet remapPointDensity(const PointSet& input, float inputMin, float inputMax, + float outputMin, float outputMax, bool clampOutput) { + PointSet result = input; + const float inputRange = inputMax - inputMin; + for (auto& point : result.points()) { + float normalized = (point.density - inputMin) / inputRange; + if (clampOutput) normalized = std::clamp(normalized, 0.f, 1.f); + point.density = outputMin + normalized * (outputMax - outputMin); + } + return result; +} + +PointSet mathPointFloatAttribute(const PointSet& input, const std::string& attribute, + const std::string& outputAttribute, + const std::string& operation, float operand, + float defaultValue) { + PointSet result = input; + for (auto& point : result.points()) { + const auto found = point.floatAttributes.find(attribute); + const float value = found == point.floatAttributes.end() ? defaultValue : found->second; + float output = value; + if (operation == "add") output += operand; + else if (operation == "subtract") output -= operand; + else if (operation == "multiply") output *= operand; + else if (operation == "divide") output /= operand; + else if (operation == "min") output = std::min(output, operand); + else if (operation == "max") output = std::max(output, operand); + point.floatAttributes[outputAttribute] = output; + } + return result; +} + PointSet filterPointFloatAttribute(const PointSet& input, const std::string& name, float minValue, float maxValue, bool invert) { if (minValue > maxValue) std::swap(minValue, maxValue); diff --git a/src/modules/procgen/PointSet.h b/src/modules/procgen/PointSet.h index ed7f13d7..60f9668f 100644 --- a/src/modules/procgen/PointSet.h +++ b/src/modules/procgen/PointSet.h @@ -109,9 +109,17 @@ PointSet mergePointSets(const PointSet& first, const PointSet& second); PointSet transformPointSet(const PointSet& input, float translateX, float translateY, float translateZ, float yawDegrees, float scaleX, float scaleY, float scaleZ); -/** @brief Instantiate every source point relative to every target point in stable target-major order. */ +/** @brief Instantiate source points relative to targets in stable target-major order. */ PointSet copyPointsToTargets(const PointSet& source, const PointSet& targets, bool inheritTargetAttributes); +/** @brief Linearly remap point density between ranges with optional output clamping. */ +PointSet remapPointDensity(const PointSet& input, float inputMin, float inputMax, + float outputMin, float outputMax, bool clampOutput); +/** @brief Apply one scalar operation to a float metadata attribute. */ +PointSet mathPointFloatAttribute(const PointSet& input, const std::string& attribute, + const std::string& outputAttribute, + const std::string& operation, float operand, + float defaultValue); /** @brief Select points whose named float attribute lies in an inclusive range. */ PointSet filterPointFloatAttribute(const PointSet& input, const std::string& name, float minValue, float maxValue, bool invert); diff --git a/test/procgen_point_graph.cpp b/test/procgen_point_graph.cpp index 44b9af5e..0d02f5b1 100644 --- a/test/procgen_point_graph.cpp +++ b/test/procgen_point_graph.cpp @@ -350,3 +350,44 @@ TEST_CASE("procgen.pointGraph.copiesSourcePointsAcrossTargetsWithBoundedOutput") CHECK(!graph.execute("copies")); CHECK(graph.getError().find("maxPoints") != std::string::npos); } + +TEST_CASE("procgen.pointGraph.remapsDensityAndComputesFloatMetadata") { + PointSet points; + const int first = points.add(0.f, 0.f, 0.f); + const int second = points.add(1.f, 0.f, 0.f); + points.setDensity(first, -1.f); + points.setDensity(second, 0.5f); + points.setFloatAttribute(first, "age", 3.f); + + PointGraph graph; + CHECK(graph.addNode("source", "input")); + CHECK(graph.addNode("density", "density.remap")); + CHECK(graph.addNode("math", "attribute.math.float")); + CHECK(graph.setNodePoints("source", &points)); + CHECK(graph.connect("source", "density")); + CHECK(graph.connect("density", "math")); + CHECK(graph.setNodeFloat("density", "inputMin", 0.f)); + CHECK(graph.setNodeFloat("density", "inputMax", 1.f)); + CHECK(graph.setNodeFloat("density", "outputMin", 10.f)); + CHECK(graph.setNodeFloat("density", "outputMax", 20.f)); + CHECK(graph.setNodeString("math", "attribute", "age")); + CHECK(graph.setNodeString("math", "outputAttribute", "score")); + CHECK(graph.setNodeString("math", "operation", "multiply")); + CHECK(graph.setNodeFloat("math", "operand", 2.f)); + CHECK(graph.setNodeFloat("math", "defaultValue", 4.f)); + PointSet* result = graph.execute("math"); + REQUIRE(bool(result)); + CHECK_EQ(result->getDensity(0), 10.f); + CHECK_EQ(result->getDensity(1), 15.f); + CHECK_EQ(result->getFloatAttribute(0, "score", -1.f), 6.f); + CHECK_EQ(result->getFloatAttribute(1, "score", -1.f), 8.f); + delete result; + + CHECK(graph.setNodeString("math", "operation", "divide")); + CHECK(graph.setNodeFloat("math", "operand", 0.f)); + CHECK(!graph.execute("math")); + CHECK(graph.getError().find("divide by zero") != std::string::npos); + CHECK(graph.setNodeFloat("density", "inputMax", 0.f)); + CHECK(!graph.execute("density")); + CHECK(graph.getError().find("non-zero input range") != std::string::npos); +}