Merge branch 'anim-mtlx-phase3-fix3' into skinning

This commit is contained in:
Syoyo Fujita
2026-01-15 05:21:37 +09:00
19 changed files with 2754 additions and 53 deletions

View File

@@ -130,7 +130,8 @@ The `MtlxOpenPBRSurface` shader supports all OpenPBR specification parameters:
### Subsurface
- `subsurface_weight` (float)
- `subsurface_color` (color3)
- `subsurface_radius` (color3)
- `subsurface_radius` (float)
- `subsurface_radius_scale` (color3)
- `subsurface_scale` (float)
- `subsurface_anisotropy` (float)

View File

@@ -89,7 +89,8 @@ Subsurface scattering simulates light penetrating and scattering beneath the sur
|-------------------|------------------------|------|---------|------------------|------------------|-------|
| `subsurface_weight` | `inputs:subsurface_weight` | float | 0.0 | ❌ | — | **NOT SUPPORTED** |
| `subsurface_color` | `inputs:subsurface_color` | color3f | (0.8, 0.8, 0.8) | ❌ | — | **NOT SUPPORTED** |
| `subsurface_radius` | `inputs:subsurface_radius` | color3f | (1.0, 1.0, 1.0) | ❌ | — | **NOT SUPPORTED** |
| `subsurface_radius` | `inputs:subsurface_radius` | float | 1.0 | ❌ | — | **NOT SUPPORTED** |
| `subsurface_radius_scale` | `inputs:subsurface_radius_scale` | color3f | (1.0, 1.0, 1.0) | ❌ | — | **NOT SUPPORTED** |
| `subsurface_scale` | `inputs:subsurface_scale` | float | 1.0 | ❌ | — | **NOT SUPPORTED** |
| `subsurface_anisotropy` | `inputs:subsurface_anisotropy` | float | 0.0 | ❌ | — | **NOT SUPPORTED** |
@@ -188,17 +189,17 @@ Geometry properties affect surface normals and tangent space, used for bump mapp
| Base Layer | 4 | 3 | 1 | 0 |
| Specular | 7 | 2 | 3 | 2 |
| Transmission | 6 | 1 | 1 | 4 |
| Subsurface | 5 | 0 | 0 | 5 |
| Subsurface | 6 | 0 | 0 | 6 |
| Sheen | 3 | 3 | 0 | 0 |
| Coat | 8 | 2 | 1 | 5 |
| Emission | 2 | 2 | 0 | 0 |
| Geometry | 3 | 2 | 1 | 0 |
| **Total** | **38** | **15 (39%)** | **7 (18%)** | **16 (42%)** |
| **Total** | **39** | **15 (38%)** | **7 (18%)** | **17 (44%)** |
### Critical Limitations for Three.js
**❌ NOT SUPPORTED (requires custom shaders):**
1. **Subsurface Scattering** - All 5 parameters (weight, color, radius, scale, anisotropy)
1. **Subsurface Scattering** - All 6 parameters (weight, color, radius, radius_scale, scale, anisotropy)
2. **Transmission Effects** - Color, scatter, dispersion (4 parameters)
3. **Coat Advanced** - Color, anisotropy, affect properties (5 parameters)
4. **Specular Advanced** - IOR level (1 parameter)

View File

@@ -694,8 +694,16 @@ std::string print_prim_metas(const PrimMeta &meta, const uint32_t indent) {
}
for (const auto &item : meta.unregisteredMetas) {
// do not quote
ss << pprint::Indent(indent) << item.first << " = " << item.second << "\n";
// Quote string values, but keep non-string values as-is
std::string value_str = item.second;
// Check if the value looks like a string (unquoted) by checking if it needs quoting
// String values that are not already quoted should be quoted
if (!value_str.empty() && value_str.front() != '"' && value_str.front() != '\'' &&
value_str.front() != '[' && !std::isdigit(value_str.front()) &&
value_str != "None" && value_str.find("(") == std::string::npos) {
value_str = quote(value_str);
}
ss << pprint::Indent(indent) << item.first << " = " << value_str << "\n";
}
// TODO: deprecate meta.meta and remove it.
@@ -726,13 +734,13 @@ std::string print_attr_metas(const AttrMeta &meta, const uint32_t indent) {
if (meta.bindMaterialAs) {
ss << pprint::Indent(indent)
<< "bindMaterialAs = " << quote(to_string(meta.bindMaterialAs.value()))
<< "bindMaterialAs = " << to_string(meta.bindMaterialAs.value())
<< "\n";
}
if (meta.connectability) {
ss << pprint::Indent(indent)
<< "connectability = " << quote(to_string(meta.connectability.value()))
<< "connectability = " << to_string(meta.connectability.value())
<< "\n";
}
@@ -748,12 +756,12 @@ std::string print_attr_metas(const AttrMeta &meta, const uint32_t indent) {
if (meta.outputName) {
ss << pprint::Indent(indent)
<< "outputName = " << quote(to_string(meta.outputName.value())) << "\n";
<< "outputName = " << to_string(meta.outputName.value()) << "\n";
}
if (meta.renderType) {
ss << pprint::Indent(indent)
<< "renderType = " << quote(to_string(meta.renderType.value())) << "\n";
<< "renderType = " << to_string(meta.renderType.value()) << "\n";
}
if (meta.sdrMetadata) {
@@ -3220,7 +3228,7 @@ std::string to_string(const GeomMesh &mesh, const uint32_t indent,
indent + 1);
ss << print_typed_attr(mesh.holeIndices, "holeIndices", indent + 1);
ss << print_typed_token_attr(mesh.subdivisionScheme, "subdivisonScheme",
ss << print_typed_token_attr(mesh.subdivisionScheme, "subdivisionScheme",
indent + 1);
ss << print_typed_token_attr(mesh.interpolateBoundary, "interpolateBoundary",
indent + 1);

View File

@@ -4496,18 +4496,25 @@ bool ReconstructPrim<GeomMesh>(
(names[0] == "subsetFamily") &&
(names[2] == "familyType")) {
DCOUT("subsetFamily" << prop.first);
TypedAttributeWithFallback<GeomSubset::FamilyType> familyType{GeomSubset::FamilyType::Unrestricted};
if (table.count(prop.first)) {
// Already processed
} else if ((prop.second.value_type_name() == value::TypeTraits<value::token>::type_name()) &&
prop.second.is_attribute() &&
!prop.second.is_empty()) {
// Parse the token enum value
const Attribute &attr = prop.second.get_attribute();
TypedAttributeWithFallback<GeomSubset::FamilyType> familyType{GeomSubset::FamilyType::Unrestricted};
std::function<nonstd::expected<GeomSubset::FamilyType, std::string>(const std::string &)> fun = FamilyTypeHandler;
PARSE_UNIFORM_ENUM_PROPERTY(table, prop, prop.first,
GeomSubset::FamilyType, FamilyTypeHandler, GeomMesh,
familyType, options.strict_allowedToken_check)
// NOTE: Ignore metadataum of familyType.
// TODO: Validate familyName
mesh->subsetFamilyTypeMap[value::token(names[1])] = familyType.get_value();
if (!ParseUniformEnumProperty(prop.first, options.strict_allowedToken_check, fun, attr, &familyType, warn, err)) {
return false;
}
// NOTE: Ignore metadata of familyType.
// TODO: Validate familyName
mesh->subsetFamilyTypeMap[value::token(names[1])] = familyType.get_value();
table.insert(prop.first);
}
}
}

View File

@@ -4,6 +4,7 @@
#include "unicode-xid.hh"
#include "common-macros.inc"
#include "external/dtoa_milo.h"
#ifdef __SSE2__
#include <emmintrin.h>
@@ -1212,4 +1213,20 @@ bool GlobMatchPath(const std::string &pattern, const std::string &path) {
return p == pattern.size();
}
char *dtoa(float f, char *buf) {
// For float, use simple sprintf for now
// dtoa_milo is optimized for double and doesn't work well with float
int n = snprintf(buf, 384, "%.9g", static_cast<double>(f));
if (n < 0 || n >= 384) {
buf[0] = '0';
return &buf[1];
}
return &buf[n];
}
char *dtoa(double d, char *buf) {
// Use dtoa_milo for double precision
return dtoa_milo(d, buf);
}
} // namespace tinyusdz

View File

@@ -6,6 +6,7 @@
#include <sstream>
#include <cstring>
#include <map>
#include "str-util.hh"
#ifdef TINYUSDZ_ENABLE_THREAD
#include <thread>
@@ -46,7 +47,7 @@ struct ThreadedPrintConfig {
return 1;
#endif
}
};;;;;
};
// Global configuration (can be customized)
static ThreadedPrintConfig g_threaded_print_config;
@@ -194,6 +195,28 @@ void print_type<char>(OutputAdapter& out, const uint8_t* data) {
out.write(static_cast<int>(value));
}
// Specialization for double - print with full precision using dtoa
template<>
void print_type<double>(OutputAdapter& out, const uint8_t* data) {
double value;
std::memcpy(&value, data, sizeof(double));
char buf[384];
char *end = dtoa(value, buf);
*end = '\0';
out.write(std::string(buf));
}
// Specialization for float - print with full precision using dtoa
template<>
void print_type<float>(OutputAdapter& out, const uint8_t* data) {
float value;
std::memcpy(&value, data, sizeof(float));
char buf[384];
char *end = dtoa(value, buf);
*end = '\0';
out.write(std::string(buf));
}
// Unified print function for vector types
template<typename T, size_t N>
void print_vector(OutputAdapter& out, const uint8_t* data) {
@@ -1399,8 +1422,16 @@ void pprint_timesamples(StreamWriter& writer, const value::TimeSamples& samples,
return;
}
// Check if using unified storage (_times non-empty) vs legacy Sample-based storage
if (!samples.get_times().empty()) {
// Check if using unified storage (_times non-empty AND has actual data in buffers)
// vs Sample-based storage (_samples vector)
// Note: Some operations like add_value_array_sample() populate _times but store data
// in _samples, so we need to check if unified storage buffers actually have data
bool has_unified_data = !samples.get_times().empty() &&
(!samples.get_values().empty() ||
!samples.get_small_values().empty() ||
!samples.get_offsets().empty());
if (has_unified_data) {
// Phase 3: Access unified storage directly from TimeSamples
// Note: TypedArray is no longer supported in Phase 3, so we skip that path
@@ -1427,6 +1458,7 @@ void pprint_timesamples(StreamWriter& writer, const value::TimeSamples& samples,
const auto& blocked = samples.get_blocked();
const auto& values = samples.get_values();
const auto& offsets = samples.get_offsets();
const auto& small_values = samples.get_small_values();
const auto& array_counts = samples.get_array_counts();
// Write samples - handle offset table if present
@@ -1476,19 +1508,54 @@ void pprint_timesamples(StreamWriter& writer, const value::TimeSamples& samples,
writer.write("\n");
}
} else {
// Legacy: blocked values still counted in offset calculation
// Handle case where values is empty but times is not
if (values.empty() && !times.empty()) {
// No offset table - using direct storage (either _values or _small_values)
// Check if using small_values (for types sizeof <= 8) or values buffer (for types sizeof > 8)
bool using_small_values = !small_values.empty();
// Handle case where both storage types are empty but times is not (error case)
if (values.empty() && small_values.empty() && !times.empty()) {
for (size_t i = 0; i < times.size(); ++i) {
writer.write(pprint::Indent(indent + 1));
writer.write(times[i]);
writer.write(": /* empty value data */");
if (i < times.size() - 1) {
writer.write(",");
}
writer.write("\n");
}
} else if (using_small_values) {
// Print small values (stored as uint64_t, need to extract typed value)
// NOTE: small_values only contains non-blocked samples, so we need a separate index
size_t small_values_index = 0;
for (size_t i = 0; i < times.size(); ++i) {
writer.write(pprint::Indent(indent + 1));
writer.write(times[i]);
writer.write(": ");
// Check blocked array bounds before accessing
bool is_blocked = (i < blocked.size()) ? blocked[i] : false;
if (is_blocked) {
writer.write("None");
} else {
// Get value from small_values and print it
if (small_values_index < small_values.size()) {
uint64_t stored_value = small_values[small_values_index];
// Cast to typed pointer and print
const uint8_t* value_ptr = reinterpret_cast<const uint8_t*>(&stored_value);
pprint_pod_value_by_type(writer, value_ptr, type_id);
small_values_index++; // Only increment for non-blocked samples
} else {
writer.write("/* ERROR: small_values index out of bounds */");
}
}
if (i < times.size() - 1) {
writer.write(",");
}
writer.write("\n");
}
} else {
// Use values buffer (large types)
size_t value_offset = 0;
for (size_t i = 0; i < times.size(); ++i) {
//TUSDZ_LOG_I("times[" << i << "] = " << times[i]);
@@ -1524,8 +1591,8 @@ void pprint_timesamples(StreamWriter& writer, const value::TimeSamples& samples,
}
writer.write("\n");
}
} // end else for values.empty() check
}
} // end else for using_small_values check
} // end else for offsets.empty() check
} else {
// Non-POD path: use regular samples
const auto& samples_vec = samples.get_samples();

View File

@@ -1451,8 +1451,12 @@ struct TimeSamples {
_times.push_back(t);
_blocked.push_back(1); // Blocked
// No data needed for blocked sample, just add a dummy offset
_offsets.push_back(SIZE_MAX); // Special marker for blocked
// For small types (sizeof <= 8), don't use offsets - just rely on _blocked flag
// For large types (sizeof > 8), need offset table entry
if (sizeof(T) > 8) {
_offsets.push_back(SIZE_MAX); // Special marker for blocked
}
// Note: No entry in _small_values for blocked samples (for small types)
_dirty = true;
return true;
@@ -1616,6 +1620,13 @@ struct TimeSamples {
return _offsets;
}
const std::vector<uint64_t>& get_small_values() const {
if (_dirty) {
update();
}
return _small_values;
}
bool is_array() const {
return _is_array;
}

View File

@@ -2998,7 +2998,8 @@ bool ListUVNames(const RenderMaterial &material,
// Subsurface
fun_float(material.openPBRShader->subsurface_weight);
fun_vec3(material.openPBRShader->subsurface_color);
fun_vec3(material.openPBRShader->subsurface_radius);
fun_float(material.openPBRShader->subsurface_radius);
fun_vec3(material.openPBRShader->subsurface_radius_scale);
fun_float(material.openPBRShader->subsurface_scale);
fun_float(material.openPBRShader->subsurface_anisotropy);
@@ -7338,6 +7339,12 @@ bool RenderSceneConverter::ConvertOpenPBRSurfaceShader(
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_radius_scale, "subsurface_radius_scale",
rshader.subsurface_radius_scale, true)) {
PushWarn(fmt::format("Failed to convert subsurface_radius_scale parameter for shader: {}", shader_abs_path.prim_part()));
return false;
}
if (!ConvertPreviewSurfaceShaderParam(
env, shader_abs_path, shader.subsurface_scale, "subsurface_scale",
rshader.subsurface_scale, true)) {

View File

@@ -1651,7 +1651,8 @@ class OpenPBRSurfaceShader {
// Subsurface scattering
ShaderParam<float> subsurface_weight{0.0f};
ShaderParam<vec3> subsurface_color{{0.8f, 0.8f, 0.8f}};
ShaderParam<vec3> subsurface_radius{{1.0f, 1.0f, 1.0f}};
ShaderParam<float> subsurface_radius{1.0f};
ShaderParam<vec3> subsurface_radius_scale{{1.0f, 1.0f, 1.0f}};
ShaderParam<float> subsurface_scale{1.0f};
ShaderParam<float> subsurface_anisotropy{0.0f};

View File

@@ -363,7 +363,8 @@ json ThreeJSMaterialExporter::ConvertOpenPBRToNodeMaterial(const OpenPBRSurfaceS
// Subsurface
add_param("subsurface_weight", shader.subsurface_weight);
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_radius", shader.subsurface_radius);
setJsonParameter(surface_node["inputs"], "subsurface_radius_scale", vec3ToJson(shader.subsurface_radius_scale.value), use_grouped);
add_param("subsurface_scale", shader.subsurface_scale);
add_param("subsurface_anisotropy", shader.subsurface_anisotropy);
@@ -468,7 +469,8 @@ json ThreeJSMaterialExporter::ConvertOpenPBRToPhysicalMaterial(const OpenPBRSurf
params["userData"]["subsurface"] = {
{"weight", shader.subsurface_weight.value},
{"color", vec3ToJson(shader.subsurface_color.value)},
{"radius", vec3ToJson(shader.subsurface_radius.value)},
{"radius", shader.subsurface_radius.value},
{"radius_scale", vec3ToJson(shader.subsurface_radius_scale.value)},
{"scale", shader.subsurface_scale.value},
{"anisotropy", shader.subsurface_anisotropy.value}
};
@@ -693,7 +695,8 @@ bool ThreeJSMaterialExporter::ExportMaterialX(const RenderMaterial& material,
// Subsurface parameters
export_float("subsurface_weight", shader.subsurface_weight);
export_color3("subsurface_color", shader.subsurface_color);
export_color3("subsurface_radius", shader.subsurface_radius);
export_float("subsurface_radius", shader.subsurface_radius);
export_color3("subsurface_radius_scale", shader.subsurface_radius_scale);
export_float("subsurface_scale", shader.subsurface_scale);
export_float("subsurface_anisotropy", shader.subsurface_anisotropy);
@@ -765,7 +768,8 @@ bool ThreeJSMaterialExporter::ExportMaterialX(const RenderMaterial& material,
export_texture_node("subsurface_weight", shader.subsurface_weight, "float");
export_texture_node_vec3("subsurface_color", shader.subsurface_color, "color3");
export_texture_node_vec3("subsurface_radius", shader.subsurface_radius, "color3");
export_texture_node("subsurface_radius", shader.subsurface_radius, "float");
export_texture_node_vec3("subsurface_radius_scale", shader.subsurface_radius_scale, "color3");
export_texture_node("subsurface_scale", shader.subsurface_scale, "float");
export_texture_node("subsurface_anisotropy", shader.subsurface_anisotropy, "float");

View File

@@ -653,7 +653,7 @@ static bool WriteMaterialXToString(const MtlxAutodeskStandardSurface &shader,
// Subsurface properties
EMIT_ATTRIBUTE("subsurface", "float", shader.subsurface)
EMIT_ATTRIBUTE("subsurface_color", "color3", shader.subsurface_color)
EMIT_ATTRIBUTE("subsurface_radius", "color3", shader.subsurface_radius)
EMIT_ATTRIBUTE("subsurface_radius", "float", shader.subsurface_radius)
EMIT_ATTRIBUTE("subsurface_scale", "float", shader.subsurface_scale)
EMIT_ATTRIBUTE("subsurface_anisotropy", "float", shader.subsurface_anisotropy)
@@ -795,7 +795,8 @@ static bool WriteMaterialXToString(const MtlxOpenPBRSurface &shader,
// Subsurface properties
EMIT_ATTRIBUTE("subsurface_weight", "float", shader.subsurface_weight)
EMIT_ATTRIBUTE("subsurface_color", "color3", shader.subsurface_color)
EMIT_ATTRIBUTE("subsurface_radius", "color3", shader.subsurface_radius)
EMIT_ATTRIBUTE("subsurface_radius", "float", shader.subsurface_radius)
EMIT_ATTRIBUTE("subsurface_radius_scale", "color3", shader.subsurface_radius_scale)
EMIT_ATTRIBUTE("subsurface_scale", "float", shader.subsurface_scale)
EMIT_ATTRIBUTE("subsurface_anisotropy", "float", shader.subsurface_anisotropy)
@@ -1757,7 +1758,7 @@ bool ReadMaterialXFromString(const std::string &str,
GET_SHADER_PARAM(name, typeName, "transmission_extra_roughness", "float", float, valueStr, surface.transmission_extra_roughness)
GET_SHADER_PARAM(name, typeName, "subsurface", "float", float, valueStr, surface.subsurface)
GET_SHADER_PARAM(name, typeName, "subsurface_color", "color3", value::color3f, valueStr, surface.subsurface_color)
GET_SHADER_PARAM(name, typeName, "subsurface_radius", "color3", value::color3f, valueStr, surface.subsurface_radius)
GET_SHADER_PARAM(name, typeName, "subsurface_radius", "float", float, valueStr, surface.subsurface_radius)
GET_SHADER_PARAM(name, typeName, "subsurface_scale", "float", float, valueStr, surface.subsurface_scale)
GET_SHADER_PARAM(name, typeName, "subsurface_anisotropy", "float", float, valueStr, surface.subsurface_anisotropy)
GET_SHADER_PARAM(name, typeName, "sheen", "float", float, valueStr, surface.sheen)
@@ -2155,7 +2156,8 @@ bool ReadMaterialXFromString(const std::string &str,
GET_SHADER_PARAM(name, typeName, "transmission_dispersion", "float", float, valueStr, surface.transmission_dispersion)
GET_SHADER_PARAM(name, typeName, "subsurface_weight", "float", float, valueStr, surface.subsurface_weight)
GET_SHADER_PARAM(name, typeName, "subsurface_color", "color3", value::color3f, valueStr, surface.subsurface_color)
GET_SHADER_PARAM(name, typeName, "subsurface_radius", "color3", value::color3f, valueStr, surface.subsurface_radius)
GET_SHADER_PARAM(name, typeName, "subsurface_radius", "float", float, valueStr, surface.subsurface_radius)
GET_SHADER_PARAM(name, typeName, "subsurface_radius_scale", "color3", value::color3f, valueStr, surface.subsurface_radius_scale)
GET_SHADER_PARAM(name, typeName, "subsurface_scale", "float", float, valueStr, surface.subsurface_scale)
GET_SHADER_PARAM(name, typeName, "subsurface_anisotropy", "float", float, valueStr, surface.subsurface_anisotropy)
GET_SHADER_PARAM(name, typeName, "sheen_weight", "float", float, valueStr, surface.sheen_weight)

View File

@@ -228,8 +228,7 @@ struct MtlxAutodeskStandardSurface : ShaderNode {
TypedAttributeWithFallback<Animatable<float>> subsurface{0.0f};
TypedAttributeWithFallback<Animatable<value::color3f>> subsurface_color{
value::color3f{1.0f, 1.0f, 1.0f}};
TypedAttributeWithFallback<Animatable<value::color3f>> subsurface_radius{
value::color3f{1.0f, 1.0f, 1.0f}};
TypedAttributeWithFallback<Animatable<float>> subsurface_radius{1.0f};
TypedAttributeWithFallback<Animatable<float>> subsurface_scale{1.0f};
TypedAttributeWithFallback<Animatable<float>> subsurface_anisotropy{0.0f};

View File

@@ -370,7 +370,8 @@ struct OpenPBRSurface : ShaderNode {
// Subsurface properties
TypedAttributeWithFallback<Animatable<float>> subsurface_weight{0.0f}; // "inputs:subsurface_weight"
TypedAttributeWithFallback<Animatable<value::color3f>> subsurface_color{value::color3f{0.8f, 0.8f, 0.8f}}; // "inputs:subsurface_color"
TypedAttributeWithFallback<Animatable<value::color3f>> subsurface_radius{value::color3f{1.0f, 1.0f, 1.0f}}; // "inputs:subsurface_radius"
TypedAttributeWithFallback<Animatable<float>> subsurface_radius{1.0f}; // "inputs:subsurface_radius"
TypedAttributeWithFallback<Animatable<value::color3f>> subsurface_radius_scale{value::color3f{1.0f, 1.0f, 1.0f}}; // "inputs:subsurface_radius_scale"
TypedAttributeWithFallback<Animatable<float>> subsurface_scale{1.0f}; // "inputs:subsurface_scale"
TypedAttributeWithFallback<Animatable<float>> subsurface_anisotropy{0.0f}; // "inputs:subsurface_anisotropy"

View File

@@ -537,13 +537,12 @@ std::ostream &operator<<(std::ostream &ofs,
if (tinyusdz::contains(in_s, '@')) {
// Escape '@@@'(to '\@@@') if the input path contains '@@@'
for (size_t i = 0; i < in_s.length(); i++) {
if ((i + 2) < in_s.length()) {
if (in_s[i] == '@' && in_s[i + 1] == '@' && in_s[i + 2] == '@') {
s += "\\@@@";
i += 2;
} else {
s += in_s[i];
}
if ((i + 2) < in_s.length() &&
in_s[i] == '@' && in_s[i + 1] == '@' && in_s[i + 2] == '@') {
s += "\\@@@";
i += 2;
} else {
s += in_s[i];
}
}

2147
tests/compare-usda.js Executable file

File diff suppressed because it is too large Load Diff

297
tests/run-usdcat-compare.sh Executable file
View File

@@ -0,0 +1,297 @@
#!/bin/bash
# Script to run batch comparisons of tusdcat vs usdcat output with detailed diffs
# Tests both USDA and USDC formats
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPARE_SCRIPT="$SCRIPT_DIR/compare-usda.js"
TUSDCAT_PATH="${TUSDCAT_PATH:-./build/tusdcat}"
USDCAT_PATH="${USDCAT_PATH:-~/local/USD/dist/bin/usdcat}"
TIMEOUT_MS="${TIMEOUT_MS:-60000}"
SHOW_DETAILED_DIFF="${SHOW_DETAILED_DIFF:-true}"
SHOW_FAILURE_SUMMARY="${SHOW_FAILURE_SUMMARY:-true}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print headers
print_header() {
echo ""
echo -e "${BLUE}════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE}$1${NC}"
echo -e "${BLUE}════════════════════════════════════════════════════════${NC}"
echo ""
}
# Function to print section
print_section() {
echo ""
echo -e "${YELLOW}$1${NC}"
echo ""
}
# Function to check if file exists
check_executable() {
local path="$1"
local name="$2"
# Expand ~ if present
path="${path/#\~/$HOME}"
if [ ! -x "$path" ]; then
echo -e "${RED}Error: $name not found or not executable at: $path${NC}"
return 1
fi
return 0
}
# Function to run comparison on a folder
run_folder_comparison() {
local folder="$1"
local folder_name="$2"
local file_pattern="$3"
if [ ! -d "$folder" ]; then
echo -e "${RED}Error: Folder not found: $folder${NC}"
return 1
fi
print_section "Testing $folder_name ($file_pattern files)"
local detailed_flag=""
if [ "$SHOW_DETAILED_DIFF" = "true" ]; then
detailed_flag="--detailed-diff"
fi
node "$COMPARE_SCRIPT" \
--tusdcat "$TUSDCAT_PATH" \
--usdcat "$USDCAT_PATH" \
--timeout "$TIMEOUT_MS" \
--continue-on-error \
$detailed_flag \
"$folder/$file_pattern"
}
# Function to print failure and warning summary from results file
# Note: Uses plain text (no ANSI colors) so output is clean when redirected
print_failure_summary() {
local results_file="$1"
if [ ! -f "$results_file" ]; then
return
fi
# Strip ANSI codes for processing
local clean_results
clean_results=$(sed 's/\x1b\[[0-9;]*m//g' "$results_file")
# Extract failed files (lines with "✗" followed by "difference(s)")
local failed_files
failed_files=$(echo "$clean_results" | grep -B1 "✗.*difference(s)" | grep "Processing:" | sed 's/.*Processing: //' | sort -u)
# Extract warning/error files (lines with "⚠" or "Error:")
local warning_files
warning_files=$(echo "$clean_results" | grep -B1 -E "(⚠|Error:)" | grep "Processing:" | sed 's/.*Processing: //' | sort -u)
local has_output=false
if [ -n "$failed_files" ] || [ -n "$warning_files" ]; then
echo ""
echo "========================================================"
echo "Failure and Warning Summary"
echo "========================================================"
echo ""
fi
if [ -n "$failed_files" ]; then
has_output=true
local fail_count
fail_count=$(echo "$failed_files" | wc -l)
echo "[X] Failed Files ($fail_count):"
echo "$failed_files" | while read -r file; do
echo " - $file"
done
echo ""
fi
if [ -n "$warning_files" ]; then
has_output=true
local warn_count
warn_count=$(echo "$warning_files" | wc -l)
echo "[!] Warning/Error Files ($warn_count):"
echo "$warning_files" | while read -r file; do
echo " - $file"
done
echo ""
fi
if [ "$has_output" = true ]; then
echo "--------------------------------------------------------"
echo ""
fi
}
# Main execution
main() {
# Expand tilde in paths
TUSDCAT_PATH="${TUSDCAT_PATH/#\~/$HOME}"
USDCAT_PATH="${USDCAT_PATH/#\~/$HOME}"
print_header "USD File Format Comparison Suite"
echo "Configuration:"
echo " Comparison Script: $COMPARE_SCRIPT"
echo " tusdcat Path: $TUSDCAT_PATH"
echo " usdcat Path: $USDCAT_PATH"
echo " Timeout: ${TIMEOUT_MS}ms"
echo " Detailed Diff: $SHOW_DETAILED_DIFF"
echo " Failure Summary: $SHOW_FAILURE_SUMMARY"
echo ""
# Check prerequisites
echo "Checking prerequisites..."
if [ ! -f "$COMPARE_SCRIPT" ]; then
echo -e "${RED}Error: compare-usda.js not found at: $COMPARE_SCRIPT${NC}"
exit 1
fi
if ! check_executable "$TUSDCAT_PATH" "tusdcat"; then
exit 1
fi
if ! check_executable "$USDCAT_PATH" "usdcat"; then
exit 1
fi
echo -e "${GREEN}✓ All prerequisites met${NC}"
echo ""
# Create results directory
RESULTS_DIR="$SCRIPT_DIR/comparison-results"
mkdir -p "$RESULTS_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
RESULTS_FILE="$RESULTS_DIR/results_${TIMESTAMP}.log"
echo "Results will be saved to: $RESULTS_FILE"
echo ""
# Run comparisons and capture output
{
print_header "USD File Format Comparison Results - $TIMESTAMP"
# Test USDA files
print_section "USDA (ASCII) Format Tests"
echo "Testing all .usda files in tests/usda directory..."
echo ""
run_folder_comparison "$SCRIPT_DIR/usda" "USDA Files" "*.usda" || true
# Test USDC files
print_section "USDC (Binary/Crate) Format Tests"
echo "Testing all .usdc files in tests/usdc directory..."
echo ""
run_folder_comparison "$SCRIPT_DIR/usdc" "USDC Files" "*.usdc" || true
print_header "Comparison Complete"
} | tee "$RESULTS_FILE"
# Print failure summary if enabled
if [ "$SHOW_FAILURE_SUMMARY" = "true" ]; then
print_failure_summary "$RESULTS_FILE"
fi
echo ""
echo -e "${GREEN}✓ Comparison complete!${NC}"
echo "Full results saved to: $RESULTS_FILE"
echo ""
echo "Usage examples for custom runs:"
echo " # Test specific USDA file with detailed diff"
echo " SHOW_DETAILED_DIFF=true node $COMPARE_SCRIPT --detailed-diff tests/usda/cube.usda"
echo ""
echo " # Test specific files with tusdcat/usdcat"
echo " TUSDCAT_PATH=./build_gcc/tusdcat USDCAT_PATH=~/local/USD/dist/bin/usdcat \\"
echo " node $COMPARE_SCRIPT --detailed-diff --tusdcat ./build_gcc/tusdcat --usdcat ~/local/USD/dist/bin/usdcat tests/usda/cube.usda"
echo ""
}
# Show help
show_help() {
cat << EOF
Usage: $0 [OPTIONS]
Run comprehensive batch comparisons of tusdcat vs usdcat outputs with detailed diffs.
Tests both USDA (ASCII) and USDC (Binary/Crate) file formats.
OPTIONS:
-h, --help Show this help message
--tusdcat PATH Path to tusdcat executable (default: ./build_gcc/tusdcat)
--usdcat PATH Path to usdcat executable (default: ~/local/USD/dist/bin/usdcat)
--timeout MS Timeout per file in milliseconds (default: 60000)
--no-detailed-diff Disable detailed diff output (shows summary only)
--no-failure-summary Disable failure/warning summary at the end
ENVIRONMENT VARIABLES:
TUSDCAT_PATH Override tusdcat path
USDCAT_PATH Override usdcat path
TIMEOUT_MS Override timeout
SHOW_DETAILED_DIFF Set to 'false' to disable detailed diffs (default: true)
SHOW_FAILURE_SUMMARY Set to 'false' to disable failure summary (default: true)
EXAMPLES:
# Run with default settings
$0
# Run without detailed diffs (faster, summary only)
$0 --no-detailed-diff
# Run with custom tool paths
TUSDCAT_PATH=./build_asan/tusdcat USDCAT_PATH=~/USD/bin/usdcat $0
# Run with longer timeout for slow systems
$0 --timeout 120000
EOF
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
exit 0
;;
--tusdcat)
TUSDCAT_PATH="$2"
shift 2
;;
--usdcat)
USDCAT_PATH="$2"
shift 2
;;
--timeout)
TIMEOUT_MS="$2"
shift 2
;;
--no-detailed-diff)
SHOW_DETAILED_DIFF="false"
shift
;;
--no-failure-summary)
SHOW_FAILURE_SUMMARY="false"
shift
;;
*)
echo "Unknown option: $1"
show_help
exit 1
;;
esac
done
# Run main function
main

View File

@@ -49,6 +49,7 @@ TEST_LIST = {
{ "strutil_test", strutil_test },
{ "tinystring_test", tinystring_test },
{ "parse_int_test", parse_int_test },
{ "dtoa_test", dtoa_test },
{ "timesamples_test", timesamples_test },
{ "materialx_config_api_struct_test", materialx_config_api_struct_test },
{ "materialx_config_api_parsing_test", materialx_config_api_parsing_test },

View File

@@ -195,5 +195,135 @@ void parse_int_test(void) {
tstring_view sv("+-123");
TEST_CHECK(!parse_int(sv, &result));
}
}
void dtoa_test(void) {
// Test double to ASCII conversion with full precision
{
// Test pi with full double precision
double pi = 3.141592653589793;
char buf[384];
char *end = dtoa(pi, buf);
*end = '\0';
std::string result(buf);
// Should preserve full precision (at least 15 significant digits)
TEST_CHECK(result.find("3.14159265358979") != std::string::npos);
TEST_MSG("pi result: %s", result.c_str());
}
{
// Test e with full double precision
double e = 2.718281828459045;
char buf[384];
char *end = dtoa(e, buf);
*end = '\0';
std::string result(buf);
// Should preserve full precision
TEST_CHECK(result.find("2.71828182845904") != std::string::npos);
TEST_MSG("e result: %s", result.c_str());
}
{
// Test negative value
double neg = -2.718281828459045;
char buf[384];
char *end = dtoa(neg, buf);
*end = '\0';
std::string result(buf);
TEST_CHECK(result[0] == '-');
TEST_CHECK(result.find("2.71828182845904") != std::string::npos);
TEST_MSG("negative e result: %s", result.c_str());
}
{
// Test zero
double zero = 0.0;
char buf[384];
char *end = dtoa(zero, buf);
*end = '\0';
std::string result(buf);
TEST_CHECK(result == "0.0");
TEST_MSG("zero result: %s", result.c_str());
}
{
// Test negative zero
double neg_zero = -0.0;
char buf[384];
char *end = dtoa(neg_zero, buf);
*end = '\0';
std::string result(buf);
// dtoa_milo should output "-0.0" for negative zero
TEST_CHECK(result == "-0.0");
TEST_MSG("negative zero result: %s", result.c_str());
}
{
// Test small number
double small = 0.000001234567890123456;
char buf[384];
char *end = dtoa(small, buf);
*end = '\0';
std::string result(buf);
// Should use scientific notation for very small numbers
TEST_MSG("small number result: %s", result.c_str());
TEST_CHECK(result.length() > 0);
}
{
// Test large number
double large = 1234567890123456.0;
char buf[384];
char *end = dtoa(large, buf);
*end = '\0';
std::string result(buf);
TEST_MSG("large number result: %s", result.c_str());
TEST_CHECK(result.length() > 0);
}
{
// Test float conversion
float f = 3.14159f;
char buf[384];
char *end = dtoa(f, buf);
*end = '\0';
std::string result(buf);
// Should output float with appropriate precision
TEST_CHECK(result.find("3.14159") != std::string::npos);
TEST_MSG("float result: %s", result.c_str());
}
{
// Test one
double one = 1.0;
char buf[384];
char *end = dtoa(one, buf);
*end = '\0';
std::string result(buf);
TEST_CHECK(result == "1.0");
TEST_MSG("one result: %s", result.c_str());
}
{
// Test integer-like double
double int_like = 42.0;
char buf[384];
char *end = dtoa(int_like, buf);
*end = '\0';
std::string result(buf);
TEST_CHECK(result == "42.0");
TEST_MSG("integer-like result: %s", result.c_str());
}
}

View File

@@ -3,3 +3,4 @@
void strutil_test(void);
void tinystring_test(void);
void parse_int_test(void);
void dtoa_test(void);