feat(materialx): Integrate MaterialX support for PBR parameters and enhance shader functionality

This commit is contained in:
2026-01-06 15:29:59 +00:00
parent a5b47479d7
commit 512eced3b0
5 changed files with 366 additions and 9 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -5,6 +5,9 @@
#include <btBulletDynamicsCommon.h>
#include <lua.hpp>
#include <MaterialXCore/Document.h>
#include <MaterialXCore/Types.h>
#include <MaterialXFormat/File.h>
#include <SDL3/SDL.h>
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
@@ -12,11 +15,191 @@
#include <algorithm>
#include <array>
#include <cctype>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <utility>
namespace {
namespace mx = MaterialX;
struct MaterialXSurfaceParameters {
std::array<float, 3> 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<float, 3>& 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<mx::Color3>()) {
return false;
}
const mx::Color3& color = value->asA<mx::Color3>();
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<float>()) {
return false;
}
outValue = value->asA<float>();
return true;
}
MaterialXSurfaceParameters ReadStandardSurfaceParameters(const mx::NodePtr& node) {
MaterialXSurfaceParameters parameters;
std::array<float, 3> 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<LuaBindingContext*>(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<LuaBindingContext*>(lua_touserdata(L, lua_upvalueindex(1)));
if (!context || !context->windowService) {

View File

@@ -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);