Add texture colorspace support and colorspace test USDA files

- Enhanced material-serializer to include texture asset identifiers and colorspace metadata
  * Added colorSpaceToString() helper for 15+ colorspace enum values
  * Pass RenderScene to serializeMaterial() for texture information access
  * Include texture metadata (width, height, channels, colorSpace, usdColorSpace)
  * Reorganize OpenPBR parameters into grouped layers (base, specular, transmission, etc.)

- Added 8 MaterialX + USDA test files with colorspace variations:
  * materialx-textured-simple.usda - basic textured material
  * materialx-srgb-ldr.usda - standard sRGB color texture
  * materialx-linear-srgb.usda - raw colorspace (non-color data)
  * materialx-aces-cg.usda - ACES CG for HDR VFX workflows
  * materialx-aces2065-1.usda - ACES 2065-1 for DCI cinema
  * materialx-rec709-linear.usda - linear Rec.709 for broadcast video
  * materialx-rec709-gamma22.usda - gamma 2.2 Rec.709 (sRGB-like)
  * materialx-displayp3.usda - Display P3 for modern displays

- Verified colorspace information correctly passes from C++ to JavaScript/WASM
  * All test files export via dump-materialx-cli.js with proper colorSpace values
  * JSON includes both colorSpace (processed) and usdColorSpace (original intent) fields

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Syoyo Fujita
2025-11-07 06:23:07 +09:00
parent 321132b6e8
commit f9e030d6bf
11 changed files with 570 additions and 39 deletions

View File

@@ -0,0 +1,56 @@
#usda 1.0
(
doc = "MaterialX material with ACES CG colorspace (HDR VFX workflow)"
metersPerUnit = 1
upAxis = "Y"
defaultPrim = "World"
)
def Xform "World"
{
def Sphere "ACESphere"
{
double radius = 1.0
rel material:binding = </World/_materials/ACESMaterial>
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "vertex"
)
}
def Scope "_materials"
{
def Material "ACESMaterial"
{
token outputs:surface.connect = </World/_materials/ACESMaterial/OpenPBRSurface.outputs:surface>
def Shader "OpenPBRSurface"
{
uniform token info:id = "OpenPBRSurface"
color3f inputs:base_color.connect = </World/_materials/ACESMaterial/ACESTexture.outputs:rgb>
float inputs:base_weight = 1.0
float inputs:base_metalness = 0.5
float inputs:specular_roughness = 0.3
token outputs:surface
}
def Shader "ACESTexture"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/checkerboard.png@ (
colorSpace = "acescg"
)
string inputs:filtertype = "linear"
float2 inputs:texcoord.connect = </World/_materials/ACESMaterial/PrimvarNode.outputs:result>
color3f outputs:rgb
}
def Shader "PrimvarNode"
{
uniform token info:id = "UsdPrimvarReader_float2"
string inputs:varname = "st"
float2 outputs:result
}
}
}
}

View File

@@ -0,0 +1,56 @@
#usda 1.0
(
doc = "MaterialX material with ACES 2065-1 colorspace (DCI cinema)"
metersPerUnit = 1
upAxis = "Y"
defaultPrim = "World"
)
def Xform "World"
{
def Cube "ACES2065Cube"
{
double size = 1.0
rel material:binding = </World/_materials/ACES2065Material>
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "vertex"
)
}
def Scope "_materials"
{
def Material "ACES2065Material"
{
token outputs:surface.connect = </World/_materials/ACES2065Material/OpenPBRSurface.outputs:surface>
def Shader "OpenPBRSurface"
{
uniform token info:id = "OpenPBRSurface"
color3f inputs:base_color.connect = </World/_materials/ACES2065Material/ACESTexture.outputs:rgb>
float inputs:base_weight = 1.0
float inputs:base_metalness = 0.0
float inputs:specular_roughness = 0.4
token outputs:surface
}
def Shader "ACESTexture"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/checkerboard.png@ (
colorSpace = "aces2065-1"
)
string inputs:filtertype = "linear"
float2 inputs:texcoord.connect = </World/_materials/ACES2065Material/PrimvarNode.outputs:result>
color3f outputs:rgb
}
def Shader "PrimvarNode"
{
uniform token info:id = "UsdPrimvarReader_float2"
string inputs:varname = "st"
float2 outputs:result
}
}
}
}

View File

@@ -0,0 +1,56 @@
#usda 1.0
(
doc = "MaterialX material with Display P3 colorspace (modern displays)"
metersPerUnit = 1
upAxis = "Y"
defaultPrim = "World"
)
def Xform "World"
{
def Cube "DisplayP3Cube"
{
double size = 1.0
rel material:binding = </World/_materials/DisplayP3Material>
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "vertex"
)
}
def Scope "_materials"
{
def Material "DisplayP3Material"
{
token outputs:surface.connect = </World/_materials/DisplayP3Material/OpenPBRSurface.outputs:surface>
def Shader "OpenPBRSurface"
{
uniform token info:id = "OpenPBRSurface"
color3f inputs:base_color.connect = </World/_materials/DisplayP3Material/DisplayP3Texture.outputs:rgb>
float inputs:base_weight = 1.0
float inputs:base_metalness = 0.0
float inputs:specular_roughness = 0.5
token outputs:surface
}
def Shader "DisplayP3Texture"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/checkerboard.png@ (
colorSpace = "srgb_displayp3"
)
string inputs:filtertype = "linear"
float2 inputs:texcoord.connect = </World/_materials/DisplayP3Material/PrimvarNode.outputs:result>
color3f outputs:rgb
}
def Shader "PrimvarNode"
{
uniform token info:id = "UsdPrimvarReader_float2"
string inputs:varname = "st"
float2 outputs:result
}
}
}
}

View File

@@ -0,0 +1,55 @@
#usda 1.0
(
doc = "MaterialX material with linear sRGB texture (raw/non-color data)"
metersPerUnit = 1
upAxis = "Y"
defaultPrim = "World"
)
def Xform "World"
{
def Cube "RoughnessCube"
{
double size = 1.0
rel material:binding = </World/_materials/LinearMaterial>
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "vertex"
)
}
def Scope "_materials"
{
def Material "LinearMaterial"
{
token outputs:surface.connect = </World/_materials/LinearMaterial/OpenPBRSurface.outputs:surface>
def Shader "OpenPBRSurface"
{
uniform token info:id = "OpenPBRSurface"
color3f inputs:base_color.connect = </World/_materials/LinearMaterial/RoughnessTexture.outputs:rgb>
float inputs:base_weight = 1.0
float inputs:base_metalness = 0.0
float inputs:specular_roughness = 0.5
token outputs:surface
}
def Shader "RoughnessTexture"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/checkerboard.png@
token inputs:sourceColorSpace = "raw"
string inputs:filtertype = "linear"
float2 inputs:texcoord.connect = </World/_materials/LinearMaterial/PrimvarNode.outputs:result>
color3f outputs:rgb
}
def Shader "PrimvarNode"
{
uniform token info:id = "UsdPrimvarReader_float2"
string inputs:varname = "st"
float2 outputs:result
}
}
}
}

View File

@@ -0,0 +1,56 @@
#usda 1.0
(
doc = "MaterialX material with gamma 2.2 Rec.709 colorspace (sRGB-like)"
metersPerUnit = 1
upAxis = "Y"
defaultPrim = "World"
)
def Xform "World"
{
def Cube "Gamma22Cube"
{
double size = 1.0
rel material:binding = </World/_materials/Gamma22Material>
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "vertex"
)
}
def Scope "_materials"
{
def Material "Gamma22Material"
{
token outputs:surface.connect = </World/_materials/Gamma22Material/OpenPBRSurface.outputs:surface>
def Shader "OpenPBRSurface"
{
uniform token info:id = "OpenPBRSurface"
color3f inputs:base_color.connect = </World/_materials/Gamma22Material/Gamma22Texture.outputs:rgb>
float inputs:base_weight = 1.0
float inputs:base_metalness = 0.1
float inputs:specular_roughness = 0.7
token outputs:surface
}
def Shader "Gamma22Texture"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/checkerboard.png@ (
colorSpace = "g22_rec709"
)
string inputs:filtertype = "linear"
float2 inputs:texcoord.connect = </World/_materials/Gamma22Material/PrimvarNode.outputs:result>
color3f outputs:rgb
}
def Shader "PrimvarNode"
{
uniform token info:id = "UsdPrimvarReader_float2"
string inputs:varname = "st"
float2 outputs:result
}
}
}
}

View File

@@ -0,0 +1,56 @@
#usda 1.0
(
doc = "MaterialX material with linear Rec.709 colorspace (broadcast video)"
metersPerUnit = 1
upAxis = "Y"
defaultPrim = "World"
)
def Xform "World"
{
def Sphere "Rec709Sphere"
{
double radius = 1.0
rel material:binding = </World/_materials/Rec709Material>
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "vertex"
)
}
def Scope "_materials"
{
def Material "Rec709Material"
{
token outputs:surface.connect = </World/_materials/Rec709Material/OpenPBRSurface.outputs:surface>
def Shader "OpenPBRSurface"
{
uniform token info:id = "OpenPBRSurface"
color3f inputs:base_color.connect = </World/_materials/Rec709Material/Rec709Texture.outputs:rgb>
float inputs:base_weight = 1.0
float inputs:base_metalness = 0.2
float inputs:specular_roughness = 0.6
token outputs:surface
}
def Shader "Rec709Texture"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/checkerboard.png@ (
colorSpace = "lin_rec709"
)
string inputs:filtertype = "linear"
float2 inputs:texcoord.connect = </World/_materials/Rec709Material/PrimvarNode.outputs:result>
color3f outputs:rgb
}
def Shader "PrimvarNode"
{
uniform token info:id = "UsdPrimvarReader_float2"
string inputs:varname = "st"
float2 outputs:result
}
}
}
}

View File

@@ -0,0 +1,55 @@
#usda 1.0
(
doc = "MaterialX material with sRGB LDR texture (standard color texture)"
metersPerUnit = 1
upAxis = "Y"
defaultPrim = "World"
)
def Xform "World"
{
def Cube "ColorCube"
{
double size = 1.0
rel material:binding = </World/_materials/SRGBMaterial>
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "vertex"
)
}
def Scope "_materials"
{
def Material "SRGBMaterial"
{
token outputs:surface.connect = </World/_materials/SRGBMaterial/OpenPBRSurface.outputs:surface>
def Shader "OpenPBRSurface"
{
uniform token info:id = "OpenPBRSurface"
color3f inputs:base_color.connect = </World/_materials/SRGBMaterial/BaseTexture.outputs:rgb>
float inputs:base_weight = 1.0
float inputs:base_metalness = 0.0
float inputs:specular_roughness = 0.5
token outputs:surface
}
def Shader "BaseTexture"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/checkerboard.png@
token inputs:sourceColorSpace = "sRGB"
string inputs:filtertype = "linear"
float2 inputs:texcoord.connect = </World/_materials/SRGBMaterial/PrimvarNode.outputs:result>
color3f outputs:rgb
}
def Shader "PrimvarNode"
{
uniform token info:id = "UsdPrimvarReader_float2"
string inputs:varname = "st"
float2 outputs:result
}
}
}
}

View File

@@ -0,0 +1,58 @@
#usda 1.0
(
doc = "Simple MaterialX material with texture for testing"
metersPerUnit = 1
upAxis = "Y"
defaultPrim = "World"
)
def Xform "World"
{
def Cube "TexturedCube"
{
double size = 1.0
rel material:binding = </World/_materials/TexturedMaterial>
texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1)] (
interpolation = "vertex"
)
}
def Scope "_materials"
{
def Material "TexturedMaterial" (
prepend apiSchemas = ["MaterialXConfigAPI"]
)
{
uniform string config:mtlx:version = "1.38"
token outputs:surface.connect = </World/_materials/TexturedMaterial/OpenPBRSurface.outputs:surface>
token outputs:mtlx:surface.connect = </World/_materials/TexturedMaterial/OpenPBRSurface.outputs:surface>
def Shader "OpenPBRSurface"
{
uniform token info:id = "OpenPBRSurface"
color3f inputs:base_color.connect = </World/_materials/TexturedMaterial/TextureNode.outputs:rgb>
float inputs:base_weight = 1.0
float inputs:base_metalness = 0.0
float inputs:specular_roughness = 0.5
token outputs:surface
}
def Shader "TextureNode"
{
uniform token info:id = "UsdUVTexture"
asset inputs:file = @./textures/checkerboard.png@
string inputs:filtertype = "linear"
float2 inputs:texcoord.connect = </World/_materials/TexturedMaterial/PrimvarNode.outputs:result>
color3f outputs:rgb
}
def Shader "PrimvarNode"
{
uniform token info:id = "UsdPrimvarReader_float2"
string inputs:varname = "st"
float2 outputs:result
}
}
}
}

View File

@@ -23,57 +23,137 @@ std::string vec3ToJson(const T& vec) {
// Serialize OpenPBRSurfaceShader to JSON
std::string serializeOpenPBRToJson(const OpenPBRSurfaceShader& shader) {
std::string serializeOpenPBRToJson(const OpenPBRSurfaceShader& shader, const RenderScene* renderScene = nullptr) {
std::stringstream json;
json << "{";
json << "\"type\": \"OpenPBRSurfaceShader\",";
// Helper function to convert ColorSpace enum to string
auto colorSpaceToString = [](tydra::ColorSpace cs) -> const char* {
switch (cs) {
case tydra::ColorSpace::sRGB: return "sRGB";
case tydra::ColorSpace::Lin_sRGB: return "lin_sRGB";
case tydra::ColorSpace::Rec709: return "rec709";
case tydra::ColorSpace::Lin_Rec709: return "lin_rec709";
case tydra::ColorSpace::g22_Rec709: return "g22_rec709";
case tydra::ColorSpace::g18_Rec709: return "g18_rec709";
case tydra::ColorSpace::sRGB_Texture: return "srgb_texture";
case tydra::ColorSpace::Raw: return "raw";
case tydra::ColorSpace::Lin_ACEScg: return "lin_acescg";
case tydra::ColorSpace::ACES2065_1: return "aces2065_1";
case tydra::ColorSpace::Lin_Rec2020: return "lin_rec2020";
case tydra::ColorSpace::OCIO: return "ocio";
case tydra::ColorSpace::Lin_DisplayP3: return "lin_displayp3";
case tydra::ColorSpace::sRGB_DisplayP3: return "srgb_displayp3";
case tydra::ColorSpace::Custom: return "custom";
case tydra::ColorSpace::Unknown: return "unknown";
default: return "unknown";
}
};
// Macro to serialize shader parameter with optional texture info
auto serializeParam = [&](const std::string& paramName, auto 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 && texId < renderScene->textures.size()) {
const auto& uvTexture = renderScene->textures[texId];
if (uvTexture.texture_image_id >= 0 && uvTexture.texture_image_id < renderScene->images.size()) {
const auto& image = renderScene->images[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\": ";
if constexpr(std::is_same_v<decltype(param.value), float>) {
json << param.value;
} else if constexpr(std::is_same_v<decltype(param.value), std::array<float, 3>>) {
json << vec3ToJson(param.value);
} else {
json << param.value;
}
}
json << "}";
};
// Base layer
json << "\"base_weight\": " << shader.base_weight.value << ",";
json << "\"base_color\": " << vec3ToJson(shader.base_color.value) << ",";
json << "\"base_roughness\": " << shader.base_roughness.value << ",";
json << "\"base_metalness\": " << shader.base_metalness.value << ",";
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);
json << "},";
// Specular layer
json << "\"specular_weight\": " << shader.specular_weight.value << ",";
json << "\"specular_color\": " << vec3ToJson(shader.specular_color.value) << ",";
json << "\"specular_roughness\": " << shader.specular_roughness.value << ",";
json << "\"specular_ior\": " << shader.specular_ior.value << ",";
json << "\"specular_anisotropy\": " << shader.specular_anisotropy.value << ",";
json << "\"specular_rotation\": " << shader.specular_rotation.value << ",";
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);
json << "},";
// Transmission layer
json << "\"transmission_weight\": " << shader.transmission_weight.value << ",";
json << "\"transmission_color\": " << vec3ToJson(shader.transmission_color.value) << ",";
json << "\"transmission_depth\": " << shader.transmission_depth.value << ",";
json << "\"transmission_scatter\": " << vec3ToJson(shader.transmission_scatter.value) << ",";
json << "\"transmission_scatter_anisotropy\": " << shader.transmission_scatter_anisotropy.value << ",";
json << "\"transmission_dispersion\": " << shader.transmission_dispersion.value << ",";
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);
json << "},";
// Subsurface layer
json << "\"subsurface_weight\": " << shader.subsurface_weight.value << ",";
json << "\"subsurface_color\": " << vec3ToJson(shader.subsurface_color.value) << ",";
json << "\"subsurface_scale\": " << shader.subsurface_scale.value << ",";
json << "\"subsurface_anisotropy\": " << shader.subsurface_anisotropy.value << ",";
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);
json << "},";
// Coat layer
json << "\"coat_weight\": " << shader.coat_weight.value << ",";
json << "\"coat_color\": " << vec3ToJson(shader.coat_color.value) << ",";
json << "\"coat_roughness\": " << shader.coat_roughness.value << ",";
json << "\"coat_anisotropy\": " << shader.coat_anisotropy.value << ",";
json << "\"coat_rotation\": " << shader.coat_rotation.value << ",";
json << "\"coat_ior\": " << shader.coat_ior.value << ",";
json << "\"coat_affect_color\": " << vec3ToJson(shader.coat_affect_color.value) << ",";
json << "\"coat_affect_roughness\": " << shader.coat_affect_roughness.value << ",";
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);
json << "},";
// Emission
json << "\"emission_luminance\": " << shader.emission_luminance.value << ",";
json << "\"emission_color\": " << vec3ToJson(shader.emission_color.value) << ",";
json << "\"emission\": {";
serializeParam("emission_luminance", shader.emission_luminance); json << ",";
serializeParam("emission_color", shader.emission_color);
json << "},";
// Geometry
json << "\"opacity\": " << shader.opacity.value << ",";
json << "\"normal\": " << vec3ToJson(shader.normal.value) << ",";
json << "\"tangent\": " << vec3ToJson(shader.tangent.value);
json << "\"geometry\": {";
serializeParam("opacity", shader.opacity); json << ",";
serializeParam("normal", shader.normal); json << ",";
serializeParam("tangent", shader.tangent);
json << "}";
json << "}";
return json.str();
@@ -161,7 +241,8 @@ std::string serializeOpenPBRToXml(const OpenPBRSurfaceShader& shader) {
nonstd::expected<std::string, std::string> serializeMaterial(
const RenderMaterial& material,
SerializationFormat format) {
SerializationFormat format,
const RenderScene* renderScene) {
if (format == SerializationFormat::JSON) {
std::stringstream json;
@@ -180,7 +261,7 @@ nonstd::expected<std::string, std::string> serializeMaterial(
}
if (material.openPBRShader.has_value()) {
json << ",\"openPBRShader\": " << serializeOpenPBRToJson(*material.openPBRShader);
json << ",\"openPBR\": " << serializeOpenPBRToJson(*material.openPBRShader, renderScene);
}
json << "}";

View File

@@ -19,9 +19,11 @@ enum class SerializationFormat {
// Serialize a RenderMaterial to JSON or XML format
// Returns serialized string on success, error message on failure
// Pass renderScene to include texture asset identifiers in serialization
nonstd::expected<std::string, std::string> serializeMaterial(
const RenderMaterial& material,
SerializationFormat format);
SerializationFormat format,
const RenderScene* renderScene = nullptr);
} // namespace tydra
} // namespace tinyusdz

View File

@@ -1360,8 +1360,8 @@ class TinyUSDZLoaderNative {
return result;
}
// Use the new serialization function
auto serialized = tinyusdz::tydra::serializeMaterial(material, serFormat);
// Use the new serialization function with RenderScene for texture info
auto serialized = tinyusdz::tydra::serializeMaterial(material, serFormat, &render_scene_);
if (serialized.has_value()) {
result.set("data", serialized.value());