Add grouped and flattened parameter export for MaterialX OpenPBR materials

Add use_grouped_parameters option to ThreeJSMaterialExporter to support both flattened (base_color) and grouped (base.color) parameter naming for JSON export. Includes setJsonParameter helper for automatic name transformation and test suite for validation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Syoyo Fujita
2025-11-14 00:29:37 +09:00
parent a224092e7a
commit 32b2e15d72
5 changed files with 288 additions and 20 deletions

View File

@@ -95,6 +95,32 @@ static json vec3ToJson(const vec3& v) {
return json::array({v[0], v[1], v[2]});
}
// Helper function to set a parameter in JSON with optional grouping
// For flattened: "base_color" stays as inputs["base_color"]
// For grouped: "base_color" becomes inputs["base"]["color"]
static void setJsonParameter(json& inputs, const std::string& param_name, const json& value, bool use_grouped) {
if (!use_grouped) {
// Flattened format: base_color
inputs[param_name] = value;
} else {
// Grouped format: base.color
size_t underscore_pos = param_name.find('_');
if (underscore_pos != std::string::npos) {
std::string group = param_name.substr(0, underscore_pos);
std::string property = param_name.substr(underscore_pos + 1);
// Create group object if it doesn't exist
if (!inputs.contains(group)) {
inputs[group] = json::object();
}
inputs[group][property] = value;
} else {
// No underscore, keep as-is
inputs[param_name] = value;
}
}
}
// Helper function to convert vec2 to JSON array
// Static function that may not be used currently
#ifdef __clang__
@@ -251,11 +277,11 @@ bool ThreeJSMaterialExporter::ExportMaterial(const RenderMaterial& material,
if (has_openpbr && options.use_webgpu) {
// Export as WebGPU node material
output["type"] = "NodeMaterial";
output["nodes"] = ConvertOpenPBRToNodeMaterial(material.openPBRShader.value());
output["nodes"] = ConvertOpenPBRToNodeMaterial(material.openPBRShader.value(), options);
} else if (has_openpbr && options.generate_fallback) {
// Export as WebGL MeshPhysicalMaterial
output["type"] = "MeshPhysicalMaterial";
json params = ConvertOpenPBRToPhysicalMaterial(material.openPBRShader.value());
json params = ConvertOpenPBRToPhysicalMaterial(material.openPBRShader.value(), options);
for (auto it = params.items().begin(); it != params.items().end(); ++it) {
output[it.key()] = it.value();
}
@@ -285,7 +311,7 @@ bool ThreeJSMaterialExporter::ExportMaterial(const RenderMaterial& material,
return true;
}
json ThreeJSMaterialExporter::ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceShader& shader) {
json ThreeJSMaterialExporter::ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceShader& shader, const ExportOptions& options) {
json nodes = json::object();
// Create OpenPBR surface node
@@ -295,27 +321,31 @@ json ThreeJSMaterialExporter::ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceS
{"inputs", json::object()}
};
bool use_grouped = options.use_grouped_parameters;
// Map all OpenPBR parameters
auto add_param = [&](const std::string& name, const auto& param) {
json value;
if (param.is_texture()) {
surface_node["inputs"][name] = {
value = {
{"type", "texture"},
{"textureId", param.texture_id}
};
} else {
surface_node["inputs"][name] = param.value;
value = param.value;
}
setJsonParameter(surface_node["inputs"], name, value, use_grouped);
};
// Base layer
add_param("base_weight", shader.base_weight);
surface_node["inputs"]["base_color"] = vec3ToJson(shader.base_color.value);
setJsonParameter(surface_node["inputs"], "base_color", vec3ToJson(shader.base_color.value), use_grouped);
add_param("base_roughness", shader.base_roughness);
add_param("base_metalness", shader.base_metalness);
// Specular layer
add_param("specular_weight", shader.specular_weight);
surface_node["inputs"]["specular_color"] = vec3ToJson(shader.specular_color.value);
setJsonParameter(surface_node["inputs"], "specular_color", vec3ToJson(shader.specular_color.value), use_grouped);
add_param("specular_roughness", shader.specular_roughness);
add_param("specular_ior", shader.specular_ior);
add_param("specular_ior_level", shader.specular_ior_level);
@@ -324,42 +354,42 @@ json ThreeJSMaterialExporter::ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceS
// Transmission
add_param("transmission_weight", shader.transmission_weight);
surface_node["inputs"]["transmission_color"] = vec3ToJson(shader.transmission_color.value);
setJsonParameter(surface_node["inputs"], "transmission_color", vec3ToJson(shader.transmission_color.value), use_grouped);
add_param("transmission_depth", shader.transmission_depth);
surface_node["inputs"]["transmission_scatter"] = vec3ToJson(shader.transmission_scatter.value);
setJsonParameter(surface_node["inputs"], "transmission_scatter", vec3ToJson(shader.transmission_scatter.value), use_grouped);
add_param("transmission_scatter_anisotropy", shader.transmission_scatter_anisotropy);
add_param("transmission_dispersion", shader.transmission_dispersion);
// Subsurface
add_param("subsurface_weight", shader.subsurface_weight);
surface_node["inputs"]["subsurface_color"] = vec3ToJson(shader.subsurface_color.value);
surface_node["inputs"]["subsurface_radius"] = vec3ToJson(shader.subsurface_radius.value);
setJsonParameter(surface_node["inputs"], "subsurface_color", vec3ToJson(shader.subsurface_color.value), use_grouped);
setJsonParameter(surface_node["inputs"], "subsurface_radius", vec3ToJson(shader.subsurface_radius.value), use_grouped);
add_param("subsurface_scale", shader.subsurface_scale);
add_param("subsurface_anisotropy", shader.subsurface_anisotropy);
// Sheen
add_param("sheen_weight", shader.sheen_weight);
surface_node["inputs"]["sheen_color"] = vec3ToJson(shader.sheen_color.value);
setJsonParameter(surface_node["inputs"], "sheen_color", vec3ToJson(shader.sheen_color.value), use_grouped);
add_param("sheen_roughness", shader.sheen_roughness);
// Coat
add_param("coat_weight", shader.coat_weight);
surface_node["inputs"]["coat_color"] = vec3ToJson(shader.coat_color.value);
setJsonParameter(surface_node["inputs"], "coat_color", vec3ToJson(shader.coat_color.value), use_grouped);
add_param("coat_roughness", shader.coat_roughness);
add_param("coat_anisotropy", shader.coat_anisotropy);
add_param("coat_rotation", shader.coat_rotation);
add_param("coat_ior", shader.coat_ior);
surface_node["inputs"]["coat_affect_color"] = vec3ToJson(shader.coat_affect_color.value);
setJsonParameter(surface_node["inputs"], "coat_affect_color", vec3ToJson(shader.coat_affect_color.value), use_grouped);
add_param("coat_affect_roughness", shader.coat_affect_roughness);
// Emission
add_param("emission_luminance", shader.emission_luminance);
surface_node["inputs"]["emission_color"] = vec3ToJson(shader.emission_color.value);
setJsonParameter(surface_node["inputs"], "emission_color", vec3ToJson(shader.emission_color.value), use_grouped);
// Geometry
add_param("opacity", shader.opacity);
surface_node["inputs"]["normal"] = vec3ToJson(shader.normal.value);
surface_node["inputs"]["tangent"] = vec3ToJson(shader.tangent.value);
setJsonParameter(surface_node["inputs"], "normal", vec3ToJson(shader.normal.value), use_grouped);
setJsonParameter(surface_node["inputs"], "tangent", vec3ToJson(shader.tangent.value), use_grouped);
nodes["surface"] = surface_node;
@@ -378,7 +408,8 @@ json ThreeJSMaterialExporter::ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceS
return nodes;
}
json ThreeJSMaterialExporter::ConvertOpenPBRToPhysicalMaterial(const OpenPBRSurfaceShader& shader) {
json ThreeJSMaterialExporter::ConvertOpenPBRToPhysicalMaterial(const OpenPBRSurfaceShader& shader, const ExportOptions& options) {
(void)options; // Options reserved for future use (e.g., grouped userData)
json params = json::object();
// Map OpenPBR to MeshPhysicalMaterial parameters

View File

@@ -49,6 +49,7 @@ public:
std::string texture_path = ""; ///< External texture directory path
bool export_mtlx = false; ///< Export as MaterialX document
std::string color_space = "sRGB"; ///< Target color space for textures
bool use_grouped_parameters = false; ///< Use grouped parameters (e.g., base.color) instead of flattened (e.g., base_color)
};
/// Export entire RenderScene to Three.js format
@@ -73,10 +74,10 @@ public:
private:
/// Convert OpenPBR shader to Three.js node material
json ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceShader& shader);
json ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceShader& shader, const ExportOptions& options);
/// Convert OpenPBR shader to Three.js MeshPhysicalMaterial (WebGL fallback)
json ConvertOpenPBRToPhysicalMaterial(const OpenPBRSurfaceShader& shader);
json ConvertOpenPBRToPhysicalMaterial(const OpenPBRSurfaceShader& shader, const ExportOptions& options);
/// Convert UsdPreviewSurface to Three.js materials
json ConvertPreviewSurfaceToNodeMaterial(const PreviewSurfaceShader& shader);

View File

@@ -0,0 +1,123 @@
# Grouped and Flattened MaterialX Parameter Export
This implementation adds support for both **grouped** and **flattened** OpenPBR parameter naming in JSON export through the ThreeJSMaterialExporter.
## Summary
Added a new `use_grouped_parameters` option to `ThreeJSMaterialExporter::ExportOptions` that controls how MaterialX (OpenPBR) parameters are structured in JSON output.
## Changes Made
### 1. Header File (`src/tydra/threejs-exporter.hh`)
- Added `bool use_grouped_parameters` to `ExportOptions` struct
### 2. Implementation (`src/tydra/threejs-exporter.cc`)
- Added `setJsonParameter()` helper function for parameter name transformation
- Updated `ConvertOpenPBRToNodeMaterial()` to support both formats
- Updated `ConvertOpenPBRToPhysicalMaterial()` signature for consistency
- Updated `ExportMaterial()` to pass options through
## Output Formats
### Flattened Format (use_grouped_parameters = false)
All parameters at the same level with underscore-separated names:
```json
{
"inputs": {
"base_color": [0.8, 0.2, 0.1],
"base_weight": 1.0,
"base_roughness": 0.5,
"base_metalness": 0.0,
"specular_weight": 1.0,
"specular_color": [1.0, 1.0, 1.0],
"specular_ior": 1.5,
...
}
}
```
### Grouped Format (use_grouped_parameters = true)
Parameters organized into logical groups with shortened property names:
```json
{
"inputs": {
"base": {
"color": [0.8, 0.2, 0.1],
"weight": 1.0,
"roughness": 0.5,
"metalness": 0.0
},
"specular": {
"weight": 1.0,
"color": [1.0, 1.0, 1.0],
"ior": 1.5,
...
},
"coat": {...},
"emission": {...},
...
}
}
```
## Parameter Groups
The grouped format organizes parameters into:
- **base**: color, weight, roughness, metalness
- **specular**: color, weight, ior, roughness, anisotropy, rotation, ior_level
- **transmission**: color, weight, depth, scatter, scatter_anisotropy, dispersion
- **coat**: color, weight, roughness, ior, anisotropy, rotation, affect_color, affect_roughness
- **emission**: color, luminance
- **subsurface**: color, weight, radius, scale, anisotropy
- **sheen**: color, weight, roughness
- **geometry params**: opacity, normal, tangent (kept at root level)
## Usage Example
```cpp
#include "tydra/threejs-exporter.hh"
ThreeJSMaterialExporter exporter;
ThreeJSMaterialExporter::ExportOptions options;
// Export with flattened parameters (default)
options.use_grouped_parameters = false;
json flattened_output;
exporter.ExportMaterial(material, options, flattened_output);
// Export with grouped parameters
options.use_grouped_parameters = true;
json grouped_output;
exporter.ExportMaterial(material, options, grouped_output);
```
## Testing
Run the test to verify both formats:
```bash
cd tests/feat/mtlx
make -f Makefile.grouped_params
./test_grouped_params
```
## Comparison with material-serializer.cc
Note that `material-serializer.cc` (used by the WASM bindings and `dump-materialx-cli.js`) has its own grouped format that uses full parameter names within groups:
```json
{
"base": {
"base_weight": {"name": "base_weight", "type": "value", "value": 1.0},
"base_color": {"name": "base_color", "type": "value", "value": [0.8, 0.2, 0.1]},
...
}
}
```
The ThreeJSMaterialExporter provides a more compact grouped format suitable for Three.js node materials and WebGPU rendering.
## Backward Compatibility
- Default behavior is **flattened** (`use_grouped_parameters = false`)
- Existing code continues to work without modification
- Grouped format is opt-in via the options flag

View File

@@ -0,0 +1,11 @@
CXX = g++
CXXFLAGS = -std=c++14 -I../../../src -I../../../
LDFLAGS = -L../../../build -ltinyusdz_static -lpthread
test_grouped_params: test_grouped_params.cc
$(CXX) $(CXXFLAGS) -o test_grouped_params test_grouped_params.cc $(LDFLAGS)
clean:
rm -f test_grouped_params
.PHONY: clean

View File

@@ -0,0 +1,102 @@
// Test for grouped/flattened parameter export functionality
#include <iostream>
#include <string>
#include "tydra/render-data.hh"
#include "tydra/threejs-exporter.hh"
using namespace tinyusdz;
using namespace tinyusdz::tydra;
int main() {
// Create a simple OpenPBR material
RenderMaterial material;
material.name = "TestMaterial";
material.handle = 12345;
// Create OpenPBR shader
OpenPBRSurfaceShader shader;
shader.handle = 67890;
// Set some basic parameters
shader.base_weight.value = 1.0f;
shader.base_color.value = {0.8f, 0.2f, 0.1f};
shader.base_roughness.value = 0.5f;
shader.base_metalness.value = 0.0f;
shader.specular_weight.value = 1.0f;
shader.specular_color.value = {1.0f, 1.0f, 1.0f};
shader.specular_ior.value = 1.5f;
shader.emission_luminance.value = 0.0f;
shader.emission_color.value = {0.0f, 0.0f, 0.0f};
shader.coat_weight.value = 0.5f;
shader.coat_color.value = {1.0f, 1.0f, 1.0f};
shader.coat_roughness.value = 0.1f;
shader.opacity.value = 1.0f;
shader.normal.value = {0.0f, 0.0f, 1.0f};
shader.tangent.value = {1.0f, 0.0f, 0.0f};
material.openPBRShader = shader;
// Create exporter
ThreeJSMaterialExporter exporter;
// Test 1: Flattened parameters (default)
std::cout << "=== Test 1: Flattened Parameters (default) ===" << std::endl;
{
ThreeJSMaterialExporter::ExportOptions options;
options.use_webgpu = true;
options.use_grouped_parameters = false; // Flattened
json output;
if (exporter.ExportMaterial(material, options, output)) {
std::cout << "Flattened output:" << std::endl;
std::cout << output.dump(2) << std::endl;
// Check if base_color exists in flattened format
if (output["nodes"]["surface"]["inputs"].contains("base_color")) {
std::cout << "✓ Found 'base_color' in flattened format" << std::endl;
} else {
std::cout << "✗ 'base_color' NOT found in flattened format" << std::endl;
}
} else {
std::cout << "Error: " << exporter.GetError() << std::endl;
}
}
std::cout << "\n=== Test 2: Grouped Parameters ===" << std::endl;
{
ThreeJSMaterialExporter::ExportOptions options;
options.use_webgpu = true;
options.use_grouped_parameters = true; // Grouped
json output;
if (exporter.ExportMaterial(material, options, output)) {
std::cout << "Grouped output:" << std::endl;
std::cout << output.dump(2) << std::endl;
// Check if base.color exists in grouped format
if (output["nodes"]["surface"]["inputs"].contains("base") &&
output["nodes"]["surface"]["inputs"]["base"].contains("color")) {
std::cout << "✓ Found 'base.color' in grouped format" << std::endl;
} else {
std::cout << "✗ 'base.color' NOT found in grouped format" << std::endl;
}
// Check if specular.weight exists
if (output["nodes"]["surface"]["inputs"].contains("specular") &&
output["nodes"]["surface"]["inputs"]["specular"].contains("weight")) {
std::cout << "✓ Found 'specular.weight' in grouped format" << std::endl;
} else {
std::cout << "✗ 'specular.weight' NOT found in grouped format" << std::endl;
}
} else {
std::cout << "Error: " << exporter.GetError() << std::endl;
}
}
return 0;
}