mirror of
https://github.com/lighttransport/tinyusdz.git
synced 2026-01-18 01:11:17 +01:00
Merge branch 'mtlx-2025' into usdlux-2025
This commit is contained in:
218
models/cube-mtlx-texture.usda
Executable file
218
models/cube-mtlx-texture.usda
Executable 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"]
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
3
src/external/yyjson.c
vendored
3
src/external/yyjson.c
vendored
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 << "}";
|
||||
|
||||
@@ -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>> ¶m,
|
||||
const std::string ¶m_name, ShaderParam<Dty> &dst_param) {
|
||||
const std::string ¶m_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 {
|
||||
|
||||
@@ -2778,7 +2778,8 @@ class RenderSceneConverter {
|
||||
bool ConvertPreviewSurfaceShaderParam(
|
||||
const RenderSceneConverterEnv &env, const Path &shader_abs_path,
|
||||
const TypedAttributeWithFallback<Animatable<T>> ¶m,
|
||||
const std::string ¶m_name, ShaderParam<Dty> &dst_param);
|
||||
const std::string ¶m_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.
|
||||
|
||||
@@ -95,6 +95,32 @@ static json vec3ToJson(const vec3& v) {
|
||||
return json::array({v[0], v[1], v[2]});
|
||||
}
|
||||
|
||||
// Helper function to set a parameter in JSON with optional grouping
|
||||
// For flattened: "base_color" stays as inputs["base_color"]
|
||||
// For grouped: "base_color" becomes inputs["base"]["color"]
|
||||
static void setJsonParameter(json& inputs, const std::string& param_name, const json& value, bool use_grouped) {
|
||||
if (!use_grouped) {
|
||||
// Flattened format: base_color
|
||||
inputs[param_name] = value;
|
||||
} else {
|
||||
// Grouped format: base.color
|
||||
size_t underscore_pos = param_name.find('_');
|
||||
if (underscore_pos != std::string::npos) {
|
||||
std::string group = param_name.substr(0, underscore_pos);
|
||||
std::string property = param_name.substr(underscore_pos + 1);
|
||||
|
||||
// Create group object if it doesn't exist
|
||||
if (!inputs.contains(group)) {
|
||||
inputs[group] = json::object();
|
||||
}
|
||||
inputs[group][property] = value;
|
||||
} else {
|
||||
// No underscore, keep as-is
|
||||
inputs[param_name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert vec2 to JSON array
|
||||
// Static function that may not be used currently
|
||||
#ifdef __clang__
|
||||
@@ -251,11 +277,11 @@ bool ThreeJSMaterialExporter::ExportMaterial(const RenderMaterial& material,
|
||||
if (has_openpbr && options.use_webgpu) {
|
||||
// Export as WebGPU node material
|
||||
output["type"] = "NodeMaterial";
|
||||
output["nodes"] = ConvertOpenPBRToNodeMaterial(material.openPBRShader.value());
|
||||
output["nodes"] = ConvertOpenPBRToNodeMaterial(material.openPBRShader.value(), options);
|
||||
} else if (has_openpbr && options.generate_fallback) {
|
||||
// Export as WebGL MeshPhysicalMaterial
|
||||
output["type"] = "MeshPhysicalMaterial";
|
||||
json params = ConvertOpenPBRToPhysicalMaterial(material.openPBRShader.value());
|
||||
json params = ConvertOpenPBRToPhysicalMaterial(material.openPBRShader.value(), options);
|
||||
for (auto it = params.items().begin(); it != params.items().end(); ++it) {
|
||||
output[it.key()] = it.value();
|
||||
}
|
||||
@@ -285,7 +311,7 @@ bool ThreeJSMaterialExporter::ExportMaterial(const RenderMaterial& material,
|
||||
return true;
|
||||
}
|
||||
|
||||
json ThreeJSMaterialExporter::ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceShader& shader) {
|
||||
json ThreeJSMaterialExporter::ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceShader& shader, const ExportOptions& options) {
|
||||
json nodes = json::object();
|
||||
|
||||
// Create OpenPBR surface node
|
||||
@@ -295,27 +321,31 @@ json ThreeJSMaterialExporter::ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceS
|
||||
{"inputs", json::object()}
|
||||
};
|
||||
|
||||
bool use_grouped = options.use_grouped_parameters;
|
||||
|
||||
// Map all OpenPBR parameters
|
||||
auto add_param = [&](const std::string& name, const auto& param) {
|
||||
json value;
|
||||
if (param.is_texture()) {
|
||||
surface_node["inputs"][name] = {
|
||||
value = {
|
||||
{"type", "texture"},
|
||||
{"textureId", param.texture_id}
|
||||
};
|
||||
} else {
|
||||
surface_node["inputs"][name] = param.value;
|
||||
value = param.value;
|
||||
}
|
||||
setJsonParameter(surface_node["inputs"], name, value, use_grouped);
|
||||
};
|
||||
|
||||
// Base layer
|
||||
add_param("base_weight", shader.base_weight);
|
||||
surface_node["inputs"]["base_color"] = vec3ToJson(shader.base_color.value);
|
||||
setJsonParameter(surface_node["inputs"], "base_color", vec3ToJson(shader.base_color.value), use_grouped);
|
||||
add_param("base_roughness", shader.base_roughness);
|
||||
add_param("base_metalness", shader.base_metalness);
|
||||
|
||||
// Specular layer
|
||||
add_param("specular_weight", shader.specular_weight);
|
||||
surface_node["inputs"]["specular_color"] = vec3ToJson(shader.specular_color.value);
|
||||
setJsonParameter(surface_node["inputs"], "specular_color", vec3ToJson(shader.specular_color.value), use_grouped);
|
||||
add_param("specular_roughness", shader.specular_roughness);
|
||||
add_param("specular_ior", shader.specular_ior);
|
||||
add_param("specular_ior_level", shader.specular_ior_level);
|
||||
@@ -324,42 +354,42 @@ json ThreeJSMaterialExporter::ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceS
|
||||
|
||||
// Transmission
|
||||
add_param("transmission_weight", shader.transmission_weight);
|
||||
surface_node["inputs"]["transmission_color"] = vec3ToJson(shader.transmission_color.value);
|
||||
setJsonParameter(surface_node["inputs"], "transmission_color", vec3ToJson(shader.transmission_color.value), use_grouped);
|
||||
add_param("transmission_depth", shader.transmission_depth);
|
||||
surface_node["inputs"]["transmission_scatter"] = vec3ToJson(shader.transmission_scatter.value);
|
||||
setJsonParameter(surface_node["inputs"], "transmission_scatter", vec3ToJson(shader.transmission_scatter.value), use_grouped);
|
||||
add_param("transmission_scatter_anisotropy", shader.transmission_scatter_anisotropy);
|
||||
add_param("transmission_dispersion", shader.transmission_dispersion);
|
||||
|
||||
// Subsurface
|
||||
add_param("subsurface_weight", shader.subsurface_weight);
|
||||
surface_node["inputs"]["subsurface_color"] = vec3ToJson(shader.subsurface_color.value);
|
||||
surface_node["inputs"]["subsurface_radius"] = vec3ToJson(shader.subsurface_radius.value);
|
||||
setJsonParameter(surface_node["inputs"], "subsurface_color", vec3ToJson(shader.subsurface_color.value), use_grouped);
|
||||
setJsonParameter(surface_node["inputs"], "subsurface_radius", vec3ToJson(shader.subsurface_radius.value), use_grouped);
|
||||
add_param("subsurface_scale", shader.subsurface_scale);
|
||||
add_param("subsurface_anisotropy", shader.subsurface_anisotropy);
|
||||
|
||||
// Sheen
|
||||
add_param("sheen_weight", shader.sheen_weight);
|
||||
surface_node["inputs"]["sheen_color"] = vec3ToJson(shader.sheen_color.value);
|
||||
setJsonParameter(surface_node["inputs"], "sheen_color", vec3ToJson(shader.sheen_color.value), use_grouped);
|
||||
add_param("sheen_roughness", shader.sheen_roughness);
|
||||
|
||||
// Coat
|
||||
add_param("coat_weight", shader.coat_weight);
|
||||
surface_node["inputs"]["coat_color"] = vec3ToJson(shader.coat_color.value);
|
||||
setJsonParameter(surface_node["inputs"], "coat_color", vec3ToJson(shader.coat_color.value), use_grouped);
|
||||
add_param("coat_roughness", shader.coat_roughness);
|
||||
add_param("coat_anisotropy", shader.coat_anisotropy);
|
||||
add_param("coat_rotation", shader.coat_rotation);
|
||||
add_param("coat_ior", shader.coat_ior);
|
||||
surface_node["inputs"]["coat_affect_color"] = vec3ToJson(shader.coat_affect_color.value);
|
||||
setJsonParameter(surface_node["inputs"], "coat_affect_color", vec3ToJson(shader.coat_affect_color.value), use_grouped);
|
||||
add_param("coat_affect_roughness", shader.coat_affect_roughness);
|
||||
|
||||
// Emission
|
||||
add_param("emission_luminance", shader.emission_luminance);
|
||||
surface_node["inputs"]["emission_color"] = vec3ToJson(shader.emission_color.value);
|
||||
setJsonParameter(surface_node["inputs"], "emission_color", vec3ToJson(shader.emission_color.value), use_grouped);
|
||||
|
||||
// Geometry
|
||||
add_param("opacity", shader.opacity);
|
||||
surface_node["inputs"]["normal"] = vec3ToJson(shader.normal.value);
|
||||
surface_node["inputs"]["tangent"] = vec3ToJson(shader.tangent.value);
|
||||
setJsonParameter(surface_node["inputs"], "normal", vec3ToJson(shader.normal.value), use_grouped);
|
||||
setJsonParameter(surface_node["inputs"], "tangent", vec3ToJson(shader.tangent.value), use_grouped);
|
||||
|
||||
nodes["surface"] = surface_node;
|
||||
|
||||
@@ -378,7 +408,8 @@ json ThreeJSMaterialExporter::ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceS
|
||||
return nodes;
|
||||
}
|
||||
|
||||
json ThreeJSMaterialExporter::ConvertOpenPBRToPhysicalMaterial(const OpenPBRSurfaceShader& shader) {
|
||||
json ThreeJSMaterialExporter::ConvertOpenPBRToPhysicalMaterial(const OpenPBRSurfaceShader& shader, const ExportOptions& options) {
|
||||
(void)options; // Options reserved for future use (e.g., grouped userData)
|
||||
json params = json::object();
|
||||
|
||||
// Map OpenPBR to MeshPhysicalMaterial parameters
|
||||
|
||||
@@ -49,6 +49,7 @@ public:
|
||||
std::string texture_path = ""; ///< External texture directory path
|
||||
bool export_mtlx = false; ///< Export as MaterialX document
|
||||
std::string color_space = "sRGB"; ///< Target color space for textures
|
||||
bool use_grouped_parameters = false; ///< Use grouped parameters (e.g., base.color) instead of flattened (e.g., base_color)
|
||||
};
|
||||
|
||||
/// Export entire RenderScene to Three.js format
|
||||
@@ -73,10 +74,10 @@ public:
|
||||
|
||||
private:
|
||||
/// Convert OpenPBR shader to Three.js node material
|
||||
json ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceShader& shader);
|
||||
json ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceShader& shader, const ExportOptions& options);
|
||||
|
||||
/// Convert OpenPBR shader to Three.js MeshPhysicalMaterial (WebGL fallback)
|
||||
json ConvertOpenPBRToPhysicalMaterial(const OpenPBRSurfaceShader& shader);
|
||||
json ConvertOpenPBRToPhysicalMaterial(const OpenPBRSurfaceShader& shader, const ExportOptions& options);
|
||||
|
||||
/// Convert UsdPreviewSurface to Three.js materials
|
||||
json ConvertPreviewSurfaceToNodeMaterial(const PreviewSurfaceShader& shader);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1679,6 +1679,7 @@ bool USDAReader::Impl::Read(const uint32_t state_flags, bool as_primspec) {
|
||||
|
||||
RegisterReconstructCallback<Material>();
|
||||
RegisterReconstructCallback<Shader>();
|
||||
RegisterReconstructCallback<NodeGraph>();
|
||||
|
||||
RegisterReconstructCallback<Scope>();
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
123
tests/feat/mtlx/GROUPED_PARAMS_README.md
Normal file
123
tests/feat/mtlx/GROUPED_PARAMS_README.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Grouped and Flattened MaterialX Parameter Export
|
||||
|
||||
This implementation adds support for both **grouped** and **flattened** OpenPBR parameter naming in JSON export through the ThreeJSMaterialExporter.
|
||||
|
||||
## Summary
|
||||
|
||||
Added a new `use_grouped_parameters` option to `ThreeJSMaterialExporter::ExportOptions` that controls how MaterialX (OpenPBR) parameters are structured in JSON output.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Header File (`src/tydra/threejs-exporter.hh`)
|
||||
- Added `bool use_grouped_parameters` to `ExportOptions` struct
|
||||
|
||||
### 2. Implementation (`src/tydra/threejs-exporter.cc`)
|
||||
- Added `setJsonParameter()` helper function for parameter name transformation
|
||||
- Updated `ConvertOpenPBRToNodeMaterial()` to support both formats
|
||||
- Updated `ConvertOpenPBRToPhysicalMaterial()` signature for consistency
|
||||
- Updated `ExportMaterial()` to pass options through
|
||||
|
||||
## Output Formats
|
||||
|
||||
### Flattened Format (use_grouped_parameters = false)
|
||||
All parameters at the same level with underscore-separated names:
|
||||
```json
|
||||
{
|
||||
"inputs": {
|
||||
"base_color": [0.8, 0.2, 0.1],
|
||||
"base_weight": 1.0,
|
||||
"base_roughness": 0.5,
|
||||
"base_metalness": 0.0,
|
||||
"specular_weight": 1.0,
|
||||
"specular_color": [1.0, 1.0, 1.0],
|
||||
"specular_ior": 1.5,
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Grouped Format (use_grouped_parameters = true)
|
||||
Parameters organized into logical groups with shortened property names:
|
||||
```json
|
||||
{
|
||||
"inputs": {
|
||||
"base": {
|
||||
"color": [0.8, 0.2, 0.1],
|
||||
"weight": 1.0,
|
||||
"roughness": 0.5,
|
||||
"metalness": 0.0
|
||||
},
|
||||
"specular": {
|
||||
"weight": 1.0,
|
||||
"color": [1.0, 1.0, 1.0],
|
||||
"ior": 1.5,
|
||||
...
|
||||
},
|
||||
"coat": {...},
|
||||
"emission": {...},
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Parameter Groups
|
||||
|
||||
The grouped format organizes parameters into:
|
||||
- **base**: color, weight, roughness, metalness
|
||||
- **specular**: color, weight, ior, roughness, anisotropy, rotation, ior_level
|
||||
- **transmission**: color, weight, depth, scatter, scatter_anisotropy, dispersion
|
||||
- **coat**: color, weight, roughness, ior, anisotropy, rotation, affect_color, affect_roughness
|
||||
- **emission**: color, luminance
|
||||
- **subsurface**: color, weight, radius, scale, anisotropy
|
||||
- **sheen**: color, weight, roughness
|
||||
- **geometry params**: opacity, normal, tangent (kept at root level)
|
||||
|
||||
## Usage Example
|
||||
|
||||
```cpp
|
||||
#include "tydra/threejs-exporter.hh"
|
||||
|
||||
ThreeJSMaterialExporter exporter;
|
||||
ThreeJSMaterialExporter::ExportOptions options;
|
||||
|
||||
// Export with flattened parameters (default)
|
||||
options.use_grouped_parameters = false;
|
||||
json flattened_output;
|
||||
exporter.ExportMaterial(material, options, flattened_output);
|
||||
|
||||
// Export with grouped parameters
|
||||
options.use_grouped_parameters = true;
|
||||
json grouped_output;
|
||||
exporter.ExportMaterial(material, options, grouped_output);
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test to verify both formats:
|
||||
```bash
|
||||
cd tests/feat/mtlx
|
||||
make -f Makefile.grouped_params
|
||||
./test_grouped_params
|
||||
```
|
||||
|
||||
## Comparison with material-serializer.cc
|
||||
|
||||
Note that `material-serializer.cc` (used by the WASM bindings and `dump-materialx-cli.js`) has its own grouped format that uses full parameter names within groups:
|
||||
|
||||
```json
|
||||
{
|
||||
"base": {
|
||||
"base_weight": {"name": "base_weight", "type": "value", "value": 1.0},
|
||||
"base_color": {"name": "base_color", "type": "value", "value": [0.8, 0.2, 0.1]},
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The ThreeJSMaterialExporter provides a more compact grouped format suitable for Three.js node materials and WebGPU rendering.
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
- Default behavior is **flattened** (`use_grouped_parameters = false`)
|
||||
- Existing code continues to work without modification
|
||||
- Grouped format is opt-in via the options flag
|
||||
11
tests/feat/mtlx/Makefile.grouped_params
Normal file
11
tests/feat/mtlx/Makefile.grouped_params
Normal file
@@ -0,0 +1,11 @@
|
||||
CXX = g++
|
||||
CXXFLAGS = -std=c++14 -I../../../src -I../../../
|
||||
LDFLAGS = -L../../../build -ltinyusdz_static -lpthread
|
||||
|
||||
test_grouped_params: test_grouped_params.cc
|
||||
$(CXX) $(CXXFLAGS) -o test_grouped_params test_grouped_params.cc $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f test_grouped_params
|
||||
|
||||
.PHONY: clean
|
||||
102
tests/feat/mtlx/test_grouped_params.cc
Normal file
102
tests/feat/mtlx/test_grouped_params.cc
Normal file
@@ -0,0 +1,102 @@
|
||||
// Test for grouped/flattened parameter export functionality
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include "tydra/render-data.hh"
|
||||
#include "tydra/threejs-exporter.hh"
|
||||
|
||||
using namespace tinyusdz;
|
||||
using namespace tinyusdz::tydra;
|
||||
|
||||
int main() {
|
||||
// Create a simple OpenPBR material
|
||||
RenderMaterial material;
|
||||
material.name = "TestMaterial";
|
||||
material.handle = 12345;
|
||||
|
||||
// Create OpenPBR shader
|
||||
OpenPBRSurfaceShader shader;
|
||||
shader.handle = 67890;
|
||||
|
||||
// Set some basic parameters
|
||||
shader.base_weight.value = 1.0f;
|
||||
shader.base_color.value = {0.8f, 0.2f, 0.1f};
|
||||
shader.base_roughness.value = 0.5f;
|
||||
shader.base_metalness.value = 0.0f;
|
||||
|
||||
shader.specular_weight.value = 1.0f;
|
||||
shader.specular_color.value = {1.0f, 1.0f, 1.0f};
|
||||
shader.specular_ior.value = 1.5f;
|
||||
|
||||
shader.emission_luminance.value = 0.0f;
|
||||
shader.emission_color.value = {0.0f, 0.0f, 0.0f};
|
||||
|
||||
shader.coat_weight.value = 0.5f;
|
||||
shader.coat_color.value = {1.0f, 1.0f, 1.0f};
|
||||
shader.coat_roughness.value = 0.1f;
|
||||
|
||||
shader.opacity.value = 1.0f;
|
||||
shader.normal.value = {0.0f, 0.0f, 1.0f};
|
||||
shader.tangent.value = {1.0f, 0.0f, 0.0f};
|
||||
|
||||
material.openPBRShader = shader;
|
||||
|
||||
// Create exporter
|
||||
ThreeJSMaterialExporter exporter;
|
||||
|
||||
// Test 1: Flattened parameters (default)
|
||||
std::cout << "=== Test 1: Flattened Parameters (default) ===" << std::endl;
|
||||
{
|
||||
ThreeJSMaterialExporter::ExportOptions options;
|
||||
options.use_webgpu = true;
|
||||
options.use_grouped_parameters = false; // Flattened
|
||||
|
||||
json output;
|
||||
if (exporter.ExportMaterial(material, options, output)) {
|
||||
std::cout << "Flattened output:" << std::endl;
|
||||
std::cout << output.dump(2) << std::endl;
|
||||
|
||||
// Check if base_color exists in flattened format
|
||||
if (output["nodes"]["surface"]["inputs"].contains("base_color")) {
|
||||
std::cout << "✓ Found 'base_color' in flattened format" << std::endl;
|
||||
} else {
|
||||
std::cout << "✗ 'base_color' NOT found in flattened format" << std::endl;
|
||||
}
|
||||
} else {
|
||||
std::cout << "Error: " << exporter.GetError() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "\n=== Test 2: Grouped Parameters ===" << std::endl;
|
||||
{
|
||||
ThreeJSMaterialExporter::ExportOptions options;
|
||||
options.use_webgpu = true;
|
||||
options.use_grouped_parameters = true; // Grouped
|
||||
|
||||
json output;
|
||||
if (exporter.ExportMaterial(material, options, output)) {
|
||||
std::cout << "Grouped output:" << std::endl;
|
||||
std::cout << output.dump(2) << std::endl;
|
||||
|
||||
// Check if base.color exists in grouped format
|
||||
if (output["nodes"]["surface"]["inputs"].contains("base") &&
|
||||
output["nodes"]["surface"]["inputs"]["base"].contains("color")) {
|
||||
std::cout << "✓ Found 'base.color' in grouped format" << std::endl;
|
||||
} else {
|
||||
std::cout << "✗ 'base.color' NOT found in grouped format" << std::endl;
|
||||
}
|
||||
|
||||
// Check if specular.weight exists
|
||||
if (output["nodes"]["surface"]["inputs"].contains("specular") &&
|
||||
output["nodes"]["surface"]["inputs"]["specular"].contains("weight")) {
|
||||
std::cout << "✓ Found 'specular.weight' in grouped format" << std::endl;
|
||||
} else {
|
||||
std::cout << "✗ 'specular.weight' NOT found in grouped format" << std::endl;
|
||||
}
|
||||
} else {
|
||||
std::cout << "Error: " << exporter.GetError() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
BIN
web/demo/public/assets/fancy-teapot-mtlx.usdz
Executable file
BIN
web/demo/public/assets/fancy-teapot-mtlx.usdz
Executable file
Binary file not shown.
3
web/demo/public/assets/fancy-teapot.txt
Normal file
3
web/demo/public/assets/fancy-teapot.txt
Normal 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
13
web/js/.gitignore
vendored
Normal 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
|
||||
159
web/js/NODEMATERIAL_README.md
Normal file
159
web/js/NODEMATERIAL_README.md
Normal 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
|
||||
367
web/js/VERIFICATION_README.md
Normal file
367
web/js/VERIFICATION_README.md
Normal 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)
|
||||
109
web/js/convert-openpbr-to-mtlx.js
Normal file
109
web/js/convert-openpbr-to-mtlx.js
Normal 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;
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
1251
web/js/materialx.js
1251
web/js/materialx.js
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
219
web/js/tests/colorspace-test.js
Normal file
219
web/js/tests/colorspace-test.js
Normal 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 };
|
||||
212
web/js/tests/render-reference.html
Normal file
212
web/js/tests/render-reference.html
Normal 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>
|
||||
208
web/js/tests/render-tinyusdz.html
Normal file
208
web/js/tests/render-tinyusdz.html
Normal 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
423
web/js/verify-materialx.js
Normal 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();
|
||||
Reference in New Issue
Block a user