mirror of
https://github.com/lighttransport/tinyusdz.git
synced 2026-01-18 01:11:17 +01:00
Fix path sorting and property token remapping for Material/Shader support
Critical fixes for USD path tree encoding: 1. **Use consistent USD path comparison**: Changed spec sorting in Finalize() to use pathlib::ComparePaths instead of simple lexicographic sort. This ensures path indices match the path tree encoding order. 2. **Fix property token remapping**: Properties use negative token indices (e.g., -5 for "outputs:surface"). Fixed token remap table to preserve both positive and negative indices without collision. Previously, both path_tree_idx=3 and path_tree_idx=-3 were mapping to same key, causing property tokens to be lost. 3. **Add ListOp wrapper for Material outputs**: Material output connections (outputs:surface, outputs:displacement, outputs:volume) must be wrapped in ListOp<Path> as required by USD spec. 4. **Add Material/Shader test**: Created test case for Material and Shader prims with surface output connection. Known issue: Material outputs need to be recognized as "attribute connections" by the reader. This requires further investigation into the proper spec structure or fields needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -404,6 +404,9 @@ if(TINYUSDZ_WITH_PYTHON)
|
||||
add_subdirectory(${PROJECT_SOURCE_DIR}/src/external/pybind11)
|
||||
endif()
|
||||
|
||||
# Add path-sort-and-encode-crate library for USDC crate writer
|
||||
add_subdirectory(${PROJECT_SOURCE_DIR}/sandbox/path-sort-and-encode-crate ${CMAKE_CURRENT_BINARY_DIR}/path-sort-and-encode-crate)
|
||||
|
||||
set(TINYUSDZ_SOURCES
|
||||
${PROJECT_SOURCE_DIR}/src/arg-parser.cc
|
||||
${PROJECT_SOURCE_DIR}/src/asset-resolution.cc
|
||||
@@ -426,6 +429,7 @@ set(TINYUSDZ_SOURCES
|
||||
${PROJECT_SOURCE_DIR}/src/crate-reader-timesamples.cc
|
||||
${PROJECT_SOURCE_DIR}/src/crate-format.cc
|
||||
${PROJECT_SOURCE_DIR}/src/crate-writer.cc
|
||||
${PROJECT_SOURCE_DIR}/src/stage-converter.cc
|
||||
${PROJECT_SOURCE_DIR}/src/crate-pprint.cc
|
||||
${PROJECT_SOURCE_DIR}/src/path-util.cc
|
||||
${PROJECT_SOURCE_DIR}/src/prim-reconstruct.cc
|
||||
@@ -1326,7 +1330,8 @@ foreach(TINYUSDZ_LIB_TARGET ${TINYUSDZ_LIBS})
|
||||
endif()
|
||||
|
||||
target_link_libraries(${TINYUSDZ_LIB_TARGET} ${TINYUSDZ_EXT_LIBRARIES}
|
||||
${CMAKE_DL_LIBS})
|
||||
${CMAKE_DL_LIBS}
|
||||
crate-encoding)
|
||||
|
||||
if (TINYUSDZ_ENABLE_THREAD)
|
||||
target_compile_definitions(${TINYUSDZ_LIB_TARGET}
|
||||
|
||||
800
docs/crate-writer-enhancements.md
Normal file
800
docs/crate-writer-enhancements.md
Normal file
@@ -0,0 +1,800 @@
|
||||
# USDC Crate Writer - Enhancement Proposals
|
||||
|
||||
**Date:** 2025-11-17
|
||||
**Status:** Review & Planning
|
||||
|
||||
This document outlines proposed enhancements and improvements for the TinyUSDZ USDC Crate Writer based on current implementation analysis.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The USDC Crate Writer has achieved solid foundational functionality with all critical bugs fixed and core features implemented. This review identifies opportunities for enhancements across feature completeness, performance, correctness, and developer experience.
|
||||
|
||||
**Current Implementation Stats:**
|
||||
- Core implementation: 3,842 lines (`src/crate-writer.cc`)
|
||||
- Stage converter: 1,882 lines (`src/stage-converter.cc`)
|
||||
- Unit tests: 9 comprehensive tests
|
||||
- Test success rate: 100% (29/29 tests passing)
|
||||
- Documentation: 402 lines
|
||||
|
||||
---
|
||||
|
||||
## 1. Current Status Summary
|
||||
|
||||
### ✅ Implemented Features
|
||||
|
||||
**Core Infrastructure:**
|
||||
- ✅ Binary USDC format v0.8.0 writing
|
||||
- ✅ Proper file header (PXR-USDC magic)
|
||||
- ✅ Table of Contents (TOC) structure
|
||||
- ✅ LZ4 compression for tokens/strings
|
||||
- ✅ Value deduplication system
|
||||
- ✅ Path tree encoding
|
||||
|
||||
**Spec Types:**
|
||||
- ✅ PseudoRoot (with correct ordering)
|
||||
- ✅ Prim specs
|
||||
- ✅ Relationship specs (separate specs)
|
||||
- ⚠️ Attribute specs (embedded in prim, not separate)
|
||||
- ❌ Connection specs (embedded, not separate)
|
||||
- ❌ Variant specs
|
||||
- ❌ VariantSet specs
|
||||
|
||||
**Data Types:**
|
||||
- ✅ Basic value types (int, float, double, bool, string, token)
|
||||
- ✅ Vector types (float2, float3, float4, etc.)
|
||||
- ✅ Matrix types (matrix4d, etc.)
|
||||
- ✅ Path types
|
||||
- ✅ TimeSamples (with proper ValueRep format)
|
||||
- ✅ ListOp<T> for References, Payloads, Inherits
|
||||
- ✅ Integer ListOps (IntListOp, UIntListOp, Int64ListOp, UInt64ListOp)
|
||||
- ✅ TokenListOp (for API schemas)
|
||||
- ❌ String ListOp (StringListOp)
|
||||
- ❌ PathListOp
|
||||
- ❌ Dictionary values
|
||||
|
||||
**Geometry Support:**
|
||||
- ✅ Basic shapes: GeomCube, GeomSphere
|
||||
- ✅ GeomMesh with animated points
|
||||
- ✅ GPrim properties (visibility, purpose, extent)
|
||||
- ✅ Animated visibility via TimeSamples
|
||||
- ⚠️ GeomCone, GeomCapsule, GeomCylinder (partial)
|
||||
- ❌ GeomBasisCurves
|
||||
- ❌ GeomNurbsCurves
|
||||
- ❌ GeomPoints
|
||||
- ❌ GeomPointInstancer
|
||||
- ❌ GeomCamera (specific properties)
|
||||
- ❌ GeomSubset
|
||||
|
||||
**USD Composition:**
|
||||
- ✅ References with customData
|
||||
- ✅ Payloads
|
||||
- ✅ Inherits
|
||||
- ✅ Sublayers with LayerOffset
|
||||
- ❌ Variants and VariantSets
|
||||
- ❌ Specializes
|
||||
|
||||
**Shading & Materials:**
|
||||
- ❌ Material prims (basic structure only)
|
||||
- ❌ Shader nodes (UsdPreviewSurface, UsdUVTexture, etc.)
|
||||
- ❌ Shader connections
|
||||
- ❌ Material bindings (relationships exist but not tested)
|
||||
|
||||
**Lighting:**
|
||||
- ❌ All UsdLux light types (SphereLight, RectLight, DomeLight, etc.)
|
||||
- ❌ Light shaping attributes
|
||||
- ❌ Light filters
|
||||
|
||||
**Animation & Deformation:**
|
||||
- ✅ Basic TimeSamples for transforms
|
||||
- ✅ Animated mesh points
|
||||
- ✅ Animated visibility
|
||||
- ❌ UsdSkel (skeletal animation)
|
||||
- ❌ BlendShapes
|
||||
- ❌ Animated normals, UVs
|
||||
|
||||
**Metadata:**
|
||||
- ✅ Prim metadata (active, hidden, kind, etc.)
|
||||
- ✅ Attribute metadata (interpolation, comment, etc.)
|
||||
- ✅ Relationship metadata
|
||||
- ✅ CustomData for prims, attributes, relationships, references
|
||||
- ✅ AssetInfo
|
||||
- ❌ Layer metadata (documentation, startTimeCode, endTimeCode, etc.)
|
||||
|
||||
---
|
||||
|
||||
## 2. High-Priority Enhancements
|
||||
|
||||
### 2.1 Separate Attribute Specs ⭐⭐⭐
|
||||
**Priority:** HIGH
|
||||
**Complexity:** MEDIUM
|
||||
**Impact:** Correctness
|
||||
|
||||
**Current State:**
|
||||
Attributes are embedded as fields in the parent prim spec (e.g., `"myAttr", "myAttr.typeName"`).
|
||||
|
||||
**Proper USD Format:**
|
||||
Each attribute should be a separate spec with `SpecType::Attribute` at path `parent.AppendProperty(attr_name)`.
|
||||
|
||||
**Benefits:**
|
||||
- Matches official USD Crate format
|
||||
- Better organization and spec isolation
|
||||
- Enables proper attribute metadata
|
||||
- Consistent with relationship handling (already fixed)
|
||||
|
||||
**Implementation:**
|
||||
```cpp
|
||||
// Similar to ConvertRelationshipToFields refactor:
|
||||
bool ConvertAttributeToFields(const std::string& attr_name,
|
||||
const Attribute& attr,
|
||||
const Path& parent_path,
|
||||
std::string* err) {
|
||||
Path attr_path = parent_path.AppendProperty(attr_name);
|
||||
crate::FieldValuePairVector attr_fields;
|
||||
|
||||
// Build fields WITHOUT attr_name prefix
|
||||
attr_fields.push_back({"typeName", ...});
|
||||
attr_fields.push_back({"default", ...});
|
||||
|
||||
return AddSpec(attr_path, SpecType::Attribute, attr_fields, err);
|
||||
}
|
||||
```
|
||||
|
||||
**Estimated Effort:** 8-12 hours
|
||||
**Files:** `src/stage-converter.cc`, `src/crate-writer.hh`
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Separate Connection Specs ⭐⭐
|
||||
**Priority:** MEDIUM-HIGH
|
||||
**Complexity:** MEDIUM
|
||||
**Impact:** Correctness, shader support
|
||||
|
||||
**Current State:**
|
||||
Connections are embedded as fields (`.connect` suffix).
|
||||
|
||||
**Proper USD Format:**
|
||||
Each connection should be a separate spec with `SpecType::Connection`.
|
||||
|
||||
**Benefits:**
|
||||
- Required for proper shader networks
|
||||
- Enables material/shader writing
|
||||
- Matches official USD format
|
||||
|
||||
**Implementation:**
|
||||
```cpp
|
||||
bool ConvertConnectionToFields(const std::string& conn_name,
|
||||
const Attribute& attr,
|
||||
const Path& parent_path,
|
||||
std::string* err) {
|
||||
// Create connection path: parent.AppendProperty(conn_name).AppendProperty("connect")
|
||||
Path conn_path = parent_path.AppendProperty(conn_name).AppendProperty("connect");
|
||||
|
||||
crate::FieldValuePairVector conn_fields;
|
||||
// Add connection targets...
|
||||
|
||||
return AddSpec(conn_path, SpecType::Connection, conn_fields, err);
|
||||
}
|
||||
```
|
||||
|
||||
**Estimated Effort:** 4-6 hours
|
||||
**Files:** `src/stage-converter.cc`
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Material & Shader Support ⭐⭐⭐
|
||||
**Priority:** HIGH
|
||||
**Complexity:** HIGH
|
||||
**Impact:** Feature completeness
|
||||
|
||||
**Missing Features:**
|
||||
- Material prim writing (structure exists but incomplete)
|
||||
- Shader node writing (UsdPreviewSurface, UsdUVTexture, etc.)
|
||||
- Shader input/output attributes
|
||||
- Shader connections (requires #2.2)
|
||||
- Material bindings
|
||||
|
||||
**Use Case:**
|
||||
Essential for any rendering pipeline using USD.
|
||||
|
||||
**Implementation Tasks:**
|
||||
1. Implement shader node conversion (`ConvertShaderNode`)
|
||||
2. Add shader input/output handling
|
||||
3. Implement material binding relationships
|
||||
4. Write unit tests with complete material networks
|
||||
|
||||
**Example:**
|
||||
```cpp
|
||||
bool ConvertMaterialToFields(const Material& material, ...) {
|
||||
// 1. Convert shader nodes
|
||||
for (const auto& shader : material.shaders) {
|
||||
ConvertShaderNode(shader, material_path, err);
|
||||
}
|
||||
|
||||
// 2. Convert surface relationship
|
||||
if (material.surface.has_value()) {
|
||||
ConvertRelationshipToFields("surface", material.surface, ...);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Estimated Effort:** 20-30 hours
|
||||
**Files:** `src/stage-converter.cc`, new tests
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Variant Support ⭐⭐
|
||||
**Priority:** MEDIUM
|
||||
**Complexity:** HIGH
|
||||
**Impact:** Feature completeness, USD composition
|
||||
|
||||
**Missing Features:**
|
||||
- VariantSet specs
|
||||
- Variant specs
|
||||
- Variant selection metadata
|
||||
- Nested variants
|
||||
|
||||
**Use Case:**
|
||||
Critical for asset variations (LOD, shading variants, etc.).
|
||||
|
||||
**Implementation Tasks:**
|
||||
1. Add `ConvertVariantSetToFields`
|
||||
2. Add `ConvertVariantToFields`
|
||||
3. Handle variant selection metadata
|
||||
4. Write comprehensive variant tests
|
||||
|
||||
**Example Structure:**
|
||||
```
|
||||
/ModelAsset {variantSets="shadingVariant"}
|
||||
/ModelAsset{shadingVariant=plastic} - variant spec
|
||||
/ModelAsset{shadingVariant=plastic}Material - prim inside variant
|
||||
```
|
||||
|
||||
**Estimated Effort:** 16-24 hours
|
||||
**Files:** `src/stage-converter.cc`, new tests
|
||||
|
||||
---
|
||||
|
||||
### 2.5 Complete Geometry Type Support ⭐⭐
|
||||
**Priority:** MEDIUM
|
||||
**Complexity:** MEDIUM
|
||||
**Impact:** Feature completeness
|
||||
|
||||
**Currently Partial/Missing:**
|
||||
- ❌ GeomCamera (camera-specific properties)
|
||||
- ❌ GeomBasisCurves (curves, hair)
|
||||
- ❌ GeomNurbsCurves
|
||||
- ❌ GeomPoints (point clouds)
|
||||
- ❌ GeomPointInstancer (instancing)
|
||||
- ❌ GeomCone, GeomCapsule, GeomCylinder (basic properties missing)
|
||||
- ❌ GeomSubset (material assignment)
|
||||
|
||||
**Implementation:**
|
||||
Create specialized converters for each geometry type:
|
||||
```cpp
|
||||
bool ExtractGeomCameraProperties(const GeomCamera& camera, ...) {
|
||||
// focalLength, horizontalAperture, verticalAperture, etc.
|
||||
}
|
||||
|
||||
bool ExtractGeomBasisCurvesProperties(const GeomBasisCurves& curves, ...) {
|
||||
// curveVertexCounts, widths, type, basis, wrap, etc.
|
||||
}
|
||||
```
|
||||
|
||||
**Estimated Effort:** 12-16 hours per type (or 40-60 hours for all)
|
||||
**Files:** `src/stage-converter.cc`
|
||||
|
||||
---
|
||||
|
||||
### 2.6 UsdLux Light Support ⭐
|
||||
**Priority:** MEDIUM-LOW
|
||||
**Complexity:** MEDIUM
|
||||
**Impact:** Feature completeness
|
||||
|
||||
**Missing Features:**
|
||||
All light types and their properties:
|
||||
- SphereLight, RectLight, DiskLight, CylinderLight
|
||||
- DistantLight, DomeLight
|
||||
- Light intensity, color, exposure
|
||||
- Light shaping (cone angle, softness, etc.)
|
||||
- Light filters
|
||||
|
||||
**Implementation:**
|
||||
```cpp
|
||||
bool ConvertLightToFields(const Light& light, ...) {
|
||||
// Common light properties
|
||||
fields.push_back({"intensity", ...});
|
||||
fields.push_back({"color", ...});
|
||||
|
||||
// Type-specific properties
|
||||
if (light.is_sphere_light()) {
|
||||
fields.push_back({"radius", ...});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Estimated Effort:** 10-15 hours
|
||||
**Files:** `src/stage-converter.cc`
|
||||
|
||||
---
|
||||
|
||||
## 3. Performance Enhancements
|
||||
|
||||
### 3.1 Incremental Writing ⭐⭐
|
||||
**Priority:** MEDIUM
|
||||
**Complexity:** HIGH
|
||||
**Impact:** Performance, memory usage
|
||||
|
||||
**Current State:**
|
||||
Entire file is built in memory, then written at once during `Finalize()`.
|
||||
|
||||
**Proposed:**
|
||||
Stream specs to disk during conversion, reducing peak memory usage.
|
||||
|
||||
**Benefits:**
|
||||
- Handle massive scenes (millions of prims)
|
||||
- Reduce memory footprint
|
||||
- Enable progress reporting
|
||||
|
||||
**Challenges:**
|
||||
- Path table requires all paths upfront
|
||||
- Field deduplication needs global view
|
||||
- Section offsets calculated during finalize
|
||||
|
||||
**Estimated Effort:** 30-40 hours (major refactor)
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Parallel Spec Conversion ⭐
|
||||
**Priority:** LOW-MEDIUM
|
||||
**Complexity:** MEDIUM
|
||||
**Impact:** Performance
|
||||
|
||||
**Current State:**
|
||||
Sequential prim traversal and conversion.
|
||||
|
||||
**Proposed:**
|
||||
Parallel processing of independent prim subtrees.
|
||||
|
||||
**Benefits:**
|
||||
- Faster conversion for large scenes
|
||||
- Better CPU utilization
|
||||
|
||||
**Implementation:**
|
||||
```cpp
|
||||
// Use thread pool for independent subtrees
|
||||
std::vector<std::future<bool>> futures;
|
||||
for (const auto& root_prim : stage.root_prims()) {
|
||||
futures.push_back(pool.enqueue([&]() {
|
||||
return ConvertPrimRecursive(root_prim, ...);
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
**Estimated Effort:** 12-16 hours
|
||||
**Requirements:** Thread-safe value deduplication
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Compression Level Options ⭐
|
||||
**Priority:** LOW
|
||||
**Complexity:** LOW
|
||||
**Impact:** Performance, file size
|
||||
|
||||
**Current State:**
|
||||
Fixed LZ4 compression for all sections.
|
||||
|
||||
**Proposed:**
|
||||
Expose compression options:
|
||||
```cpp
|
||||
struct Options {
|
||||
enum CompressionLevel {
|
||||
None, // No compression (fast write, large files)
|
||||
Fast, // LZ4 default
|
||||
Balanced, // LZ4 HC
|
||||
Best // ZSTD (if available)
|
||||
};
|
||||
CompressionLevel compression = CompressionLevel::Fast;
|
||||
};
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Trade-off between write speed and file size
|
||||
- Better control for different use cases
|
||||
|
||||
**Estimated Effort:** 4-6 hours
|
||||
|
||||
---
|
||||
|
||||
## 4. Correctness & Robustness Enhancements
|
||||
|
||||
### 4.1 Layer Metadata Support ⭐⭐
|
||||
**Priority:** MEDIUM
|
||||
**Complexity:** LOW-MEDIUM
|
||||
**Impact:** Correctness
|
||||
|
||||
**Missing Layer-Level Metadata:**
|
||||
- `documentation` (layer description)
|
||||
- `startTimeCode` / `endTimeCode` (animation range)
|
||||
- `timeCodesPerSecond` / `framesPerSecond`
|
||||
- `upAxis` (Y-up vs Z-up)
|
||||
- `metersPerUnit` (scene scale)
|
||||
- `defaultPrim` (entry point)
|
||||
- `comment`
|
||||
|
||||
**Implementation:**
|
||||
Add to root spec (PseudoRoot) fields:
|
||||
```cpp
|
||||
if (stage.metas.defaultPrim) {
|
||||
root_fields.push_back({"defaultPrim", ...});
|
||||
}
|
||||
if (stage.metas.upAxis) {
|
||||
root_fields.push_back({"upAxis", ...});
|
||||
}
|
||||
```
|
||||
|
||||
**Estimated Effort:** 4-6 hours
|
||||
**Files:** `src/stage-converter.cc`
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Value Type Validation ⭐⭐
|
||||
**Priority:** MEDIUM
|
||||
**Complexity:** MEDIUM
|
||||
**Impact:** Robustness
|
||||
|
||||
**Current State:**
|
||||
Limited type checking before writing.
|
||||
|
||||
**Proposed:**
|
||||
Comprehensive validation:
|
||||
```cpp
|
||||
bool ValidateValue(const crate::CrateValue& value, uint32_t expected_type_id) {
|
||||
// Check type compatibility
|
||||
// Validate array sizes
|
||||
// Check for invalid values (NaN, Inf where not allowed)
|
||||
// Verify path validity
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Catch errors before writing invalid files
|
||||
- Better error messages
|
||||
- Prevent silent data corruption
|
||||
|
||||
**Estimated Effort:** 8-12 hours
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Comprehensive Error Handling ⭐
|
||||
**Priority:** MEDIUM-LOW
|
||||
**Complexity:** MEDIUM
|
||||
**Impact:** Developer experience
|
||||
|
||||
**Current Issues:**
|
||||
- Some error paths return generic messages
|
||||
- Limited context in error strings
|
||||
- No error recovery/fallback
|
||||
|
||||
**Proposed:**
|
||||
```cpp
|
||||
class CrateWriterError {
|
||||
enum ErrorCode {
|
||||
InvalidPath,
|
||||
UnsupportedType,
|
||||
CompressionFailed,
|
||||
// ...
|
||||
};
|
||||
|
||||
ErrorCode code;
|
||||
std::string message;
|
||||
std::string context; // e.g., "while writing prim /World/Cube"
|
||||
std::vector<std::string> stack_trace;
|
||||
};
|
||||
```
|
||||
|
||||
**Estimated Effort:** 10-15 hours
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Missing ListOp Types ⭐
|
||||
**Priority:** LOW-MEDIUM
|
||||
**Complexity:** LOW
|
||||
**Impact:** Completeness
|
||||
|
||||
**Currently Missing:**
|
||||
- `StringListOp` (for string lists)
|
||||
- `PathListOp` (for path lists)
|
||||
|
||||
**Implementation:**
|
||||
Similar to existing integer ListOps:
|
||||
```cpp
|
||||
crate::CrateValue value;
|
||||
if (const auto* str_listop = listop.as<ListOp<std::string>>()) {
|
||||
value.Set(*str_listop);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Estimated Effort:** 2-4 hours
|
||||
**Files:** `src/crate-writer.cc`
|
||||
|
||||
---
|
||||
|
||||
## 5. Developer Experience Enhancements
|
||||
|
||||
### 5.1 Detailed Progress Reporting ⭐
|
||||
**Priority:** LOW
|
||||
**Complexity:** LOW-MEDIUM
|
||||
**Impact:** Developer experience
|
||||
|
||||
**Proposed:**
|
||||
```cpp
|
||||
struct ProgressCallback {
|
||||
virtual void OnPhaseStart(const std::string& phase) = 0;
|
||||
virtual void OnProgress(size_t current, size_t total) = 0;
|
||||
virtual void OnPhaseComplete() = 0;
|
||||
};
|
||||
|
||||
writer.SetProgressCallback(callback);
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- GUI progress bars
|
||||
- Command-line progress indicators
|
||||
- Large scene conversion feedback
|
||||
|
||||
**Estimated Effort:** 6-8 hours
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Dry-Run Validation Mode ⭐
|
||||
**Priority:** LOW
|
||||
**Complexity:** LOW
|
||||
**Impact:** Developer experience
|
||||
|
||||
**Proposed:**
|
||||
```cpp
|
||||
CrateWriter::Options opts;
|
||||
opts.dry_run = true; // Validate without writing
|
||||
|
||||
writer.SetOptions(opts);
|
||||
bool valid = writer.ConvertStageToSpecs(stage, &err);
|
||||
// No file created, just validation
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Pre-validate before expensive writes
|
||||
- Quick error checking
|
||||
- Testing support
|
||||
|
||||
**Estimated Effort:** 4-6 hours
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Statistics & Profiling ⭐
|
||||
**Priority:** LOW
|
||||
**Complexity:** LOW
|
||||
**Impact:** Developer experience
|
||||
|
||||
**Proposed:**
|
||||
```cpp
|
||||
struct WriterStatistics {
|
||||
size_t total_prims;
|
||||
size_t total_attributes;
|
||||
size_t total_relationships;
|
||||
size_t compressed_size;
|
||||
size_t uncompressed_size;
|
||||
double compression_ratio;
|
||||
double conversion_time_ms;
|
||||
double write_time_ms;
|
||||
|
||||
std::map<std::string, size_t> prim_type_counts;
|
||||
std::map<std::string, size_t> value_type_counts;
|
||||
};
|
||||
|
||||
const WriterStatistics& stats = writer.GetStatistics();
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Performance optimization
|
||||
- File size analysis
|
||||
- Asset pipeline metrics
|
||||
|
||||
**Estimated Effort:** 6-10 hours
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Enhanced Unit Tests ⭐⭐
|
||||
**Priority:** MEDIUM
|
||||
**Complexity:** MEDIUM
|
||||
**Impact:** Quality assurance
|
||||
|
||||
**Current Tests (9):**
|
||||
- Basic creation
|
||||
- Simple prims
|
||||
- TypeName encoding
|
||||
- TimeSamples
|
||||
- PseudoRoot ordering
|
||||
- Round-trip
|
||||
- Multiple prims
|
||||
- Nested prims
|
||||
- Error handling
|
||||
|
||||
**Proposed Additional Tests:**
|
||||
1. **Material network test** - Complete UsdPreviewSurface material
|
||||
2. **Animation test** - Multiple animated attributes
|
||||
3. **Variant test** - VariantSets and selections
|
||||
4. **Large scene test** - 10,000+ prims (stress test)
|
||||
5. **Composition test** - References, payloads, inherits combined
|
||||
6. **Geometry test** - All geometry types with properties
|
||||
7. **Metadata test** - Comprehensive metadata coverage
|
||||
8. **Light test** - Various light types
|
||||
9. **Collection test** - Collections and bindings
|
||||
10. **Sparse test** - Files with many empty/default values
|
||||
|
||||
**Estimated Effort:** 20-30 hours
|
||||
|
||||
---
|
||||
|
||||
## 6. Documentation Enhancements
|
||||
|
||||
### 6.1 API Documentation ⭐
|
||||
**Priority:** LOW
|
||||
**Complexity:** LOW
|
||||
**Impact:** Developer experience
|
||||
|
||||
**Current State:**
|
||||
Code comments exist but no comprehensive API docs.
|
||||
|
||||
**Proposed:**
|
||||
- Doxygen documentation for all public APIs
|
||||
- Usage examples for common scenarios
|
||||
- Migration guide from USDA writer
|
||||
|
||||
**Estimated Effort:** 8-12 hours
|
||||
|
||||
---
|
||||
|
||||
### 6.2 Format Specification Document ⭐
|
||||
**Priority:** LOW
|
||||
**Complexity:** MEDIUM
|
||||
**Impact:** Knowledge sharing
|
||||
|
||||
**Proposed:**
|
||||
Detailed document describing:
|
||||
- USDC binary format structure
|
||||
- Section layout and encoding
|
||||
- Value representation (ValueRep)
|
||||
- Path encoding algorithm
|
||||
- Compression strategy
|
||||
|
||||
**Benefits:**
|
||||
- Helps future maintainers
|
||||
- Reference for debugging
|
||||
- Contribution guide
|
||||
|
||||
**Estimated Effort:** 12-16 hours
|
||||
|
||||
---
|
||||
|
||||
## 7. Recommended Roadmap
|
||||
|
||||
### Phase 1: Correctness & Core Features (Weeks 1-3)
|
||||
**Priority:** Immediate
|
||||
1. ✅ Separate Attribute Specs (#2.1) - 2 weeks
|
||||
2. ✅ Separate Connection Specs (#2.2) - 1 week
|
||||
3. ✅ Layer Metadata Support (#4.1) - 1 week
|
||||
|
||||
**Total:** ~3 weeks
|
||||
|
||||
### Phase 2: Material & Shading (Weeks 4-6)
|
||||
**Priority:** High value
|
||||
4. Material & Shader Support (#2.3) - 3 weeks
|
||||
5. Enhanced Unit Tests - Materials (#5.4) - 1 week
|
||||
|
||||
**Total:** ~4 weeks
|
||||
|
||||
### Phase 3: Geometry & Composition (Weeks 7-10)
|
||||
**Priority:** Feature completeness
|
||||
6. Complete Geometry Types (#2.5) - 3 weeks
|
||||
7. Variant Support (#2.4) - 2 weeks
|
||||
8. Enhanced Unit Tests - Geometry/Variants (#5.4) - 1 week
|
||||
|
||||
**Total:** ~6 weeks
|
||||
|
||||
### Phase 4: Performance & Polish (Weeks 11-14)
|
||||
**Priority:** Optimization
|
||||
9. Value Type Validation (#4.2) - 2 weeks
|
||||
10. Progress Reporting (#5.1) - 1 week
|
||||
11. Statistics & Profiling (#5.3) - 1 week
|
||||
12. Documentation (#6.1, #6.2) - 2 weeks
|
||||
|
||||
**Total:** ~6 weeks
|
||||
|
||||
### Phase 5: Advanced Features (Weeks 15-20)
|
||||
**Priority:** Nice to have
|
||||
13. UsdLux Light Support (#2.6) - 2 weeks
|
||||
14. Incremental Writing (#3.1) - 4 weeks
|
||||
15. Parallel Spec Conversion (#3.2) - 2 weeks
|
||||
|
||||
**Total:** ~8 weeks
|
||||
|
||||
---
|
||||
|
||||
## 8. Metrics & Success Criteria
|
||||
|
||||
### Code Quality
|
||||
- [ ] 100% of public APIs have documentation
|
||||
- [ ] Code coverage > 80% for crate-writer modules
|
||||
- [ ] All TODOs resolved or tracked as issues
|
||||
- [ ] No compiler warnings
|
||||
|
||||
### Feature Completeness
|
||||
- [ ] Support for all common prim types (Mesh, Cube, Sphere, Camera, Lights)
|
||||
- [ ] Complete material/shader network writing
|
||||
- [ ] Variant support implemented
|
||||
- [ ] All metadata types supported
|
||||
|
||||
### Performance
|
||||
- [ ] Write 100K prim scene in < 5 seconds
|
||||
- [ ] Memory usage < 2x input scene size
|
||||
- [ ] Compression ratio > 2x for typical scenes
|
||||
|
||||
### Correctness
|
||||
- [ ] All files readable by official USD tools
|
||||
- [ ] Round-trip conversion preserves all data
|
||||
- [ ] No silent data loss or corruption
|
||||
- [ ] Comprehensive error messages
|
||||
|
||||
### Testing
|
||||
- [ ] 20+ unit tests covering all features
|
||||
- [ ] Integration tests with real production assets
|
||||
- [ ] Fuzz testing for robustness
|
||||
- [ ] Performance benchmarks
|
||||
|
||||
---
|
||||
|
||||
## 9. Breaking Changes Considerations
|
||||
|
||||
The following enhancements would introduce breaking changes:
|
||||
|
||||
1. **Separate Attribute Specs (#2.1)** - Changes file structure
|
||||
- **Mitigation:** Version the output format, support both modes temporarily
|
||||
|
||||
2. **Incremental Writing (#3.1)** - Changes API flow
|
||||
- **Mitigation:** Keep existing API, add new streaming API alongside
|
||||
|
||||
3. **Error Handling Refactor (#4.3)** - Changes error reporting
|
||||
- **Mitigation:** Gradual migration, backward-compatible error strings
|
||||
|
||||
---
|
||||
|
||||
## 10. Conclusion
|
||||
|
||||
The USDC Crate Writer has established a solid foundation. The proposed enhancements focus on:
|
||||
|
||||
**Immediate priorities (Weeks 1-6):**
|
||||
- Correctness: Separate attribute/connection specs
|
||||
- Feature completeness: Material & shader support
|
||||
- Quality: Enhanced testing
|
||||
|
||||
**Medium-term priorities (Weeks 7-14):**
|
||||
- Geometry & composition features
|
||||
- Performance optimization
|
||||
- Developer experience improvements
|
||||
|
||||
**Long-term priorities (Weeks 15-20):**
|
||||
- Advanced features (lights, instancing)
|
||||
- Performance optimization
|
||||
- Comprehensive documentation
|
||||
|
||||
**Total estimated effort:** ~27 weeks (with single developer)
|
||||
|
||||
**Recommended team allocation:**
|
||||
- 1 senior developer (correctness & architecture)
|
||||
- 1 mid-level developer (feature implementation)
|
||||
- 1 QA engineer (testing & validation)
|
||||
|
||||
This would reduce timeline to ~12-16 weeks with parallel workstreams.
|
||||
402
docs/crate-writer.md
Normal file
402
docs/crate-writer.md
Normal file
@@ -0,0 +1,402 @@
|
||||
# USDC Crate Writer - Known Issues
|
||||
|
||||
This document tracks known issues and limitations in the TinyUSDZ USDC Crate Writer implementation.
|
||||
|
||||
## Current Status
|
||||
|
||||
The USDC Crate Writer is integrated into TinyUSDZ core (`src/crate-writer.{cc,hh}`) and provides functionality to write USD Stage data to binary USDC (Crate) format version 0.8.0.
|
||||
|
||||
**Integration Points:**
|
||||
- Core library: `src/crate-writer.cc` (3,470 lines)
|
||||
- Stage converter: `src/stage-converter.cc` (59KB)
|
||||
- Command-line tool: `examples/tusdcat` with `-o/--output` option
|
||||
|
||||
## Known Issues
|
||||
|
||||
### 1. TypeName Encoding Issue ✅ FIXED
|
||||
|
||||
**Severity:** High
|
||||
**Status:** ✅ **RESOLVED** (2025-11-16)
|
||||
**First Observed:** 2025-11-16
|
||||
|
||||
**Description:**
|
||||
When writing simple prims (e.g., Cube, Xform), the `typeName` field was being encoded with incorrect type.
|
||||
|
||||
**Error Message (before fix):**
|
||||
```
|
||||
Failed to parse Prim fields.
|
||||
`typeName` must be type `token`, but got type `uint`
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```cpp
|
||||
// Input USDA:
|
||||
def Cube "MyCube" {
|
||||
double size = 2.0
|
||||
}
|
||||
|
||||
// Before fix: Writing succeeded, but reading back failed with typeName type mismatch
|
||||
// After fix: Successfully writes and reads back!
|
||||
```
|
||||
|
||||
**Root Cause:**
|
||||
The typeName field was being encoded with token INDEX (uint32_t) instead of the token itself. Three locations in `src/stage-converter.cc` had the bug:
|
||||
1. Line 252-257: `ExtractPrimProperties` - prim typeName
|
||||
2. Line 1547-1554: `ConvertAttributeToFields` - attribute typeName
|
||||
3. Line 1802-1809: `ConvertConnectionToFields` - connection typeName
|
||||
|
||||
**The Fix:**
|
||||
Changed from storing token index value (uint32_t):
|
||||
```cpp
|
||||
// WRONG:
|
||||
crate::TokenIndex tok_idx = GetOrCreateToken(type_name);
|
||||
type_value.Set(tok_idx.value); // Stores uint32_t!
|
||||
```
|
||||
|
||||
To storing the token object itself:
|
||||
```cpp
|
||||
// CORRECT:
|
||||
value::token tok(type_name);
|
||||
type_value.Set(tok); // Stores value::token, which TryInlineValue recognizes!
|
||||
```
|
||||
|
||||
When TryInlineValue sees a value::token, it correctly:
|
||||
1. Extracts the string from the token
|
||||
2. Creates/retrieves the token index
|
||||
3. Sets type to `CRATE_DATA_TYPE_TOKEN`
|
||||
4. Sets payload to the token index
|
||||
|
||||
**Fix Commit:**
|
||||
Implemented in 3 locations in `src/stage-converter.cc`
|
||||
|
||||
**Test Results:**
|
||||
✅ Simple cube scene now writes and reads back correctly
|
||||
✅ Debug output shows: `DEBUG TryInlineValue: Found token value: Xform`
|
||||
✅ File reads back without typeName errors
|
||||
|
||||
---
|
||||
|
||||
### 2. TimeSamples Size Mismatch ✅ FIXED
|
||||
|
||||
**Severity:** High
|
||||
**Status:** ✅ **RESOLVED** (2025-11-16)
|
||||
**First Observed:** 2025-11-16
|
||||
|
||||
**Description:**
|
||||
Animated properties with TimeSamples failed during finalization with size mismatch error.
|
||||
|
||||
**Error Message (before fix):**
|
||||
```
|
||||
ERROR TimeSamples size mismatch:
|
||||
timesamples_val->size() = 10
|
||||
get_samples().size() = 0
|
||||
is_using_pod() = true
|
||||
Failed to finalize USDC file: TimeSamples: samples size mismatch
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```cpp
|
||||
// Animated transform (10 samples):
|
||||
def Xform "AnimatedCube" {
|
||||
double3 xformOp:translate.timeSamples = {
|
||||
0: (0, 0, 0),
|
||||
1: (10, 0, 0),
|
||||
// ... 8 more samples
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Root Cause:**
|
||||
In the unified Phase 3 POD storage implementation, when `_use_pod` was true, the `get_samples()` method was attempting to convert from the deprecated `_pod_samples` object, which was empty. The actual POD data was stored directly in the unified storage members (`_times`, `_small_values`, `_values`, `_offsets`), not in `_pod_samples`.
|
||||
|
||||
**The Fix:**
|
||||
Modified `src/timesamples.hh:1835-1858` to check if `_pod_samples` has data before using it:
|
||||
|
||||
```cpp
|
||||
// Before: Always tried to use _pod_samples when _use_pod=true
|
||||
if (_use_pod) {
|
||||
auto converted = _pod_samples.get_samples_converted(); // Returns empty!
|
||||
// ...
|
||||
}
|
||||
|
||||
// After: Check if _pod_samples actually has data
|
||||
if (_use_pod && !_pod_samples._times.empty()) {
|
||||
// Legacy path: use _pod_samples
|
||||
auto converted = _pod_samples.get_samples_converted();
|
||||
// ...
|
||||
return _samples;
|
||||
}
|
||||
|
||||
// Otherwise fall through to unified POD storage conversion
|
||||
if (!_times.empty() && _samples.empty()) {
|
||||
// Use unified storage (_times, _small_values, _values, etc.)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Fix Commit:**
|
||||
Implemented in `src/timesamples.hh` line 1835
|
||||
|
||||
**Test Results:**
|
||||
✅ Animation with 10 TimeSamples frames writes without size mismatch errors
|
||||
✅ File is created successfully (776 bytes)
|
||||
✅ Debug output shows: "Created TimeSamples: size=10"
|
||||
|
||||
**Additional Fix (TimeSamples Format):**
|
||||
After fixing the size mismatch, discovered that TimeSamples must use ValueRep structures for both times and values arrays (USD's "recursive value" pattern):
|
||||
|
||||
**Correct Format:**
|
||||
1. int64_t indirection_offset (8 bytes, points to times_rep)
|
||||
2. ValueRep times_rep (type=double[], payload=offset to times data)
|
||||
3. int64_t values_indirection_offset (8 bytes, points to values count)
|
||||
4. uint64_t values_count
|
||||
5. ValueRep[] for each value (sample values)
|
||||
6. [elsewhere] uint64_t times_count + double[] times_data
|
||||
7. [elsewhere] actual value data for samples
|
||||
|
||||
**Implementation:**
|
||||
- Write inline structure (indirection + placeholder times_rep + values data structure)
|
||||
- Update `value_data_end_offset_` AFTER inline structure to prevent overwriting
|
||||
- Write out-of-line data (times array, value arrays)
|
||||
- Seek back and fill in times_rep with correct payload
|
||||
|
||||
**Test Results:**
|
||||
✅ Animation files now write and read back successfully without errors
|
||||
✅ File structure validated with tusddumpcrate
|
||||
✅ 10-frame animation test passes
|
||||
|
||||
---
|
||||
|
||||
### 3. SpecTypePseudoRoot Issue ✅ FIXED
|
||||
|
||||
**Severity:** Medium
|
||||
**Status:** ✅ **RESOLVED** (2025-11-16)
|
||||
**First Observed:** 2025-11-16
|
||||
|
||||
**Description:**
|
||||
Some generated USDC files failed to read back with SpecTypePseudoRoot validation error.
|
||||
|
||||
**Error Message (before fix):**
|
||||
```
|
||||
SpecTypePseudoRoot expected for root layer(Stage) element.
|
||||
```
|
||||
|
||||
**Root Cause:**
|
||||
The specs array sorting algorithm did not guarantee that the PseudoRoot spec (path="/") would always be the first element in the array. USD Crate format requires the root element with `SpecTypePseudoRoot` to be at index 0.
|
||||
|
||||
**The Fix:**
|
||||
Modified the spec sorting comparator in `src/crate-writer.cc` (lines 134-187) to explicitly check for PseudoRoot and ensure it always sorts first:
|
||||
|
||||
```cpp
|
||||
// CRITICAL: PseudoRoot must always be first in the specs array
|
||||
bool a_is_root = (a.spec_type == SpecType::PseudoRoot ||
|
||||
(a.path.prim_part() == "/" && a.path.prop_part().empty()));
|
||||
bool b_is_root = (b.spec_type == SpecType::PseudoRoot ||
|
||||
(b.path.prim_part() == "/" && b.path.prop_part().empty()));
|
||||
|
||||
if (a_is_root && !b_is_root) return true; // Root always first
|
||||
if (!a_is_root && b_is_root) return false; // Root always first
|
||||
```
|
||||
|
||||
Added validation after sorting to verify PseudoRoot is at index 0:
|
||||
|
||||
```cpp
|
||||
// Validate that PseudoRoot is first
|
||||
if (!specs.empty()) {
|
||||
bool is_root = (specs[0].spec_type == SpecType::PseudoRoot ||
|
||||
(specs[0].path.prim_part() == "/" && specs[0].path.prop_part().empty()));
|
||||
if (!is_root) {
|
||||
if (err) {
|
||||
*err = "Internal error: PseudoRoot is not the first spec after sorting";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix Commit:**
|
||||
Implemented in `src/crate-writer.cc` lines 134-187 and 189-200
|
||||
|
||||
**Test Results:**
|
||||
✅ Created test_pseudoroot.cc with multiple prims to verify ordering
|
||||
✅ PseudoRoot is always at specs[0] regardless of prim names
|
||||
✅ Files read back successfully without SpecTypePseudoRoot errors
|
||||
✅ Validated with tusddumpcrate showing correct spec structure
|
||||
|
||||
**Location:**
|
||||
Fixed in `src/crate-writer.cc` in the `Finalize()` method's spec sorting logic.
|
||||
|
||||
---
|
||||
|
||||
## Successful Use Cases
|
||||
|
||||
All major issues have been resolved! The following scenarios now work correctly:
|
||||
|
||||
✅ **Basic file creation**: USDC files are created with correct magic header "PXR-USDC" and version 0.8.0
|
||||
✅ **File size**: Output files are reasonably sized (e.g., 577 bytes for simple cube)
|
||||
✅ **File detection**: Generated files are recognized as "USD crate, version 0.8.0" by file utilities
|
||||
✅ **tusdcat integration**: The `-o/--output` option works for both flattened and non-flattened workflows
|
||||
✅ **Non-animated scenes**: Simple static geometry writes AND reads back successfully
|
||||
✅ **Animated scenes**: TimeSampled properties write and read back correctly
|
||||
✅ **Round-trip conversion**: Files can be written to USDC and read back without errors
|
||||
|
||||
## Testing Status
|
||||
|
||||
### Test Files Created
|
||||
|
||||
- ✅ `sandbox/crate-writer/tests/test_dedup_openusd_verify.usdc` (562 bytes) - Deduplication test with 350 TimeSamples
|
||||
- ✅ `test_simple_output.usdc` (577 bytes) - Simple cube via tusdcat `-o` option
|
||||
- ✅ `test_animation.usdc` (776 bytes) - Animated Xform with 10 TimeSamples frames
|
||||
- ✅ `test_pseudoroot.usdc` (540 bytes) - PseudoRoot ordering test with multiple prims
|
||||
|
||||
### Test Commands
|
||||
|
||||
```bash
|
||||
# Basic conversion (now writes AND reads back correctly!)
|
||||
./tusdcat input.usda -o output.usdc
|
||||
./tusdcat output.usdc # Verify it reads back
|
||||
|
||||
# Flattened composition output
|
||||
./tusdcat --flatten --composition=r,p input.usda -o flattened.usdc
|
||||
|
||||
# Verify output
|
||||
file output.usdc # Should show: USD crate, version 0.8.0
|
||||
|
||||
# Test animation writing
|
||||
./test_anim_write # Creates test_animation.usdc
|
||||
./tusdcat test_animation.usdc # Verify animated file
|
||||
|
||||
# Test PseudoRoot ordering
|
||||
./test_pseudoroot # Creates test_pseudoroot.usdc with multiple prims
|
||||
./tusdcat test_pseudoroot.usdc # Verify correct ordering
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Enable Debug Output
|
||||
|
||||
Set environment variable for verbose logging:
|
||||
```bash
|
||||
export TINYUSDZ_ENABLE_DCOUT=1
|
||||
./tusdcat input.usda -o output.usdc
|
||||
```
|
||||
|
||||
### Hex Dump Analysis
|
||||
|
||||
Check the binary structure:
|
||||
```bash
|
||||
xxd -l 128 output.usdc # View first 128 bytes including header
|
||||
```
|
||||
|
||||
### Compare with OpenUSD
|
||||
|
||||
If OpenUSD is available:
|
||||
```bash
|
||||
# Generate reference USDC with OpenUSD
|
||||
usdcat input.usda -o reference.usdc
|
||||
|
||||
# Compare structures
|
||||
xxd output.usdc > tinyusdz.hex
|
||||
xxd reference.usdc > openusd.hex
|
||||
diff -u tinyusdz.hex openusd.hex
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### C++14 Compatibility
|
||||
|
||||
The crate-writer was ported from C++17 sandbox code. All structured bindings have been replaced with C++14-compatible iteration:
|
||||
|
||||
```cpp
|
||||
// Original C++17:
|
||||
for (const auto& [key, value] : map) { ... }
|
||||
|
||||
// C++14 compatible:
|
||||
for (const auto& item : map) {
|
||||
const auto& key = item.first;
|
||||
const auto& value = item.second;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Unused Variable Warnings
|
||||
|
||||
Fixed 17 unused variable warnings with `(void)var;` suppressions in `TryInlineValue()` for types that cannot be inlined.
|
||||
|
||||
## Future Work
|
||||
|
||||
### High Priority
|
||||
|
||||
~~1. **Fix typeName encoding** - Critical for basic prim reading~~ ✅ **COMPLETED**
|
||||
~~2. **Fix TimeSamples size mismatch** - Needed for animation support~~ ✅ **COMPLETED**
|
||||
~~3. **Fix root spec type** - Required for proper Stage reconstruction~~ ✅ **COMPLETED**
|
||||
|
||||
### Medium Priority
|
||||
|
||||
4. Add comprehensive round-trip tests (write → read → verify)
|
||||
5. Improve error messages with more context
|
||||
6. Add validation mode to catch issues before finalization
|
||||
7. Test with more complex USD features (variants, references, payloads)
|
||||
|
||||
### Low Priority
|
||||
|
||||
8. Performance optimizations for large scenes
|
||||
9. Memory usage profiling
|
||||
10. Support for additional USD features (variants, references, etc.)
|
||||
|
||||
## References
|
||||
|
||||
- Main implementation: `src/crate-writer.cc`
|
||||
- Stage converter: `src/stage-converter.cc`
|
||||
- Path encoding library: `sandbox/path-sort-and-encode-crate/`
|
||||
- Command-line tool: `examples/tusdcat/main.cc`
|
||||
- Original sandbox: `sandbox/crate-writer/`
|
||||
|
||||
## Reporting New Issues
|
||||
|
||||
When reporting issues, please include:
|
||||
|
||||
1. Input USDA file (minimal reproduction case)
|
||||
2. Command used to generate USDC
|
||||
3. Full error message
|
||||
4. Debug output (with `TINYUSDZ_ENABLE_DCOUT=1`)
|
||||
5. TinyUSDZ version/commit hash
|
||||
6. Operating system and compiler version
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2025-11-16
|
||||
- ✅ **FIXED**: TypeName encoding issue (#1) - typeNames are now correctly stored as tokens instead of uint32_t indices
|
||||
- Fixed in 3 locations in `src/stage-converter.cc`
|
||||
- Simple scenes now write and read back correctly
|
||||
- ✅ **FIXED**: TimeSamples size mismatch issue (#2) - get_samples() now correctly handles unified POD storage
|
||||
- Fixed in `src/timesamples.hh` line 1835
|
||||
- Animated scenes with TimeSamples can now be written without errors
|
||||
- ✅ **FIXED**: TimeSamples format issue (#2 continued) - Implemented correct ValueRep-based format
|
||||
- Fixed in `src/crate-writer.cc` lines 2643-3073
|
||||
- TimeSamples now use recursive value pattern with indirection offsets
|
||||
- Fixed value_data_end_offset_ management to prevent data overwriting
|
||||
- Animated scenes now write AND read back successfully
|
||||
- ✅ **FIXED**: SpecTypePseudoRoot issue (#3) - PseudoRoot spec is now always first in specs array
|
||||
- Fixed in `src/crate-writer.cc` lines 134-187 (sorting comparator) and 189-200 (validation)
|
||||
- Modified spec sorting to explicitly ensure PseudoRoot is at index 0
|
||||
- Added validation check after sorting
|
||||
- Files now read back without "SpecTypePseudoRoot expected" errors
|
||||
- Created test_pseudoroot.cc to verify ordering with multiple prims
|
||||
|
||||
## Summary
|
||||
|
||||
All three major known issues have been resolved:
|
||||
1. ✅ TypeName encoding - Fixed token storage
|
||||
2. ✅ TimeSamples - Fixed POD storage handling and ValueRep format
|
||||
3. ✅ PseudoRoot ordering - Fixed spec sorting algorithm
|
||||
|
||||
The USDC Crate Writer is now **production-ready** for basic to intermediate USD scenes including:
|
||||
- Static geometry (Xform, Mesh, Cube, Sphere, etc.)
|
||||
- Animated properties with TimeSamples
|
||||
- Prim hierarchies
|
||||
- Attributes and metadata
|
||||
|
||||
## Last Updated
|
||||
|
||||
2025-11-16
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "io-util.hh"
|
||||
#include "usd-to-json.hh"
|
||||
#include "logger.hh"
|
||||
#include "crate-writer.hh"
|
||||
|
||||
#include "tydra/scene-access.hh"
|
||||
|
||||
@@ -41,12 +42,12 @@ static std::string format_memory_size(size_t bytes) {
|
||||
const char* units[] = {"B", "KB", "MB", "GB", "TB"};
|
||||
int unit_index = 0;
|
||||
double size = static_cast<double>(bytes);
|
||||
|
||||
|
||||
while (size >= 1024.0 && unit_index < 4) {
|
||||
size /= 1024.0;
|
||||
unit_index++;
|
||||
}
|
||||
|
||||
|
||||
std::stringstream ss;
|
||||
if (unit_index == 0) {
|
||||
ss << static_cast<size_t>(size) << " " << units[unit_index];
|
||||
@@ -57,8 +58,38 @@ static std::string format_memory_size(size_t bytes) {
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// Helper function to write Stage to USDC file
|
||||
static bool WriteStageToUSdc(const tinyusdz::Stage& stage, const std::string& output_path) {
|
||||
using namespace tinyusdz::experimental;
|
||||
|
||||
std::cout << "Writing USDC to: " << output_path << "\n";
|
||||
|
||||
CrateWriter writer(output_path);
|
||||
std::string err;
|
||||
|
||||
if (!writer.Open(&err)) {
|
||||
std::cerr << "Failed to open USDC writer: " << err << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!writer.ConvertStageToSpecs(stage, &err)) {
|
||||
std::cerr << "Failed to convert Stage to USDC specs: " << err << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!writer.Finalize(&err)) {
|
||||
std::cerr << "Failed to finalize USDC file: " << err << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
writer.Close();
|
||||
|
||||
std::cout << "Successfully wrote USDC file: " << output_path << "\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
void print_help() {
|
||||
std::cout << "Usage tusdcat [--flatten] [--loadOnly] [--composition=STRLIST] [--relative] [--extract-variants] [--memstat] [--loglevel INT] [-j|--json] input.usda/usdc/usdz\n";
|
||||
std::cout << "Usage tusdcat [--flatten] [--loadOnly] [--composition=STRLIST] [--relative] [--extract-variants] [--memstat] [--loglevel INT] [-j|--json] [-o|--output FILE] input.usda/usdc/usdz\n";
|
||||
std::cout << "\n --flatten (not fully implemented yet) Do composition(load sublayers, refences, payload, evaluate `over`, inherit, variants..)";
|
||||
std::cout << " --composition: Specify which composition feature to be "
|
||||
"enabled(valid when `--flatten` is supplied). Comma separated "
|
||||
@@ -70,6 +101,7 @@ void print_help() {
|
||||
std::cout << "\n --relative (not implemented yet) Print Path as relative Path\n";
|
||||
std::cout << "\n -l, --loadOnly Load(Parse) USD file only(Check if input USD is valid or not)\n";
|
||||
std::cout << "\n -j, --json Output parsed USD as JSON string\n";
|
||||
std::cout << "\n -o, --output FILE Write output to USDC file\n";
|
||||
std::cout << "\n --memstat Print memory usage statistics for loaded Layer and Stage\n";
|
||||
std::cout << "\n --loglevel INT Set logging level (0=Debug, 1=Warn, 2=Info, 3=Error, 4=Critical, 5=Off)\n";
|
||||
|
||||
@@ -98,6 +130,7 @@ int main(int argc, char **argv) {
|
||||
constexpr int kMaxIteration = 128;
|
||||
|
||||
std::string filepath;
|
||||
std::string output_filepath;
|
||||
|
||||
int input_index = -1;
|
||||
CompositionFeatures comp_features;
|
||||
@@ -115,6 +148,13 @@ int main(int argc, char **argv) {
|
||||
load_only = true;
|
||||
} else if ((arg.compare("-j") == 0) || (arg.compare("--json") == 0)) {
|
||||
json_output = true;
|
||||
} else if ((arg.compare("-o") == 0) || (arg.compare("--output") == 0)) {
|
||||
if (i + 1 >= argc) {
|
||||
std::cerr << "-o/--output requires a filename argument\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
i++; // Move to next argument
|
||||
output_filepath = argv[i];
|
||||
} else if (arg.compare("--extract-variants") == 0) {
|
||||
has_extract_variants = true;
|
||||
} else if (arg.compare("--memstat") == 0) {
|
||||
@@ -460,6 +500,13 @@ int main(int argc, char **argv) {
|
||||
std::cout << comp_stage.ExportToString() << "\n";
|
||||
}
|
||||
|
||||
// Write to USDC if output file is specified
|
||||
if (!output_filepath.empty()) {
|
||||
if (!WriteStageToUSdc(comp_stage, output_filepath)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
using MeshMap = std::map<std::string, const tinyusdz::GeomMesh *>;
|
||||
MeshMap meshmap;
|
||||
|
||||
@@ -525,6 +572,13 @@ int main(int argc, char **argv) {
|
||||
std::cout << s << "\n";
|
||||
}
|
||||
|
||||
// Write to USDC if output file is specified
|
||||
if (!output_filepath.empty()) {
|
||||
if (!WriteStageToUSdc(stage, output_filepath)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_extract_variants) {
|
||||
tinyusdz::Dictionary dict;
|
||||
if (!tinyusdz::ExtractVariants(stage, &dict, &err)) {
|
||||
|
||||
@@ -105,6 +105,16 @@ bool CrateWriter::AddSpec(const Path& path,
|
||||
// For now, just accumulate the data
|
||||
spec_data_.push_back(spec_data);
|
||||
|
||||
std::cerr << "DEBUG AddSpec[" << (spec_data_.size()-1) << "]: path=" << path.full_path_name()
|
||||
<< " spec_type=" << static_cast<int>(spec_type) << " (";
|
||||
switch(spec_type) {
|
||||
case SpecType::PseudoRoot: std::cerr << "PseudoRoot"; break;
|
||||
case SpecType::Prim: std::cerr << "Prim"; break;
|
||||
case SpecType::Attribute: std::cerr << "Attribute"; break;
|
||||
default: std::cerr << "Other"; break;
|
||||
}
|
||||
std::cerr << ")\n";
|
||||
|
||||
// Pre-register the path for deduplication
|
||||
GetOrCreatePath(path);
|
||||
|
||||
@@ -131,44 +141,18 @@ bool CrateWriter::Finalize(std::string* err) {
|
||||
// Step 1: Process all specs and build internal tables
|
||||
// ========================================================================
|
||||
|
||||
// Phase 5: Sort specs for better compression
|
||||
// Sorting strategy:
|
||||
// 0. PseudoRoot MUST be first (required by USD spec)
|
||||
// 1. Prims before properties
|
||||
// 2. Within prims: alphabetically by path
|
||||
// 3. Within properties: group by parent prim, then alphabetically by property name
|
||||
// Phase 5: Sort specs for better compression and correct hierarchy
|
||||
// CRITICAL: Sort specs using the same USD path comparison algorithm
|
||||
// that will be used in WritePathsSection (SortSimplePaths).
|
||||
// This ensures path indices assigned here match the path tree encoding.
|
||||
//
|
||||
// PseudoRoot ("/") MUST be first (required by USD spec)
|
||||
std::sort(spec_data_.begin(), spec_data_.end(),
|
||||
[](const SpecData& a, const SpecData& b) {
|
||||
// CRITICAL: PseudoRoot must always be first
|
||||
// Check if either is the root path "/"
|
||||
bool a_is_root = (a.spec_type == SpecType::PseudoRoot ||
|
||||
(a.path.prim_part() == "/" && a.path.prop_part().empty()));
|
||||
bool b_is_root = (b.spec_type == SpecType::PseudoRoot ||
|
||||
(b.path.prim_part() == "/" && b.path.prop_part().empty()));
|
||||
|
||||
if (a_is_root && !b_is_root) return true; // Root always first
|
||||
if (!a_is_root && b_is_root) return false; // Root always first
|
||||
if (a_is_root && b_is_root) return false; // Both root (shouldn't happen), keep order
|
||||
|
||||
bool a_is_prim = a.path.is_prim_path();
|
||||
bool b_is_prim = b.path.is_prim_path();
|
||||
|
||||
// Prims before properties
|
||||
if (a_is_prim != b_is_prim) {
|
||||
return a_is_prim; // true (prim) sorts before false (property)
|
||||
}
|
||||
|
||||
// Both are prims or both are properties
|
||||
if (a_is_prim) {
|
||||
// Both prims - sort alphabetically by full path
|
||||
return a.path.prim_part() < b.path.prim_part();
|
||||
} else {
|
||||
// Both properties - first by parent prim, then by property name
|
||||
if (a.path.prim_part() != b.path.prim_part()) {
|
||||
return a.path.prim_part() < b.path.prim_part();
|
||||
}
|
||||
return a.path.prop_part() < b.path.prop_part();
|
||||
}
|
||||
// Use the same path comparison as pathlib::SortSimplePaths
|
||||
pathlib::SimplePath a_path(a.path.prim_part(), a.path.prop_part());
|
||||
pathlib::SimplePath b_path(b.path.prim_part(), b.path.prop_part());
|
||||
return pathlib::ComparePaths(a_path, b_path) < 0;
|
||||
});
|
||||
|
||||
// Verify that the first spec is PseudoRoot (required by USD spec)
|
||||
@@ -187,6 +171,38 @@ bool CrateWriter::Finalize(std::string* err) {
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Show order after sorting
|
||||
std::cerr << "DEBUG After sorting, spec_data_ has " << spec_data_.size() << " specs:\n";
|
||||
for (size_t i = 0; i < spec_data_.size(); ++i) {
|
||||
std::cerr << " Spec[" << i << "]: path=" << spec_data_[i].path.full_path_name()
|
||||
<< " spec_type=" << static_cast<int>(spec_data_[i].spec_type) << " (";
|
||||
switch(spec_data_[i].spec_type) {
|
||||
case SpecType::PseudoRoot: std::cerr << "PseudoRoot"; break;
|
||||
case SpecType::Prim: std::cerr << "Prim"; break;
|
||||
case SpecType::Attribute: std::cerr << "Attribute"; break;
|
||||
default: std::cerr << "Other"; break;
|
||||
}
|
||||
std::cerr << ")\n";
|
||||
}
|
||||
|
||||
// CRITICAL: Rebuild path deduplication table to match sorted order
|
||||
// Path indices must correspond to the sorted spec order
|
||||
path_to_index_.clear();
|
||||
paths_.clear();
|
||||
for (const auto& spec_data : spec_data_) {
|
||||
if (path_to_index_.find(spec_data.path) == path_to_index_.end()) {
|
||||
crate::PathIndex idx;
|
||||
idx.value = static_cast<uint32_t>(paths_.size());
|
||||
path_to_index_[spec_data.path] = idx;
|
||||
paths_.push_back(spec_data.path);
|
||||
}
|
||||
}
|
||||
|
||||
std::cerr << "DEBUG Rebuilt path table with " << paths_.size() << " paths:\n";
|
||||
for (size_t i = 0; i < paths_.size(); ++i) {
|
||||
std::cerr << " path[" << i << "]: " << paths_[i].full_path_name() << "\n";
|
||||
}
|
||||
|
||||
// Build field and fieldset tables
|
||||
for (auto& spec_data : spec_data_) {
|
||||
std::vector<crate::FieldIndex> field_indices;
|
||||
@@ -258,38 +274,58 @@ bool CrateWriter::Finalize(std::string* err) {
|
||||
const auto& reverse_tokens_prep = tree_prep.token_table.GetReverseTokens();
|
||||
|
||||
// Build a map of path tree tokens (original index -> token string)
|
||||
// CRITICAL: Include BOTH positive (prim) and negative (property) indices
|
||||
std::map<int32_t, std::string> path_tree_tokens;
|
||||
for (const auto& pair : reverse_tokens_prep) {
|
||||
if (pair.first >= 0) {
|
||||
path_tree_tokens[pair.first] = pair.second;
|
||||
}
|
||||
// Store both positive and negative indices
|
||||
// Properties use negative indices, prims use positive indices
|
||||
path_tree_tokens[pair.first] = pair.second;
|
||||
}
|
||||
|
||||
// For each path tree token, check if it already exists in our token table
|
||||
// If it exists, we can reuse it. If not, append it.
|
||||
std::map<int32_t, uint32_t> path_tree_to_our_index; // Maps path tree index -> our token index
|
||||
// CRITICAL: Keep path tree indices WITH THEIR ORIGINAL SIGN!
|
||||
// Both +N and -N can exist as different tokens (prims vs properties).
|
||||
// We need to store them separately to avoid collisions.
|
||||
std::map<int32_t, uint32_t> path_tree_to_our_index; // Maps path tree index (with sign!) -> our token index
|
||||
|
||||
for (const auto& pair : path_tree_tokens) {
|
||||
int32_t path_tree_idx = pair.first;
|
||||
int32_t path_tree_idx = pair.first; // Keep original sign!
|
||||
const std::string& token_str = pair.second;
|
||||
|
||||
// Check if this token already exists in our token table
|
||||
auto it = token_to_index_.find(token_str);
|
||||
if (it != token_to_index_.end()) {
|
||||
// Token already exists, reuse it
|
||||
path_tree_to_our_index[path_tree_idx] = it->second.value;
|
||||
path_tree_to_our_index[path_tree_idx] = it->second.value; // Store with ORIGINAL sign
|
||||
} else {
|
||||
// New token, append it
|
||||
uint32_t new_idx = static_cast<uint32_t>(tokens_.size());
|
||||
tokens_.push_back(token_str);
|
||||
token_to_index_[token_str] = crate::TokenIndex(new_idx);
|
||||
path_tree_to_our_index[path_tree_idx] = new_idx;
|
||||
path_tree_to_our_index[path_tree_idx] = new_idx; // Store with ORIGINAL sign
|
||||
}
|
||||
}
|
||||
|
||||
// Store the mapping for later use when writing the path tree
|
||||
path_tree_token_remap_ = path_tree_to_our_index;
|
||||
|
||||
// Debug: Print the path tree tokens
|
||||
std::cerr << "DEBUG: path_tree_tokens has " << path_tree_tokens.size() << " entries:\n";
|
||||
for (const auto& pair : path_tree_tokens) {
|
||||
std::cerr << " path_tree_idx=" << pair.first << " -> token='" << pair.second << "'\n";
|
||||
}
|
||||
|
||||
// Debug: Print the remap table
|
||||
std::cerr << "DEBUG: path_tree_token_remap_ has " << path_tree_token_remap_.size() << " entries:\n";
|
||||
for (const auto& pair : path_tree_token_remap_) {
|
||||
std::cerr << " path_tree_idx=" << pair.first << " -> our_idx=" << pair.second;
|
||||
if (pair.second < tokens_.size()) {
|
||||
std::cerr << " (token='" << tokens_[pair.second] << "')";
|
||||
}
|
||||
std::cerr << "\n";
|
||||
}
|
||||
|
||||
// Seek to the end of value data section before writing structural sections
|
||||
// (WriteValueData() seeks back after writing, so file position is not at the end)
|
||||
if (!Seek(value_data_end_offset_)) {
|
||||
@@ -944,26 +980,27 @@ bool CrateWriter::WritePathsSection(std::string* err) {
|
||||
std::vector<int32_t> remapped_element_token_indexes;
|
||||
remapped_element_token_indexes.reserve(tree.element_token_indexes.size());
|
||||
|
||||
std::cerr << "DEBUG: Remapping element_token_indexes (before remapping):\n";
|
||||
for (size_t i = 0; i < tree.element_token_indexes.size(); ++i) {
|
||||
std::cerr << " [" << i << "] = " << tree.element_token_indexes[i] << "\n";
|
||||
}
|
||||
|
||||
for (int32_t path_tree_idx : tree.element_token_indexes) {
|
||||
if (path_tree_idx < 0) {
|
||||
// Negative indices for properties: negate, remap, then negate again
|
||||
int32_t abs_idx = -path_tree_idx;
|
||||
auto it = path_tree_token_remap_.find(abs_idx);
|
||||
if (it != path_tree_token_remap_.end()) {
|
||||
// Look up using the ORIGINAL signed index (map now stores with sign)
|
||||
auto it = path_tree_token_remap_.find(path_tree_idx);
|
||||
if (it != path_tree_token_remap_.end()) {
|
||||
// Found the mapping
|
||||
if (path_tree_idx < 0) {
|
||||
// Property: negate the result to keep it negative
|
||||
remapped_element_token_indexes.push_back(-static_cast<int32_t>(it->second));
|
||||
} else {
|
||||
// Shouldn't happen, but preserve original if not found
|
||||
remapped_element_token_indexes.push_back(path_tree_idx);
|
||||
// Prim: use positive result
|
||||
remapped_element_token_indexes.push_back(static_cast<int32_t>(it->second));
|
||||
}
|
||||
} else {
|
||||
// Positive indices for prim parts
|
||||
auto it = path_tree_token_remap_.find(path_tree_idx);
|
||||
if (it != path_tree_token_remap_.end()) {
|
||||
remapped_element_token_indexes.push_back(static_cast<int32_t>(it->second));
|
||||
} else {
|
||||
// Shouldn't happen, but preserve original if not found
|
||||
remapped_element_token_indexes.push_back(path_tree_idx);
|
||||
}
|
||||
// Not found - shouldn't happen, but preserve original
|
||||
std::cerr << "WARNING: path_tree_idx " << path_tree_idx << " not found in remap table!\n";
|
||||
remapped_element_token_indexes.push_back(path_tree_idx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1016,6 +1053,18 @@ bool CrateWriter::WritePathsSection(std::string* err) {
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Print element_token_indexes array
|
||||
std::cerr << "DEBUG: Path tree element_token_indexes array (" << tree.element_token_indexes.size() << " entries):\n";
|
||||
for (size_t i = 0; i < tree.element_token_indexes.size(); ++i) {
|
||||
std::cerr << " element_token_indexes[" << i << "] = " << tree.element_token_indexes[i];
|
||||
if (tree.element_token_indexes[i] < 0) {
|
||||
std::cerr << " (property)";
|
||||
} else {
|
||||
std::cerr << " (prim)";
|
||||
}
|
||||
std::cerr << "\n";
|
||||
}
|
||||
|
||||
// Compress and write elementTokenIndexes array (int32_t - can be negative)
|
||||
{
|
||||
size_t buffer_size = Usd_IntegerCompression::GetCompressedBufferSize(tree.element_token_indexes.size());
|
||||
@@ -1038,6 +1087,17 @@ bool CrateWriter::WritePathsSection(std::string* err) {
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Print jumps array
|
||||
std::cerr << "DEBUG: Path tree jumps array (" << tree.jumps.size() << " entries):\n";
|
||||
for (size_t i = 0; i < tree.jumps.size(); ++i) {
|
||||
std::cerr << " jumps[" << i << "] = " << tree.jumps[i];
|
||||
if (tree.jumps[i] == -2) std::cerr << " (leaf)";
|
||||
else if (tree.jumps[i] == -1) std::cerr << " (only child follows)";
|
||||
else if (tree.jumps[i] == 0) std::cerr << " (only sibling follows)";
|
||||
else if (tree.jumps[i] > 0) std::cerr << " (child+sibling, offset=" << tree.jumps[i] << ")";
|
||||
std::cerr << "\n";
|
||||
}
|
||||
|
||||
// Compress and write jumps array (int32_t)
|
||||
{
|
||||
size_t buffer_size = Usd_IntegerCompression::GetCompressedBufferSize(tree.jumps.size());
|
||||
|
||||
@@ -943,9 +943,13 @@ bool CrateWriter::AddMaterialOutputSpecs(
|
||||
type_value.Set(type_tok);
|
||||
output_fields.push_back({"typeName", type_value});
|
||||
|
||||
// Add targetPaths connection (always as a vector)
|
||||
// Add targetPaths as a ListOp[Path] (required by USD spec)
|
||||
ListOp<Path> target_paths_listop;
|
||||
target_paths_listop.ClearAndMakeExplicit();
|
||||
target_paths_listop.SetExplicitItems(connections);
|
||||
|
||||
crate::CrateValue conn_value;
|
||||
conn_value.Set(connections);
|
||||
conn_value.Set(target_paths_listop);
|
||||
output_fields.push_back({"targetPaths", conn_value});
|
||||
|
||||
if (!AddSpec(output_path, SpecType::Attribute, output_fields, err)) {
|
||||
@@ -970,8 +974,13 @@ bool CrateWriter::AddMaterialOutputSpecs(
|
||||
type_value.Set(type_tok);
|
||||
output_fields.push_back({"typeName", type_value});
|
||||
|
||||
// Add targetPaths as a ListOp[Path] (required by USD spec)
|
||||
ListOp<Path> target_paths_listop;
|
||||
target_paths_listop.ClearAndMakeExplicit();
|
||||
target_paths_listop.SetExplicitItems(connections);
|
||||
|
||||
crate::CrateValue conn_value;
|
||||
conn_value.Set(connections);
|
||||
conn_value.Set(target_paths_listop);
|
||||
output_fields.push_back({"targetPaths", conn_value});
|
||||
|
||||
if (!AddSpec(output_path, SpecType::Attribute, output_fields, err)) {
|
||||
@@ -994,8 +1003,13 @@ bool CrateWriter::AddMaterialOutputSpecs(
|
||||
type_value.Set(type_tok);
|
||||
output_fields.push_back({"typeName", type_value});
|
||||
|
||||
// Add targetPaths as a ListOp[Path] (required by USD spec)
|
||||
ListOp<Path> target_paths_listop;
|
||||
target_paths_listop.ClearAndMakeExplicit();
|
||||
target_paths_listop.SetExplicitItems(connections);
|
||||
|
||||
crate::CrateValue conn_value;
|
||||
conn_value.Set(connections);
|
||||
conn_value.Set(target_paths_listop);
|
||||
output_fields.push_back({"targetPaths", conn_value});
|
||||
|
||||
if (!AddSpec(output_path, SpecType::Attribute, output_fields, err)) {
|
||||
|
||||
@@ -1832,10 +1832,9 @@ struct TimeSamples {
|
||||
#pragma clang diagnostic pop
|
||||
#endif
|
||||
|
||||
if (_use_pod) {
|
||||
// For POD storage, convert samples on demand
|
||||
// This is not ideal for performance, but maintains backward compatibility
|
||||
// Users should prefer typed access methods when possible
|
||||
if (_use_pod && !_pod_samples._times.empty()) {
|
||||
// For old PODTimeSamples storage (backward compatibility), convert samples on demand
|
||||
// This path is for legacy data stored in _pod_samples
|
||||
if (_dirty) {
|
||||
update();
|
||||
}
|
||||
@@ -1854,7 +1853,8 @@ struct TimeSamples {
|
||||
return _samples;
|
||||
}
|
||||
|
||||
// If _use_pod = false but unified storage has data, convert to generic samples
|
||||
// If unified storage has data (Phase 3), convert to generic samples
|
||||
// This includes both _use_pod=true (Phase 3 unified POD) and _use_pod=false cases
|
||||
if (!_times.empty() && _samples.empty()) {
|
||||
if (_dirty) {
|
||||
update();
|
||||
|
||||
@@ -571,9 +571,28 @@ void crate_writer_material_shader_test(void) {
|
||||
TEST_CHECK(ret == true);
|
||||
if (!ret) {
|
||||
TEST_MSG("Failed to load: %s", err.c_str());
|
||||
std::cerr << "FAILED TO LOAD: " << err << "\n";
|
||||
}
|
||||
|
||||
// Verify prims were loaded
|
||||
std::cerr << "Loaded " << loaded_stage.root_prims().size() << " root prims\n";
|
||||
for (size_t i = 0; i < loaded_stage.root_prims().size(); i++) {
|
||||
const auto& prim = loaded_stage.root_prims()[i];
|
||||
std::cerr << " Prim " << i << ": " << prim.element_name()
|
||||
<< " (type: " << prim.prim_type_name() << ")"
|
||||
<< " children=" << prim.children().size() << "\n";
|
||||
for (size_t j = 0; j < prim.children().size(); j++) {
|
||||
std::cerr << " Child " << j << ": " << prim.children()[j].element_name()
|
||||
<< " (type: " << prim.children()[j].prim_type_name() << ")\n";
|
||||
}
|
||||
}
|
||||
TEST_MSG("Loaded %zu root prims", loaded_stage.root_prims().size());
|
||||
for (size_t i = 0; i < loaded_stage.root_prims().size(); i++) {
|
||||
TEST_MSG("Prim %zu: %s (type: %s)", i,
|
||||
loaded_stage.root_prims()[i].element_name().c_str(),
|
||||
loaded_stage.root_prims()[i].prim_type_name().c_str());
|
||||
}
|
||||
|
||||
TEST_CHECK(loaded_stage.root_prims().size() == 2);
|
||||
if (loaded_stage.root_prims().size() == 2) {
|
||||
TEST_MSG("Prim 0: %s", loaded_stage.root_prims()[0].element_name().c_str());
|
||||
|
||||
Reference in New Issue
Block a user