diff --git a/docs/usr/modules/procgen.md b/docs/usr/modules/procgen.md index 3c0f2dea..1dd90915 100644 --- a/docs/usr/modules/procgen.md +++ b/docs/usr/modules/procgen.md @@ -49,6 +49,36 @@ for (local i = 0; i < gen.getAlgorithmParamCount(algorithm); ++i) { 公共字段编写旁路逻辑。`examples/composable-editor` 在项目脚本中把 schema 映射为 普通 `ui.slider` / `ui.checkbox` / `ui.combo`,C++ 没有固定 Procgen 面板。 +`getAlgorithmSchema` 返回通用 `ProcgenRecipeSchema`。同一个对象模型也由 +`getTextureRecipeSchema` 和 `getPbrRecipeSchema` 返回,因此项目只需要一个字段组件: +`getId`、`getDisplayName`、`getCategory`、`getParamCount`、`getParamKey`、 +`getParamLabel`、`getParamDescription`、`getParamCategory`、`getParamKind`、 +`getParamDefault`、`paramHasMinimum`、`paramHasMaximum`、`getParamMinimum`、 +`getParamMaximum`、`getParamStep`、`isParamAdvanced`、`getParamChoiceCount` 和 +`getParamChoice`。`applyTextureRecipeDefaults` / `applyPbrRecipeDefaults` 把缺失值写入 +`Params`,已有的项目覆盖值保持不变。 + +```squirrel +local recipe = "pbr.rock"; +local values = gen.newParams(); +values.setSize(128, 128); +gen.applyPbrRecipeDefaults(recipe, values); +local schema = gen.getPbrRecipeSchema(recipe); +for (local i = 0; i < schema.getParamCount(); ++i) + buildProjectField(schema, values, i); +local maps = gen.generatePbrMaterial(recipe, values); +local albedo = maps.getAlbedo(); +local normal = maps.getNormal(); +local roughness = maps.getRoughness(); +local metallic = maps.getMetallic(); +local height = maps.getHeight(); +local ao = maps.getAo(); +maps.destroy(); +``` + +当前 `Material` 可直接使用 albedo、normal、height 纹理以及 scalar roughness / metallic。 +roughness、metallic、AO 图仍可导出或交给自定义 shader;默认材质还没有对应纹理槽。 + ## 对象关系与调用时机 `Params` 描述 seed、尺寸和算法参数;`Grid2D` 是结果;`OutputSpec` 决定写入 TileLayer、Image 或 Texture;`Procgen` 按注册算法名执行。 diff --git a/examples/composable-editor/README.md b/examples/composable-editor/README.md index 9e6d27cc..b4d09dce 100644 --- a/examples/composable-editor/README.md +++ b/examples/composable-editor/README.md @@ -11,6 +11,9 @@ The example demonstrates: - an MVVM-style reflected `WorldEditorVM` with two-way controls; - schema-driven procgen controls assembled by the project from reflected parameter types, bounds, defaults, advanced flags, and choices (no built-in procgen panel); +- one reusable recipe-field builder shared by grid algorithms and procedural PBR + materials; the project chooses its fields and layout, then uploads generated + albedo/normal/height maps into a normal engine `Material`; - a live embedded 3D runtime viewport using `procgen`, materials, and a field tool assembled from reusable target, kernel, falloff, operation, and transaction components; - channelled semantic selection shared by hierarchy, viewport, and inspector; @@ -37,3 +40,6 @@ Projects can replace either function without changing the C++ editor library. The bottom panel deliberately chooses `cave.cellular` in project code, enumerates its registered schema, and maps each semantic field kind to ordinary UI widgets. Changing the algorithm or the widget mapping produces a different tool without changing the engine. +The inspector uses that exact field builder with `pbr.rock`, filters the project-relevant +knobs, and composes `ProcgenRecipeSchema`, `Params`, `ImageData`, GPU textures and +`Material`; there is no C++ material-editor window. diff --git a/examples/composable-editor/main.nut b/examples/composable-editor/main.nut index 4b309c7b..998fb0b5 100644 --- a/examples/composable-editor/main.nut +++ b/examples/composable-editor/main.nut @@ -58,7 +58,13 @@ state <- persist("composableEditor", function() { brushTool = null procgenAlgorithm = "cave.cellular" procgenParams = null + procgenSchema = null procgenPreview = null + materialRecipe = "pbr.rock" + materialParams = null + materialSchema = null + materialResource = null + materialSet = null camera = null panelsMounted = false objects = [] @@ -142,6 +148,44 @@ function panelScene() { ui.text("", "scene-status"); } +// The project decides layout and filtering; RecipeDescriptor only supplies +// portable metadata. This same builder handles algorithms, textures and PBR. +function renderRecipeFields(schema, params, prefix, horizontal, wanted) { + if (horizontal) ui.beginRow(prefix + "schema-row", 8.0); + for (local i = 0; i < schema.getParamCount(); ++i) { + if (schema.isParamAdvanced(i)) continue; + local key = schema.getParamKey(i); + if (wanted != null && !(key in wanted)) continue; + local label = schema.getParamLabel(i); + local kind = schema.getParamKind(i); + local id = prefix + key; + local hiddenLabel = "##" + prefix + key; + ui.beginColumn(prefix + "field-" + key, 2.0); + ui.text(label, prefix + "label-" + key); + if (kind == "float") + ui.slider(hiddenLabel, params.getFloat(key, 0.0), schema.getParamMinimum(i), schema.getParamMaximum(i), id); + else if (kind == "int") + ui.slider(hiddenLabel, params.getInt(key, 0).tofloat(), schema.getParamMinimum(i), schema.getParamMaximum(i), id); + else if (kind == "bool") ui.checkbox(hiddenLabel, params.getInt(key, 0) != 0, id); + else if (kind == "choice") { + local choices = ""; + local selected = 0; + local current = params.getString(key, ""); + for (local c = 0; c < schema.getParamChoiceCount(i); ++c) { + local value = schema.getParamChoice(i, c); + if (c > 0) choices += ","; + choices += value; + if (value == current) selected = c; + } + ui.combo(hiddenLabel, choices, selected, id); + } else ui.inputText(hiddenLabel, params.getString(key, ""), id); + ui.setItemSize(horizontal ? 145.0 : 235.0, 0.0); + ui.end(); + if (horizontal) ui.setItemSize(165.0, 0.0); + } + if (horizontal) ui.end(); +} + function panelInspector() { ui.text("MVVM Terrain Tool", "inspector-title"); ui.text("", "selection"); @@ -154,6 +198,13 @@ function panelInspector() { ui.button("Rock", "material-rock"); ui.button("Sand", "material-sand"); ui.text("", "material-current"); + ui.separator("sep-pbr"); + ui.text(state.materialSchema.getDisplayName() + " · recipe schema", "pbr-title"); + renderRecipeFields(state.materialSchema, state.materialParams, "pbr-", false, { + scale=true, roughnessLow=true, roughnessHigh=true, metallic=true, + normalStrength=true, heightStrength=true + }); + ui.button("Generate + Apply PBR", "material-generate-pbr"); ui.textWrapped("All controls write the reflected ViewModel; the model then updates terrain/runtime data.", 235.0, "mvvm-help"); } @@ -168,47 +219,9 @@ function panelPalette() { ui.button("Avatar Preview", "spawn-avatar"); ui.end(); ui.separator("sep-procgen"); - ui.text(procgen.getAlgorithmDisplayName(state.procgenAlgorithm) + " · reflected schema", "procgen-title"); - ui.beginRow("procgen-schema-row", 8.0); - for (local i = 0; i < procgen.getAlgorithmParamCount(state.procgenAlgorithm); ++i) { - if (procgen.isAlgorithmParamAdvanced(state.procgenAlgorithm, i)) continue; - local key = procgen.getAlgorithmParamKey(state.procgenAlgorithm, i); - local label = procgen.getAlgorithmParamLabel(state.procgenAlgorithm, i); - local kind = procgen.getAlgorithmParamKind(state.procgenAlgorithm, i); - local id = "procgen-" + key; - local hiddenLabel = "##" + key; - ui.beginColumn("procgen-field-" + key, 2.0); - ui.text(label, "procgen-label-" + key); - if (kind == "float") { - ui.slider(hiddenLabel, state.procgenParams.getFloat(key, 0.0), - procgen.getAlgorithmParamMinimum(state.procgenAlgorithm, i), - procgen.getAlgorithmParamMaximum(state.procgenAlgorithm, i), id); - } else if (kind == "int") { - ui.slider(hiddenLabel, state.procgenParams.getInt(key, 0).tofloat(), - procgen.getAlgorithmParamMinimum(state.procgenAlgorithm, i), - procgen.getAlgorithmParamMaximum(state.procgenAlgorithm, i), id); - } else if (kind == "bool") { - ui.checkbox(hiddenLabel, state.procgenParams.getInt(key, 0) != 0, id); - } else if (kind == "choice") { - local choices = ""; - local selected = 0; - local current = state.procgenParams.getString(key, ""); - for (local c = 0; c < procgen.getAlgorithmParamChoiceCount(state.procgenAlgorithm, i); ++c) { - local value = procgen.getAlgorithmParamChoice(state.procgenAlgorithm, i, c); - if (c > 0) choices += ","; - choices += value; - if (value == current) selected = c; - } - ui.combo(hiddenLabel, choices, selected, id); - } else { - ui.inputText(hiddenLabel, state.procgenParams.getString(key, ""), id); - } - ui.setItemSize(145.0, 0.0); - ui.end(); - ui.setItemSize(165.0, 0.0); - } + ui.text(state.procgenSchema.getDisplayName() + " · reflected schema", "procgen-title"); + renderRecipeFields(state.procgenSchema, state.procgenParams, "procgen-", true, null); ui.button("Generate", "procgen-generate"); - ui.end(); ui.text("", "palette-status"); } @@ -292,6 +305,31 @@ function applyMaterial(name) { state.terrainEntity.setRoughness(m.roughness); } +function generateAndApplyPbrMaterial() { + if (state.terrainEntity == null) return; + state.materialSet = procgen.generatePbrMaterial(state.materialRecipe, state.materialParams); + if (state.materialSet == null) { + state.vm.status = "Material generation failed: " + procgen.lastError(); + return; + } + local albedo = gfx.newTexture(state.materialSet.getAlbedo(), true, true); + local normal = gfx.newTexture(state.materialSet.getNormal(), true, true); + local height = gfx.newTexture(state.materialSet.getHeight(), true, true); + state.materialResource = gfx.newMaterial(); + state.materialResource.setAlbedoTexture(albedo); + state.materialResource.setNormalTexture(normal); + state.materialResource.setHeightTexture(height); + state.materialResource.setMetallic(state.materialParams.getFloat("metallic", 0.0)); + local roughness = (state.materialParams.getFloat("roughnessLow", 0.5) + + state.materialParams.getFloat("roughnessHigh", 0.8)) * 0.5; + state.materialResource.setRoughness(roughness); + state.materialResource.setParallax(state.materialParams.getFloat("heightStrength", 1.0) * 0.025, 8.0, 24.0); + state.terrainEntity.setMaterial(state.materialResource); + state.materialSet.destroy(); + state.vm.material = state.materialRecipe; + state.vm.status = "Generated " + state.materialSchema.getDisplayName() + " from reflected recipe parameters"; +} + function registerProjectCommands() { editor.registerScriptCommand("world.spawn-archetype", "Spawn Project Archetype", "World/Create", function(payload) { @@ -316,15 +354,15 @@ function spawnArchetype(kind) { if (!result.accepted) state.vm.status = "Command rejected: " + kind; } -function updateProcgenParam(id) { - local key = id.slice(8); - for (local i = 0; i < procgen.getAlgorithmParamCount(state.procgenAlgorithm); ++i) { - if (procgen.getAlgorithmParamKey(state.procgenAlgorithm, i) != key) continue; - local kind = procgen.getAlgorithmParamKind(state.procgenAlgorithm, i); - if (kind == "float") state.procgenParams.setFloat(key, ui.getValue(id)); - else if (kind == "int") state.procgenParams.setInt(key, ui.getValue(id).tointeger()); - else if (kind == "bool") state.procgenParams.setInt(key, ui.getChecked(id) ? 1 : 0); - else state.procgenParams.setString(key, ui.getValueText(id)); +function updateRecipeParam(schema, params, prefix, id) { + local key = id.slice(prefix.len()); + for (local i = 0; i < schema.getParamCount(); ++i) { + if (schema.getParamKey(i) != key) continue; + local kind = schema.getParamKind(i); + if (kind == "float") params.setFloat(key, ui.getValue(id)); + else if (kind == "int") params.setInt(key, ui.getValue(id).tointeger()); + else if (kind == "bool") params.setInt(key, ui.getChecked(id) ? 1 : 0); + else params.setString(key, ui.getValueText(id)); state.vm.status = "Schema property changed: " + key; return; } @@ -384,6 +422,8 @@ function handlePanelEvents() { ui.inspectObject(state.vm); } else if (id == "procgen-generate") { generateProcgenPreview(); + } else if (id == "material-generate-pbr") { + generateAndApplyPbrMaterial(); } else if (id == "material-grass") applyMaterial("grass"); else if (id == "material-rock") applyMaterial("rock"); else if (id == "material-sand") applyMaterial("sand"); @@ -405,7 +445,10 @@ function handlePanelEvents() { if (id == "brush") state.vm.brushRadius = ui.getValue("brush"); else if (id == "strength") state.vm.strength = ui.getValue("strength"); else if (id == "tool") state.vm.tool = ui.getValueText("tool"); - else if (id.find("procgen-") == 0) updateProcgenParam(id); + else if (id.find("procgen-") == 0) + updateRecipeParam(state.procgenSchema, state.procgenParams, "procgen-", id); + else if (id.find("pbr-") == 0) + updateRecipeParam(state.materialSchema, state.materialParams, "pbr-", id); change = ui.consumeChange(); } } @@ -474,9 +517,15 @@ eve_init = function() { state.vm = WorldEditorVM(); state.procgenParams = procgen.newParams(); procgen.applyAlgorithmDefaults(state.procgenAlgorithm, state.procgenParams); + state.procgenSchema = procgen.getAlgorithmSchema(state.procgenAlgorithm); + state.materialParams = procgen.newParams(); + state.materialParams.setSize(128, 128); + procgen.applyPbrRecipeDefaults(state.materialRecipe, state.materialParams); + state.materialSchema = procgen.getPbrRecipeSchema(state.materialRecipe); configureWorkspace(); registerProjectCommands(); generateTerrain(); + generateAndApplyPbrMaterial(); configureTerrainTool(); state.camera = eve.Camera3D(); state.camera.setFov(50.0); diff --git a/src/modules/procgen/GeneratorRegistry.cpp b/src/modules/procgen/GeneratorRegistry.cpp index 556500d6..b293cb1f 100644 --- a/src/modules/procgen/GeneratorRegistry.cpp +++ b/src/modules/procgen/GeneratorRegistry.cpp @@ -1,93 +1,7 @@ #include "procgen/GeneratorRegistry.h" #include -#include -#include - namespace eve::procgen { -namespace { - -std::string floatString(float value) { - std::ostringstream stream; - stream << std::setprecision(9) << value; - return stream.str(); -} - -} // namespace - -ParamDescriptor ParamDescriptor::integer(std::string key, std::string label, int defaultValue, - int minimum, int maximum, int step) { - ParamDescriptor result; - result.key = std::move(key); - result.displayName = std::move(label); - result.kind = ParamKind::Integer; - result.defaultValue = std::to_string(defaultValue); - result.hasMinimum = result.hasMaximum = true; - result.minimum = minimum; - result.maximum = maximum; - result.step = step; - return result; -} - -ParamDescriptor ParamDescriptor::floating(std::string key, std::string label, float defaultValue, - float minimum, float maximum, float step) { - ParamDescriptor result; - result.key = std::move(key); - result.displayName = std::move(label); - result.kind = ParamKind::Float; - result.defaultValue = floatString(defaultValue); - result.hasMinimum = result.hasMaximum = true; - result.minimum = minimum; - result.maximum = maximum; - result.step = step; - return result; -} - -ParamDescriptor ParamDescriptor::boolean(std::string key, std::string label, bool defaultValue) { - ParamDescriptor result; - result.key = std::move(key); - result.displayName = std::move(label); - result.kind = ParamKind::Boolean; - result.defaultValue = defaultValue ? "1" : "0"; - return result; -} - -ParamDescriptor ParamDescriptor::text(std::string key, std::string label, std::string defaultValue) { - ParamDescriptor result; - result.key = std::move(key); - result.displayName = std::move(label); - result.kind = ParamKind::String; - result.defaultValue = std::move(defaultValue); - return result; -} - -ParamDescriptor ParamDescriptor::choice(std::string key, std::string label, std::string defaultValue, - std::vector choices) { - ParamDescriptor result = text(std::move(key), std::move(label), std::move(defaultValue)); - result.kind = ParamKind::Choice; - result.choices = std::move(choices); - return result; -} - -GeneratorDescriptor GeneratorDescriptor::grid(std::string id, std::string displayName, - std::string category, int minimumWidth, - int minimumHeight, int maximumWidth, - int maximumHeight) { - GeneratorDescriptor result; - result.id = std::move(id); - result.displayName = std::move(displayName); - result.category = std::move(category); - result.params.push_back(ParamDescriptor::integer("seed", "Seed", 1, 0, 2147483647, 1)); - result.params.push_back(ParamDescriptor::integer("width", "Width", 32, minimumWidth, maximumWidth, 1)); - result.params.push_back(ParamDescriptor::integer("height", "Height", 32, minimumHeight, maximumHeight, 1)); - return result; -} - -const ParamDescriptor *GeneratorDescriptor::find(const std::string &key) const { - const auto it = std::find_if(params.begin(), params.end(), - [&](const ParamDescriptor ¶m) { return param.key == key; }); - return it == params.end() ? nullptr : &*it; -} GeneratorRegistry &GeneratorRegistry::instance() { static GeneratorRegistry reg; @@ -128,16 +42,7 @@ const GeneratorDescriptor *GeneratorRegistry::descriptor(const std::string &id) bool GeneratorRegistry::applyDefaults(const std::string &id, Params ¶ms) const { const GeneratorDescriptor *schema = descriptor(id); if (!schema) return false; - for (const ParamDescriptor ¶m : schema->params) { - if (params.has(param.key)) continue; - switch (param.kind) { - case ParamKind::Integer: - case ParamKind::Boolean: params.setInt(param.key, std::stoi(param.defaultValue)); break; - case ParamKind::Float: params.setFloat(param.key, std::stof(param.defaultValue)); break; - case ParamKind::String: - case ParamKind::Choice: params.setString(param.key, param.defaultValue); break; - } - } + schema->applyDefaults(params); return true; } diff --git a/src/modules/procgen/GeneratorRegistry.h b/src/modules/procgen/GeneratorRegistry.h index 473731c6..c84cfcd7 100644 --- a/src/modules/procgen/GeneratorRegistry.h +++ b/src/modules/procgen/GeneratorRegistry.h @@ -1,6 +1,7 @@ #pragma once #include "procgen/Grid2D.h" +#include "procgen/ParamSchema.h" #include "procgen/Params.h" #include @@ -13,90 +14,8 @@ namespace eve::procgen { using GeneratorFn = std::function; -/** @brief Semantic type used by schema-driven generator parameter editors. */ -enum class ParamKind { Integer, Float, Boolean, String, Choice }; - -/** @brief UI-independent metadata for one procedural generator parameter. */ -struct ParamDescriptor { - std::string key; - std::string displayName; - std::string description; - std::string category = "General"; - ParamKind kind = ParamKind::String; - std::string defaultValue; - bool hasMinimum = false; - bool hasMaximum = false; - double minimum = 0.0; - double maximum = 0.0; - double step = 0.0; - std::vector choices; - bool advanced = false; - - /** - * @brief Construct a bounded integer descriptor. - * @param key Stable key. @param label Display label. @param defaultValue Default value. - * @param minimum Minimum value. @param maximum Maximum value. @param step Editing step. - * @return Complete descriptor. - */ - static ParamDescriptor integer(std::string key, std::string label, int defaultValue, - int minimum, int maximum, int step = 1); - /** - * @brief Construct a bounded floating-point descriptor. - * @param key Stable key. @param label Display label. @param defaultValue Default value. - * @param minimum Minimum value. @param maximum Maximum value. @param step Editing step. - * @return Complete descriptor. - */ - static ParamDescriptor floating(std::string key, std::string label, float defaultValue, - float minimum, float maximum, float step); - /** - * @brief Construct a Boolean descriptor stored as zero or one in Params. - * @param key Stable key. @param label Display label. @param defaultValue Default value. - * @return Complete descriptor. - */ - static ParamDescriptor boolean(std::string key, std::string label, bool defaultValue); - /** - * @brief Construct a free-form string descriptor. - * @param key Stable key. @param label Display label. @param defaultValue Default value. - * @return Complete descriptor. - */ - static ParamDescriptor text(std::string key, std::string label, std::string defaultValue); - /** - * @brief Construct a finite-choice string descriptor. - * @param key Stable key. @param label Display label. @param defaultValue Default choice. - * @param choices Allowed values. @return Complete descriptor. - */ - static ParamDescriptor choice(std::string key, std::string label, std::string defaultValue, - std::vector choices); -}; - -/** @brief Complete metadata and parameter schema for one generator algorithm. */ -struct GeneratorDescriptor { - std::string id; - std::string displayName; - std::string category; - std::vector params; - - /** - * @brief Create a grid generator schema with seed, width and height parameters. - * @param id Stable algorithm id. - * @param displayName Human-readable name. - * @param category Browser category. - * @param minimumWidth Minimum supported width. - * @param minimumHeight Minimum supported height. - * @param maximumWidth Maximum supported width. - * @param maximumHeight Maximum supported height. - * @return Descriptor initialized with common grid parameters. - */ - static GeneratorDescriptor grid(std::string id, std::string displayName, - std::string category, int minimumWidth, - int minimumHeight, int maximumWidth = 4096, - int maximumHeight = 4096); - /** - * @brief Find one parameter descriptor by stable key. - * @param key Stable parameter key. @return Descriptor or nullptr. - */ - const ParamDescriptor *find(const std::string &key) const; -}; +/** @brief Backward-compatible name for the shared recipe schema. */ +using GeneratorDescriptor = RecipeDescriptor; /** @brief Registry for executable generators and their reflection metadata. */ class GeneratorRegistry { diff --git a/src/modules/procgen/ParamSchema.cpp b/src/modules/procgen/ParamSchema.cpp new file mode 100644 index 00000000..505f5eef --- /dev/null +++ b/src/modules/procgen/ParamSchema.cpp @@ -0,0 +1,187 @@ +#include "procgen/ParamSchema.h" + +#include +#include +#include +#include + +namespace eve::procgen { +namespace { + +std::string floatString(float value) { + std::ostringstream stream; + stream << std::setprecision(9) << value; + return stream.str(); +} + +} // namespace + +ParamDescriptor ParamDescriptor::integer(std::string key, std::string label, int defaultValue, int minimum, int maximum, + int step) { + ParamDescriptor result; + result.key = std::move(key); + result.displayName = std::move(label); + result.kind = ParamKind::Integer; + result.defaultValue = std::to_string(defaultValue); + result.hasMinimum = result.hasMaximum = true; + result.minimum = minimum; + result.maximum = maximum; + result.step = step; + return result; +} + +ParamDescriptor ParamDescriptor::floating(std::string key, std::string label, float defaultValue, float minimum, + float maximum, float step) { + ParamDescriptor result; + result.key = std::move(key); + result.displayName = std::move(label); + result.kind = ParamKind::Float; + result.defaultValue = floatString(defaultValue); + result.hasMinimum = result.hasMaximum = true; + result.minimum = minimum; + result.maximum = maximum; + result.step = step; + return result; +} + +ParamDescriptor ParamDescriptor::boolean(std::string key, std::string label, bool defaultValue) { + ParamDescriptor result; + result.key = std::move(key); + result.displayName = std::move(label); + result.kind = ParamKind::Boolean; + result.defaultValue = defaultValue ? "1" : "0"; + return result; +} + +ParamDescriptor ParamDescriptor::text(std::string key, std::string label, std::string defaultValue) { + ParamDescriptor result; + result.key = std::move(key); + result.displayName = std::move(label); + result.kind = ParamKind::String; + result.defaultValue = std::move(defaultValue); + return result; +} + +ParamDescriptor ParamDescriptor::choice(std::string key, std::string label, std::string defaultValue, + std::vector choices) { + ParamDescriptor result = text(std::move(key), std::move(label), std::move(defaultValue)); + result.kind = ParamKind::Choice; + result.choices = std::move(choices); + return result; +} + +RecipeDescriptor RecipeDescriptor::grid(std::string id, std::string displayName, std::string category, int minimumWidth, + int minimumHeight, int maximumWidth, int maximumHeight) { + RecipeDescriptor result; + result.id = std::move(id); + result.displayName = std::move(displayName); + result.category = std::move(category); + result.params.push_back(ParamDescriptor::integer("seed", "Seed", 1, 0, 2147483647, 1)); + result.params.push_back(ParamDescriptor::integer("width", "Width", 32, minimumWidth, maximumWidth, 1)); + result.params.push_back(ParamDescriptor::integer("height", "Height", 32, minimumHeight, maximumHeight, 1)); + return result; +} + +const ParamDescriptor* RecipeDescriptor::find(const std::string& key) const { + const auto it = + std::find_if(params.begin(), params.end(), [&](const ParamDescriptor& param) { return param.key == key; }); + return it == params.end() ? nullptr : &*it; +} + +void RecipeDescriptor::applyDefaults(Params& values) const { + for (const ParamDescriptor& param : params) { + if (values.has(param.key)) continue; + switch (param.kind) { + case ParamKind::Integer: + case ParamKind::Boolean: values.setInt(param.key, std::stoi(param.defaultValue)); break; + case ParamKind::Float: values.setFloat(param.key, std::stof(param.defaultValue)); break; + case ParamKind::String: + case ParamKind::Choice: values.setString(param.key, param.defaultValue); break; + } + } +} + +const ParamDescriptor* RecipeDescriptor::at(int index) const { + return index < 0 || index >= int(params.size()) ? nullptr : ¶ms[size_t(index)]; +} + +std::string RecipeDescriptor::getParamKey(int index) const { + const ParamDescriptor* param = at(index); + return param ? param->key : std::string{}; +} + +std::string RecipeDescriptor::getParamLabel(int index) const { + const ParamDescriptor* param = at(index); + return param ? param->displayName : std::string{}; +} + +std::string RecipeDescriptor::getParamDescription(int index) const { + const ParamDescriptor* param = at(index); + return param ? param->description : std::string{}; +} + +std::string RecipeDescriptor::getParamCategory(int index) const { + const ParamDescriptor* param = at(index); + return param ? param->category : std::string{}; +} + +std::string RecipeDescriptor::getParamKind(int index) const { + const ParamDescriptor* param = at(index); + if (!param) return {}; + switch (param->kind) { + case ParamKind::Integer: return "int"; + case ParamKind::Float: return "float"; + case ParamKind::Boolean: return "bool"; + case ParamKind::String: return "string"; + case ParamKind::Choice: return "choice"; + } + return {}; +} + +std::string RecipeDescriptor::getParamDefault(int index) const { + const ParamDescriptor* param = at(index); + return param ? param->defaultValue : std::string{}; +} + +bool RecipeDescriptor::paramHasMinimum(int index) const { + const ParamDescriptor* param = at(index); + return param && param->hasMinimum; +} + +bool RecipeDescriptor::paramHasMaximum(int index) const { + const ParamDescriptor* param = at(index); + return param && param->hasMaximum; +} + +float RecipeDescriptor::getParamMinimum(int index) const { + const ParamDescriptor* param = at(index); + return param ? float(param->minimum) : 0.f; +} + +float RecipeDescriptor::getParamMaximum(int index) const { + const ParamDescriptor* param = at(index); + return param ? float(param->maximum) : 0.f; +} + +float RecipeDescriptor::getParamStep(int index) const { + const ParamDescriptor* param = at(index); + return param ? float(param->step) : 0.f; +} + +bool RecipeDescriptor::isParamAdvanced(int index) const { + const ParamDescriptor* param = at(index); + return param && param->advanced; +} + +int RecipeDescriptor::getParamChoiceCount(int index) const { + const ParamDescriptor* param = at(index); + return param ? int(param->choices.size()) : 0; +} + +std::string RecipeDescriptor::getParamChoice(int paramIndex, int choiceIndex) const { + const ParamDescriptor* param = at(paramIndex); + if (!param || choiceIndex < 0 || choiceIndex >= int(param->choices.size())) return {}; + return param->choices[size_t(choiceIndex)]; +} + +} // namespace eve::procgen diff --git a/src/modules/procgen/ParamSchema.h b/src/modules/procgen/ParamSchema.h new file mode 100644 index 00000000..7395642c --- /dev/null +++ b/src/modules/procgen/ParamSchema.h @@ -0,0 +1,115 @@ +#pragma once + +#include "procgen/Params.h" + +#include +#include + +namespace eve::procgen { + +/** @brief Semantic type used by schema-driven recipe parameter editors. */ +enum class ParamKind { Integer, Float, Boolean, String, Choice }; + +/** @brief UI-independent metadata for one procedural recipe parameter. */ +struct ParamDescriptor { + std::string key; + std::string displayName; + std::string description; + std::string category = "General"; + ParamKind kind = ParamKind::String; + std::string defaultValue; + bool hasMinimum = false; + bool hasMaximum = false; + double minimum = 0.0; + double maximum = 0.0; + double step = 0.0; + std::vector choices; + bool advanced = false; + + /** @brief Construct a bounded integer descriptor. @param key Stable parameter key. @param label Display label. + * @param defaultValue Default value. @param minimum Inclusive minimum. @param maximum Inclusive maximum. @param + * step Suggested editor step. @return Parameter metadata. */ + static ParamDescriptor integer(std::string key, std::string label, int defaultValue, int minimum, int maximum, + int step = 1); + /** @brief Construct a bounded floating-point descriptor. @param key Stable parameter key. @param label Display + * label. @param defaultValue Default value. @param minimum Inclusive minimum. @param maximum Inclusive maximum. + * @param step Suggested editor step. @return Parameter metadata. */ + static ParamDescriptor floating(std::string key, std::string label, float defaultValue, float minimum, + float maximum, float step); + /** @brief Construct a Boolean descriptor stored as zero or one in Params. @param key Stable parameter key. @param + * label Display label. @param defaultValue Default value. @return Parameter metadata. */ + static ParamDescriptor boolean(std::string key, std::string label, bool defaultValue); + /** @brief Construct a free-form string descriptor. @param key Stable parameter key. @param label Display label. + * @param defaultValue Default value. @return Parameter metadata. */ + static ParamDescriptor text(std::string key, std::string label, std::string defaultValue); + /** @brief Construct a finite-choice string descriptor. @param key Stable parameter key. @param label Display label. + * @param defaultValue Default value. @param choices Allowed values. @return Parameter metadata. */ + static ParamDescriptor choice(std::string key, std::string label, std::string defaultValue, + std::vector choices); +}; + +/** @brief Complete metadata and parameter schema shared by every procgen recipe family. */ +struct RecipeDescriptor { + std::string id; + std::string displayName; + std::string category; + std::vector params; + + /** @brief Create a schema with common seed, width and height parameters. @param id Stable recipe id. @param + * displayName Display name. @param category Browser category. @param minimumWidth Minimum width. @param + * minimumHeight Minimum height. @param maximumWidth Maximum width. @param maximumHeight Maximum height. @return + * Recipe metadata. */ + static RecipeDescriptor grid(std::string id, std::string displayName, std::string category, int minimumWidth, + int minimumHeight, int maximumWidth = 4096, int maximumHeight = 4096); + /** @brief Find one parameter descriptor by stable key. @param key Parameter key. @return Schema-owned descriptor or + * nullptr. */ + const ParamDescriptor* find(const std::string& key) const; + /** @brief Fill missing Params values from this schema. @param params Values to update. */ + void applyDefaults(Params& params) const; + + /** @brief Return the number of reflected parameters. @return Parameter count. */ + int getParamCount() const { return int(params.size()); } + /** @brief Return the stable recipe id. @return Recipe id. */ + std::string getId() const { return id; } + /** @brief Return the human-readable recipe name. @return Display name. */ + std::string getDisplayName() const { return displayName; } + /** @brief Return the recipe browser category. @return Category name. */ + std::string getCategory() const { return category; } + /** @brief Return a parameter key by index. @param index Parameter index. @return Key or empty text. */ + std::string getParamKey(int index) const; + /** @brief Return a parameter label by index. @param index Parameter index. @return Label or empty text. */ + std::string getParamLabel(int index) const; + /** @brief Return parameter help text by index. @param index Parameter index. @return Help text or empty text. */ + std::string getParamDescription(int index) const; + /** @brief Return a parameter category by index. @param index Parameter index. @return Category or empty text. */ + std::string getParamCategory(int index) const; + /** @brief Return the portable parameter kind. @param index Parameter index. @return int, float, bool, string, + * choice, or empty text. */ + std::string getParamKind(int index) const; + /** @brief Return the schema default encoded as text. @param index Parameter index. @return Encoded default or empty + * text. */ + std::string getParamDefault(int index) const; + /** @brief Test whether a numeric minimum exists. @param index Parameter index. @return True when bounded below. */ + bool paramHasMinimum(int index) const; + /** @brief Test whether a numeric maximum exists. @param index Parameter index. @return True when bounded above. */ + bool paramHasMaximum(int index) const; + /** @brief Return a numeric minimum. @param index Parameter index. @return Minimum or zero. */ + float getParamMinimum(int index) const; + /** @brief Return a numeric maximum. @param index Parameter index. @return Maximum or zero. */ + float getParamMaximum(int index) const; + /** @brief Return the suggested numeric editing step. @param index Parameter index. @return Step or zero. */ + float getParamStep(int index) const; + /** @brief Test whether compact inspectors should hide the field. @param index Parameter index. @return Advanced + * flag. */ + bool isParamAdvanced(int index) const; + /** @brief Return the number of choices for one parameter. @param index Parameter index. @return Choice count. */ + int getParamChoiceCount(int index) const; + /** @brief Return one choice. @param paramIndex Parameter index. @param choiceIndex Choice index. @return Choice or + * empty text. */ + std::string getParamChoice(int paramIndex, int choiceIndex) const; + +private: + const ParamDescriptor* at(int index) const; +}; + +} // namespace eve::procgen diff --git a/src/modules/procgen/Procgen.cpp b/src/modules/procgen/Procgen.cpp index 51ce6f9b..27316ef1 100644 --- a/src/modules/procgen/Procgen.cpp +++ b/src/modules/procgen/Procgen.cpp @@ -482,6 +482,11 @@ bool Procgen::hasAlgorithm(const std::string &algorithmId) const { return GeneratorRegistry::instance().has(algorithmId); } +RecipeDescriptor *Procgen::getAlgorithmSchema(const std::string &algorithmId) const { + const RecipeDescriptor *schema = GeneratorRegistry::instance().descriptor(algorithmId); + return schema ? new RecipeDescriptor(*schema) : nullptr; +} + std::string Procgen::getAlgorithmDisplayName(const std::string &algorithmId) const { const GeneratorDescriptor *descriptor = GeneratorRegistry::instance().descriptor(algorithmId); return descriptor ? descriptor->displayName : std::string{}; @@ -666,6 +671,18 @@ bool Procgen::hasTextureRecipe(const std::string &recipeId) const { return TextureRecipeRegistry::instance().has(recipeId); } +RecipeDescriptor *Procgen::getTextureRecipeSchema(const std::string &recipeId) const { + TextureRecipeRegistry::instance().registerBuiltins(); + const RecipeDescriptor *schema = TextureRecipeRegistry::instance().descriptor(recipeId); + return schema ? new RecipeDescriptor(*schema) : nullptr; +} + +bool Procgen::applyTextureRecipeDefaults(const std::string &recipeId, Params *params) const { + if (!params) return false; + TextureRecipeRegistry::instance().registerBuiltins(); + return TextureRecipeRegistry::instance().applyDefaults(recipeId, *params); +} + CloudField *Procgen::newCloudField() { return new CloudField(); } CloudShadow *Procgen::newCloudShadow() { return new CloudShadow(); } @@ -740,6 +757,18 @@ bool Procgen::hasPbrRecipe(const std::string &recipeId) const { return PbrRecipeRegistry::instance().has(recipeId); } +RecipeDescriptor *Procgen::getPbrRecipeSchema(const std::string &recipeId) const { + PbrRecipeRegistry::instance().registerPbrBuiltins(); + const RecipeDescriptor *schema = PbrRecipeRegistry::instance().descriptor(recipeId); + return schema ? new RecipeDescriptor(*schema) : nullptr; +} + +bool Procgen::applyPbrRecipeDefaults(const std::string &recipeId, Params *params) const { + if (!params) return false; + PbrRecipeRegistry::instance().registerPbrBuiltins(); + return PbrRecipeRegistry::instance().applyDefaults(recipeId, *params); +} + MeshBuild *Procgen::buildMesh(const std::string &recipeId, Params *params) { lastError_.clear(); if (!params) { @@ -829,6 +858,28 @@ void Procgen::expose(ssq::Table &table) { auto cls = table.addClass(name, Procgen::create, false); expose(cls); + auto recipe = table.addClass( + "ProcgenRecipeSchema", + std::function([]() -> RecipeDescriptor * { return nullptr; }), true); + recipe.addFunc("getId", &RecipeDescriptor::getId); + recipe.addFunc("getDisplayName", &RecipeDescriptor::getDisplayName); + recipe.addFunc("getCategory", &RecipeDescriptor::getCategory); + recipe.addFunc("getParamCount", &RecipeDescriptor::getParamCount); + recipe.addFunc("getParamKey", &RecipeDescriptor::getParamKey); + recipe.addFunc("getParamLabel", &RecipeDescriptor::getParamLabel); + recipe.addFunc("getParamDescription", &RecipeDescriptor::getParamDescription); + recipe.addFunc("getParamCategory", &RecipeDescriptor::getParamCategory); + recipe.addFunc("getParamKind", &RecipeDescriptor::getParamKind); + recipe.addFunc("getParamDefault", &RecipeDescriptor::getParamDefault); + recipe.addFunc("paramHasMinimum", &RecipeDescriptor::paramHasMinimum); + recipe.addFunc("paramHasMaximum", &RecipeDescriptor::paramHasMaximum); + recipe.addFunc("getParamMinimum", &RecipeDescriptor::getParamMinimum); + recipe.addFunc("getParamMaximum", &RecipeDescriptor::getParamMaximum); + recipe.addFunc("getParamStep", &RecipeDescriptor::getParamStep); + recipe.addFunc("isParamAdvanced", &RecipeDescriptor::isParamAdvanced); + recipe.addFunc("getParamChoiceCount", &RecipeDescriptor::getParamChoiceCount); + recipe.addFunc("getParamChoice", &RecipeDescriptor::getParamChoice); + auto params = table.addClass( "ProcgenParams", std::function([]() -> Params * { return nullptr; }), true); params.addFunc("setSeed", &Params::setSeed); @@ -1046,6 +1097,12 @@ void Procgen::expose(ssq::Table &table) { "ProcgenPbrMaterial", std::function([]() -> PbrTextureSet * { return nullptr; }), true); pbr.addFunc("destroy", &PbrTextureSet::destroy); + pbr.addFunc("getAlbedo", &PbrTextureSet::getAlbedo); + pbr.addFunc("getNormal", &PbrTextureSet::getNormal); + pbr.addFunc("getRoughness", &PbrTextureSet::getRoughness); + pbr.addFunc("getMetallic", &PbrTextureSet::getMetallic); + pbr.addFunc("getHeight", &PbrTextureSet::getHeight); + pbr.addFunc("getAo", &PbrTextureSet::getAo); pbr.addFunc("getAlbedoWidth", [](const PbrTextureSet *s) { return s->albedo->getWidth(); }); pbr.addFunc("getAlbedoHeight", [](const PbrTextureSet *s) { return s->albedo->getHeight(); }); pbr.addFunc("getNormalWidth", [](const PbrTextureSet *s) { return s->normal->getWidth(); }); @@ -1106,6 +1163,7 @@ void Procgen::expose(ssq::Class &cls) { cls.addFunc("getAlgorithmCount", &Procgen::getAlgorithmCount); cls.addFunc("getAlgorithmId", &Procgen::getAlgorithmId); cls.addFunc("hasAlgorithm", &Procgen::hasAlgorithm); + cls.addFunc("getAlgorithmSchema", &Procgen::getAlgorithmSchema); cls.addFunc("getAlgorithmDisplayName", &Procgen::getAlgorithmDisplayName); cls.addFunc("getAlgorithmCategory", &Procgen::getAlgorithmCategory); cls.addFunc("getAlgorithmParamCount", &Procgen::getAlgorithmParamCount); @@ -1134,10 +1192,14 @@ void Procgen::expose(ssq::Class &cls) { cls.addFunc("getTextureRecipeCount", &Procgen::getTextureRecipeCount); cls.addFunc("getTextureRecipeId", &Procgen::getTextureRecipeId); cls.addFunc("hasTextureRecipe", &Procgen::hasTextureRecipe); + cls.addFunc("getTextureRecipeSchema", &Procgen::getTextureRecipeSchema); + cls.addFunc("applyTextureRecipeDefaults", &Procgen::applyTextureRecipeDefaults); cls.addFunc("generatePbrMaterial", &Procgen::generatePbrMaterial); cls.addFunc("getPbrRecipeCount", &Procgen::getPbrRecipeCount); cls.addFunc("getPbrRecipeId", &Procgen::getPbrRecipeId); cls.addFunc("hasPbrRecipe", &Procgen::hasPbrRecipe); + cls.addFunc("getPbrRecipeSchema", &Procgen::getPbrRecipeSchema); + cls.addFunc("applyPbrRecipeDefaults", &Procgen::applyPbrRecipeDefaults); cls.addFunc("buildMesh", &Procgen::buildMesh); cls.addFunc("generateMesh", &Procgen::generateMesh); cls.addFunc("getMeshRecipeCount", &Procgen::getMeshRecipeCount); diff --git a/src/modules/procgen/Procgen.h b/src/modules/procgen/Procgen.h index b09dd95f..f0db9cfa 100644 --- a/src/modules/procgen/Procgen.h +++ b/src/modules/procgen/Procgen.h @@ -5,6 +5,7 @@ #include "procgen/MeshBuild.h" #include "procgen/OutputSpec.h" #include "procgen/Palette.h" +#include "procgen/ParamSchema.h" #include "procgen/Params.h" #include "procgen/PointSet.h" #include "procgen/ProcgenSystem.h" @@ -109,6 +110,8 @@ public: int getAlgorithmCount() const; std::string getAlgorithmId(int index) const; bool hasAlgorithm(const std::string &algorithmId) const; + /** @brief Copy an algorithm schema. @param algorithmId Algorithm id. @return Caller-owned schema or nullptr. */ + RecipeDescriptor *getAlgorithmSchema(const std::string &algorithmId) const; /** @brief Human-readable algorithm label from its schema. * @param algorithmId Algorithm id. @return Display name or empty text. */ std::string getAlgorithmDisplayName(const std::string &algorithmId) const; @@ -194,6 +197,10 @@ public: int getTextureRecipeCount() const; std::string getTextureRecipeId(int index) const; bool hasTextureRecipe(const std::string &recipeId) const; + /** @brief Copy a texture schema. @param recipeId Recipe id. @return Caller-owned schema or nullptr. */ + RecipeDescriptor *getTextureRecipeSchema(const std::string &recipeId) const; + /** @brief Fill missing texture parameters. @param recipeId Recipe id. @param params Values to update. @return False for invalid input or an unknown recipe. */ + bool applyTextureRecipeDefaults(const std::string &recipeId, Params *params) const; // --- Phase E: dynamic clouds + cloud shadows --- /** @brief New deterministic, tiling, time-animated cloud field (caller owns). */ @@ -220,6 +227,10 @@ public: int getPbrRecipeCount() const; std::string getPbrRecipeId(int index) const; bool hasPbrRecipe(const std::string &recipeId) const; + /** @brief Copy a PBR schema. @param recipeId Recipe id. @return Caller-owned schema or nullptr. */ + RecipeDescriptor *getPbrRecipeSchema(const std::string &recipeId) const; + /** @brief Fill missing PBR parameters. @param recipeId Recipe id. @param params Values to update. @return False for invalid input or an unknown recipe. */ + bool applyPbrRecipeDefaults(const std::string &recipeId, Params *params) const; // --- Mesh recipes (Marching Cubes, …) --- /** @brief CPU mesh (caller owns). Recipes: mesh.marchingcubes, mesh.hexplanet. */ diff --git a/src/modules/procgen/texture/BuiltinTextures.cpp b/src/modules/procgen/texture/BuiltinTextures.cpp index 78b5fc42..2233ce7a 100644 --- a/src/modules/procgen/texture/BuiltinTextures.cpp +++ b/src/modules/procgen/texture/BuiltinTextures.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace eve::procgen { @@ -483,10 +484,27 @@ const std::vector &builtinTextureDefs() { void TextureRecipeRegistry::registerBuiltins() { if (builtinsRegistered_) return; - registerRecipe("tex.cloud", genCloud); - registerRecipe("tex.cloud_shadow", genCloudShadow); + auto cloudDescriptor = [](std::string id, std::string name, bool shadow) { + RecipeDescriptor schema = RecipeDescriptor::grid(std::move(id), std::move(name), "Atmosphere", 1, 1); + schema.params.push_back(ParamDescriptor::floating("worldScale", "World Scale", 96.f, 1.f, 4096.f, 1.f)); + schema.params.push_back(ParamDescriptor::floating("cloudCoverage", "Coverage", 0.55f, 0.f, 1.f, 0.01f)); + schema.params.push_back(ParamDescriptor::floating("cloudSoftness", "Softness", 0.12f, 0.001f, 1.f, 0.01f)); + schema.params.push_back(ParamDescriptor::floating("cloudDetail", "Detail", 0.5f, 0.f, 2.f, 0.01f)); + schema.params.push_back(ParamDescriptor::floating("windSpeed", "Wind Speed", 4.f, 0.f, 256.f, 0.1f)); + schema.params.push_back(ParamDescriptor::floating("windAngle", "Wind Angle", 0.f, -6.2832f, 6.2832f, 0.01f)); + schema.params.push_back(ParamDescriptor::integer("octaves", "Octaves", 4, 1, 12)); + schema.params.push_back(ParamDescriptor::floating("time", "Time", 0.f, 0.f, 86400.f, 0.1f)); + schema.params.push_back(ParamDescriptor::floating("extent", "Extent", 96.f, 1.f, 4096.f, 1.f)); + if (shadow) { + schema.params.push_back(ParamDescriptor::floating("cloudAltitude", "Cloud Altitude", 60.f, 0.f, 10000.f, 1.f)); + schema.params.push_back(ParamDescriptor::floating("cloudShadowStrength", "Shadow Strength", 0.8f, 0.f, 1.f, 0.01f)); + } + return schema; + }; + registerRecipe(cloudDescriptor("tex.cloud", "Cloud", false), genCloud); + registerRecipe(cloudDescriptor("tex.cloud_shadow", "Cloud Shadow", true), genCloudShadow); for (const TextureRecipeDef &def : builtinTextureDefs()) { - registerRecipe(def.id, [def](const Params ¶ms, std::string &error) { + registerRecipe(makeTextureRecipeDescriptor(def), [def](const Params ¶ms, std::string &error) { return makeFromHeightFn(params, error, def); }); } diff --git a/src/modules/procgen/texture/PbrMaterial.cpp b/src/modules/procgen/texture/PbrMaterial.cpp index b80eaba6..532e7523 100644 --- a/src/modules/procgen/texture/PbrMaterial.cpp +++ b/src/modules/procgen/texture/PbrMaterial.cpp @@ -6,6 +6,7 @@ #include #include +#include namespace eve::procgen { @@ -25,7 +26,15 @@ PbrRecipeRegistry &PbrRecipeRegistry::instance() { } void PbrRecipeRegistry::registerPbrRecipe(const std::string &id, PbrRecipeFn fn) { - recipes_[id] = std::move(fn); + RecipeDescriptor descriptor; + descriptor.id = id; + descriptor.displayName = id; + registerPbrRecipe(std::move(descriptor), std::move(fn)); +} + +void PbrRecipeRegistry::registerPbrRecipe(RecipeDescriptor descriptor, PbrRecipeFn fn) { + const std::string id = descriptor.id; + recipes_[id] = Entry{std::move(fn), std::move(descriptor)}; } bool PbrRecipeRegistry::has(const std::string &id) const { @@ -39,7 +48,19 @@ PbrTextureSet *PbrRecipeRegistry::generate(const std::string &id, const Params & error = "unknown pbr recipe: " + id; return nullptr; } - return it->second(params, error); + return it->second.fn(params, error); +} + +const RecipeDescriptor *PbrRecipeRegistry::descriptor(const std::string &id) const { + const auto it = recipes_.find(id); + return it == recipes_.end() ? nullptr : &it->second.descriptor; +} + +bool PbrRecipeRegistry::applyDefaults(const std::string &id, Params ¶ms) const { + const RecipeDescriptor *schema = descriptor(id); + if (!schema) return false; + schema->applyDefaults(params); + return true; } std::vector PbrRecipeRegistry::list() const { @@ -54,7 +75,16 @@ void PbrRecipeRegistry::registerPbrBuiltins() { if (builtinsRegistered_) return; for (const TextureRecipeDef &def : builtinTextureDefs()) { const std::string id = "pbr." + def.id.substr(def.id.find('.') + 1); - registerPbrRecipe(id, [def](const Params ¶ms, std::string &error) { + RecipeDescriptor descriptor = makeTextureRecipeDescriptor(def); + descriptor.id = id; + descriptor.category = "Material"; + descriptor.params.push_back(ParamDescriptor::floating("roughnessLow", "Low Roughness", def.pbr.roughnessLow, 0.f, 1.f, 0.01f)); + descriptor.params.push_back(ParamDescriptor::floating("roughnessHigh", "High Roughness", def.pbr.roughnessHigh, 0.f, 1.f, 0.01f)); + descriptor.params.push_back(ParamDescriptor::floating("metallic", "Metallic", def.pbr.metallic, 0.f, 1.f, 0.01f)); + descriptor.params.push_back(ParamDescriptor::floating("normalStrength", "Normal Strength", def.pbr.normalStrength, 0.01f, 32.f, 0.05f)); + descriptor.params.push_back(ParamDescriptor::floating("aoStrength", "AO Strength", def.pbr.aoStrength, 0.f, 5.f, 0.05f)); + descriptor.params.push_back(ParamDescriptor::floating("heightStrength", "Height Strength", def.pbr.heightStrength, 0.f, 16.f, 0.05f)); + registerPbrRecipe(std::move(descriptor), [def](const Params ¶ms, std::string &error) { return generatePbrSet(def, params, error); }); } diff --git a/src/modules/procgen/texture/PbrMaterial.h b/src/modules/procgen/texture/PbrMaterial.h index 9d5804f7..2d08e7b6 100644 --- a/src/modules/procgen/texture/PbrMaterial.h +++ b/src/modules/procgen/texture/PbrMaterial.h @@ -29,6 +29,18 @@ struct PbrTextureSet { image::ImageData *height = nullptr; // RGBA8, grayscale displacement image::ImageData *ao = nullptr; // RGBA8, grayscale ambient occlusion + /** @brief Return the generated albedo image. @return Borrowed image. */ + image::ImageData *getAlbedo() const { return albedo; } + /** @brief Return the generated normal image. @return Borrowed image. */ + image::ImageData *getNormal() const { return normal; } + /** @brief Return the generated roughness image. @return Borrowed image. */ + image::ImageData *getRoughness() const { return roughness; } + /** @brief Return the generated metallic image. @return Borrowed image. */ + image::ImageData *getMetallic() const { return metallic; } + /** @brief Return the generated height image. @return Borrowed image. */ + image::ImageData *getHeight() const { return height; } + /** @brief Return the generated ambient-occlusion image. @return Borrowed image. */ + image::ImageData *getAo() const { return ao; } /** @brief Deletes all maps and nulls the pointers (defined in PbrMaterial.cpp). */ void destroy(); }; @@ -38,18 +50,34 @@ using PbrRecipeFn = std::function list() const; + /** @brief Look up recipe metadata. @param id Recipe id. @return Registry-owned schema or nullptr. */ + const RecipeDescriptor *descriptor(const std::string &id) const; + /** @brief Fill missing values from metadata. @param id Recipe id. @param params Values to update. @return False for an unknown recipe. */ + bool applyDefaults(const std::string &id, Params ¶ms) const; + /** @brief Register engine-provided PBR recipes once. */ void registerPbrBuiltins(); private: + struct Entry { + PbrRecipeFn fn; + RecipeDescriptor descriptor; + }; PbrRecipeRegistry() = default; - std::unordered_map recipes_; + std::unordered_map recipes_; bool builtinsRegistered_ = false; }; diff --git a/src/modules/procgen/texture/TextureRecipe.cpp b/src/modules/procgen/texture/TextureRecipe.cpp index 1eb555b5..44474f1e 100644 --- a/src/modules/procgen/texture/TextureRecipe.cpp +++ b/src/modules/procgen/texture/TextureRecipe.cpp @@ -3,7 +3,9 @@ #include "image/ImageData.h" #include +#include #include +#include namespace eve::procgen { @@ -13,7 +15,15 @@ TextureRecipeRegistry &TextureRecipeRegistry::instance() { } void TextureRecipeRegistry::registerRecipe(const std::string &id, TextureRecipeFn fn) { - recipes_[id] = std::move(fn); + RecipeDescriptor descriptor; + descriptor.id = id; + descriptor.displayName = id; + registerRecipe(std::move(descriptor), std::move(fn)); +} + +void TextureRecipeRegistry::registerRecipe(RecipeDescriptor descriptor, TextureRecipeFn fn) { + const std::string id = descriptor.id; + recipes_[id] = Entry{std::move(fn), std::move(descriptor)}; } bool TextureRecipeRegistry::has(const std::string &id) const { @@ -27,7 +37,19 @@ image::ImageData *TextureRecipeRegistry::generate(const std::string &id, const P error = "unknown texture recipe: " + id; return nullptr; } - return it->second(params, error); + return it->second.fn(params, error); +} + +const RecipeDescriptor *TextureRecipeRegistry::descriptor(const std::string &id) const { + const auto it = recipes_.find(id); + return it == recipes_.end() ? nullptr : &it->second.descriptor; +} + +bool TextureRecipeRegistry::applyDefaults(const std::string &id, Params ¶ms) const { + const RecipeDescriptor *schema = descriptor(id); + if (!schema) return false; + schema->applyDefaults(params); + return true; } std::vector TextureRecipeRegistry::list() const { @@ -63,6 +85,26 @@ PbrParams PbrParams::fromParams(const Params ¶ms) { return p; } +RecipeDescriptor makeTextureRecipeDescriptor(const TextureRecipeDef &definition) { + std::string displayName = definition.id; + const size_t dot = displayName.find('.'); + if (dot != std::string::npos) displayName.erase(0, dot + 1); + for (char &ch : displayName) { + if (ch == '_') ch = ' '; + } + if (!displayName.empty()) + displayName[0] = char(std::toupper(static_cast(displayName[0]))); + + RecipeDescriptor descriptor = RecipeDescriptor::grid(definition.id, displayName, "Texture", 1, 1); + descriptor.params.push_back(ParamDescriptor::floating("scale", "Scale", 4.f, 0.05f, 256.f, 0.05f)); + descriptor.params.push_back(ParamDescriptor::integer("octaves", "Octaves", 4, 1, 16)); + descriptor.params.push_back(ParamDescriptor::integer("colors", "Color Bands", 6, 2, 64)); + descriptor.params.push_back(ParamDescriptor::integer("pixelSize", "Pixel Size", 1, 1, 64)); + descriptor.params.push_back(ParamDescriptor::boolean("seamless", "Seamless", true)); + descriptor.params.push_back(ParamDescriptor::floating("warp", "Warp", 1.f, 0.f, 32.f, 0.05f)); + return descriptor; +} + void fillHeightField(const TextureGenContext &ctx, const std::function &fn, std::vector &height) { diff --git a/src/modules/procgen/texture/TextureRecipe.h b/src/modules/procgen/texture/TextureRecipe.h index b0ed0586..501a465a 100644 --- a/src/modules/procgen/texture/TextureRecipe.h +++ b/src/modules/procgen/texture/TextureRecipe.h @@ -1,6 +1,7 @@ #pragma once #include "procgen/Params.h" +#include "procgen/ParamSchema.h" #include "procgen/texture/ColorRamp.h" #include "procgen/texture/NoiseField.h" @@ -22,18 +23,34 @@ using TextureRecipeFn = class TextureRecipeRegistry { public: + /** @brief Access the process-wide texture recipe registry. @return Registry instance. */ static TextureRecipeRegistry &instance(); + /** @brief Register a recipe without metadata. @param id Stable recipe id. @param fn Generator callback. */ void registerRecipe(const std::string &id, TextureRecipeFn fn); + /** @brief Register a texture recipe with reusable metadata. @param descriptor Recipe schema. @param fn Generator callback. */ + void registerRecipe(RecipeDescriptor descriptor, TextureRecipeFn fn); + /** @brief Test whether a recipe exists. @param id Recipe id. @return True when registered. */ bool has(const std::string &id) const; + /** @brief Generate an image. @param id Recipe id. @param params Values. @param error Failure text. @return Caller-owned image or nullptr. */ image::ImageData *generate(const std::string &id, const Params ¶ms, std::string &error) const; + /** @brief List registered recipe ids. @return Sorted ids. */ std::vector list() const; + /** @brief Look up recipe metadata. @param id Recipe id. @return Registry-owned schema or nullptr. */ + const RecipeDescriptor *descriptor(const std::string &id) const; + /** @brief Fill missing values from metadata. @param id Recipe id. @param params Values to update. @return False for an unknown recipe. */ + bool applyDefaults(const std::string &id, Params ¶ms) const; + /** @brief Register engine-provided recipes once. */ void registerBuiltins(); private: + struct Entry { + TextureRecipeFn fn; + RecipeDescriptor descriptor; + }; TextureRecipeRegistry() = default; - std::unordered_map recipes_; + std::unordered_map recipes_; bool builtinsRegistered_ = false; }; @@ -92,4 +109,7 @@ struct TextureRecipeDef { /** @brief All built-in texture definitions (albedo + PBR). Populated lazily. */ const std::vector &builtinTextureDefs(); +/** @brief Build editable metadata for a texture definition. @param definition Definition to describe. @return Recipe metadata. */ +RecipeDescriptor makeTextureRecipeDescriptor(const TextureRecipeDef &definition); + } // namespace eve::procgen diff --git a/test/procgen.cpp b/test/procgen.cpp index 44a39d47..7860d6db 100644 --- a/test/procgen.cpp +++ b/test/procgen.cpp @@ -1901,6 +1901,37 @@ TEST_CASE("procgen.texture.builtinRecipes.expanded") { } } +TEST_CASE("procgen.recipeSchemas.textureAndPbrDefaults") { + auto &textures = TextureRecipeRegistry::instance(); + auto &materials = PbrRecipeRegistry::instance(); + textures.registerBuiltins(); + materials.registerPbrBuiltins(); + for (const std::string &id : textures.list()) { + const RecipeDescriptor *schema = textures.descriptor(id); + REQUIRE(schema != nullptr); + CHECK_EQ(schema->id, id); + CHECK(!schema->displayName.empty()); + CHECK(schema->find("seed") != nullptr); + } + for (const std::string &id : materials.list()) { + const RecipeDescriptor *schema = materials.descriptor(id); + REQUIRE(schema != nullptr); + CHECK_EQ(schema->category, std::string("Material")); + CHECK(schema->find("metallic") != nullptr); + CHECK(schema->find("normalStrength") != nullptr); + } + Params defaults; + REQUIRE(materials.applyDefaults("pbr.rock", defaults)); + const RecipeDescriptor *rock = materials.descriptor("pbr.rock"); + REQUIRE(rock != nullptr); + REQUIRE(rock->find("metallic") != nullptr); + CHECK_EQ(defaults.getFloat("metallic", -1.f), std::stof(rock->find("metallic")->defaultValue)); + CHECK(!materials.applyDefaults("pbr.missing", defaults)); + RecipeDescriptor copied = *rock; + copied.displayName = "Project Rock"; + CHECK_NE(copied.displayName, rock->displayName); +} + TEST_CASE("procgen.pbr.registry.builtinsAndReproducible") { PbrRecipeRegistry::instance().registerPbrBuiltins(); const char *ids[] = {"pbr.soil", "pbr.rock", "pbr.marble", "pbr.water", "pbr.wood", diff --git a/test/procgen_schema_script.cpp b/test/procgen_schema_script.cpp index 24bc00ff..958c0ace 100644 --- a/test/procgen_schema_script.cpp +++ b/test/procgen_schema_script.cpp @@ -34,6 +34,32 @@ TEST_CASE("procgen.script.reflects_schema_and_generates_from_dynamic_values") { fillMax <- fillIndex < 0 ? -1.0 : procgen.getAlgorithmParamMaximum(algorithm, fillIndex); wfcChoiceCount <- procgen.getAlgorithmParamChoiceCount("wfc.simple", 3); wfcFirstChoice <- procgen.getAlgorithmParamChoice("wfc.simple", 3, 0); + algorithmSchema <- procgen.getAlgorithmSchema(algorithm); + genericAlgorithmId <- algorithmSchema.getId(); + textureParams <- procgen.newParams(); + textureDefaultsApplied <- procgen.applyTextureRecipeDefaults("tex.rock", textureParams); + textureSchema <- procgen.getTextureRecipeSchema("tex.rock"); + textureCategory <- textureSchema.getCategory(); + textureScaleFound <- false; + for (local i = 0; i < textureSchema.getParamCount(); ++i) + if (textureSchema.getParamKey(i) == "scale") textureScaleFound = true; + pbrParams <- procgen.newParams(); + pbrParams.setSize(16, 12); + pbrDefaultsApplied <- procgen.applyPbrRecipeDefaults("pbr.rock", pbrParams); + pbrSchema <- procgen.getPbrRecipeSchema("pbr.rock"); + pbrMetallicFound <- false; + pbrNormalFound <- false; + for (local i = 0; i < pbrSchema.getParamCount(); ++i) { + local key = pbrSchema.getParamKey(i); + if (key == "metallic") pbrMetallicFound = true; + if (key == "normalStrength") pbrNormalFound = true; + } + pbrSet <- procgen.generatePbrMaterial("pbr.rock", pbrParams); + pbrHasAllMaps <- pbrSet != null && pbrSet.hasAllMaps(); + pbrAlbedoWidth <- pbrSet == null ? 0 : pbrSet.getAlbedo().getWidth(); + pbrNormalWidth <- pbrSet == null ? 0 : pbrSet.getNormal().getWidth(); + pbrHeightWidth <- pbrSet == null ? 0 : pbrSet.getHeight().getWidth(); + if (pbrSet != null) pbrSet.destroy(); )")); CHECK(vm.find("defaultsApplied").toBool()); @@ -46,4 +72,15 @@ TEST_CASE("procgen.script.reflects_schema_and_generates_from_dynamic_values") { CHECK_EQ(vm.find("height").toInt(), 24); CHECK_EQ(vm.find("wfcChoiceCount").toInt(), 3); CHECK_EQ(vm.find("wfcFirstChoice").toString(), std::string("dungeon")); + CHECK_EQ(vm.find("genericAlgorithmId").toString(), std::string("cave.cellular")); + CHECK(vm.find("textureDefaultsApplied").toBool()); + CHECK_EQ(vm.find("textureCategory").toString(), std::string("Texture")); + CHECK(vm.find("textureScaleFound").toBool()); + CHECK(vm.find("pbrDefaultsApplied").toBool()); + CHECK(vm.find("pbrMetallicFound").toBool()); + CHECK(vm.find("pbrNormalFound").toBool()); + CHECK(vm.find("pbrHasAllMaps").toBool()); + CHECK_EQ(vm.find("pbrAlbedoWidth").toInt(), 16); + CHECK_EQ(vm.find("pbrNormalWidth").toInt(), 16); + CHECK_EQ(vm.find("pbrHeightWidth").toInt(), 16); }