diff --git a/src/tydra/render-data.cc b/src/tydra/render-data.cc
index ae318b1c..d6c8bff8 100644
--- a/src/tydra/render-data.cc
+++ b/src/tydra/render-data.cc
@@ -1129,6 +1129,9 @@ nonstd::expected GetTextureCoordinate(
(void)stage;
+ // HACK
+ return nonstd::make_unexpected("Disabled");
+
std::string err;
GeomPrimvar primvar;
if (!GetGeomPrimvar(stage, &mesh, name, &primvar, &err)) {
@@ -1140,6 +1143,7 @@ nonstd::expected GetTextureCoordinate(
"\n");
}
+ TUSDZ_LOG_I("get tex\n");
// TODO: allow float2?
if (primvar.get_type_id() !=
value::TypeTraits>::type_id()) {
@@ -1148,8 +1152,10 @@ nonstd::expected GetTextureCoordinate(
primvar.get_type_name() + "\n");
}
+ TUSDZ_LOG_I("flatten_with_indices\n");
std::vector uvs;
if (!primvar.flatten_with_indices(t, &uvs, tinterp)) {
+ TUSDZ_LOG_I("flatten_with_indices failed\n");
return nonstd::make_unexpected(
"Failed to retrieve texture coordinate primvar with concrete type.\n");
}
@@ -1167,6 +1173,7 @@ nonstd::expected GetTextureCoordinate(
}
+ TUSDZ_LOG_I("texcoord. " << name << ", " << uvs.size());
DCOUT("texcoord " << name << " : " << uvs);
vattr.format = VertexAttributeFormat::Vec2;
@@ -1175,6 +1182,7 @@ nonstd::expected GetTextureCoordinate(
vattr.indices.clear(); // just in case.
vattr.name = name; // TODO: add "primvars:" namespace?
+ TUSDZ_LOG_I("end");
return std::move(vattr);
}
@@ -3634,6 +3642,8 @@ bool RenderSceneConverter::ConvertMesh(
if (ret) {
const VertexAttribute &vattr = ret.value();
+ TUSDZ_LOG_I("uv attr");
+
// Use slotId 0
uvAttrs[0] = vattr;
} else {
@@ -3699,6 +3709,7 @@ bool RenderSceneConverter::ConvertMesh(
}
}
+ TUSDZ_LOG_I("done uvAttr");
if (mesh.has_primvar(env.mesh_config.default_tangents_primvar_name)) {
GeomPrimvar pvar;
@@ -5494,6 +5505,7 @@ bool RenderSceneConverter::ConvertUVTexture(const RenderSceneConverterEnv &env,
}
} else {
+ TUSDZ_LOG_I("get_value");
Animatable fallbacks = texture.st.get_value();
value::texcoord2f uv;
if (fallbacks.get(env.timecode, &uv)) {
@@ -5503,6 +5515,7 @@ bool RenderSceneConverter::ConvertUVTexture(const RenderSceneConverterEnv &env,
// TODO: report warning.
PUSH_WARN("Failed to get fallback `st` texcoord attribute.");
}
+ TUSDZ_LOG_I("uv done");
}
}
diff --git a/src/usdGeom.cc b/src/usdGeom.cc
index bfb65952..61ff05b7 100644
--- a/src/usdGeom.cc
+++ b/src/usdGeom.cc
@@ -21,6 +21,7 @@
#include "math-util.inc"
#include "str-util.hh"
#include "value-pprint.hh"
+#include "logger.hh"
#define SET_ERROR_AND_RETURN(msg) \
if (err) { \
@@ -257,10 +258,12 @@ bool GeomPrimvar::flatten_with_indices(const double t, std::vector *dest, con
return false;
}
+ TUSDZ_LOG_I("get_value");
std::vector value;
if (_attr.get_value>(t, &value, tinterp)) {
uint32_t elementSize = _attr.metas().elementSize.value_or(1);
+ TUSDZ_LOG_I("elementSize" << elementSize);
// Get indices at specified time
std::vector indices;
@@ -273,9 +276,11 @@ bool GeomPrimvar::flatten_with_indices(const double t, std::vector *dest, con
} else {
_ts_indices.get(&indices, t, tinterp);
}
+ TUSDZ_LOG_I("indices.size " << indices.size());
std::vector expanded_val;
auto ret = ExpandWithIndices(value, elementSize, indices, &expanded_val);
+ TUSDZ_LOG_I("ExpandWithIndices done");
if (ret) {
(*dest) = expanded_val;
// Currently we ignore ret.value()
diff --git a/web/CMakeLists.txt b/web/CMakeLists.txt
index 10b0e6cd..edea0be4 100644
--- a/web/CMakeLists.txt
+++ b/web/CMakeLists.txt
@@ -23,7 +23,17 @@ set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
-if (CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
+if (CMAKE_BUILD_TYPE STREQUAL "Debug")
+
+ if (TINYUSDZ_WASM64)
+ set(CMAKE_CXX_FLAGS_DEBUG "-g4 -sMEMORY64")
+ set(CMAKE_C_FLAGS_DEBUG "-g4 -sMEMORY64")
+ else()
+ set(CMAKE_CXX_FLAGS_DEBUG "-g4") # -sMEMORY64"
+ set(CMAKE_C_FLAGS_DEBUG "-g4") # -sMEMORY64"
+ endif()
+
+elseif (CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
# Use Oz to reduce size further than Os
if (TINYUSDZ_WASM64)
@@ -97,16 +107,29 @@ endif()
# --source-map-base http://localhost:8021/
# flags = try to lower heap memory use
-set(TINYUSDZ_EMCC_LINK_FLAGS "-Oz -sENVIRONMENT='web,worker,node' \
- -sMALLOC=emmalloc \
- -sSTACK_SIZE=512000 \
- -sALLOW_MEMORY_GROWTH=1 \
- -sFILESYSTEM=0 \
- -sSAFE_HEAP=0 \
- -sWASM_BIGINT=1 \
- -sMODULARIZE=1 -sEXPORT_ES6 \
- -sINVOKE_RUN=0 --bind ")
+if (CMAKE_BUILD_TYPE STREQUAL "Debug")
+ set(TINYUSDZ_EMCC_LINK_FLAGS "-g4 -sENVIRONMENT='web,worker,node' \
+ -sMALLOC=emmalloc \
+ -sSTACK_SIZE=1024000 \
+ -sALLOW_MEMORY_GROWTH=1 \
+ -sFILESYSTEM=0 \
+ -sSAFE_HEAP=1 \
+ -sWASM_BIGINT=1 \
+ -sMODULARIZE=1 -sEXPORT_ES6 \
+ -sINVOKE_RUN=0 --bind ")
+else()
+ set(TINYUSDZ_EMCC_LINK_FLAGS "-Oz -sENVIRONMENT='web,worker,node' \
+ -sMALLOC=emmalloc \
+ -sSTACK_SIZE=512000 \
+ -sALLOW_MEMORY_GROWTH=1 \
+ -sFILESYSTEM=0 \
+ -sSAFE_HEAP=10 \
+ -sWASM_BIGINT=1 \
+ -sMODULARIZE=1 -sEXPORT_ES6 \
+ -sINVOKE_RUN=0 --bind ")
+endif()
+
if (TINYUSDZ_WASM64)
# assertion=1 cause runtime error(Cannot mix BigInt and ... in assert()), so use 2
string(APPEND TINYUSDZ_EMCC_LINK_FLAGS " -sASSERTIONS=2 -sMEMORY64 -sMAXIMUM_MEMORY=8GB")
diff --git a/web/bootstrap-linux-debug.sh b/web/bootstrap-linux-debug.sh
new file mode 100755
index 00000000..e9d105ba
--- /dev/null
+++ b/web/bootstrap-linux-debug.sh
@@ -0,0 +1,4 @@
+rm -rf build_debug
+mkdir build_debug
+
+emcmake cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_VERBOSE_MAKEFILE=1 -Bbuild_debug
diff --git a/web/js/animation-info.js b/web/js/animation-info.js
index 9633fa21..ac2fb850 100644
--- a/web/js/animation-info.js
+++ b/web/js/animation-info.js
@@ -25,7 +25,7 @@ function reportMemUsage() {
}
// Print animation clip info
-function printAnimationClips(usd, detailed = false) {
+function printAnimationClips(usd, detailed = false, dumpKeyframes = false) {
const numAnims = usd.numAnimations();
if (numAnims === 0) {
@@ -110,6 +110,60 @@ function printAnimationClips(usd, detailed = false) {
if (sampler.times && sampler.times.length > 0) {
console.log(` Keyframes: ${sampler.times.length}`);
console.log(` Time Range: ${sampler.times[0].toFixed(3)}s - ${sampler.times[sampler.times.length - 1].toFixed(3)}s`);
+
+ // Dump keyframe data if --keyframes flag is set
+ if (dumpKeyframes) {
+ console.log(` Keyframe Data:`);
+ for (let k = 0; k < sampler.times.length; k++) {
+ const time = sampler.times[k].toFixed(3);
+ let valueStr = '';
+
+ if (sampler.values) {
+ // Handle different value types
+ if (channel.path && channel.path.includes('translation')) {
+ // Translation: 3 floats per keyframe
+ const idx = k * 3;
+ if (sampler.values[idx] !== undefined) {
+ valueStr = `[${sampler.values[idx].toFixed(3)}, ${sampler.values[idx+1].toFixed(3)}, ${sampler.values[idx+2].toFixed(3)}]`;
+ }
+ } else if (channel.path && channel.path.includes('rotation')) {
+ // Rotation: could be 3 (euler) or 4 (quaternion) floats
+ const componentsPerKey = sampler.values.length / sampler.times.length;
+ const idx = k * componentsPerKey;
+ if (componentsPerKey === 4) {
+ valueStr = `[${sampler.values[idx].toFixed(3)}, ${sampler.values[idx+1].toFixed(3)}, ${sampler.values[idx+2].toFixed(3)}, ${sampler.values[idx+3].toFixed(3)}]`;
+ } else if (componentsPerKey === 3) {
+ valueStr = `[${sampler.values[idx].toFixed(3)}, ${sampler.values[idx+1].toFixed(3)}, ${sampler.values[idx+2].toFixed(3)}]`;
+ }
+ } else if (channel.path && channel.path.includes('scale')) {
+ // Scale: 3 floats per keyframe
+ const idx = k * 3;
+ if (sampler.values[idx] !== undefined) {
+ valueStr = `[${sampler.values[idx].toFixed(3)}, ${sampler.values[idx+1].toFixed(3)}, ${sampler.values[idx+2].toFixed(3)}]`;
+ }
+ } else {
+ // Generic handling: try to determine component count
+ const componentsPerKey = sampler.values.length / sampler.times.length;
+ const idx = k * componentsPerKey;
+ const components = [];
+ for (let c = 0; c < componentsPerKey; c++) {
+ if (sampler.values[idx + c] !== undefined) {
+ components.push(sampler.values[idx + c].toFixed(3));
+ }
+ }
+ if (components.length > 0) {
+ valueStr = componentsPerKey === 1 ? components[0] : `[${components.join(', ')}]`;
+ }
+ }
+ }
+
+ if (valueStr) {
+ console.log(` Frame ${k}: t=${time}s, value=${valueStr}`);
+ } else {
+ console.log(` Frame ${k}: t=${time}s`);
+ }
+ }
+ }
}
if (sampler.interpolation) {
console.log(` Interpolation: ${sampler.interpolation}`);
@@ -123,18 +177,73 @@ function printAnimationClips(usd, detailed = false) {
console.log(` Skeletal Joint Channels: ${skeletalChannels}`);
}
- // Legacy track information (if present)
+ // Track information (main animation data in TinyUSDZ)
if (anim.tracks && anim.tracks.length) {
- console.log(`\n Legacy Track Information:`);
- console.log(` Tracks: ${anim.tracks.length}`);
+ console.log(`\n Track Information:`);
+ console.log(` Total Tracks: ${anim.tracks.length}`);
anim.tracks.forEach((track, idx) => {
- console.log(` Track ${idx}: ${track.name || 'unnamed'}`);
- if (track.type) console.log(` Type: ${track.type}`);
- if (track.keyframes && track.keyframes.length) {
- console.log(` Keyframes: ${track.keyframes.length}`);
- const times = track.times || [];
- if (times.length > 0) {
- console.log(` Time range: ${times[0].toFixed(3)}s - ${times[times.length - 1].toFixed(3)}s`);
+ console.log(`\n Track ${idx}: ${track.name || 'unnamed'}`);
+ if (track.type) console.log(` Type: ${track.type}`);
+ if (track.path) console.log(` Path: ${track.path}`);
+ if (track.target) console.log(` Target: ${track.target}`);
+ if (track.interpolation) console.log(` Interpolation: ${track.interpolation}`);
+
+ // Show keyframe count and time range
+ if (track.times && track.times.length > 0) {
+ console.log(` Keyframes: ${track.times.length}`);
+ console.log(` Time range: ${track.times[0].toFixed(3)}s - ${track.times[track.times.length - 1].toFixed(3)}s`);
+ } else if (track.keyframes && track.keyframes.length) {
+ console.log(` Keyframes: ${track.keyframes.length}`);
+ }
+
+ // Dump keyframe data if requested
+ if (dumpKeyframes) {
+ if (track.times && track.values) {
+ console.log(` Keyframe Data:`);
+ for (let k = 0; k < track.times.length; k++) {
+ const time = track.times[k].toFixed(3);
+ let valueStr = '';
+
+ // Determine the number of components per keyframe
+ const numValues = track.values.length;
+ const numKeys = track.times.length;
+ const componentsPerKey = numValues / numKeys;
+
+ // Extract value based on component count
+ const idx = k * componentsPerKey;
+ if (componentsPerKey === 1) {
+ valueStr = track.values[idx].toFixed(3);
+ } else if (componentsPerKey === 3) {
+ valueStr = `[${track.values[idx].toFixed(3)}, ${track.values[idx+1].toFixed(3)}, ${track.values[idx+2].toFixed(3)}]`;
+ } else if (componentsPerKey === 4) {
+ valueStr = `[${track.values[idx].toFixed(3)}, ${track.values[idx+1].toFixed(3)}, ${track.values[idx+2].toFixed(3)}, ${track.values[idx+3].toFixed(3)}]`;
+ } else {
+ // Generic handling for any number of components
+ const components = [];
+ for (let c = 0; c < componentsPerKey; c++) {
+ components.push(track.values[idx + c].toFixed(3));
+ }
+ valueStr = `[${components.join(', ')}]`;
+ }
+
+ console.log(` Frame ${k}: t=${time}s, value=${valueStr}`);
+ }
+ } else if (track.keyframes) {
+ console.log(` Keyframe Data:`);
+ track.keyframes.forEach((keyframe, k) => {
+ let timeStr = keyframe.time !== undefined ? keyframe.time.toFixed(3) : 'unknown';
+ let valueStr = '';
+
+ if (keyframe.value !== undefined) {
+ if (Array.isArray(keyframe.value)) {
+ valueStr = `[${keyframe.value.map(v => v.toFixed(3)).join(', ')}]`;
+ } else {
+ valueStr = keyframe.value.toFixed(3);
+ }
+ }
+
+ console.log(` Frame ${k}: t=${timeStr}s, value=${valueStr}`);
+ });
}
}
});
@@ -180,11 +289,13 @@ async function main() {
console.log('Arguments:');
console.log(' Path to USD file (.usd, .usda, .usdc, .usdz)');
console.log(' --detailed Print detailed animation track information');
+ console.log(' --keyframes Dump all keyframe data (times and values)');
console.log(' --memory Print memory usage statistics');
console.log(' --help Show this help message\n');
console.log('Examples:');
console.log(' node animation-info.js ../../models/suzanne-subd-lv4.usdc');
console.log(' node animation-info.js animation.usd --detailed');
+ console.log(' node animation-info.js cube-animation.usda --detailed --keyframes');
console.log(' node animation-info.js model.usdz --detailed --memory');
return;
}
@@ -194,6 +305,7 @@ async function main() {
const detailed = args.includes('--detailed');
const showMemory = args.includes('--memory');
const showHelp = args.includes('--help');
+ const dumpKeyframes = args.includes('--keyframes');
if (showHelp) {
console.log('node animation-info.js - USD Animation Information Viewer\n');
@@ -201,6 +313,7 @@ async function main() {
console.log('Arguments:');
console.log(' Path to USD file (.usd, .usda, .usdc, .usdz)');
console.log(' --detailed Print detailed animation track information');
+ console.log(' --keyframes Dump all keyframe data (times and values)');
console.log(' --memory Print memory usage statistics');
console.log(' --help Show this help message');
return;
@@ -247,7 +360,7 @@ async function main() {
// Print information
printSceneInfo(usd);
- printAnimationClips(usd, detailed);
+ printAnimationClips(usd, detailed, dumpKeyframes);
// Print memory usage if requested
if (showMemory) {
diff --git a/web/js/animation.html b/web/js/animation.html
index 05decbc8..72ae9659 100644
--- a/web/js/animation.html
+++ b/web/js/animation.html
@@ -82,11 +82,11 @@
The scene loads a USD model and can play both synthetic and USD-embedded animations.
- Current file: suzanne.usdc
+ Current file:
-
+
@@ -123,6 +123,34 @@
const item = document.createElement('div');
item.className = 'animation-item';
+ // Analyze tracks to determine what's included
+ let trackLabels = [];
+ let hasTranslation = false;
+ let hasRotation = false;
+ let hasScale = false;
+
+ if (anim.tracks && anim.tracks.length > 0) {
+ anim.tracks.forEach(track => {
+ const trackName = track.name.toLowerCase();
+ if (trackName.includes('position') || trackName.includes('translation')) {
+ hasTranslation = true;
+ } else if (trackName.includes('quaternion') || trackName.includes('rotation')) {
+ hasRotation = true;
+ } else if (trackName.includes('scale')) {
+ hasScale = true;
+ }
+ });
+
+ // Build track label string
+ if (hasTranslation) trackLabels.push('t');
+ if (hasRotation) trackLabels.push('r');
+ if (hasScale) trackLabels.push('s');
+ }
+
+ const trackInfo = trackLabels.length > 0
+ ? ` [${trackLabels.join(',')}]`
+ : '';
+
// Get animation type information if available
let typeInfo = '';
if (animationInfos && animationInfos[index]) {
@@ -135,7 +163,7 @@
}
}
- item.innerHTML = `${index}: ${anim.name || 'Unnamed'} - ${anim.duration.toFixed(2)}s, ${anim.tracks.length} tracks${typeInfo}`;
+ item.innerHTML = `${index}: ${anim.name || 'Unnamed'} - ${anim.duration.toFixed(2)}s, ${anim.tracks.length} tracks${trackInfo}${typeInfo}`;
animList.appendChild(item);
});
} else {
@@ -145,7 +173,7 @@
// Reset to default model
window.reloadDefaultModel = function() {
- document.getElementById('currentFile').textContent = 'suzanne.usdc';
+ document.getElementById('currentFile').textContent = 'cube-animation.usda';
const customEvent = new CustomEvent('loadDefaultModel');
window.dispatchEvent(customEvent);
};
diff --git a/web/js/animation.js b/web/js/animation.js
index 5276bb01..38309eb6 100644
--- a/web/js/animation.js
+++ b/web/js/animation.js
@@ -73,7 +73,7 @@ let usdAnimations = []; // Store USD animations from the file
/**
* Convert USD animation data to Three.js AnimationClip
- * Supports the new animation structure with channels and samplers
+ * Supports both channel/sampler and track-based animation structures
* @param {Object} usdLoader - TinyUSDZ loader instance
* @param {THREE.Object3D} sceneRoot - Three.js scene containing the loaded geometry
* @returns {Array} Array of Three.js AnimationClips
@@ -101,8 +101,115 @@ function convertUSDAnimationsToThreeJS(usdLoader, sceneRoot) {
const usdAnimation = usdLoader.getAnimation(i);
console.log(`Processing animation ${i}: ${usdAnimation.name}`);
+ // Check if this is a track-based animation (legacy format)
+ if (usdAnimation.tracks && usdAnimation.tracks.length > 0) {
+ console.log(`Animation ${i} uses track-based format with ${usdAnimation.tracks.length} tracks`);
+
+ // Process track-based animation
+ const keyframeTracks = [];
+
+ // Find the target object - for track animations, usually the first child after scene root
+ let targetObject = sceneRoot;
+ // Try to find the animated object by name from the animation
+ if (usdAnimation.name && usdAnimation.name.includes('_')) {
+ const targetName = usdAnimation.name.split('_')[0]; // e.g., "AnimatedCube" from "AnimatedCube_xform"
+ sceneRoot.traverse((obj) => {
+ if (obj.name && obj.name.includes(targetName)) {
+ targetObject = obj;
+ }
+ });
+ }
+
+ // If we can't find it by name, use the first mesh or group
+ if (targetObject === sceneRoot) {
+ sceneRoot.traverse((obj) => {
+ if ((obj.isMesh || obj.isGroup) && obj !== sceneRoot) {
+ targetObject = obj;
+ return; // Stop traversal once we find the first mesh/group
+ }
+ });
+ }
+
+ const targetName = targetObject.name || 'AnimatedObject';
+ console.log(`Target object for animation: ${targetName}`);
+
+ // Process each track
+ for (const track of usdAnimation.tracks) {
+ if (!track.times || !track.values) {
+ console.warn('Track missing times or values');
+ continue;
+ }
+
+ // Convert times and values to arrays
+ const times = Array.isArray(track.times) ? track.times : Array.from(track.times);
+ const values = Array.isArray(track.values) ? track.values : Array.from(track.values);
+ const interpolation = getUSDInterpolationMode(track.interpolation);
+
+ console.log(`Processing track: ${track.path}, ${times.length} keyframes`);
+
+ // Create appropriate Three.js KeyframeTrack based on path
+ let keyframeTrack;
+
+ switch (track.path) {
+ case 'translation':
+ case 'Translation':
+ keyframeTrack = new THREE.VectorKeyframeTrack(
+ `${targetName}.position`,
+ times,
+ values,
+ interpolation
+ );
+ break;
+
+ case 'rotation':
+ case 'Rotation':
+ // Rotation is stored as quaternions (x, y, z, w)
+ keyframeTrack = new THREE.QuaternionKeyframeTrack(
+ `${targetName}.quaternion`,
+ times,
+ values,
+ interpolation
+ );
+ break;
+
+ case 'scale':
+ case 'Scale':
+ keyframeTrack = new THREE.VectorKeyframeTrack(
+ `${targetName}.scale`,
+ times,
+ values,
+ interpolation
+ );
+ break;
+
+ default:
+ console.warn(`Unknown track path: ${track.path}`);
+ continue;
+ }
+
+ if (keyframeTrack) {
+ keyframeTracks.push(keyframeTrack);
+ }
+ }
+
+ // Create Three.js AnimationClip from tracks
+ if (keyframeTracks.length > 0) {
+ const clip = new THREE.AnimationClip(
+ usdAnimation.name || `Animation_${i}`,
+ usdAnimation.duration || -1, // -1 will auto-calculate from tracks
+ keyframeTracks
+ );
+
+ animationClips.push(clip);
+ console.log(`Created clip: ${clip.name}, duration: ${clip.duration}s, tracks: ${clip.tracks.length}`);
+ }
+
+ continue; // Skip to next animation
+ }
+
+ // Handle channel-based animation (newer format)
if (!usdAnimation.channels || !usdAnimation.samplers) {
- console.warn(`Animation ${i} missing channels or samplers`);
+ console.warn(`Animation ${i} missing channels/samplers and tracks`);
continue;
}
@@ -240,10 +347,12 @@ async function loadUSDModel() {
// Use useZstdCompressedWasm: false since compressed WASM is not available
await loader.init({ useZstdCompressedWasm: false, useMemory64: false });
- const suzanne_filename = "./assets/suzanne.usdc";
+ //const usd_filename = "./assets/cube-animation.usda";
+ //const usd_filename = "./assets/suzanne-xform.usdc";
+ const usd_filename = "./assets/wings-3.usdc";
// Load USD scene
- const usd_scene = await loader.loadAsync(suzanne_filename);
+ const usd_scene = await loader.loadAsync(usd_filename);
// Get the default root node from USD
const usdRootNode = usd_scene.getDefaultRootNode();
@@ -288,6 +397,7 @@ async function loadUSDModel() {
// Update animation parameters
animationParams.hasUSDAnimations = true;
animationParams.usdAnimationCount = usdAnimations.length;
+ animationParams.useUSDAnimation = true; // Auto-enable USD animations
// Show the USD animation folder in GUI
if (window.usdAnimationFolder) {
@@ -311,18 +421,44 @@ async function loadUSDModel() {
}
console.log(`Animation ${index}: ${clip.name}, duration: ${clip.duration}s, tracks: ${clip.tracks.length}${typeStr}`);
});
+
+ // Set time range from first USD animation
+ const firstClip = usdAnimations[0];
+ if (firstClip && firstClip.duration > 0) {
+ animationParams.beginTime = 0;
+ animationParams.endTime = firstClip.duration;
+ animationParams.duration = firstClip.duration;
+ animationParams.time = 0; // Reset time to beginning
+ console.log(`Set time range from USD animation: 0s - ${firstClip.duration}s`);
+
+ // Update GUI controllers if they exist
+ updateTimeRangeGUIControllers(firstClip.duration);
+ }
+
+ // Play the first USD animation automatically
+ playUSDAnimation(0);
+ } else {
+ // No USD animations found - use synthetic animations
+ console.log('No USD animations found, using synthetic animations');
+ animationParams.useUSDAnimation = false;
+ updateAnimationClip();
}
} catch (error) {
console.log('No animations found in USD file or animation extraction not supported:', error);
+ // Fallback to synthetic animations
+ animationParams.useUSDAnimation = false;
+ updateAnimationClip();
}
-
- // Initialize animation after USD model is loaded
- updateAnimationClip();
}
// Play USD animation by index
function playUSDAnimation(index) {
if (index >= 0 && index < usdAnimations.length) {
+ // Ensure mixer exists
+ if (!mixer && parentCube) {
+ mixer = new THREE.AnimationMixer(parentCube);
+ }
+
// Stop current animation
if (animationAction) {
animationAction.stop();
@@ -562,13 +698,25 @@ const animationParams = {
const gui = new GUI();
gui.title('Animation Controls');
+// Store references to GUI controllers for dynamic updates
+let timelineController = null;
+let beginTimeController = null;
+let endTimeController = null;
+
// Playback controls
const playbackFolder = gui.addFolder('Playback');
playbackFolder.add(animationParams, 'playPause').name('Play / Pause');
playbackFolder.add(animationParams, 'reset').name('Reset');
playbackFolder.add(animationParams, 'speed', 0, 3, 0.1).name('Speed');
-playbackFolder.add(animationParams, 'time', 0, 30, 0.01)
- .name('Timeline').listen();
+timelineController = playbackFolder.add(animationParams, 'time', 0, 30, 0.01)
+ .name('Timeline')
+ .listen()
+ .onChange((value) => {
+ // When user manually scrubs the timeline, update animation action
+ if (animationAction) {
+ animationAction.time = value;
+ }
+ });
playbackFolder.open();
// USD Animation controls (will be populated when USD file is loaded)
@@ -586,7 +734,7 @@ usdAnimationFolder.hide();
// Time range controls
const timeRangeFolder = gui.addFolder('Time Range');
-timeRangeFolder.add(animationParams, 'beginTime', 0, 29, 0.1)
+beginTimeController = timeRangeFolder.add(animationParams, 'beginTime', 0, 29, 0.1)
.name('Begin Time (s)')
.onChange(() => {
if (animationParams.beginTime >= animationParams.endTime) {
@@ -594,7 +742,7 @@ timeRangeFolder.add(animationParams, 'beginTime', 0, 29, 0.1)
}
animationParams.updateDuration();
});
-timeRangeFolder.add(animationParams, 'endTime', 0.1, 30, 0.1)
+endTimeController = timeRangeFolder.add(animationParams, 'endTime', 0.1, 30, 0.1)
.name('End Time (s)')
.onChange(() => {
if (animationParams.endTime <= animationParams.beginTime) {
@@ -608,6 +756,31 @@ timeRangeFolder.add(animationParams, 'duration', 0.1, 30, 0.1)
.disable();
timeRangeFolder.open();
+// Function to update time range GUI controllers when animation is loaded
+function updateTimeRangeGUIControllers(maxDuration) {
+ const newMax = Math.max(maxDuration, 30); // Ensure minimum of 30s for usability
+
+ // Update timeline controller
+ if (timelineController) {
+ timelineController.max(newMax);
+ timelineController.updateDisplay();
+ }
+
+ // Update begin time controller
+ if (beginTimeController) {
+ beginTimeController.max(newMax - 0.1);
+ beginTimeController.updateDisplay();
+ }
+
+ // Update end time controller
+ if (endTimeController) {
+ endTimeController.max(newMax);
+ endTimeController.updateDisplay();
+ }
+
+ console.log(`Updated GUI time range to 0-${newMax}s`);
+}
+
// Cube KeyframeTrack controls
const cubeTracksFolder = gui.addFolder('Cube KeyframeTracks');
cubeTracksFolder.add(animationParams, 'cubePosition')
@@ -664,11 +837,18 @@ async function loadUSDFromArrayBuffer(arrayBuffer, filename) {
const loader = new TinyUSDZLoader();
await loader.init({ useZstdCompressedWasm: false, useMemory64: false });
- // Convert ArrayBuffer to Uint8Array
- const uint8Array = new Uint8Array(arrayBuffer);
+ // Create a Blob URL from the ArrayBuffer
+ // This allows the loader to load the file as if it were a normal URL
+ const blob = new Blob([arrayBuffer]);
+ const blobUrl = URL.createObjectURL(blob);
- // Load USD scene from binary data
- const usd_scene = await loader.loadFromBinary(uint8Array, filename);
+ console.log(`Loading USD from file: ${filename} (${(arrayBuffer.byteLength / 1024).toFixed(2)} KB)`);
+
+ // Load USD scene from Blob URL
+ const usd_scene = await loader.loadAsync(blobUrl);
+
+ // Clean up the Blob URL after loading
+ URL.revokeObjectURL(blobUrl);
// Get the default root node from USD
const usdRootNode = usd_scene.getDefaultRootNode();
@@ -713,6 +893,7 @@ async function loadUSDFromArrayBuffer(arrayBuffer, filename) {
// Update animation parameters
animationParams.hasUSDAnimations = true;
animationParams.usdAnimationCount = usdAnimations.length;
+ animationParams.useUSDAnimation = true; // Auto-enable USD animations
// Show the USD animation folder in GUI
if (window.usdAnimationFolder) {
@@ -736,27 +917,54 @@ async function loadUSDFromArrayBuffer(arrayBuffer, filename) {
}
console.log(`Animation ${index}: ${clip.name}, duration: ${clip.duration}s, tracks: ${clip.tracks.length}${typeStr}`);
});
+
+ // Set time range from first USD animation
+ const firstClip = usdAnimations[0];
+ if (firstClip && firstClip.duration > 0) {
+ animationParams.beginTime = 0;
+ animationParams.endTime = firstClip.duration;
+ animationParams.duration = firstClip.duration;
+ animationParams.time = 0; // Reset time to beginning
+ console.log(`Set time range from USD animation: 0s - ${firstClip.duration}s`);
+
+ // Update GUI controllers if they exist
+ updateTimeRangeGUIControllers(firstClip.duration);
+ }
+
+ // Play the first USD animation automatically
+ playUSDAnimation(0);
} else {
- // Hide USD animations if none found
+ // No USD animations found - use synthetic animations
+ console.log('No USD animations found, using synthetic animations');
+ animationParams.useUSDAnimation = false;
+
+ // Hide USD animations folder
if (window.usdAnimationFolder) {
window.usdAnimationFolder.hide();
}
if (window.updateAnimationList) {
window.updateAnimationList([], []);
}
+
+ // Initialize synthetic animation
+ updateAnimationClip();
}
} catch (error) {
console.log('No animations found in USD file or animation extraction not supported:', error);
+
+ // Fallback to synthetic animations
+ animationParams.useUSDAnimation = false;
+
if (window.usdAnimationFolder) {
window.usdAnimationFolder.hide();
}
if (window.updateAnimationList) {
window.updateAnimationList([], []);
}
- }
- // Initialize synthetic animation
- updateAnimationClip();
+ // Initialize synthetic animation
+ updateAnimationClip();
+ }
}
// Listen for file upload events
diff --git a/web/js/src/tinyusdz/TinyUSDZLoader.js b/web/js/src/tinyusdz/TinyUSDZLoader.js
index 36be56aa..874e2063 100644
--- a/web/js/src/tinyusdz/TinyUSDZLoader.js
+++ b/web/js/src/tinyusdz/TinyUSDZLoader.js
@@ -35,8 +35,11 @@ class FileFetcher {
async fetch(url) {
await this.init();
- if (this.is_node) {
- // Node.js environment - use fs.readFileSync
+ // Check if this is a blob URL - always use fetch for blob URLs
+ const isBlobUrl = url.startsWith('blob:');
+
+ if (this.is_node && !isBlobUrl) {
+ // Node.js environment - use fs.readFileSync for file paths
try {
if (url.startsWith('file://')) {
url = url.substring(7); // Remove file:// prefix
@@ -55,7 +58,7 @@ class FileFetcher {
throw new Error(`Failed to read file: ${url} - ${error.message}`);
}
} else {
- // Browser environment - use fetch API and convert to File
+ // Browser environment or blob URL - use fetch API and convert to File
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.statusText}`);
@@ -454,10 +457,11 @@ class TinyUSDZLoader extends Loader {
return fetch(url);
})
.then((response) => {
- //console.log('fetch USDZ file done:', url);
+ console.log('fetch USDZ file done:', url);
return response.arrayBuffer();
})
.then((usd_data) => {
+ console.log('usd_data done:', url);
const usd_binary = new Uint8Array(usd_data);
//console.log('Loaded USD binary data:', usd_binary.length, 'bytes');