mirror of
https://github.com/lighttransport/tinyusdz.git
synced 2026-01-18 01:11:17 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
123
tests/feat/mtlx/GROUPED_PARAMS_README.md
Normal file
123
tests/feat/mtlx/GROUPED_PARAMS_README.md
Normal 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
|
||||
11
tests/feat/mtlx/Makefile.grouped_params
Normal file
11
tests/feat/mtlx/Makefile.grouped_params
Normal 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
|
||||
102
tests/feat/mtlx/test_grouped_params.cc
Normal file
102
tests/feat/mtlx/test_grouped_params.cc
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user