From 512eced3b0b2d9e7545241d3a8bd6bcbb2e0c87a Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Tue, 6 Jan 2026 15:29:59 +0000 Subject: [PATCH] feat(materialx): Integrate MaterialX support for PBR parameters and enhance shader functionality --- config/seed_runtime.json | 37 ++- scripts/cube_logic.lua | 4 +- scripts/shader_variants.lua | 68 +++++ src/services/impl/script_engine_service.cpp | 265 ++++++++++++++++++++ src/services/impl/script_engine_service.hpp | 1 + 5 files changed, 366 insertions(+), 9 deletions(-) diff --git a/config/seed_runtime.json b/config/seed_runtime.json index c7dd606..04251ef 100644 --- a/config/seed_runtime.json +++ b/config/seed_runtime.json @@ -60,19 +60,42 @@ "bgfx": { "renderer": "vulkan" }, + "materialx": { + "enabled": false, + "parameters_enabled": true, + "document": "MaterialX/resources/Materials/Examples/StandardSurface/standard_surface_carpaint.mtlx", + "shader_key": "materialx", + "material": "Car_Paint", + "library_path": "MaterialX/libraries", + "library_folders": [ + "stdlib", + "pbrlib", + "lights", + "bxdf", + "cmlib", + "nprlib", + "targets" + ], + "use_constant_color": false, + "constant_color": [ + 1.0, + 1.0, + 1.0 + ] + }, "atmospherics": { - "ambient_strength": 0.01, - "fog_density": 0.003, - "fog_color": [0.05, 0.05, 0.08], - "sky_color": [0.04, 0.05, 0.08], + "ambient_strength": 0.006, + "fog_density": 0.006, + "fog_color": [0.03, 0.04, 0.06], + "sky_color": [0.02, 0.03, 0.05], "gamma": 2.2, - "exposure": 1.0, + "exposure": 1.15, "enable_tone_mapping": true, "enable_shadows": true, "enable_ssgi": true, "enable_volumetric_lighting": true, - "pbr_roughness": 0.3, - "pbr_metallic": 0.1 + "pbr_roughness": 0.28, + "pbr_metallic": 0.08 }, "gui_opacity": 1.0, "config_file": "config/seed_runtime.json" diff --git a/scripts/cube_logic.lua b/scripts/cube_logic.lua index 078493e..18fb140 100644 --- a/scripts/cube_logic.lua +++ b/scripts/cube_logic.lua @@ -564,7 +564,7 @@ local function create_skybox() end local function create_spinning_cube() - log_debug("Spinning cube shader=default (rainbow wrap)") + log_debug("Spinning cube shader=pbr (MaterialX-driven)") local function compute_model_matrix(time) local rotation = math3d.rotation_y(time * rotation_speed) local scale = scale_matrix(1.5, 1.5, 1.5) -- Make cube 3x3x3 units @@ -576,7 +576,7 @@ local function create_spinning_cube() vertices = cube_vertices, indices = (#cube_indices_double_sided > 0) and cube_indices_double_sided or cube_indices, compute_model_matrix = compute_model_matrix, - shader_key = "default", + shader_key = "pbr", } end diff --git a/scripts/shader_variants.lua b/scripts/shader_variants.lua index 3642190..3a8e0ab 100644 --- a/scripts/shader_variants.lua +++ b/scripts/shader_variants.lua @@ -141,6 +141,51 @@ local function build_shader_parameter_overrides(config, log_debug) return parameters end +local function load_materialx_parameters(config, log_debug) + if type(config) ~= "table" then + return nil + end + local materialx = config.materialx + if type(materialx) ~= "table" then + return nil + end + local enabled = materialx.enabled + if type(materialx.parameters_enabled) == "boolean" then + enabled = materialx.parameters_enabled + end + if not enabled then + return nil + end + if type(materialx.document) ~= "string" or materialx.document == "" then + log_debug("MaterialX enabled but document path is missing") + return nil + end + if type(materialx_get_surface_parameters) ~= "function" then + log_debug("MaterialX loader unavailable (materialx_get_surface_parameters missing)") + return nil + end + + local material_name = nil + if type(materialx.material) == "string" and materialx.material ~= "" then + material_name = materialx.material + end + + local ok, result, err = pcall(materialx_get_surface_parameters, materialx.document, material_name) + if not ok then + log_debug("MaterialX parameter load failed: %s", tostring(result)) + return nil + end + if result == nil then + log_debug("MaterialX parameter load failed: %s", tostring(err)) + return nil + end + if type(result) ~= "table" then + log_debug("MaterialX parameter load returned unexpected result") + return nil + end + return result +end + local function resolve_skybox_color(config, default_color) if type(config) ~= "table" then return default_color @@ -241,6 +286,29 @@ function M.build_cube_variants(config, log_debug, base_skybox_color) local logger = get_logger(log_debug) local skybox_color = resolve_skybox_color(config, base_skybox_color or {0.04, 0.05, 0.08}) local shader_parameters = build_shader_parameter_overrides(config, logger) + local materialx_parameters = load_materialx_parameters(config, logger) + if materialx_parameters then + local entry = shader_parameters.pbr or {} + local albedo = resolve_color3_optional(materialx_parameters.material_albedo) + local roughness = resolve_number_optional(materialx_parameters.material_roughness) + local metallic = resolve_number_optional(materialx_parameters.material_metallic) + if albedo ~= nil then + entry.material_albedo = albedo + end + if roughness ~= nil then + entry.material_roughness = roughness + end + if metallic ~= nil then + entry.material_metallic = metallic + end + if next(entry) ~= nil then + shader_parameters.pbr = entry + logger("MaterialX PBR overrides: albedo=%s roughness=%s metallic=%s", + format_optional_color(albedo), + format_optional_number(roughness), + format_optional_number(metallic)) + end + end local ok, toolkit = pcall(require, "shader_toolkit") if not ok then diff --git a/src/services/impl/script_engine_service.cpp b/src/services/impl/script_engine_service.cpp index 3bf479e..bbb6c08 100644 --- a/src/services/impl/script_engine_service.cpp +++ b/src/services/impl/script_engine_service.cpp @@ -5,6 +5,9 @@ #include #include +#include +#include +#include #include #include #include @@ -12,11 +15,191 @@ #include #include #include +#include #include #include #include namespace { +namespace mx = MaterialX; + +struct MaterialXSurfaceParameters { + std::array albedo = {1.0f, 1.0f, 1.0f}; + float roughness = 0.3f; + float metallic = 0.0f; + bool hasAlbedo = false; + bool hasRoughness = false; + bool hasMetallic = false; +}; + +std::filesystem::path ResolveMaterialXPath(const std::filesystem::path& path, + const std::filesystem::path& scriptDirectory) { + if (path.empty()) { + return {}; + } + if (path.is_absolute()) { + return path; + } + if (!scriptDirectory.empty()) { + auto projectRoot = scriptDirectory.parent_path(); + if (!projectRoot.empty()) { + std::error_code ec; + auto resolved = std::filesystem::weakly_canonical(projectRoot / path, ec); + if (!ec) { + return resolved; + } + } + } + std::error_code ec; + auto resolved = std::filesystem::weakly_canonical(path, ec); + if (ec) { + return {}; + } + return resolved; +} + +mx::FileSearchPath BuildMaterialXSearchPath(const sdl3cpp::services::MaterialXConfig& config, + const std::filesystem::path& scriptDirectory) { + mx::FileSearchPath searchPath; + std::filesystem::path libraryPath = ResolveMaterialXPath(config.libraryPath, scriptDirectory); + if (libraryPath.empty() && !scriptDirectory.empty()) { + auto fallback = scriptDirectory.parent_path() / "MaterialX" / "libraries"; + if (std::filesystem::exists(fallback)) { + libraryPath = fallback; + } + } + if (!libraryPath.empty()) { + searchPath.append(mx::FilePath(libraryPath.string())); + } + return searchPath; +} + +mx::DocumentPtr LoadMaterialXDocument(const std::filesystem::path& documentPath, + const sdl3cpp::services::MaterialXConfig& config, + const std::filesystem::path& scriptDirectory) { + mx::DocumentPtr document = mx::createDocument(); + mx::FileSearchPath searchPath = BuildMaterialXSearchPath(config, scriptDirectory); + mx::readFromXmlFile(document, mx::FilePath(documentPath.string()), &searchPath); + + if (!config.libraryFolders.empty()) { + mx::DocumentPtr stdLib = mx::createDocument(); + mx::FilePathVec folders; + folders.reserve(config.libraryFolders.size()); + for (const auto& folder : config.libraryFolders) { + folders.emplace_back(folder); + } + mx::loadLibraries(folders, searchPath, stdLib); + document->importLibrary(stdLib); + } + + return document; +} + +mx::NodePtr ResolveSurfaceNode(const mx::DocumentPtr& document, const std::string& materialName) { + if (!materialName.empty()) { + mx::NodePtr candidate = document->getNode(materialName); + if (candidate && candidate->getCategory() == "surfacematerial") { + mx::NodePtr surfaceNode = candidate->getConnectedNode("surfaceshader"); + if (surfaceNode) { + return surfaceNode; + } + } + if (candidate && (candidate->getCategory() == "standard_surface" + || candidate->getCategory() == "usd_preview_surface")) { + return candidate; + } + } + + for (const auto& node : document->getNodes()) { + if (node->getCategory() == "surfacematerial") { + mx::NodePtr surfaceNode = node->getConnectedNode("surfaceshader"); + if (surfaceNode) { + return surfaceNode; + } + } + } + + for (const auto& node : document->getNodes()) { + if (node->getCategory() == "standard_surface" + || node->getCategory() == "usd_preview_surface") { + return node; + } + } + + return {}; +} + +bool TryReadColor3(const mx::NodePtr& node, const char* name, std::array& outColor) { + if (!node) { + return false; + } + mx::InputPtr input = node->getInput(name); + if (!input || !input->hasValueString()) { + return false; + } + mx::ValuePtr value = input->getValue(); + if (!value || !value->isA()) { + return false; + } + const mx::Color3& color = value->asA(); + outColor = {color[0], color[1], color[2]}; + return true; +} + +bool TryReadFloat(const mx::NodePtr& node, const char* name, float& outValue) { + if (!node) { + return false; + } + mx::InputPtr input = node->getInput(name); + if (!input || !input->hasValueString()) { + return false; + } + mx::ValuePtr value = input->getValue(); + if (!value || !value->isA()) { + return false; + } + outValue = value->asA(); + return true; +} + +MaterialXSurfaceParameters ReadStandardSurfaceParameters(const mx::NodePtr& node) { + MaterialXSurfaceParameters parameters; + std::array baseColor = parameters.albedo; + bool hasBaseColor = TryReadColor3(node, "base_color", baseColor); + if (!hasBaseColor) { + hasBaseColor = TryReadColor3(node, "diffuse_color", baseColor); + } + float baseStrength = 1.0f; + bool hasBaseStrength = TryReadFloat(node, "base", baseStrength); + if (hasBaseColor) { + parameters.albedo = { + baseColor[0] * baseStrength, + baseColor[1] * baseStrength, + baseColor[2] * baseStrength + }; + parameters.hasAlbedo = true; + } else if (hasBaseStrength) { + parameters.albedo = {baseStrength, baseStrength, baseStrength}; + parameters.hasAlbedo = true; + } + + float roughness = parameters.roughness; + if (TryReadFloat(node, "specular_roughness", roughness) + || TryReadFloat(node, "roughness", roughness)) { + parameters.roughness = roughness; + parameters.hasRoughness = true; + } + + float metallic = parameters.metallic; + if (TryReadFloat(node, "metalness", metallic) + || TryReadFloat(node, "metallic", metallic)) { + parameters.metallic = metallic; + parameters.hasMetallic = true; + } + + return parameters; +} + bool TryParseMouseButtonName(const char* name, uint8_t& button) { if (!name) { return false; @@ -302,6 +485,7 @@ void ScriptEngineService::RegisterBindings(lua_State* L) { bind("input_get_text", &ScriptEngineService::InputGetText); bind("config_get_json", &ScriptEngineService::ConfigGetJson); bind("config_get_table", &ScriptEngineService::ConfigGetTable); + bind("materialx_get_surface_parameters", &ScriptEngineService::MaterialXGetSurfaceParameters); bind("window_get_size", &ScriptEngineService::WindowGetSize); bind("window_set_title", &ScriptEngineService::WindowSetTitle); bind("window_is_minimized", &ScriptEngineService::WindowIsMinimized); @@ -673,6 +857,87 @@ int ScriptEngineService::ConfigGetTable(lua_State* L) { return 1; } +int ScriptEngineService::MaterialXGetSurfaceParameters(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto logger = context ? context->logger : nullptr; + if (!context || !context->configService) { + lua_pushnil(L); + lua_pushstring(L, "Config service not available"); + return 2; + } + + const char* documentArg = luaL_checkstring(L, 1); + const char* materialArg = luaL_optstring(L, 2, ""); + std::filesystem::path scriptDirectory = context->configService->GetScriptPath().parent_path(); + if (logger) { + logger->Trace("ScriptEngineService", "MaterialXGetSurfaceParameters", + "document=" + std::string(documentArg ? documentArg : "") + + ", material=" + std::string(materialArg ? materialArg : "")); + } + + std::filesystem::path documentPath = ResolveMaterialXPath(documentArg ? documentArg : "", scriptDirectory); + if (documentPath.empty()) { + lua_pushnil(L); + lua_pushstring(L, "MaterialX document path could not be resolved"); + return 2; + } + if (!std::filesystem::exists(documentPath)) { + lua_pushnil(L); + std::string message = "MaterialX document not found: " + documentPath.string(); + lua_pushstring(L, message.c_str()); + return 2; + } + + const auto& materialConfig = context->configService->GetMaterialXConfig(); + mx::DocumentPtr document; + try { + document = LoadMaterialXDocument(documentPath, materialConfig, scriptDirectory); + } catch (const std::exception& ex) { + lua_pushnil(L); + std::string message = "MaterialX document load failed: "; + message += ex.what(); + lua_pushstring(L, message.c_str()); + return 2; + } + + mx::NodePtr surfaceNode = ResolveSurfaceNode(document, materialArg ? materialArg : ""); + if (!surfaceNode) { + lua_pushnil(L); + lua_pushstring(L, "MaterialX document has no standard_surface material"); + return 2; + } + + MaterialXSurfaceParameters parameters = ReadStandardSurfaceParameters(surfaceNode); + if (!parameters.hasAlbedo && !parameters.hasRoughness && !parameters.hasMetallic) { + lua_pushnil(L); + lua_pushstring(L, "MaterialX material does not expose supported PBR parameters"); + return 2; + } + + lua_newtable(L); + if (parameters.hasAlbedo) { + lua_newtable(L); + lua_pushnumber(L, parameters.albedo[0]); + lua_rawseti(L, -2, 1); + lua_pushnumber(L, parameters.albedo[1]); + lua_rawseti(L, -2, 2); + lua_pushnumber(L, parameters.albedo[2]); + lua_rawseti(L, -2, 3); + lua_setfield(L, -2, "material_albedo"); + } + if (parameters.hasRoughness) { + lua_pushnumber(L, parameters.roughness); + lua_setfield(L, -2, "material_roughness"); + } + if (parameters.hasMetallic) { + lua_pushnumber(L, parameters.metallic); + lua_setfield(L, -2, "material_metallic"); + } + + lua_pushnil(L); + return 2; +} + int ScriptEngineService::WindowGetSize(lua_State* L) { auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); if (!context || !context->windowService) { diff --git a/src/services/impl/script_engine_service.hpp b/src/services/impl/script_engine_service.hpp index 771cc9d..59f9646 100644 --- a/src/services/impl/script_engine_service.hpp +++ b/src/services/impl/script_engine_service.hpp @@ -77,6 +77,7 @@ private: static int InputGetText(lua_State* L); static int ConfigGetJson(lua_State* L); static int ConfigGetTable(lua_State* L); + static int MaterialXGetSurfaceParameters(lua_State* L); static int WindowGetSize(lua_State* L); static int WindowSetTitle(lua_State* L); static int WindowIsMinimized(lua_State* L);