Add role type casting support for TimeSamples in USDC reader

When reading USDC files, TimeSamples were stored with base types
(e.g., float3) instead of role types (e.g., color3f, point3f).
This caused type mismatches when the typeName specified a role type.

Changes:
- timesamples.hh: Add cast_to_role_type() method declaration
- timesamples.cc: Implement cast_to_role_type() with a helper function
  that maps role types to their underlying base types
- usdc-reader.cc: Apply role type casting when processing timeSamples
  fields based on the attribute's typeName

The casting is a zero-copy operation that only updates the type_id
metadata without modifying the underlying data.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Syoyo Fujita
2026-01-17 06:58:52 +09:00
parent c3381718e8
commit c330da7160
3 changed files with 105 additions and 2 deletions

View File

@@ -991,6 +991,87 @@ bool TimeSamples::init(uint32_t type_id) {
return true;
}
namespace {
// Helper function to get underlying type_id from a type_id
// For role types (color3f, point3f, etc.), returns the base type's type_id
// For non-role types, returns the same type_id
uint32_t GetUnderlyingTypeIdFromTypeId(uint32_t tyid) {
// Strip array bit if present
bool is_array = (tyid & TYPE_ID_1D_ARRAY_BIT) != 0;
uint32_t base_tyid = tyid & (~TYPE_ID_1D_ARRAY_BIT);
// Map role types to their underlying types
// This is needed because we don't have TypeTraits access at runtime
#define MAP_ROLE_TO_UNDERLYING(role_id, underlying_id) \
if (base_tyid == role_id) return is_array ? (underlying_id | TYPE_ID_1D_ARRAY_BIT) : underlying_id;
// Texcoord types
MAP_ROLE_TO_UNDERLYING(TYPE_ID_TEXCOORD2H, TYPE_ID_HALF2)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_TEXCOORD2F, TYPE_ID_FLOAT2)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_TEXCOORD2D, TYPE_ID_DOUBLE2)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_TEXCOORD3H, TYPE_ID_HALF3)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_TEXCOORD3F, TYPE_ID_FLOAT3)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_TEXCOORD3D, TYPE_ID_DOUBLE3)
// Normal types
MAP_ROLE_TO_UNDERLYING(TYPE_ID_NORMAL3H, TYPE_ID_HALF3)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_NORMAL3F, TYPE_ID_FLOAT3)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_NORMAL3D, TYPE_ID_DOUBLE3)
// Vector types
MAP_ROLE_TO_UNDERLYING(TYPE_ID_VECTOR3H, TYPE_ID_HALF3)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_VECTOR3F, TYPE_ID_FLOAT3)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_VECTOR3D, TYPE_ID_DOUBLE3)
// Point types
MAP_ROLE_TO_UNDERLYING(TYPE_ID_POINT3H, TYPE_ID_HALF3)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_POINT3F, TYPE_ID_FLOAT3)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_POINT3D, TYPE_ID_DOUBLE3)
// Color types
MAP_ROLE_TO_UNDERLYING(TYPE_ID_COLOR3H, TYPE_ID_HALF3)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_COLOR3F, TYPE_ID_FLOAT3)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_COLOR3D, TYPE_ID_DOUBLE3)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_COLOR4H, TYPE_ID_HALF4)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_COLOR4F, TYPE_ID_FLOAT4)
MAP_ROLE_TO_UNDERLYING(TYPE_ID_COLOR4D, TYPE_ID_DOUBLE4)
// Frame type
MAP_ROLE_TO_UNDERLYING(TYPE_ID_FRAME4D, TYPE_ID_MATRIX4D)
#undef MAP_ROLE_TO_UNDERLYING
// Not a role type, return as-is
return tyid;
}
} // namespace
bool TimeSamples::cast_to_role_type(uint32_t role_type_id) {
if (_type_id == 0) {
return false; // Not initialized
}
// If already the target type, nothing to do
if (_type_id == role_type_id) {
return true;
}
// Get underlying type_ids for both current and target types
uint32_t current_underlying = GetUnderlyingTypeIdFromTypeId(_type_id);
uint32_t target_underlying = GetUnderlyingTypeIdFromTypeId(role_type_id);
// Check if the underlying types match
if (current_underlying != target_underlying) {
return false; // Incompatible types
}
// Safe to cast - just update the type_id
_type_id = role_type_id;
return true;
}
} // namespace value
} // namespace tinyusdz

View File

@@ -180,6 +180,12 @@ struct TimeSamples {
/// This determines whether to use POD optimization or regular storage
bool init(uint32_t type_id);
/// Cast the TimeSamples' type to a role type if the underlying types are compatible.
/// This allows reinterpreting stored base types (e.g., float3) as role types (e.g., color3f).
/// @param role_type_id The target role type's type_id
/// @return true if the cast was successful, false if the underlying types don't match
bool cast_to_role_type(uint32_t role_type_id);
/// Check if unified storage has samples (i.e., _times is not empty)
/// Returns true if any samples have been added to unified storage path.
/// Note: After PODTimeSamples removal, this checks unified storage, not a separate POD type.

View File

@@ -1032,7 +1032,23 @@ bool USDCReader::Impl::ParseProperty(const SpecType spec_type,
// same TimeSamples from the fieldset. Using std::move would leave the
// CrateValue empty after the first use, causing subsequent attributes
// to get an empty TimeSamples.
var.set_timesamples(ts);
//
// We make a copy and apply role type casting to the copy if needed.
value::TimeSamples ts_copy = ts;
// Apply role type casting if typeName specifies a role type
// (e.g., cast float3 to color3f, point3f, etc.)
if (typeName) {
uint32_t role_type_id = value::GetTypeId(typeName.value().str());
if (role_type_id != value::TYPE_ID_INVALID) {
if (ts_copy.cast_to_role_type(role_type_id)) {
DCOUT(fmt::format("Cast TimeSamples to role type {}", typeName.value().str()));
}
// It's ok if casting fails - the base type is still valid
}
}
var.set_timesamples(ts_copy);
} else {
PUSH_ERROR_AND_RETURN_TAG(kTag,
"`timeSamples` is not TimeSamples data.");
@@ -1320,7 +1336,7 @@ bool USDCReader::Impl::ParseProperty(const SpecType spec_type,
#endif
// Do role type cast for default value.
// (TODO: do role type cast for timeSamples?)
// (NOTE: role type cast for timeSamples is done earlier when processing timeSamples field)
if (defaultValue.has_value()) {
if (typeName) {
if (defaultValue.value().type_id() == value::TypeTraits<value::ValueBlock>::type_id()) {