Merge branch 'mtlx-2025' into usdlux-2025

This commit is contained in:
Syoyo Fujita
2025-11-18 22:02:52 +09:00
28 changed files with 4008 additions and 525 deletions

218
models/cube-mtlx-texture.usda Executable file
View File

@@ -0,0 +1,218 @@
#usda 1.0
(
defaultPrim = "root"
doc = "Blender v4.5.4 LTS"
metersPerUnit = 1
upAxis = "Z"
)
def Xform "root" (
customData = {
dictionary Blender = {
bool generated = 1
}
}
)
{
def Xform "Cube"
{
custom string userProperties:blender:object_name = "Cube"
def Mesh "Cube" (
active = true
prepend apiSchemas = ["MaterialBindingAPI"]
)
{
uniform bool doubleSided = 1
float3[] extent = [(-1, -1, -1), (1, 1, 1)]
int[] faceVertexCounts = [4, 4, 4, 4, 4, 4]
int[] faceVertexIndices = [0, 4, 6, 2, 3, 2, 6, 7, 7, 6, 4, 5, 5, 1, 3, 7, 1, 0, 2, 3, 5, 4, 0, 1]
rel material:binding = </root/_materials/Material>
normal3f[] normals = [(0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0)] (
interpolation = "faceVarying"
)
point3f[] points = [(1, 1, 1), (1, 1, -1), (1, -1, 1), (1, -1, -1), (-1, 1, 1), (-1, 1, -1), (-1, -1, 1), (-1, -1, -1)]
texCoord2f[] primvars:st = [(0.625, 0.5), (0.875, 0.5), (0.875, 0.75), (0.625, 0.75), (0.375, 0.75), (0.625, 0.75), (0.625, 1), (0.375, 1), (0.375, 0), (0.625, 0), (0.625, 0.25), (0.375, 0.25), (0.125, 0.5), (0.375, 0.5), (0.375, 0.75), (0.125, 0.75), (0.375, 0.5), (0.625, 0.5), (0.625, 0.75), (0.375, 0.75), (0.375, 0.25), (0.625, 0.25), (0.625, 0.5), (0.375, 0.5)] (
interpolation = "faceVarying"
)
uniform token subdivisionScheme = "none"
custom string userProperties:blender:data_name = "Cube"
}
}
def Scope "_materials"
{
def Material "Material" (
prepend apiSchemas = ["MaterialXConfigAPI"]
)
{
string config:mtlx:version = "1.39"
token outputs:mtlx:surface.connect = </root/_materials/Material/Principled_BSDF_mtlx1.outputs:surface>
token outputs:surface.connect = </root/_materials/Material/Principled_BSDF.outputs:surface>
custom string userProperties:blender:data_name = "Material"
def Shader "Principled_BSDF"
{
uniform token info:id = "UsdPreviewSurface"
float inputs:clearcoat = 0
float inputs:clearcoatRoughness = 0.03
color3f inputs:diffuseColor.connect = </root/_materials/Material/Image_Texture.outputs:rgb>
float inputs:ior = 1.45
float inputs:metallic = 0
float inputs:opacity = 1
float inputs:roughness = 0.5
float inputs:specular = 0.5
token outputs:surface
}
def Shader "Image_Texture"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/texture-cat.jpg@
token inputs:sourceColorSpace = "sRGB"
float2 inputs:st.connect = </root/_materials/Material/uvmap.outputs:result>
token inputs:wrapS = "repeat"
token inputs:wrapT = "repeat"
float3 outputs:rgb
}
def Shader "uvmap"
{
uniform token info:id = "UsdPrimvarReader_float2"
string inputs:varname = "st"
float2 outputs:result
}
def Shader "Principled_BSDF_mtlx1"
{
uniform token info:id = "ND_open_pbr_surface_surfaceshader"
color3f inputs:base_color.connect = </root/_materials/Material/NodeGraphs.outputs:node_out>
float inputs:base_diffuse_roughness = 0
float inputs:base_metalness = 0
float inputs:base_weight = 1
color3f inputs:coat_color = (1, 1, 1)
float inputs:coat_darkening
float inputs:coat_ior = 1.5
float inputs:coat_roughness = 0.03
float inputs:coat_roughness_anisotropy
float inputs:coat_weight = 0
color3f inputs:emission_color = (1, 1, 1)
float inputs:emission_luminance = 0
color3f inputs:fuzz_color = (1, 1, 1)
float inputs:fuzz_roughness = 0.5
float inputs:fuzz_weight = 0
float3 inputs:geometry_coat_normal
float3 inputs:geometry_coat_tangent
float3 inputs:geometry_normal
float inputs:geometry_opacity = 1
float3 inputs:geometry_tangent.connect = </root/_materials/Material/NodeGraphs.outputs:node_004_out>
bool inputs:geometry_thin_walled
color3f inputs:specular_color = (1, 1, 1)
float inputs:specular_ior = 1.45
float inputs:specular_roughness = 0.5
float inputs:specular_roughness_anisotropy = 0
float inputs:specular_weight = 1
color3f inputs:subsurface_color.connect = </root/_materials/Material/NodeGraphs.outputs:node_out>
float inputs:subsurface_radius = 0.05
color3f inputs:subsurface_radius_scale = (1, 0.2, 0.1)
float inputs:subsurface_scatter_anisotropy = 0
float inputs:subsurface_weight = 0
float inputs:thin_film_ior = 1.33
float inputs:thin_film_thickness = 0
float inputs:thin_film_weight = 0
color3f inputs:transmission_color.connect = </root/_materials/Material/NodeGraphs.outputs:node_out>
float inputs:transmission_depth
float inputs:transmission_dispersion_abbe_number
float inputs:transmission_dispersion_scale
color3f inputs:transmission_scatter
float inputs:transmission_scatter_anisotropy
float inputs:transmission_weight = 0
token outputs:surface
}
def NodeGraph "NodeGraphs"
{
float3 outputs:node_004_out.connect = </root/_materials/Material/NodeGraphs/node_004.outputs:out>
color3f outputs:node_out.connect = </root/_materials/Material/NodeGraphs/node.outputs:out>
def Shader "node_texcoord"
{
uniform token info:id = "ND_texcoord_vector2"
float2 outputs:out
}
def Shader "Image_Texture_Color"
{
uniform token info:id = "ND_image_color4"
asset inputs:file = @./textures/texture-cat.jpg@ (
colorSpace = "srgb_texture"
)
string inputs:filtertype = "linear"
float2 inputs:texcoord.connect = </root/_materials/Material/NodeGraphs/node_texcoord.outputs:out>
string inputs:uaddressmode = "periodic"
string inputs:vaddressmode = "periodic"
color4f outputs:out
}
def Shader "node"
{
uniform token info:id = "ND_convert_color4_color3"
color4f inputs:in.connect = </root/_materials/Material/NodeGraphs/Image_Texture_Color.outputs:out>
color3f outputs:out
}
def Shader "node_001"
{
uniform token info:id = "ND_normal_vector3"
string inputs:space = "world"
float3 outputs:out
}
def Shader "node_002"
{
uniform token info:id = "ND_normalize_vector3"
float3 inputs:in.connect = </root/_materials/Material/NodeGraphs/node_001.outputs:out>
float3 outputs:out
}
def Shader "node_003"
{
uniform token info:id = "ND_tangent_vector3"
string inputs:space = "world"
float3 outputs:out
}
def Shader "node_004"
{
uniform token info:id = "ND_normalize_vector3"
float3 inputs:in.connect = </root/_materials/Material/NodeGraphs/node_003.outputs:out>
float3 outputs:out
}
def Shader "node_005"
{
uniform token info:id = "ND_rotate3d_vector3"
float inputs:amount = -90
float3 inputs:axis.connect = </root/_materials/Material/NodeGraphs/node_002.outputs:out>
float3 inputs:in.connect = </root/_materials/Material/NodeGraphs/node_004.outputs:out>
float3 outputs:out
}
def Shader "node_006"
{
uniform token info:id = "ND_normalize_vector3"
float3 inputs:in.connect = </root/_materials/Material/NodeGraphs/node_005.outputs:out>
float3 outputs:out
}
}
}
}
def DomeLight "env_light"
{
float inputs:intensity = 1
asset inputs:texture:file = @.\textures\color_121212.hdr@
float3 xformOp:rotateXYZ = (90, 1.2722219e-14, 90)
uniform token[] xformOpOrder = ["xformOp:rotateXYZ"]
}
}

View File

@@ -4169,7 +4169,7 @@ bool CrateReader::UnpackValueRep(const crate::ValueRep &rep,
if (rep.IsArray()) {
if (rep.GetPayload() == 0) { // empty array
TypedArray<value::double2> empty_v;
std::vector<value::double2> empty_v;
value->Set(std::move(empty_v));
return true;
}
@@ -4195,7 +4195,7 @@ bool CrateReader::UnpackValueRep(const crate::ValueRep &rep,
}
if (n == 0) {
TypedArray<value::double2> empty_v;
std::vector<value::double2> empty_v;
value->Set(std::move(empty_v));
return true;
}
@@ -4206,29 +4206,14 @@ bool CrateReader::UnpackValueRep(const crate::ValueRep &rep,
CHECK_MEMORY_USAGE(n * sizeof(value::double2));
TypedArray<value::double2> v;
if (!rep.IsCompressed() && _config.use_mmap) {
// Use TypedArray view mode - no allocation, just point to mmap'd data
uint64_t current_pos = _sr->tell();
const uint8_t* data_ptr = _sr->data() + current_pos;
// Create a view over the mmap'd data
v = TypedArray<value::double2>(new TypedArrayImpl<value::double2>(const_cast<value::double2*>(reinterpret_cast<const value::double2*>(data_ptr)), static_cast<size_t>(n), true), true);
// Advance stream reader position
if (!_sr->seek_set(current_pos + n * sizeof(value::double2))) {
PUSH_ERROR("Failed to advance stream reader position.");
return false;
}
} else {
// Regular allocation for compressed data or when mmap is disabled
v.resize(static_cast<size_t>(n));
if (!_sr->read(size_t(n) * sizeof(value::double2),
size_t(n) * sizeof(value::double2),
reinterpret_cast<uint8_t *>(v.data()))) {
PUSH_ERROR("Failed to read double2 array.");
return false;
}
std::vector<value::double2> v;
// Always use std::vector - no mmap view mode
v.resize(static_cast<size_t>(n));
if (!_sr->read(size_t(n) * sizeof(value::double2),
size_t(n) * sizeof(value::double2),
reinterpret_cast<uint8_t *>(v.data()))) {
PUSH_ERROR("Failed to read double2 array.");
return false;
}
DCOUT("double2[] = " << value::print_array_snipped(v));
@@ -4256,7 +4241,7 @@ bool CrateReader::UnpackValueRep(const crate::ValueRep &rep,
if (rep.IsArray()) {
if (rep.GetPayload() == 0) { // empty array
TypedArray<value::float2> empty_v;
std::vector<value::float2> empty_v;
value->Set(std::move(empty_v));
return true;
}
@@ -4286,42 +4271,26 @@ bool CrateReader::UnpackValueRep(const crate::ValueRep &rep,
}
if (n == 0) {
TypedArray<value::float2> empty_v;
std::vector<value::float2> empty_v;
value->Set(std::move(empty_v));
return true;
}
CHECK_MEMORY_USAGE(n * sizeof(value::float2));
TypedArray<value::float2> v;
if (!rep.IsCompressed() && _config.use_mmap) {
// Use TypedArray view mode - no allocation, just point to mmap'd data
uint64_t current_pos = _sr->tell();
const uint8_t* data_ptr = _sr->data() + current_pos;
// Create a view over the mmap'd data
v = TypedArray<value::float2>(new TypedArrayImpl<value::float2>(const_cast<value::float2*>(reinterpret_cast<const value::float2*>(data_ptr)), static_cast<size_t>(n), true), true);
// Advance stream reader position
if (!_sr->seek_set(current_pos + n * sizeof(value::float2))) {
PUSH_ERROR("Failed to advance stream reader position.");
return false;
}
} else {
// Regular allocation for compressed data or when mmap is disabled
if (!v.resize(static_cast<size_t>(n))) {
PUSH_ERROR_AND_RETURN_TAG(kTag, "Internal error. failed to resize TypedArray.");
}
if (!_sr->read(size_t(n) * sizeof(value::float2),
size_t(n) * sizeof(value::float2),
reinterpret_cast<uint8_t *>(v.data()))) {
PUSH_ERROR("Failed to read float2 array.");
return false;
}
std::vector<value::float2> v;
// Always use std::vector - no mmap view mode
v.resize(static_cast<size_t>(n));
if (!_sr->read(size_t(n) * sizeof(value::float2),
size_t(n) * sizeof(value::float2),
reinterpret_cast<uint8_t *>(v.data()))) {
PUSH_ERROR("Failed to read float2 array.");
return false;
}
DCOUT("float2[] = " << value::print_array_snipped(v));
//TUSDZ_LOG_D("float2[] = " << value::print_array_snipped(v));
TUSDZ_LOG_I("float2[].size" << v.size());
value->Set(std::move(v));
return true;
@@ -4346,7 +4315,7 @@ bool CrateReader::UnpackValueRep(const crate::ValueRep &rep,
if (rep.IsArray()) {
if (rep.GetPayload() == 0) { // empty array
TypedArray<value::half2> empty_v;
std::vector<value::half2> empty_v;
value->Set(std::move(empty_v));
return true;
}
@@ -4376,29 +4345,14 @@ bool CrateReader::UnpackValueRep(const crate::ValueRep &rep,
CHECK_MEMORY_USAGE(n * sizeof(value::half2));
TypedArray<value::half2> v;
if (!rep.IsCompressed() && _config.use_mmap) {
// Use TypedArray view mode - no allocation, just point to mmap'd data
uint64_t current_pos = _sr->tell();
const uint8_t* data_ptr = _sr->data() + current_pos;
// Create a view over the mmap'd data
v = TypedArray<value::half2>(new TypedArrayImpl<value::half2>(const_cast<value::half2*>(reinterpret_cast<const value::half2*>(data_ptr)), static_cast<size_t>(n), true), true);
// Advance stream reader position
if (!_sr->seek_set(current_pos + n * sizeof(value::half2))) {
PUSH_ERROR("Failed to advance stream reader position.");
return false;
}
} else {
// Regular allocation for compressed data or when mmap is disabled
v.resize(static_cast<size_t>(n));
if (!_sr->read(size_t(n) * sizeof(value::half2),
size_t(n) * sizeof(value::half2),
reinterpret_cast<uint8_t *>(v.data()))) {
PUSH_ERROR("Failed to read half2 array.");
return false;
}
std::vector<value::half2> v;
// Always use std::vector - no mmap view mode
v.resize(static_cast<size_t>(n));
if (!_sr->read(size_t(n) * sizeof(value::half2),
size_t(n) * sizeof(value::half2),
reinterpret_cast<uint8_t *>(v.data()))) {
PUSH_ERROR("Failed to read half2 array.");
return false;
}
DCOUT("half2[] = " << value::print_array_snipped(v));
@@ -4488,7 +4442,7 @@ bool CrateReader::UnpackValueRep(const crate::ValueRep &rep,
if (rep.IsArray()) {
if (rep.GetPayload() == 0) { // empty array
TypedArray<value::double3> empty_v;
std::vector<value::double3> empty_v;
value->Set(std::move(empty_v));
return true;
}
@@ -4519,29 +4473,14 @@ bool CrateReader::UnpackValueRep(const crate::ValueRep &rep,
CHECK_MEMORY_USAGE(n * sizeof(value::double3));
TypedArray<value::double3> v;
if (!rep.IsCompressed() && _config.use_mmap) {
// Use TypedArray view mode - no allocation, just point to mmap'd data
uint64_t current_pos = _sr->tell();
const uint8_t* data_ptr = _sr->data() + current_pos;
// Create a view over the mmap'd data
v = TypedArray<value::double3>(new TypedArrayImpl<value::double3>(const_cast<value::double3*>(reinterpret_cast<const value::double3*>(data_ptr)), static_cast<size_t>(n), true), true);
// Advance stream reader position
if (!_sr->seek_set(current_pos + n * sizeof(value::double3))) {
PUSH_ERROR("Failed to advance stream reader position.");
return false;
}
} else {
// Regular allocation for compressed data or when mmap is disabled
v.resize(static_cast<size_t>(n));
if (!_sr->read(size_t(n) * sizeof(value::double3),
size_t(n) * sizeof(value::double3),
reinterpret_cast<uint8_t *>(v.data()))) {
PUSH_ERROR("Failed to read double3 array.");
return false;
}
std::vector<value::double3> v;
// Always use std::vector - no mmap view mode
v.resize(static_cast<size_t>(n));
if (!_sr->read(size_t(n) * sizeof(value::double3),
size_t(n) * sizeof(value::double3),
reinterpret_cast<uint8_t *>(v.data()))) {
PUSH_ERROR("Failed to read double3 array.");
return false;
}
DCOUT("double3[] = " << value::print_array_snipped(v));
@@ -4992,7 +4931,7 @@ bool CrateReader::UnpackValueRep(const crate::ValueRep &rep,
if (rep.IsArray()) {
if (rep.GetPayload() == 0) { // empty array
TypedArray<value::half4> empty_v;
std::vector<value::half4> empty_v;
value->Set(std::move(empty_v));
return true;
}
@@ -5022,29 +4961,14 @@ bool CrateReader::UnpackValueRep(const crate::ValueRep &rep,
CHECK_MEMORY_USAGE(n * sizeof(value::half4));
TypedArray<value::half4> v;
if (!rep.IsCompressed() && _config.use_mmap) {
// Use TypedArray view mode - no allocation, just point to mmap'd data
uint64_t current_pos = _sr->tell();
const uint8_t* data_ptr = _sr->data() + current_pos;
// Create a view over the mmap'd data
v = TypedArray<value::half4>(new TypedArrayImpl<value::half4>(const_cast<value::half4*>(reinterpret_cast<const value::half4*>(data_ptr)), static_cast<size_t>(n), true), true);
// Advance stream reader position
if (!_sr->seek_set(current_pos + n * sizeof(value::half4))) {
PUSH_ERROR("Failed to advance stream reader position.");
return false;
}
} else {
// Regular allocation for compressed data or when mmap is disabled
v.resize(static_cast<size_t>(n));
if (!_sr->read(size_t(n) * sizeof(value::half4),
size_t(n) * sizeof(value::half4),
reinterpret_cast<uint8_t *>(v.data()))) {
PUSH_ERROR("Failed to read half4 array.");
return false;
}
std::vector<value::half4> v;
// Always use std::vector - no mmap view mode
v.resize(static_cast<size_t>(n));
if (!_sr->read(size_t(n) * sizeof(value::half4),
size_t(n) * sizeof(value::half4),
reinterpret_cast<uint8_t *>(v.data()))) {
PUSH_ERROR("Failed to read half4 array.");
return false;
}
DCOUT("half4[] = " << value::print_array_snipped(v));

View File

@@ -35,6 +35,9 @@
# pragma clang diagnostic ignored "-Wunused-label"
# pragma clang diagnostic ignored "-Wunused-macros"
# pragma clang diagnostic ignored "-Wunused-variable"
# pragma clang diagnostic ignored "-Wreserved-macro-identifier"
# pragma clang diagnostic ignored "-Wold-style-cast"
# pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant"
#elif defined(__GNUC__)
# pragma GCC diagnostic ignored "-Wunused-function"
# pragma GCC diagnostic ignored "-Wunused-parameter"

View File

@@ -1401,7 +1401,9 @@ std::string print_prop(const Property &prop, const std::string &prop_name,
// timeSamples and connect cannot have attrMeta
//
if (attr.metas().authored() || attr.has_value()) {
// Print attribute if it has metadata, has a value, OR is just typed
// NOTE: Some attributes (like outputs:out) may be typed but not have a value
if (attr.metas().authored() || attr.has_value() || !attr.type_name().empty()) {
ss << pprint::Indent(indent);
@@ -4065,6 +4067,86 @@ static std::string print_shader_params(const UsdUVTexture &shader,
return ss.str();
}
static std::string print_shader_params(const MtlxOpenPBRSurface &shader,
const uint32_t indent) {
std::stringstream ss;
// Base properties
ss << print_typed_attr(shader.base_weight, "inputs:base_weight", indent);
ss << print_typed_attr(shader.base_color, "inputs:base_color", indent);
ss << print_typed_attr(shader.base_metalness, "inputs:base_metalness", indent);
ss << print_typed_attr(shader.base_diffuse_roughness, "inputs:base_diffuse_roughness", indent);
// Specular properties
ss << print_typed_attr(shader.specular_weight, "inputs:specular_weight", indent);
ss << print_typed_attr(shader.specular_color, "inputs:specular_color", indent);
ss << print_typed_attr(shader.specular_roughness, "inputs:specular_roughness", indent);
ss << print_typed_attr(shader.specular_ior, "inputs:specular_ior", indent);
ss << print_typed_attr(shader.specular_anisotropy, "inputs:specular_anisotropy", indent);
ss << print_typed_attr(shader.specular_rotation, "inputs:specular_rotation", indent);
ss << print_typed_attr(shader.specular_roughness_anisotropy, "inputs:specular_roughness_anisotropy", indent);
// Transmission properties
ss << print_typed_attr(shader.transmission_weight, "inputs:transmission_weight", indent);
ss << print_typed_attr(shader.transmission_color, "inputs:transmission_color", indent);
ss << print_typed_attr(shader.transmission_depth, "inputs:transmission_depth", indent);
ss << print_typed_attr(shader.transmission_scatter, "inputs:transmission_scatter", indent);
ss << print_typed_attr(shader.transmission_scatter_anisotropy, "inputs:transmission_scatter_anisotropy", indent);
ss << print_typed_attr(shader.transmission_dispersion, "inputs:transmission_dispersion", indent);
ss << print_typed_attr(shader.transmission_dispersion_abbe_number, "inputs:transmission_dispersion_abbe_number", indent);
ss << print_typed_attr(shader.transmission_dispersion_scale, "inputs:transmission_dispersion_scale", indent);
// Subsurface properties
ss << print_typed_attr(shader.subsurface_weight, "inputs:subsurface_weight", indent);
ss << print_typed_attr(shader.subsurface_color, "inputs:subsurface_color", indent);
ss << print_typed_attr(shader.subsurface_radius, "inputs:subsurface_radius", indent);
ss << print_typed_attr(shader.subsurface_radius_scale, "inputs:subsurface_radius_scale", indent);
ss << print_typed_attr(shader.subsurface_scale, "inputs:subsurface_scale", indent);
ss << print_typed_attr(shader.subsurface_anisotropy, "inputs:subsurface_anisotropy", indent);
ss << print_typed_attr(shader.subsurface_scatter_anisotropy, "inputs:subsurface_scatter_anisotropy", indent);
// Coat properties
ss << print_typed_attr(shader.coat_weight, "inputs:coat_weight", indent);
ss << print_typed_attr(shader.coat_color, "inputs:coat_color", indent);
ss << print_typed_attr(shader.coat_roughness, "inputs:coat_roughness", indent);
ss << print_typed_attr(shader.coat_anisotropy, "inputs:coat_anisotropy", indent);
ss << print_typed_attr(shader.coat_rotation, "inputs:coat_rotation", indent);
ss << print_typed_attr(shader.coat_roughness_anisotropy, "inputs:coat_roughness_anisotropy", indent);
ss << print_typed_attr(shader.coat_ior, "inputs:coat_ior", indent);
ss << print_typed_attr(shader.coat_darkening, "inputs:coat_darkening", indent);
ss << print_typed_attr(shader.coat_affect_color, "inputs:coat_affect_color", indent);
ss << print_typed_attr(shader.coat_affect_roughness, "inputs:coat_affect_roughness", indent);
// Fuzz properties
ss << print_typed_attr(shader.fuzz_weight, "inputs:fuzz_weight", indent);
ss << print_typed_attr(shader.fuzz_color, "inputs:fuzz_color", indent);
ss << print_typed_attr(shader.fuzz_roughness, "inputs:fuzz_roughness", indent);
// Thin film properties
ss << print_typed_attr(shader.thin_film_thickness, "inputs:thin_film_thickness", indent);
ss << print_typed_attr(shader.thin_film_ior, "inputs:thin_film_ior", indent);
ss << print_typed_attr(shader.thin_film_weight, "inputs:thin_film_weight", indent);
// Emission properties
ss << print_typed_attr(shader.emission_luminance, "inputs:emission_luminance", indent);
ss << print_typed_attr(shader.emission_color, "inputs:emission_color", indent);
// Geometry properties
ss << print_typed_attr(shader.geometry_opacity, "inputs:geometry_opacity", indent);
ss << print_typed_attr(shader.geometry_thin_walled, "inputs:geometry_thin_walled", indent);
ss << print_typed_attr(shader.geometry_normal, "inputs:geometry_normal", indent);
ss << print_typed_attr(shader.geometry_tangent, "inputs:geometry_tangent", indent);
ss << print_typed_attr(shader.geometry_coat_normal, "inputs:geometry_coat_normal", indent);
ss << print_typed_attr(shader.geometry_coat_tangent, "inputs:geometry_coat_tangent", indent);
// Output
ss << print_typed_terminal_attr(shader.surface, "outputs:surface", indent);
ss << print_common_shader_params(shader, indent);
return ss.str();
}
std::string to_string(const Shader &shader, const uint32_t indent,
bool closing_brace) {
// generic Shader class
@@ -4111,7 +4193,7 @@ std::string to_string(const Shader &shader, const uint32_t indent,
ss << print_shader_params(pvs.value(), indent + 1);
} else if (auto mtlx_opbr = shader.value.get_value<MtlxOpenPBRSurface>()) {
// Blender v4.5 MaterialX OpenPBR Surface
ss << print_common_shader_params(mtlx_opbr.value(), indent + 1);
ss << print_shader_params(mtlx_opbr.value(), indent + 1);
} else if (auto pvsn = shader.value.get_value<ShaderNode>()) {
// Generic ShaderNode
ss << print_common_shader_params(pvsn.value(), indent + 1);

View File

@@ -76,11 +76,20 @@ std::string serializeOpenPBRToJson(const OpenPBRSurfaceShader& shader, const Ren
case tydra::ColorSpace::Custom: return "custom";
case tydra::ColorSpace::Unknown: return "unknown";
}
return "unknown"; // Should never reach here
return "unknown"; // fallback for any unhandled case
};
// Macro to serialize shader parameter with optional texture info
auto serializeParam = [&](const std::string& paramName, auto param) {
// Helper functions to serialize values based on type
auto serializeValue = [&](const ShaderParam<float>& param) {
json << param.value;
};
auto serializeVec3Value = [&](const ShaderParam<vec3>& param) {
json << vec3ToJson(param.value);
};
// Generic lambda to serialize shader parameters
auto serializeFloatParam = [&](const std::string& paramName, const ShaderParam<float>& param) {
json << "\"" << paramName << "\": {";
json << "\"name\": \"" << paramName << "\",";
@@ -111,70 +120,106 @@ std::string serializeOpenPBRToJson(const OpenPBRSurfaceShader& shader, const Ren
} else {
// Scalar parameter
json << "\"type\": \"value\", \"value\": ";
serializeValue(json, param.value);
serializeValue(param);
}
json << "}";
};
auto serializeVec3Param = [&](const std::string& paramName, const ShaderParam<vec3>& param) {
json << "\"" << paramName << "\": {";
json << "\"name\": \"" << paramName << "\",";
if (param.is_texture() && renderScene) {
// Texture parameter
int texId = param.texture_id;
json << "\"type\": \"texture\",";
json << "\"textureId\": " << texId;
// Look up texture image information
if (texId >= 0 && static_cast<size_t>(texId) < renderScene->textures.size()) {
const auto& uvTexture = renderScene->textures[static_cast<size_t>(texId)];
if (uvTexture.texture_image_id >= 0 && static_cast<size_t>(uvTexture.texture_image_id) < renderScene->images.size()) {
const auto& image = renderScene->images[static_cast<size_t>(uvTexture.texture_image_id)];
json << ", \"assetIdentifier\": \"" << image.asset_identifier << "\"";
// Include texture metadata
json << ", \"texture\": {";
json << "\"width\": " << image.width << ",";
json << "\"height\": " << image.height << ",";
json << "\"channels\": " << image.channels << ",";
json << "\"colorSpace\": \"" << colorSpaceToString(image.colorSpace) << "\",";
json << "\"usdColorSpace\": \"" << colorSpaceToString(image.usdColorSpace) << "\",";
json << "\"decoded\": " << (image.decoded ? "true" : "false");
json << "}";
}
}
} else {
// Scalar parameter
json << "\"type\": \"value\", \"value\": ";
serializeVec3Value(param);
}
json << "}";
};
// Base layer
json << "\"base\": {";
serializeParam("base_weight", shader.base_weight); json << ",";
serializeParam("base_color", shader.base_color); json << ",";
serializeParam("base_roughness", shader.base_roughness); json << ",";
serializeParam("base_metalness", shader.base_metalness);
serializeFloatParam("base_weight", shader.base_weight); json << ",";
serializeVec3Param("base_color", shader.base_color); json << ",";
serializeFloatParam("base_roughness", shader.base_roughness); json << ",";
serializeFloatParam("base_metalness", shader.base_metalness);
json << "},";
// Specular layer
json << "\"specular\": {";
serializeParam("specular_weight", shader.specular_weight); json << ",";
serializeParam("specular_color", shader.specular_color); json << ",";
serializeParam("specular_roughness", shader.specular_roughness); json << ",";
serializeParam("specular_ior", shader.specular_ior); json << ",";
serializeParam("specular_anisotropy", shader.specular_anisotropy); json << ",";
serializeParam("specular_rotation", shader.specular_rotation);
serializeFloatParam("specular_weight", shader.specular_weight); json << ",";
serializeVec3Param("specular_color", shader.specular_color); json << ",";
serializeFloatParam("specular_roughness", shader.specular_roughness); json << ",";
serializeFloatParam("specular_ior", shader.specular_ior); json << ",";
serializeFloatParam("specular_anisotropy", shader.specular_anisotropy); json << ",";
serializeFloatParam("specular_rotation", shader.specular_rotation);
json << "},";
// Transmission layer
json << "\"transmission\": {";
serializeParam("transmission_weight", shader.transmission_weight); json << ",";
serializeParam("transmission_color", shader.transmission_color); json << ",";
serializeParam("transmission_depth", shader.transmission_depth); json << ",";
serializeParam("transmission_scatter", shader.transmission_scatter); json << ",";
serializeParam("transmission_scatter_anisotropy", shader.transmission_scatter_anisotropy); json << ",";
serializeParam("transmission_dispersion", shader.transmission_dispersion);
serializeFloatParam("transmission_weight", shader.transmission_weight); json << ",";
serializeVec3Param("transmission_color", shader.transmission_color); json << ",";
serializeFloatParam("transmission_depth", shader.transmission_depth); json << ",";
serializeVec3Param("transmission_scatter", shader.transmission_scatter); json << ",";
serializeFloatParam("transmission_scatter_anisotropy", shader.transmission_scatter_anisotropy); json << ",";
serializeFloatParam("transmission_dispersion", shader.transmission_dispersion);
json << "},";
// Subsurface layer
json << "\"subsurface\": {";
serializeParam("subsurface_weight", shader.subsurface_weight); json << ",";
serializeParam("subsurface_color", shader.subsurface_color); json << ",";
serializeParam("subsurface_scale", shader.subsurface_scale); json << ",";
serializeParam("subsurface_anisotropy", shader.subsurface_anisotropy);
serializeFloatParam("subsurface_weight", shader.subsurface_weight); json << ",";
serializeVec3Param("subsurface_color", shader.subsurface_color); json << ",";
serializeFloatParam("subsurface_scale", shader.subsurface_scale); json << ",";
serializeFloatParam("subsurface_anisotropy", shader.subsurface_anisotropy);
json << "},";
// Coat layer
json << "\"coat\": {";
serializeParam("coat_weight", shader.coat_weight); json << ",";
serializeParam("coat_color", shader.coat_color); json << ",";
serializeParam("coat_roughness", shader.coat_roughness); json << ",";
serializeParam("coat_anisotropy", shader.coat_anisotropy); json << ",";
serializeParam("coat_rotation", shader.coat_rotation); json << ",";
serializeParam("coat_ior", shader.coat_ior); json << ",";
serializeParam("coat_affect_color", shader.coat_affect_color); json << ",";
serializeParam("coat_affect_roughness", shader.coat_affect_roughness);
serializeFloatParam("coat_weight", shader.coat_weight); json << ",";
serializeVec3Param("coat_color", shader.coat_color); json << ",";
serializeFloatParam("coat_roughness", shader.coat_roughness); json << ",";
serializeFloatParam("coat_anisotropy", shader.coat_anisotropy); json << ",";
serializeFloatParam("coat_rotation", shader.coat_rotation); json << ",";
serializeFloatParam("coat_ior", shader.coat_ior); json << ",";
serializeVec3Param("coat_affect_color", shader.coat_affect_color); json << ",";
serializeFloatParam("coat_affect_roughness", shader.coat_affect_roughness);
json << "},";
// Emission
json << "\"emission\": {";
serializeParam("emission_luminance", shader.emission_luminance); json << ",";
serializeParam("emission_color", shader.emission_color);
serializeFloatParam("emission_luminance", shader.emission_luminance); json << ",";
serializeVec3Param("emission_color", shader.emission_color);
json << "},";
// Geometry
json << "\"geometry\": {";
serializeParam("opacity", shader.opacity); json << ",";
serializeParam("normal", shader.normal); json << ",";
serializeParam("tangent", shader.tangent);
serializeFloatParam("opacity", shader.opacity); json << ",";
serializeVec3Param("normal", shader.normal); json << ",";
serializeVec3Param("tangent", shader.tangent);
json << "}";
json << "}";

View File

@@ -5122,6 +5122,311 @@ nonstd::expected<bool, std::string> GetConnectedUVTexture(
prim->prim_type_name()));
}
// Helper function to find ND_image_color4 texture nodes in a MaterialX NodeGraph
// by traversing connections from the given output
template <typename T>
nonstd::expected<bool, std::string> GetConnectedMtlxTexture(
const Stage &stage, const TypedAnimatableAttributeWithFallback<T> &src,
Path *tex_abs_path, const Shader **image_shader_out,
std::string *st_varname_out, const AssetInfo **assetInfo_out) {
if (!src.is_connection()) {
return nonstd::make_unexpected("Attribute must be connection.\n");
}
if (src.get_connections().size() != 1) {
return nonstd::make_unexpected(
"Attribute connections must be single connection Path.\n");
}
const Path &path = src.get_connections()[0];
const std::string prim_part = path.prim_part();
const std::string prop_part = path.prop_part();
DCOUT("Checking MaterialX connection: " << path.full_path_name());
DCOUT(" prim_part: " << prim_part);
DCOUT(" prop_part: " << prop_part);
// The prim_part should be the NodeGraph path itself
// For </root/_materials/Material/NodeGraphs.outputs:node_out>,
// prim_part = "/root/_materials/Material/NodeGraphs"
// First, try to find via stage lookup
const Prim *ng_prim{nullptr};
std::string err;
bool found_in_stage = stage.find_prim_at_path(Path(prim_part, ""), ng_prim, &err);
// If not found in stage lookup, try to navigate through Material's children
if (!found_in_stage || !ng_prim) {
DCOUT("Prim not found in stage lookup, trying Material children approach");
// Extract Material path - it should be everything before the last element
size_t last_slash = prim_part.rfind('/');
if (last_slash == std::string::npos) {
return nonstd::make_unexpected(
fmt::format("Invalid NodeGraph path structure: {}\n", prim_part));
}
std::string material_path = prim_part.substr(0, last_slash);
std::string nodegraph_name = prim_part.substr(last_slash + 1);
DCOUT("Looking for Material at: " << material_path);
DCOUT("NodeGraph name: " << nodegraph_name);
// Find the Material
const Prim *mat_prim{nullptr};
if (!stage.find_prim_at_path(Path(material_path, ""), mat_prim, &err)) {
return nonstd::make_unexpected(
fmt::format("Material {} not found: {}\n", material_path, err));
}
// Look for NodeGraph child
if (mat_prim) {
std::string children_info = "Material has " + std::to_string(mat_prim->children().size()) + " children: ";
for (const auto& child : mat_prim->children()) {
std::string child_name = child.element_name();
std::string child_type = child.data().type_name();
children_info += "'" + child_name + "'(" + child_type + ") ";
// Check if this is a NodeGraph (by type, since name might be empty)
if (child_type == "NodeGraph") {
// If the child has no name but is the right type, use it
// This handles the case where the NodeGraph doesn't have element_name set
ng_prim = &child;
break;
} else if (child_name == nodegraph_name) {
// Also check by exact name match
ng_prim = &child;
break;
}
}
if (!ng_prim) {
return nonstd::make_unexpected(
fmt::format("NodeGraph '{}' not found. {}\n", nodegraph_name, children_info));
}
} else {
return nonstd::make_unexpected(
fmt::format("Material prim is null\n"));
}
}
DCOUT("Found prim: " << prim_part << ", type: " << (ng_prim ? ng_prim->data().type_name() : "null"));
const NodeGraph *ng = ng_prim ? ng_prim->as<NodeGraph>() : nullptr;
if (!ng) {
// Debug output to understand why it's not a NodeGraph
if (ng_prim) {
return nonstd::make_unexpected(
fmt::format("{} is not a NodeGraph, prim_type: {}\n", prim_part, ng_prim->data().type_name()));
}
return nonstd::make_unexpected(
fmt::format("{} is not a NodeGraph\n", prim_part));
}
// Find the output connection we're looking for
// The prop_part should be like "outputs:node_out"
std::string output_name = prop_part;
if (startsWith(output_name, "outputs:")) {
output_name = output_name.substr(8); // Remove "outputs:" prefix
}
// Look for the connection in props
// Try both with and without ".connect" suffix
std::string conn_prop_name = "outputs:" + output_name + ".connect";
auto it = ng->props.find(conn_prop_name);
if (it == ng->props.end()) {
// Try without .connect suffix
conn_prop_name = "outputs:" + output_name;
it = ng->props.find(conn_prop_name);
if (it == ng->props.end()) {
// List available props for debugging
std::string available_props = "Available props: ";
for (const auto& prop : ng->props) {
available_props += prop.first + " ";
}
return nonstd::make_unexpected(
fmt::format("Output connection '{}' not found in NodeGraph. {}\n",
conn_prop_name, available_props));
}
}
// NodeGraph outputs can be stored as attributes or relationships
Path current_path;
bool found_connection = false;
if (it->second.is_attribute()) {
// It's an attribute - look for connections on the attribute
const Attribute &attr = it->second.get_attribute();
if (attr.has_connections() && !attr.connections().empty()) {
current_path = attr.connections()[0];
found_connection = true;
}
} else if (it->second.is_relationship()) {
// Also support relationship format
auto targets = it->second.get_relationTargets();
if (!targets.empty()) {
current_path = targets[0];
found_connection = true;
}
}
if (!found_connection) {
return nonstd::make_unexpected(
fmt::format("Output {} has no connection targets\n", conn_prop_name));
}
const Shader *image_shader = nullptr;
// Traverse the node connections to find ND_image_color4
// Maximum depth to prevent infinite loops
int max_depth = 10;
std::string traversal_log = "Traversal: ";
while (max_depth-- > 0) {
std::string current_prim_part = current_path.prim_part();
const Prim *current_prim{nullptr};
// First, try regular stage lookup
bool found_in_stage = stage.find_prim_at_path(Path(current_prim_part, ""), current_prim, &err);
// If not found and this is under a NodeGraph, look in NodeGraph children
if (!found_in_stage || !current_prim) {
// Check if this path is under the NodeGraph we found earlier
size_t last_slash = current_prim_part.rfind('/');
if (last_slash != std::string::npos) {
std::string parent_path = current_prim_part.substr(0, last_slash);
std::string child_name = current_prim_part.substr(last_slash + 1);
// Check if parent is our NodeGraph
if (ng_prim && parent_path.find("NodeGraphs") != std::string::npos) {
// Look for the child in the NodeGraph prim
for (const auto& child : ng_prim->children()) {
if (child.element_name() == child_name) {
current_prim = &child;
break;
}
}
}
}
if (!current_prim) {
return nonstd::make_unexpected(
fmt::format("Shader {} not found\n", current_prim_part));
}
}
const Shader *current_shader = current_prim ? current_prim->as<Shader>() : nullptr;
if (!current_shader) {
return nonstd::make_unexpected(
fmt::format("{} is not a Shader. {}\n", current_prim_part, traversal_log));
}
// Log this node
traversal_log += current_shader->info_id + " -> ";
// Check if this is an ND_image_color4 node
if (current_shader->info_id == "ND_image_color4" ||
current_shader->info_id == "ND_image_color3") {
image_shader = current_shader;
if (tex_abs_path) {
*tex_abs_path = current_path;
}
if (image_shader_out) {
*image_shader_out = image_shader;
}
if (assetInfo_out) {
// get_assetInfo returns AssetInfo converted from customData/assetInfo
bool authored = false;
const AssetInfo &info = current_shader->metas().get_assetInfo(&authored);
if (authored) {
*assetInfo_out = &info;
}
}
// For MaterialX, we don't have an explicit st varname,
// so we'll use "st" as default (same as UsdPreviewSurface)
if (st_varname_out) {
*st_varname_out = "st";
}
return true;
}
// Check if this node has an input connection we should follow
// For ND_convert_color4_color3, follow inputs:in
bool found_next = false;
DCOUT("Checking shader " << current_shader->info_id << " at " << current_prim_part);
// Debug: log all properties from both Shader and ShaderNode
std::string props_list = "ShaderProps: ";
for (const auto& prop : current_shader->props) {
props_list += prop.first + " ";
}
// Check if the shader has a ShaderNode value with properties
const ShaderNode *shader_node = current_shader->value.as<ShaderNode>();
if (shader_node && !shader_node->props.empty()) {
props_list += " NodeProps: ";
for (const auto& prop : shader_node->props) {
props_list += prop.first + " ";
}
}
traversal_log += "[" + props_list + "] ";
// Helper lambda to check for connections in a property map
auto find_connection = [&](const std::map<std::string, Property>& props_map) -> bool {
for (const auto& prop : props_map) {
if (startsWith(prop.first, "inputs:")) {
bool is_connection = false;
Path next_path;
if (endsWith(prop.first, ".connect")) {
// Explicit .connect suffix
is_connection = true;
if (prop.second.is_relationship()) {
auto next_targets = prop.second.get_relationTargets();
if (!next_targets.empty()) {
next_path = next_targets[0];
}
}
} else if (prop.second.is_attribute()) {
// Check if attribute has connections
const Attribute &attr = prop.second.get_attribute();
if (attr.has_connections() && !attr.connections().empty()) {
is_connection = true;
next_path = attr.connections()[0];
}
}
if (is_connection && !next_path.full_path_name().empty()) {
DCOUT(" Following connection from " << prop.first << " to " << next_path);
current_path = next_path;
return true;
}
}
}
return false;
};
// Try shader_node->props first, then fall back to current_shader->props
if (shader_node && !shader_node->props.empty()) {
found_next = find_connection(shader_node->props);
}
if (!found_next) {
found_next = find_connection(current_shader->props);
}
if (!found_next) {
break;
}
}
return nonstd::make_unexpected(
fmt::format("No ND_image_color4 texture node found. {}\n", traversal_log));
}
static bool RawAssetRead(
const value::AssetPath &assetPath, const AssetInfo &assetInfo,
const AssetResolutionResolver &assetResolver,
@@ -5764,7 +6069,8 @@ template <typename T, typename Dty>
bool RenderSceneConverter::ConvertPreviewSurfaceShaderParam(
const RenderSceneConverterEnv &env, const Path &shader_abs_path,
const TypedAttributeWithFallback<Animatable<T>> &param,
const std::string &param_name, ShaderParam<Dty> &dst_param) {
const std::string &param_name, ShaderParam<Dty> &dst_param,
bool is_materialx) {
if (!param.authored()) {
return true;
}
@@ -5774,6 +6080,139 @@ bool RenderSceneConverter::ConvertPreviewSurfaceShaderParam(
} else if (param.is_connection()) {
DCOUT(fmt::format("{} is attribute connection.", param_name));
// Check if this is a MaterialX connection to a NodeGraph
if (is_materialx && param.get_connections().size() == 1) {
const Path &conn_path = param.get_connections()[0];
if (conn_path.prim_part().find("/NodeGraphs") != std::string::npos) {
// This is a MaterialX NodeGraph connection, traverse to find texture
const Shader *image_shader{nullptr};
Path texPath;
std::string st_varname;
const AssetInfo *assetInfo{nullptr};
auto mtlx_result = GetConnectedMtlxTexture(
env.stage, param, &texPath, &image_shader, &st_varname, &assetInfo);
if (mtlx_result) {
// Found a MaterialX texture node
DCOUT("Found MaterialX texture node: " << texPath);
// Extract the file path from the image shader
value::AssetPath texAssetPath;
bool found_file = false;
// Helper lambda to find file input in a property map
auto find_file_input = [&](const std::map<std::string, Property>& props_map) -> bool {
for (const auto& prop : props_map) {
if (prop.first == "inputs:file" && prop.second.is_attribute()) {
const Attribute &attr = prop.second.get_attribute();
if (attr.has_value()) {
auto asset_val = attr.get_value<value::AssetPath>();
if (asset_val) {
texAssetPath = *asset_val;
return true;
}
}
}
}
return false;
};
// Check both ShaderNode props and Shader props
const ShaderNode *shader_node = image_shader->value.as<ShaderNode>();
if (shader_node && !shader_node->props.empty()) {
found_file = find_file_input(shader_node->props);
}
if (!found_file) {
found_file = find_file_input(image_shader->props);
}
if (!found_file) {
PUSH_WARN(fmt::format("MaterialX image node {} has no file input", texPath.prim_part()));
return true;
}
// Create a synthetic UsdUVTexture to pass to ConvertUVTexture
UsdUVTexture synth_tex;
synth_tex.file.set_value(texAssetPath);
// Helper lambda to extract wrap mode from properties
auto extract_wrap_modes = [&](const std::map<std::string, Property>& props_map) {
for (const auto& prop : props_map) {
if (prop.first == "inputs:uaddressmode" && prop.second.is_attribute()) {
const Attribute &attr = prop.second.get_attribute();
if (attr.has_value()) {
auto val = attr.get_value<std::string>();
if (val) {
if (*val == "periodic") {
synth_tex.wrapS.set_value(UsdUVTexture::Wrap::Repeat);
} else if (*val == "clamp") {
synth_tex.wrapS.set_value(UsdUVTexture::Wrap::Clamp);
}
}
}
}
if (prop.first == "inputs:vaddressmode" && prop.second.is_attribute()) {
const Attribute &attr = prop.second.get_attribute();
if (attr.has_value()) {
auto val = attr.get_value<std::string>();
if (val) {
if (*val == "periodic") {
synth_tex.wrapT.set_value(UsdUVTexture::Wrap::Repeat);
} else if (*val == "clamp") {
synth_tex.wrapT.set_value(UsdUVTexture::Wrap::Clamp);
}
}
}
}
}
};
// Map MaterialX wrap modes to USD - check both ShaderNode and Shader props
if (shader_node && !shader_node->props.empty()) {
extract_wrap_modes(shader_node->props);
}
extract_wrap_modes(image_shader->props);
// Use ConvertUVTexture to properly handle the texture
UVTexture rtex;
AssetInfo mtlx_assetInfo; // Use the assetInfo if available
if (assetInfo) {
mtlx_assetInfo = *assetInfo;
}
// Handle colorSpace from attribute metadata if available
// AssetInfo doesn't have set_string, so we'll need to handle this differently
// For now, just use the assetInfo as-is
if (!ConvertUVTexture(env, texPath, mtlx_assetInfo, synth_tex, &rtex)) {
PUSH_ERROR_AND_RETURN(fmt::format(
"Failed to convert MaterialX texture for {}", param_name));
}
// Set the connected output channel and UV primvar name
rtex.connectedOutputChannel = tydra::UVTexture::Channel::RGB;
rtex.varname_uv = st_varname;
uint64_t texId = textures.size();
textures.push_back(rtex);
textureMap.add(texId, shader_abs_path.prim_part() + "." + param_name);
DCOUT(fmt::format("MaterialX TexId {}.{} = {}",
shader_abs_path.prim_part(), param_name, texId));
dst_param.texture_id = int32_t(texId);
return true;
} else {
PUSH_WARN(fmt::format("Failed to find MaterialX texture for {}: {}",
param_name, mtlx_result.error()));
}
}
}
// Fall back to standard UsdUVTexture handling
const UsdUVTexture *ptex{nullptr};
const Shader *pshader{nullptr};
Path texPath;
@@ -5958,22 +6397,26 @@ bool RenderSceneConverter::ConvertOpenPBRSurfaceShader(
// Convert base layer parameters
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.base_weight, "base_weight",
rshader.base_weight)) {
rshader.base_weight, true)) {
PushWarn(fmt::format("Failed to convert base_weight parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.base_color, "base_color",
rshader.base_color)) {
rshader.base_color, true)) {
PushWarn(fmt::format("Failed to convert base_color parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.base_roughness, "base_roughness",
rshader.base_roughness)) {
PushWarn(fmt::format("Failed to convert base_roughness parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.base_metalness, "base_metalness",
rshader.base_metalness)) {
PushWarn(fmt::format("Failed to convert base_metalness parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
@@ -5981,36 +6424,43 @@ bool RenderSceneConverter::ConvertOpenPBRSurfaceShader(
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.specular_weight, "specular_weight",
rshader.specular_weight)) {
PushWarn(fmt::format("Failed to convert specular_weight parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.specular_color, "specular_color",
rshader.specular_color)) {
PushWarn(fmt::format("Failed to convert specular_color parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.specular_roughness, "specular_roughness",
rshader.specular_roughness)) {
PushWarn(fmt::format("Failed to convert specular_roughness parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.specular_ior, "specular_ior",
rshader.specular_ior)) {
PushWarn(fmt::format("Failed to convert specular_ior parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.specular_ior_level, "specular_ior_level",
rshader.specular_ior_level)) {
PushWarn(fmt::format("Failed to convert specular_ior_level parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.specular_anisotropy, "specular_anisotropy",
rshader.specular_anisotropy)) {
PushWarn(fmt::format("Failed to convert specular_anisotropy parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.specular_rotation, "specular_rotation",
rshader.specular_rotation)) {
PushWarn(fmt::format("Failed to convert specular_rotation parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
@@ -6018,31 +6468,37 @@ bool RenderSceneConverter::ConvertOpenPBRSurfaceShader(
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.transmission_weight, "transmission_weight",
rshader.transmission_weight)) {
PushWarn(fmt::format("Failed to convert transmission_weight parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.transmission_color, "transmission_color",
rshader.transmission_color)) {
rshader.transmission_color, true)) {
PushWarn(fmt::format("Failed to convert transmission_color parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.transmission_depth, "transmission_depth",
rshader.transmission_depth)) {
PushWarn(fmt::format("Failed to convert transmission_depth parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.transmission_scatter, "transmission_scatter",
rshader.transmission_scatter)) {
PushWarn(fmt::format("Failed to convert transmission_scatter parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.transmission_scatter_anisotropy,
"transmission_scatter_anisotropy", rshader.transmission_scatter_anisotropy)) {
PushWarn(fmt::format("Failed to convert transmission_scatter_anisotropy parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.transmission_dispersion,
"transmission_dispersion", rshader.transmission_dispersion)) {
PushWarn(fmt::format("Failed to convert transmission_dispersion parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
@@ -6050,26 +6506,31 @@ bool RenderSceneConverter::ConvertOpenPBRSurfaceShader(
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.subsurface_weight, "subsurface_weight",
rshader.subsurface_weight)) {
PushWarn(fmt::format("Failed to convert subsurface_weight parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.subsurface_color, "subsurface_color",
rshader.subsurface_color)) {
rshader.subsurface_color, true)) {
PushWarn(fmt::format("Failed to convert subsurface_color parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.subsurface_radius, "subsurface_radius",
rshader.subsurface_radius)) {
PushWarn(fmt::format("Failed to convert subsurface_radius parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.subsurface_scale, "subsurface_scale",
rshader.subsurface_scale)) {
PushWarn(fmt::format("Failed to convert subsurface_scale parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.subsurface_anisotropy,
"subsurface_anisotropy", rshader.subsurface_anisotropy)) {
PushWarn(fmt::format("Failed to convert subsurface_anisotropy parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
@@ -6077,16 +6538,19 @@ bool RenderSceneConverter::ConvertOpenPBRSurfaceShader(
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.sheen_weight, "sheen_weight",
rshader.sheen_weight)) {
PushWarn(fmt::format("Failed to convert sheen_weight parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.sheen_color, "sheen_color",
rshader.sheen_color)) {
PushWarn(fmt::format("Failed to convert sheen_color parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.sheen_roughness, "sheen_roughness",
rshader.sheen_roughness)) {
PushWarn(fmt::format("Failed to convert sheen_roughness parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
@@ -6094,41 +6558,49 @@ bool RenderSceneConverter::ConvertOpenPBRSurfaceShader(
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.coat_weight, "coat_weight",
rshader.coat_weight)) {
PushWarn(fmt::format("Failed to convert coat_weight parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.coat_color, "coat_color",
rshader.coat_color)) {
PushWarn(fmt::format("Failed to convert coat_color parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.coat_roughness, "coat_roughness",
rshader.coat_roughness)) {
PushWarn(fmt::format("Failed to convert coat_roughness parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.coat_anisotropy, "coat_anisotropy",
rshader.coat_anisotropy)) {
PushWarn(fmt::format("Failed to convert coat_anisotropy parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.coat_rotation, "coat_rotation",
rshader.coat_rotation)) {
PushWarn(fmt::format("Failed to convert coat_rotation parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.coat_ior, "coat_ior",
rshader.coat_ior)) {
PushWarn(fmt::format("Failed to convert coat_ior parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.coat_affect_color, "coat_affect_color",
rshader.coat_affect_color)) {
PushWarn(fmt::format("Failed to convert coat_affect_color parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.coat_affect_roughness, "coat_affect_roughness",
rshader.coat_affect_roughness)) {
PushWarn(fmt::format("Failed to convert coat_affect_roughness parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
@@ -6136,11 +6608,13 @@ bool RenderSceneConverter::ConvertOpenPBRSurfaceShader(
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.emission_luminance, "emission_luminance",
rshader.emission_luminance)) {
PushWarn(fmt::format("Failed to convert emission_luminance parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.emission_color, "emission_color",
rshader.emission_color)) {
PushWarn(fmt::format("Failed to convert emission_color parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
@@ -6148,16 +6622,19 @@ bool RenderSceneConverter::ConvertOpenPBRSurfaceShader(
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.opacity, "opacity",
rshader.opacity)) {
PushWarn(fmt::format("Failed to convert opacity parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.normal, "normal",
rshader.normal)) {
PushWarn(fmt::format("Failed to convert normal parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.tangent, "tangent",
rshader.tangent)) {
PushWarn(fmt::format("Failed to convert tangent parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
@@ -8352,6 +8829,8 @@ bool InferColorSpace(const value::token &tok, ColorSpace *cty) {
return true;
}
#if 0 // Deprecated: Use implementation in render-scene-dump.cc instead
namespace {
template <typename T>
@@ -9062,6 +9541,8 @@ std::string DumpRenderScene(const RenderScene &scene,
return ss.str();
}
#endif // Deprecated dump functions
// Memory usage estimation implementations
size_t RenderMesh::estimate_memory_usage() const {

View File

@@ -2778,7 +2778,8 @@ class RenderSceneConverter {
bool ConvertPreviewSurfaceShaderParam(
const RenderSceneConverterEnv &env, const Path &shader_abs_path,
const TypedAttributeWithFallback<Animatable<T>> &param,
const std::string &param_name, ShaderParam<Dty> &dst_param);
const std::string &param_name, ShaderParam<Dty> &dst_param,
bool is_materialx = false);
///
/// Build (single) vertex indices for RenderMesh.
@@ -2817,9 +2818,9 @@ class RenderSceneConverter {
const XformNode &node,
Node &out_rnode);
void PushInfo(const std::string &msg) { _info += msg; }
void PushWarn(const std::string &msg) { _warn += msg; }
void PushError(const std::string &msg) { _err += msg; }
void PushInfo(const std::string &msg) { _info += msg + "\n"; }
void PushWarn(const std::string &msg) { _warn += msg + "\n"; }
void PushError(const std::string &msg) { _err += msg + "\n"; }
///
/// Call progress callback if set.

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

@@ -322,6 +322,7 @@ bool GeomPrimvar::flatten_with_indices(const double t, std::vector<T> *dest, con
TUSDZ_LOG_I("get_value");
#if 0 // FIXME: seems not work in emscripten build
// Try to use TypedArrayView for zero-copy access when possible (default values only)
// Only for trivially copyable types (excluding bool due to std::vector<bool> specialization)
// Using SFINAE helper function for C++14 compatibility (avoids 'if constexpr' requirement)
@@ -334,11 +335,24 @@ bool GeomPrimvar::flatten_with_indices(const double t, std::vector<T> *dest, con
return true; // Zero-copy path succeeded
}
}
#endif
// Fallback to std::vector for timesamples or if view failed
// Use std::vector instead of TypedArray to avoid potential corruption issues
std::vector<T> value;
if (_attr.get_value<std::vector<T>>(t, &value, tinterp)) {
TUSDZ_LOG_I("vsize " << value.size());
// Sanity check for corrupted size
if (value.size() > 1000000000) { // 1 billion elements is unreasonable
if (err) {
(*err) += fmt::format(
"[Internal Error] Array has invalid size: {} for attribute type `{}`",
value.size(), _attr.type_name());
}
return false;
}
uint32_t elementSize = _attr.metas().elementSize.value_or(1);
TUSDZ_LOG_I("elementSize" << elementSize);

View File

@@ -1679,6 +1679,7 @@ bool USDAReader::Impl::Read(const uint32_t state_flags, bool as_primspec) {
RegisterReconstructCallback<Material>();
RegisterReconstructCallback<Shader>();
RegisterReconstructCallback<NodeGraph>();
RegisterReconstructCallback<Scope>();

View File

@@ -1898,6 +1898,7 @@ nonstd::optional<Prim> USDCReader::Impl::ReconstructPrimFromTypeName(
RECONSTRUCT_PRIM(SkelAnimation, typeName, prim_name, spec)
RECONSTRUCT_PRIM(BlendShape, typeName, prim_name, spec)
RECONSTRUCT_PRIM(Shader, typeName, prim_name, spec)
RECONSTRUCT_PRIM(NodeGraph, typeName, prim_name, spec)
RECONSTRUCT_PRIM(Material, typeName, prim_name, spec) {
PUSH_WARN("TODO or unsupported prim type: " << typeName);
if (is_unsupported_prim) {

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;
}

Binary file not shown.

View File

@@ -0,0 +1,3 @@
https://github.com/usd-wg/assets/tree/main/full_assets/Teapot
The original Fancy Teapot data was part of the Tea Set created by James Ray Cook, Jurita Burger, and Rico Cilliers at PolyHaven. They generously made it available under a Public Domain - CC0 license.

13
web/js/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Verification artifacts (generated output)
verification-results/
# npm
node_modules/
package-lock.json
# WASM build artifacts
src/tinyusdz/*.wasm
src/tinyusdz/*.js
# Patch scripts (temporary)
apply-nodematerial-patch.sh

View File

@@ -0,0 +1,159 @@
# NodeMaterial Support via MaterialXLoader
This implementation adds support for routing TinyUSDZ-generated OpenPBR materials to Three.js `NodeMaterial` via `MaterialXLoader`, with a switchable option in the UI.
## Features
### 1. **Dual Material System**
- **MeshPhysicalMaterial** (default): Traditional Three.js material with manual parameter mapping
- **NodeMaterial**: Advanced node-based material system using MaterialX specification
### 2. **MaterialXLoader Integration**
- Converts OpenPBR data to MaterialX XML format
- Uses Three.js MaterialXLoader to parse and create NodeMaterial
- Supports both flat and grouped parameter formats from TinyUSDZ
### 3. **UI Toggle**
New "Material Rendering" panel in the GUI with:
- **Use NodeMaterial (MaterialX)**: Toggle checkbox
- **Reload Materials**: Button to refresh materials with new settings
## How It Works
### Material Creation Flow
```
TinyUSDZ OpenPBR Data
[Toggle Check]
┌─────────┴─────────┐
│ │
▼ ▼
NodeMaterial MeshPhysicalMaterial
(MaterialX) (Manual Mapping)
```
### NodeMaterial Path
1. **OpenPBR → MaterialX XML**: `convertOpenPBRToMaterialXML()` function converts OpenPBR data to MaterialX 1.39 XML format
2. **MaterialX → NodeMaterial**: Three.js MaterialXLoader parses the XML and creates NodeMaterial with shader graph
3. **Rendering**: NodeMaterial uses WebGPU-ready node-based shading system
### MeshPhysicalMaterial Path (Default)
1. **Direct Mapping**: OpenPBR parameters are manually mapped to MeshPhysicalMaterial properties
2. **Fallback**: Used when NodeMaterial creation fails or is disabled
3. **Compatibility**: Works with standard WebGL rendering
## Files Modified
- **materialx.js**:
- Added `MaterialXLoader` import
- Added `useNodeMaterial` and `materialXLoader` global variables
- Modified `createOpenPBRMaterial()` to support both material types
- Added "Material Rendering" GUI panel
- **convert-openpbr-to-mtlx.js** (new):
- Utility function to convert OpenPBR data to MaterialX XML
- Supports both flat (`base_color`) and grouped (`base.base_color`) formats
- Generates MaterialX 1.39 compatible XML
## Usage
### Via UI
1. Load a USD file with OpenPBR materials
2. Open the **Material Rendering** panel in the GUI
3. Toggle **Use NodeMaterial (MaterialX)** checkbox
4. Materials will automatically reload with the new setting
### Programmatically
```javascript
// Enable NodeMaterial mode
useNodeMaterial = true;
// Reload materials
await loadMaterials();
```
## MaterialX XML Structure
Generated MaterialX XML follows this structure:
```xml
<?xml version="1.0"?>
<materialx version="1.39">
<open_pbr_surface name="Material_shader" type="surfaceshader">
<input name="base_color" type="color3" value="0.9, 0.7, 0.3" />
<input name="base_metalness" type="float" value="0.85" />
<input name="base_roughness" type="float" value="0.5" />
<!-- ... all OpenPBR parameters ... -->
</open_pbr_surface>
<surfacematerial name="Material" type="material">
<input name="surfaceshader" type="surfaceshader" nodename="Material_shader" />
</surfacematerial>
</materialx>
```
## Parameter Mapping
### Supported OpenPBR Parameters
| Category | Parameters |
|----------|-----------|
| **Base** | base_weight, base_color, base_roughness, base_metalness |
| **Specular** | specular_weight, specular_color, specular_roughness, specular_ior, specular_ior_level, specular_anisotropy, specular_rotation |
| **Transmission** | transmission_weight, transmission_color, transmission_depth, transmission_scatter, transmission_scatter_anisotropy, transmission_dispersion |
| **Subsurface** | subsurface_weight, subsurface_color, subsurface_radius, subsurface_scale, subsurface_anisotropy |
| **Sheen** | sheen_weight, sheen_color, sheen_roughness |
| **Coat** | coat_weight, coat_color, coat_roughness, coat_anisotropy, coat_rotation, coat_ior, coat_affect_color, coat_affect_roughness |
| **Emission** | emission_luminance, emission_color |
| **Geometry** | opacity, geometry_normal, geometry_tangent |
## Benefits of NodeMaterial
1. **Shader Graph**: Visual node-based material system
2. **WebGPU Ready**: Optimized for modern graphics APIs
3. **Standard Compliance**: Uses MaterialX industry standard
4. **Extensibility**: Easy to extend with custom nodes
5. **Performance**: Can be more efficient than traditional materials
## Fallback Behavior
If NodeMaterial creation fails:
1. Error is logged to console
2. Automatically falls back to MeshPhysicalMaterial
3. User is notified via console warning
4. Scene continues to render normally
## Testing
Test the implementation:
1. Open `materialx.html` in browser
2. Load `models/openpbr-glass-sphere.usda` or similar
3. Toggle between material types in the GUI
4. Verify materials render correctly in both modes
5. Check console for any errors
## Browser Requirements
- Modern browser with ES modules support
- WebGL 2.0 or WebGPU support
- Three.js r161 or later
## Known Limitations
1. Texture mapping not yet implemented in NodeMaterial path
2. Some advanced OpenPBR features may not have direct MaterialX equivalents
3. MaterialXLoader requires specific MaterialX XML structure
## Future Enhancements
- [ ] Add texture support in MaterialX XML generation
- [ ] Implement normal map and displacement support
- [ ] Add color space conversions in MaterialX
- [ ] Support custom shader nodes
- [ ] Export NodeMaterial back to MaterialX

View File

@@ -0,0 +1,367 @@
# MaterialX Verification System
Automated verification system for MaterialX shading implementation using headless Chrome rendering and pixel-level comparison.
## Overview
This verification system provides:
1. **Headless Chrome Rendering** - Uses Puppeteer with SwiftShader fallback for GPU-less environments
2. **Visual Regression Testing** - Pixel-level comparison between TinyUSDZ and reference implementations
3. **Colorspace Validation** - Pure Node.js tests for colorspace conversions (no GPU required)
4. **Automated Reporting** - HTML reports with side-by-side comparisons and diff visualization
## System Requirements
### Required
- Node.js 18+ with ES modules support
- npm packages (automatically installed via `package.json`)
### Optional
- Google Chrome at `/opt/google/chrome/chrome` (for hardware GPU rendering)
- If not available, bundled Chromium will be used
- SwiftShader software rendering is used by default (no GPU required)
## Installation
```bash
cd web/js
npm install
```
This installs all required dependencies:
- `puppeteer` - Headless Chrome automation
- `pixelmatch` - Image comparison algorithm
- `pngjs` - PNG image reading/writing
- `commander` - CLI argument parsing
## Usage
### 1. Colorspace Tests (Pure Node.js)
Test colorspace conversions without requiring GPU or browser:
```bash
npm run test:colorspace
```
**Tests include:**
- sRGB ↔ Linear conversions
- Rec.709 → XYZ color space transforms
- Validation against MaterialX specification reference values
**Example output:**
```
🎨 MaterialX Colorspace Conversion Tests
✓ sRGB to Linear - Mid Gray - PASSED
Input: [0.500000, 0.500000, 0.500000]
Expected: [0.214041, 0.214041, 0.214041]
Result: [0.214041, 0.214041, 0.214041]
✓ Passed: 9
✗ Failed: 0
Total: 9
```
### 2. Material Rendering Verification
Render materials with headless Chrome and compare against reference:
```bash
npm run verify-materialx render
```
**Options:**
```bash
# Test specific materials
npm run verify-materialx render --materials brass,glass,gold
# Use hardware GPU acceleration (if available)
npm run verify-materialx render --gpu
# Verbose output (show browser console logs)
npm run verify-materialx render --verbose
# Custom Chrome path
CHROME_PATH=/usr/bin/google-chrome npm run verify-materialx render
```
**Output:**
- Screenshots: `verification-results/screenshots/`
- `tinyusdz-{material}.png` - Rendered with TinyUSDZ
- `reference-{material}.png` - Rendered with MaterialX reference
- Diff images: `verification-results/diffs/`
- `diff-{material}.png` - Pixel difference visualization
- Report: `verification-results/report.html` - Interactive HTML report
### 3. Clean Results
Remove all verification results:
```bash
npm run verify-materialx clean
```
## How It Works
### Rendering Pipeline
```
┌─────────────────────────────────────────────────────────┐
│ 1. Launch Headless Chrome │
│ - SwiftShader (software) or GPU acceleration │
│ - WebGL/WebGPU enabled │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 2. Load Test HTML Pages │
│ - render-tinyusdz.html (TinyUSDZ + Three.js) │
│ - render-reference.html (MaterialXLoader) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 3. Render Material (120 frames) │
│ - Setup: Camera, lights, geometry, environment │
│ - Render: Fixed 800x600 @ 1.0 pixel ratio │
│ - Signal: window.renderComplete = true │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 4. Take Screenshots │
│ - PNG format for lossless comparison │
│ - Consistent viewport and rendering settings │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 5. Compare Images (pixelmatch) │
│ - Pixel-by-pixel color difference │
│ - Threshold: 0.1 (0-1 scale) │
│ - Generate diff visualization │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 6. Generate Report │
│ - Pass/Fail: < 2% difference = PASS │
│ - HTML with side-by-side comparison │
│ - Diff highlighting in third column │
└─────────────────────────────────────────────────────────┘
```
### Comparison Criteria
**Pass Conditions:**
- Average pixel difference < 2.0%
- Images have identical dimensions
- Both renderers completed successfully
**Metrics Reported:**
- Pixels different: Absolute count of non-matching pixels
- Total pixels: Width × Height
- Percent different: (pixels different / total pixels) × 100
- Status: PASSED or FAILED
## Test Materials
Default test materials (OpenPBR):
| Material | Properties |
|----------|-----------|
| **brass** | Metallic: 1.0, Roughness: 0.3, Color: Gold-brown |
| **glass** | Transmission: 1.0, IOR: 1.52, Clear |
| **gold** | Metallic: 1.0, Roughness: 0.2, Color: Gold |
| **copper** | Metallic: 1.0, Roughness: 0.25, Color: Copper |
| **plastic** | Metallic: 0.0, Roughness: 0.5, Color: Red |
| **marble** | Subsurface: 0.3, Roughness: 0.1, Color: Off-white |
## Architecture
### Files Structure
```
web/js/
├── verify-materialx.js # Main CLI tool
├── tests/
│ ├── colorspace-test.js # Pure Node.js colorspace tests
│ ├── render-tinyusdz.html # TinyUSDZ renderer page
│ └── render-reference.html # MaterialX reference renderer page
├── verification-results/ # Generated output (gitignored)
│ ├── screenshots/
│ ├── diffs/
│ └── report.html
└── VERIFICATION_README.md # This file
```
### Technologies
- **Puppeteer**: Chrome automation and screenshot capture
- **SwiftShader**: Software GPU implementation (no hardware GPU required)
- **pixelmatch**: Perceptual image comparison algorithm
- **Three.js**: WebGL rendering framework
- **MaterialXLoader**: Official Three.js MaterialX loader
## SwiftShader Rendering
SwiftShader is used by default for reproducible, GPU-independent rendering:
**Advantages:**
- ✓ No GPU hardware required
- ✓ Consistent results across machines
- ✓ Works in CI/CD environments
- ✓ Deterministic rendering
**Launch arguments:**
```javascript
--disable-gpu
--use-gl=swiftshader
--use-angle=swiftshader
```
**Performance:**
- Rendering: ~2-5 seconds per material
- Screenshot: Instant
- Comparison: < 100ms
## CI/CD Integration
Example GitHub Actions workflow:
```yaml
name: MaterialX Verification
on: [push, pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: |
cd web/js
npm install
- name: Run colorspace tests
run: npm run test:colorspace
- name: Run rendering verification
run: npm run verify-materialx render
- name: Upload report
if: always()
uses: actions/upload-artifact@v3
with:
name: verification-report
path: web/js/verification-results/
```
## Troubleshooting
### Chrome not found
**Error:** "Chrome not found at /opt/google/chrome/chrome"
**Solution:** Puppeteer will use bundled Chromium automatically
Or set custom path:
```bash
export CHROME_PATH=/usr/bin/google-chrome
npm run verify-materialx render
```
### Rendering timeout
**Error:** "Timeout waiting for renderComplete"
**Causes:**
- WebGL/WebGPU not available
- JavaScript errors in test page
- Network issues loading Three.js modules
**Solution:** Check browser console with `--verbose`:
```bash
npm run verify-materialx render --verbose
```
### Image dimension mismatch
**Error:** "Image dimensions do not match"
**Cause:** Different viewport sizes between renderers
**Solution:** Both test HTML pages use fixed 800x600 viewport
### High pixel difference
**Error:** "FAILED (≥ 2% difference)"
**Causes:**
- MaterialX parameter mismatch
- Shader implementation differences
- Lighting or environment differences
- Colorspace handling differences
**Solution:** Review diff image in `verification-results/diffs/`
## Extending Tests
### Add New Material
Edit test HTML files to add new material definitions:
```javascript
const MATERIALS = {
// ... existing materials ...
myMaterial: {
name: 'MyMaterial',
base_color: [0.5, 0.7, 0.9],
base_metalness: 0.5,
base_roughness: 0.4,
// ... other OpenPBR parameters
}
};
```
Then run:
```bash
npm run verify-materialx render --materials myMaterial
```
### Add New Colorspace Test
Edit `tests/colorspace-test.js`:
```javascript
const tests = [
// ... existing tests ...
{
name: 'My Custom Test',
input: [0.5, 0.5, 0.5],
operation: 'srgb_to_linear',
expected: [0.214041, 0.214041, 0.214041],
tolerance: 0.001
}
];
```
## Future Enhancements
- [ ] Test with real ASWF MaterialX example files
- [ ] Add texture-mapped material tests
- [ ] Performance benchmarking
- [ ] WebGPU rendering path comparison
- [ ] Automated regression tracking
- [ ] Statistical analysis of rendering differences
- [ ] Support for custom geometry (not just sphere)
- [ ] Export MaterialX XML from TinyUSDZ for roundtrip tests
## References
- [MaterialX Official Site](https://materialx.org/)
- [ASWF MaterialX Repository](https://github.com/AcademySoftwareFoundation/MaterialX)
- [MaterialX Web Viewer](https://academysoftwarefoundation.github.io/MaterialX/)
- [Three.js MaterialXLoader](https://threejs.org/docs/#examples/en/loaders/MaterialXLoader)
- [SwiftShader](https://github.com/google/swiftshader)

View File

@@ -0,0 +1,109 @@
// Helper function to convert OpenPBR material data to MaterialX XML string
// This generates MaterialX XML that can be loaded by Three.js MaterialXLoader
export function convertOpenPBRToMaterialXML(materialData, materialName = 'Material') {
let xml = `<?xml version="1.0"?>
<materialx version="1.39">
<open_pbr_surface name="${materialName}_shader" type="surfaceshader">
`;
// Helper to add parameter
const addParam = (name, value, type = 'float') => {
if (value === undefined || value === null) return '';
if (type === 'color3' && Array.isArray(value)) {
return ` <input name="${name}" type="color3" value="${value[0]}, ${value[1]}, ${value[2]}" />\n`;
} else if (type === 'float') {
return ` <input name="${name}" type="float" value="${value}" />\n`;
} else if (type === 'vector3' && Array.isArray(value)) {
return ` <input name="${name}" type="vector3" value="${value[0]}, ${value[1]}, ${value[2]}" />\n`;
}
return '';
};
// Extract values from either flat or grouped format
const extractValue = (flatPath, groupedPath) => {
// Try flat format first
if (materialData[flatPath] !== undefined) {
const val = materialData[flatPath];
return typeof val === 'object' && val.value !== undefined ? val.value : val;
}
// Try grouped format
if (groupedPath) {
const parts = groupedPath.split('.');
let current = materialData;
for (const part of parts) {
if (!current || !current[part]) return undefined;
current = current[part];
}
const val = current;
return typeof val === 'object' && val.value !== undefined ? val.value : val;
}
return undefined;
};
// Base layer
xml += addParam('base_weight', extractValue('base_weight', 'base.base_weight'));
xml += addParam('base_color', extractValue('base_color', 'base.base_color'), 'color3');
xml += addParam('base_roughness', extractValue('base_roughness', 'base.base_roughness'));
xml += addParam('base_metalness', extractValue('base_metalness', 'base.base_metalness'));
// Specular layer
xml += addParam('specular_weight', extractValue('specular_weight', 'specular.specular_weight'));
xml += addParam('specular_color', extractValue('specular_color', 'specular.specular_color'), 'color3');
xml += addParam('specular_roughness', extractValue('specular_roughness', 'specular.specular_roughness'));
xml += addParam('specular_ior', extractValue('specular_ior', 'specular.specular_ior'));
xml += addParam('specular_ior_level', extractValue('specular_ior_level', 'specular.specular_ior_level'));
xml += addParam('specular_anisotropy', extractValue('specular_anisotropy', 'specular.specular_anisotropy'));
xml += addParam('specular_rotation', extractValue('specular_rotation', 'specular.specular_rotation'));
// Transmission
xml += addParam('transmission_weight', extractValue('transmission_weight', 'transmission.transmission_weight'));
xml += addParam('transmission_color', extractValue('transmission_color', 'transmission.transmission_color'), 'color3');
xml += addParam('transmission_depth', extractValue('transmission_depth', 'transmission.transmission_depth'));
xml += addParam('transmission_scatter', extractValue('transmission_scatter', 'transmission.transmission_scatter'), 'color3');
xml += addParam('transmission_scatter_anisotropy', extractValue('transmission_scatter_anisotropy', 'transmission.transmission_scatter_anisotropy'));
xml += addParam('transmission_dispersion', extractValue('transmission_dispersion', 'transmission.transmission_dispersion'));
// Subsurface
xml += addParam('subsurface_weight', extractValue('subsurface_weight', 'subsurface.subsurface_weight'));
xml += addParam('subsurface_color', extractValue('subsurface_color', 'subsurface.subsurface_color'), 'color3');
xml += addParam('subsurface_radius', extractValue('subsurface_radius', 'subsurface.subsurface_radius'), 'color3');
xml += addParam('subsurface_scale', extractValue('subsurface_scale', 'subsurface.subsurface_scale'));
xml += addParam('subsurface_anisotropy', extractValue('subsurface_anisotropy', 'subsurface.subsurface_anisotropy'));
// Sheen
xml += addParam('sheen_weight', extractValue('sheen_weight', 'sheen.sheen_weight'));
xml += addParam('sheen_color', extractValue('sheen_color', 'sheen.sheen_color'), 'color3');
xml += addParam('sheen_roughness', extractValue('sheen_roughness', 'sheen.sheen_roughness'));
// Coat
xml += addParam('coat_weight', extractValue('coat_weight', 'coat.coat_weight'));
xml += addParam('coat_color', extractValue('coat_color', 'coat.coat_color'), 'color3');
xml += addParam('coat_roughness', extractValue('coat_roughness', 'coat.coat_roughness'));
xml += addParam('coat_anisotropy', extractValue('coat_anisotropy', 'coat.coat_anisotropy'));
xml += addParam('coat_rotation', extractValue('coat_rotation', 'coat.coat_rotation'));
xml += addParam('coat_ior', extractValue('coat_ior', 'coat.coat_ior'));
xml += addParam('coat_affect_color', extractValue('coat_affect_color', 'coat.coat_affect_color'), 'color3');
xml += addParam('coat_affect_roughness', extractValue('coat_affect_roughness', 'coat.coat_affect_roughness'));
// Emission
xml += addParam('emission_luminance', extractValue('emission_luminance', 'emission.emission_luminance'));
xml += addParam('emission_color', extractValue('emission_color', 'emission.emission_color'), 'color3');
// Geometry
xml += addParam('opacity', extractValue('opacity', 'geometry.opacity'));
xml += addParam('geometry_normal', extractValue('geometry_normal', 'geometry.normal'), 'vector3');
xml += addParam('geometry_tangent', extractValue('geometry_tangent', 'geometry.tangent'), 'vector3');
xml += ` </open_pbr_surface>
<surfacematerial name="${materialName}" type="material">
<input name="surfaceshader" type="surfaceshader" nodename="${materialName}_shader" />
</surfacematerial>
</materialx>
`;
return xml;
}

View File

@@ -165,6 +165,124 @@
width: 55% !important;
}
/* Floatable Controls Panel */
#controls-wrapper {
position: absolute;
top: 150px;
right: 10px;
z-index: 200;
background: rgba(0, 0, 0, 0.85);
border-radius: 5px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
overflow: visible;
min-width: 350px;
max-width: 600px;
}
#controls-wrapper.minimized .lil-gui {
display: none;
}
#controls-header {
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
color: white;
padding: 8px 12px;
cursor: move;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
font-weight: 500;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
z-index: 1;
}
#controls-header:active {
cursor: grabbing;
}
#controls-title {
display: flex;
align-items: center;
gap: 8px;
}
#controls-buttons {
display: flex;
gap: 5px;
}
.control-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 24px;
height: 24px;
border-radius: 3px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: background 0.2s;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.control-btn:active {
background: rgba(255, 255, 255, 0.4);
}
#controls-wrapper .lil-gui {
position: relative !important;
top: 0 !important;
right: 0 !important;
max-height: calc(100vh - 200px);
overflow-y: auto;
border-radius: 0 0 5px 5px;
z-index: 2;
pointer-events: auto;
}
#controls-wrapper .lil-gui * {
pointer-events: auto;
}
/* Override resize cursor when dragging */
#controls-wrapper.dragging,
#texture-panel-wrapper.dragging {
cursor: grabbing !important;
}
#controls-wrapper.dragging .lil-gui,
#controls-wrapper.dragging .lil-gui * {
cursor: default !important;
}
#texture-panel-wrapper.dragging * {
cursor: grabbing !important;
}
/* Floatable Texture Panel */
#texture-panel-wrapper {
resize: both;
min-width: 300px;
max-width: 600px;
z-index: 150;
}
#texture-panel-wrapper.minimized #texture-panel {
display: none;
}
#texture-panel-header:active {
cursor: grabbing;
}
/* Texture panel styling */
#texture-panel .texture-item {
margin: 10px 0;
@@ -268,6 +386,10 @@
<div>Materials: <span id="material-count">0</span></div>
<div>Selected: <span id="selected-object">None</span></div>
</div>
<div id="scene-metadata" style="display: none; margin-top: 10px; padding-top: 10px; border-top: 1px solid #666;">
<h3 style="margin: 0 0 5px 0; font-size: 13px;">Scene Metadata</h3>
<div id="metadata-content" style="font-size: 11px; line-height: 1.6;"></div>
</div>
</div>
<div id="file-input-container">
@@ -312,9 +434,19 @@
</div>
</div>
<div id="texture-panel" style="display: none; position: absolute; bottom: 10px; right: 10px; background: rgba(0, 0, 0, 0.8); color: white; padding: 15px; border-radius: 5px; font-size: 12px; max-width: 400px; max-height: 500px; overflow-y: auto;">
<h3 style="margin: 0 0 10px 0; font-size: 14px; border-bottom: 1px solid #666; padding-bottom: 5px;">Textures</h3>
<div id="texture-list"></div>
<div id="texture-panel-wrapper" style="display: none; position: absolute; bottom: 10px; right: 10px;">
<div id="texture-panel-header" style="background: linear-gradient(135deg, #4CAF50 0%, #388E3C 100%); color: white; padding: 8px 12px; cursor: move; user-select: none; display: flex; justify-content: space-between; align-items: center; font-size: 13px; font-weight: 500; border-radius: 5px 5px 0 0; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="display: flex; align-items: center; gap: 8px;">
<span>🖼️</span>
<span>Textures</span>
</div>
<div style="display: flex; gap: 5px;">
<button class="control-btn" id="texture-minimize-btn" style="background: rgba(255, 255, 255, 0.2); border: none; color: white; width: 24px; height: 24px; border-radius: 3px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; transition: background 0.2s;"></button>
</div>
</div>
<div id="texture-panel" style="background: rgba(0, 0, 0, 0.85); color: white; padding: 15px; border-radius: 0 0 5px 5px; font-size: 12px; max-width: 400px; max-height: 500px; overflow-y: auto; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);">
<div id="texture-list"></div>
</div>
</div>
<div id="loading-overlay">

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,9 @@
"test:streaming": "vite-node load-test-node.js",
"dump-materialx": "vite-node dump-materialx-cli.js",
"anim-info": "vite-node animation-info.js",
"anim-info:detailed": "vite-node animation-info.js --detailed"
"anim-info:detailed": "vite-node animation-info.js --detailed",
"verify-materialx": "vite-node verify-materialx.js",
"test:colorspace": "vite-node tests/colorspace-test.js"
},
"devDependencies": {
"@types/bun": "latest",
@@ -25,7 +27,10 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.16.0",
"commander": "^12.1.0",
"fzstd": "^0.1.1",
"pixelmatch": "^6.0.0",
"pngjs": "^7.0.0",
"puppeteer": "^24.25.0",
"three": ">=0.177.0",
"vite-node": "^3.2.4",

View File

@@ -0,0 +1,219 @@
/**
* Colorspace Conversion Tests (Pure Node.js)
*
* Tests various colorspace conversions without requiring WebGL/WebGPU.
* Validates against known reference values from MaterialX specification.
*/
// Reference colorspace conversion functions
// Based on MaterialX specification and OpenColorIO
/**
* sRGB to Linear conversion
*/
function srgbToLinear(value) {
if (value <= 0.04045) {
return value / 12.92;
}
return Math.pow((value + 0.055) / 1.055, 2.4);
}
/**
* Linear to sRGB conversion
*/
function linearToSrgb(value) {
if (value <= 0.0031308) {
return value * 12.92;
}
return 1.055 * Math.pow(value, 1.0 / 2.4) - 0.055;
}
/**
* Convert color array between sRGB and Linear
*/
function convertSrgbLinear(color, toLinear = true) {
const fn = toLinear ? srgbToLinear : linearToSrgb;
return color.map(fn);
}
/**
* Simple Rec.709 matrix (for demonstration)
* In reality, this should match MaterialX's exact matrices
*/
const REC709_TO_XYZ = [
[0.4124564, 0.3575761, 0.1804375],
[0.2126729, 0.7151522, 0.0721750],
[0.0193339, 0.1191920, 0.9503041]
];
const XYZ_TO_REC709 = [
[ 3.2404542, -1.5371385, -0.4985314],
[-0.9692660, 1.8760108, 0.0415560],
[ 0.0556434, -0.2040259, 1.0572252]
];
/**
* Matrix multiply for color conversion
*/
function matrixMultiply(matrix, color) {
return matrix.map(row =>
row.reduce((sum, val, i) => sum + val * color[i], 0)
);
}
/**
* Compare two colors with tolerance
*/
function colorsMatch(c1, c2, tolerance = 0.001) {
return c1.every((val, i) => Math.abs(val - c2[i]) < tolerance);
}
/**
* Format color for display
*/
function formatColor(color) {
return `[${color.map(v => v.toFixed(6)).join(', ')}]`;
}
// Test suite
const tests = [
{
name: 'sRGB to Linear - Mid Gray',
input: [0.5, 0.5, 0.5],
operation: 'srgb_to_linear',
expected: [0.214041, 0.214041, 0.214041],
tolerance: 0.001
},
{
name: 'sRGB to Linear - Black',
input: [0.0, 0.0, 0.0],
operation: 'srgb_to_linear',
expected: [0.0, 0.0, 0.0],
tolerance: 0.001
},
{
name: 'sRGB to Linear - White',
input: [1.0, 1.0, 1.0],
operation: 'srgb_to_linear',
expected: [1.0, 1.0, 1.0],
tolerance: 0.001
},
{
name: 'sRGB to Linear - Red',
input: [1.0, 0.0, 0.0],
operation: 'srgb_to_linear',
expected: [1.0, 0.0, 0.0],
tolerance: 0.001
},
{
name: 'Linear to sRGB - Mid Gray',
input: [0.214041, 0.214041, 0.214041],
operation: 'linear_to_srgb',
expected: [0.5, 0.5, 0.5],
tolerance: 0.001
},
{
name: 'sRGB to Linear - Quarter Gray',
input: [0.25, 0.25, 0.25],
operation: 'srgb_to_linear',
expected: [0.050876, 0.050876, 0.050876],
tolerance: 0.001
},
{
name: 'sRGB to Linear - Three Quarter Gray',
input: [0.75, 0.75, 0.75],
operation: 'srgb_to_linear',
expected: [0.522522, 0.522522, 0.522522],
tolerance: 0.001
},
{
name: 'sRGB to Linear - Orange',
input: [1.0, 0.5, 0.0],
operation: 'srgb_to_linear',
expected: [1.0, 0.214041, 0.0],
tolerance: 0.001
},
{
name: 'Rec.709 to XYZ - White',
input: [1.0, 1.0, 1.0],
operation: 'rec709_to_xyz',
expected: [0.9505, 1.0000, 1.0890], // D65 white point
tolerance: 0.01 // Looser tolerance for matrix ops
},
];
/**
* Execute a test
*/
function runTest(test) {
let result;
switch (test.operation) {
case 'srgb_to_linear':
result = convertSrgbLinear(test.input, true);
break;
case 'linear_to_srgb':
result = convertSrgbLinear(test.input, false);
break;
case 'rec709_to_xyz':
result = matrixMultiply(REC709_TO_XYZ, test.input);
break;
default:
throw new Error(`Unknown operation: ${test.operation}`);
}
const passed = colorsMatch(result, test.expected, test.tolerance);
return {
name: test.name,
input: test.input,
expected: test.expected,
result,
passed,
error: passed ? 0 : Math.max(...result.map((v, i) => Math.abs(v - test.expected[i])))
};
}
/**
* Run all tests
*/
function runAllTests() {
console.log('🎨 MaterialX Colorspace Conversion Tests\n');
console.log('='.repeat(80));
const results = tests.map(runTest);
results.forEach(result => {
const icon = result.passed ? '✓' : '✗';
const status = result.passed ? '\x1b[32mPASSED\x1b[0m' : '\x1b[31mFAILED\x1b[0m';
console.log(`\n${icon} ${result.name} - ${status}`);
console.log(` Input: ${formatColor(result.input)}`);
console.log(` Expected: ${formatColor(result.expected)}`);
console.log(` Result: ${formatColor(result.result)}`);
if (!result.passed) {
console.log(` \x1b[31mMax Error: ${result.error.toFixed(6)}\x1b[0m`);
}
});
console.log('\n' + '='.repeat(80));
const passed = results.filter(r => r.passed).length;
const failed = results.filter(r => !r.passed).length;
console.log(`\n✓ Passed: ${passed}`);
console.log(`✗ Failed: ${failed}`);
console.log(`Total: ${results.length}`);
console.log('='.repeat(80));
return failed === 0;
}
// Run tests if executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
const success = runAllTests();
process.exit(success ? 0 : 1);
}
export { runAllTests, runTest, convertSrgbLinear, srgbToLinear, linearToSrgb };

View File

@@ -0,0 +1,212 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MaterialX Reference Renderer</title>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.177.0/build/three.module.js",
"three/examples/jsm/": "https://cdn.jsdelivr.net/npm/three@0.177.0/examples/jsm/",
"three/webgpu": "https://cdn.jsdelivr.net/npm/three@0.177.0/build/three.webgpu.js",
"three/tsl": "https://cdn.jsdelivr.net/npm/three@0.177.0/build/three.webgpu.js"
}
}
</script>
<style>
body {
margin: 0;
overflow: hidden;
background: #1a1a1a;
}
#canvas {
display: block;
width: 100vw;
height: 100vh;
}
#status {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 4px;
display: none;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="status">Rendering...</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
// Note: Using same MeshPhysicalMaterial as TinyUSDZ for now
// TODO: Integrate actual MaterialX reference implementation
// Material definitions (same as TinyUSDZ for consistency)
const MATERIALS = {
brass: {
name: 'Brass',
base_color: [0.95, 0.65, 0.25],
base_metalness: 1.0,
base_roughness: 0.3,
specular_ior: 1.5
},
glass: {
name: 'Glass',
base_color: [1.0, 1.0, 1.0],
base_metalness: 0.0,
base_roughness: 0.0,
transmission_weight: 1.0,
transmission_color: [1.0, 1.0, 1.0],
specular_ior: 1.52
},
gold: {
name: 'Gold',
base_color: [1.0, 0.766, 0.336],
base_metalness: 1.0,
base_roughness: 0.2,
specular_ior: 1.5
},
copper: {
name: 'Copper',
base_color: [0.955, 0.637, 0.538],
base_metalness: 1.0,
base_roughness: 0.25,
specular_ior: 1.5
},
plastic: {
name: 'Plastic',
base_color: [0.8, 0.2, 0.1],
base_metalness: 0.0,
base_roughness: 0.5,
specular_ior: 1.5
},
marble: {
name: 'Marble',
base_color: [0.9, 0.9, 0.85],
base_metalness: 0.0,
base_roughness: 0.1,
subsurface_weight: 0.3,
subsurface_color: [0.9, 0.9, 0.85]
}
};
// Get material name from URL parameter
const urlParams = new URLSearchParams(window.location.search);
const materialName = urlParams.get('material') || 'brass';
const materialData = MATERIALS[materialName] || MATERIALS.brass;
// Create material from OpenPBR data (same as TinyUSDZ for baseline comparison)
function createMaterial(data) {
const material = new THREE.MeshPhysicalMaterial({
color: new THREE.Color(...data.base_color),
metalness: data.base_metalness || 0.0,
roughness: data.base_roughness || 0.5,
transmission: data.transmission_weight || 0.0,
ior: data.specular_ior || 1.5,
thickness: 1.0,
clearcoat: data.coat_weight || 0.0,
clearcoatRoughness: data.coat_roughness || 0.0,
});
// Handle transmission color
if (data.transmission_weight && data.transmission_weight > 0) {
material.transparent = true;
material.opacity = 1.0;
}
return material;
}
// Setup scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 0, 5);
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('canvas'),
antialias: true,
alpha: false
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(1); // Fixed pixel ratio for consistent screenshots
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// Lighting setup (identical to TinyUSDZ renderer)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const keyLight = new THREE.DirectionalLight(0xffffff, 1.0);
keyLight.position.set(5, 5, 5);
scene.add(keyLight);
const fillLight = new THREE.DirectionalLight(0xffffff, 0.5);
fillLight.position.set(-5, 0, -5);
scene.add(fillLight);
// Create test geometry (shaderball-like sphere)
const geometry = new THREE.SphereGeometry(1.5, 64, 64);
const material = createMaterial(materialData);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Environment map for reflections
const pmremGenerator = new THREE.PMREMGenerator(renderer);
const envMap = pmremGenerator.fromScene(
new RoomEnvironment(renderer),
0.04
).texture;
scene.environment = envMap;
material.envMap = envMap;
material.envMapIntensity = 1.0;
console.log(`Rendering reference material: ${materialData.name}`);
// Animation loop
let frameCount = 0;
const RENDER_FRAMES = 120;
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
frameCount++;
if (frameCount === RENDER_FRAMES) {
window.renderComplete = true;
console.log('Rendering complete');
}
}
// Start rendering
animate();
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TinyUSDZ Material Renderer</title>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.177.0/build/three.module.js",
"three/examples/jsm/": "https://cdn.jsdelivr.net/npm/three@0.177.0/examples/jsm/"
}
}
</script>
<style>
body {
margin: 0;
overflow: hidden;
background: #1a1a1a;
}
#canvas {
display: block;
width: 100vw;
height: 100vh;
}
#status {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 4px;
display: none;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="status">Rendering...</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
// Material definitions for testing
const MATERIALS = {
brass: {
name: 'Brass',
base_color: [0.95, 0.65, 0.25],
base_metalness: 1.0,
base_roughness: 0.3,
specular_ior: 1.5
},
glass: {
name: 'Glass',
base_color: [1.0, 1.0, 1.0],
base_metalness: 0.0,
base_roughness: 0.0,
transmission_weight: 1.0,
transmission_color: [1.0, 1.0, 1.0],
specular_ior: 1.52
},
gold: {
name: 'Gold',
base_color: [1.0, 0.766, 0.336],
base_metalness: 1.0,
base_roughness: 0.2,
specular_ior: 1.5
},
copper: {
name: 'Copper',
base_color: [0.955, 0.637, 0.538],
base_metalness: 1.0,
base_roughness: 0.25,
specular_ior: 1.5
},
plastic: {
name: 'Plastic',
base_color: [0.8, 0.2, 0.1],
base_metalness: 0.0,
base_roughness: 0.5,
specular_ior: 1.5
},
marble: {
name: 'Marble',
base_color: [0.9, 0.9, 0.85],
base_metalness: 0.0,
base_roughness: 0.1,
subsurface_weight: 0.3,
subsurface_color: [0.9, 0.9, 0.85]
}
};
// Get material name from URL parameter or default to 'brass'
const urlParams = new URLSearchParams(window.location.search);
const materialName = urlParams.get('material') || 'brass';
const materialData = MATERIALS[materialName] || MATERIALS.brass;
// Setup scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 0, 5);
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('canvas'),
antialias: true,
alpha: false
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(1); // Fixed pixel ratio for consistent screenshots
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// Lighting setup (studio-style)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const keyLight = new THREE.DirectionalLight(0xffffff, 1.0);
keyLight.position.set(5, 5, 5);
scene.add(keyLight);
const fillLight = new THREE.DirectionalLight(0xffffff, 0.5);
fillLight.position.set(-5, 0, -5);
scene.add(fillLight);
// Create material from OpenPBR data
function createMaterial(data) {
const material = new THREE.MeshPhysicalMaterial({
color: new THREE.Color(...data.base_color),
metalness: data.base_metalness || 0.0,
roughness: data.base_roughness || 0.5,
transmission: data.transmission_weight || 0.0,
ior: data.specular_ior || 1.5,
thickness: 1.0,
clearcoat: data.coat_weight || 0.0,
clearcoatRoughness: data.coat_roughness || 0.0,
});
// Handle transmission color
if (data.transmission_weight && data.transmission_weight > 0) {
material.transparent = true;
material.opacity = 1.0;
}
return material;
}
// Create test geometry (shaderball-like sphere)
const geometry = new THREE.SphereGeometry(1.5, 64, 64);
const material = createMaterial(materialData);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Environment map for reflections
const pmremGenerator = new THREE.PMREMGenerator(renderer);
const envMap = pmremGenerator.fromScene(
new RoomEnvironment(renderer),
0.04
).texture;
scene.environment = envMap;
material.envMap = envMap;
material.envMapIntensity = 1.0;
// Animation loop
let frameCount = 0;
const RENDER_FRAMES = 120; // Render for 120 frames before marking complete
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
frameCount++;
// Mark rendering as complete after enough frames
if (frameCount === RENDER_FRAMES) {
window.renderComplete = true;
console.log('Rendering complete');
}
}
// Start rendering
console.log(`Rendering material: ${materialData.name}`);
animate();
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>

423
web/js/verify-materialx.js Normal file
View File

@@ -0,0 +1,423 @@
#!/usr/bin/env node
/**
* MaterialX Verification CLI Tool
*
* Renders materials with headless Chrome (using SwiftShader fallback)
* and compares against reference implementations.
*/
import { program } from 'commander';
import puppeteer from 'puppeteer';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { PNG } from 'pngjs';
import pixelmatch from 'pixelmatch';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Configuration
const CHROME_PATH = process.env.CHROME_PATH || '/opt/google/chrome/chrome';
const OUTPUT_DIR = path.join(__dirname, 'verification-results');
const SCREENSHOTS_DIR = path.join(OUTPUT_DIR, 'screenshots');
const DIFFS_DIR = path.join(OUTPUT_DIR, 'diffs');
// Ensure output directories exist
[OUTPUT_DIR, SCREENSHOTS_DIR, DIFFS_DIR].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
/**
* Launch headless Chrome with SwiftShader fallback
*/
async function launchBrowser(useGPU = false) {
const args = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-web-security', // Allow loading local files
];
if (!useGPU) {
// Force SwiftShader (software rendering)
args.push(
'--disable-gpu',
'--use-gl=swiftshader',
'--use-angle=swiftshader-webgl',
'--ignore-gpu-blocklist',
'--enable-unsafe-swiftshader'
);
console.log('🔧 Using SwiftShader (software rendering)');
} else {
args.push('--enable-webgl', '--enable-webgpu');
console.log('🎮 Using hardware GPU acceleration');
}
const launchOptions = {
headless: 'new',
args,
ignoreDefaultArgs: ['--disable-extensions'],
};
// Try to use system Chrome if available
if (fs.existsSync(CHROME_PATH)) {
launchOptions.executablePath = CHROME_PATH;
console.log(`✓ Using Chrome at: ${CHROME_PATH}`);
} else {
console.log('⚠ Using bundled Chromium (system Chrome not found)');
}
return await puppeteer.launch(launchOptions);
}
/**
* Render a material to PNG using headless Chrome
*/
async function renderMaterial(browser, htmlPath, materialName, outputPath, options = {}) {
const page = await browser.newPage();
await page.setViewport({
width: options.width || 800,
height: options.height || 600,
deviceScaleFactor: 1,
});
// Enable console logging from the page
page.on('console', msg => {
if (options.verbose) {
console.log(` [Browser] ${msg.text()}`);
}
});
// Handle errors
page.on('pageerror', error => {
console.error(` ❌ Page error: ${error.message}`);
});
try {
// Load the HTML page
const url = `file://${htmlPath}`;
console.log(` Loading: ${url}`);
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
// Wait for rendering to complete (give it plenty of time for CDN loads + rendering)
await page.waitForFunction(
() => window.renderComplete === true,
{ timeout: 120000 }
);
// Take screenshot
await page.screenshot({
path: outputPath,
type: 'png',
});
console.log(` ✓ Rendered: ${outputPath}`);
await page.close();
return true;
} catch (error) {
console.error(` ❌ Render failed: ${error.message}`);
await page.close();
return false;
}
}
/**
* Compare two PNG images and generate diff
*/
function compareImages(img1Path, img2Path, diffPath) {
if (!fs.existsSync(img1Path) || !fs.existsSync(img2Path)) {
return {
error: 'Missing image file(s)',
pixelsDifferent: -1,
percentDifferent: -1,
};
}
const img1 = PNG.sync.read(fs.readFileSync(img1Path));
const img2 = PNG.sync.read(fs.readFileSync(img2Path));
if (img1.width !== img2.width || img1.height !== img2.height) {
return {
error: 'Image dimensions do not match',
pixelsDifferent: -1,
percentDifferent: -1,
};
}
const { width, height } = img1;
const diff = new PNG({ width, height });
const numDiffPixels = pixelmatch(
img1.data,
img2.data,
diff.data,
width,
height,
{ threshold: 0.1 } // 0-1 range, lower = more strict
);
// Save diff image
fs.writeFileSync(diffPath, PNG.sync.write(diff));
const totalPixels = width * height;
const percentDifferent = (numDiffPixels / totalPixels) * 100;
return {
pixelsDifferent: numDiffPixels,
totalPixels,
percentDifferent: percentDifferent.toFixed(2),
passed: percentDifferent < 2.0, // < 2% difference = pass
};
}
/**
* Generate HTML report
*/
function generateReport(results, outputPath) {
const timestamp = new Date().toISOString();
const passed = results.filter(r => r.passed).length;
const failed = results.filter(r => !r.passed).length;
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>MaterialX Verification Report</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
h1 { color: #333; }
.summary {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stats {
display: flex;
gap: 20px;
margin-top: 15px;
}
.stat {
padding: 10px 20px;
border-radius: 4px;
font-weight: bold;
}
.stat.passed { background: #d4edda; color: #155724; }
.stat.failed { background: #f8d7da; color: #721c24; }
.test-card {
background: white;
padding: 20px;
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.test-card.passed { border-left: 4px solid #28a745; }
.test-card.failed { border-left: 4px solid #dc3545; }
.comparison {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
margin-top: 15px;
}
.comparison img {
width: 100%;
border: 1px solid #ddd;
border-radius: 4px;
}
.label {
font-size: 12px;
color: #666;
margin-bottom: 5px;
font-weight: bold;
}
.metrics {
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
font-family: monospace;
font-size: 13px;
}
.timestamp {
color: #666;
font-size: 14px;
}
</style>
</head>
<body>
<h1>MaterialX Verification Report</h1>
<div class="summary">
<h2>Summary</h2>
<p class="timestamp">Generated: ${timestamp}</p>
<div class="stats">
<div class="stat passed">✓ ${passed} Passed</div>
<div class="stat failed">✗ ${failed} Failed</div>
<div class="stat">Total: ${results.length}</div>
</div>
</div>
${results.map(result => `
<div class="test-card ${result.passed ? 'passed' : 'failed'}">
<h3>${result.passed ? '✓' : '✗'} ${result.material}</h3>
<div class="comparison">
<div>
<div class="label">TinyUSDZ Renderer</div>
<img src="${path.relative(path.dirname(outputPath), result.tinyusdz)}" alt="TinyUSDZ" />
</div>
<div>
<div class="label">Reference (MaterialX)</div>
<img src="${path.relative(path.dirname(outputPath), result.reference)}" alt="Reference" />
</div>
<div>
<div class="label">Difference (highlighted)</div>
<img src="${path.relative(path.dirname(outputPath), result.diff)}" alt="Diff" />
</div>
</div>
<div class="metrics">
<div>Pixels Different: ${result.comparison.pixelsDifferent} / ${result.comparison.totalPixels}</div>
<div>Difference: ${result.comparison.percentDifferent}%</div>
<div>Status: ${result.passed ? 'PASSED (< 2% difference)' : 'FAILED (≥ 2% difference)'}</div>
</div>
</div>
`).join('\n')}
</body>
</html>`;
fs.writeFileSync(outputPath, html);
console.log(`\n📊 Report generated: ${outputPath}`);
}
/**
* Main verification command
*/
async function verify(options) {
console.log('🚀 MaterialX Verification Tool\n');
const browser = await launchBrowser(options.gpu);
const results = [];
try {
// Test materials list
const testMaterials = options.materials
? options.materials.split(',')
: ['brass', 'glass', 'gold', 'copper'];
for (const material of testMaterials) {
console.log(`\n📦 Testing material: ${material}`);
// Paths for test HTML pages
const tinyusdHtmlPath = path.join(__dirname, 'tests', 'render-tinyusdz.html');
const referencHtmlPath = path.join(__dirname, 'tests', 'render-reference.html');
// Output paths
const tinyusdOutput = path.join(SCREENSHOTS_DIR, `tinyusdz-${material}.png`);
const referenceOutput = path.join(SCREENSHOTS_DIR, `reference-${material}.png`);
const diffOutput = path.join(DIFFS_DIR, `diff-${material}.png`);
// Render with TinyUSDZ
console.log(' Rendering with TinyUSDZ...');
const tinySuccess = await renderMaterial(
browser,
tinyusdHtmlPath,
material,
tinyusdOutput,
{ verbose: options.verbose }
);
// Render with reference implementation
console.log(' Rendering with MaterialX reference...');
const refSuccess = await renderMaterial(
browser,
referencHtmlPath,
material,
referenceOutput,
{ verbose: options.verbose }
);
if (!tinySuccess || !refSuccess) {
console.log(` ⚠ Skipping comparison (render failed)`);
continue;
}
// Compare images
console.log(' Comparing images...');
const comparison = compareImages(tinyusdOutput, referenceOutput, diffOutput);
const result = {
material,
tinyusdz: tinyusdOutput,
reference: referenceOutput,
diff: diffOutput,
comparison,
passed: comparison.passed,
};
results.push(result);
console.log(` ${comparison.passed ? '✓' : '✗'} Difference: ${comparison.percentDifferent}%`);
}
// Generate report
const reportPath = path.join(OUTPUT_DIR, 'report.html');
generateReport(results, reportPath);
// Print summary
console.log('\n' + '='.repeat(60));
console.log('SUMMARY');
console.log('='.repeat(60));
const passed = results.filter(r => r.passed).length;
const failed = results.filter(r => !r.passed).length;
console.log(`✓ Passed: ${passed}`);
console.log(`✗ Failed: ${failed}`);
console.log(`Total: ${results.length}`);
console.log('='.repeat(60));
// Exit with appropriate code
process.exit(failed > 0 ? 1 : 0);
} finally {
await browser.close();
}
}
// CLI Definition
program
.name('verify-materialx')
.description('Verify MaterialX rendering with headless Chrome')
.version('1.0.0');
program
.command('render')
.description('Render and compare materials')
.option('-m, --materials <list>', 'Comma-separated list of materials to test', 'brass,glass,gold,copper')
.option('--gpu', 'Use GPU acceleration (default: SwiftShader)', false)
.option('-v, --verbose', 'Verbose output', false)
.action(verify);
program
.command('clean')
.description('Clean verification results directory')
.action(() => {
if (fs.existsSync(OUTPUT_DIR)) {
fs.rmSync(OUTPUT_DIR, { recursive: true });
console.log('✓ Cleaned verification results');
}
});
program.parse();