timesamples debug w.i.p.

This commit is contained in:
Syoyo Fujita
2025-10-28 12:08:30 +09:00
parent af2115490e
commit 97a5ca2192
8 changed files with 447 additions and 49 deletions

View File

@@ -1129,6 +1129,9 @@ nonstd::expected<VertexAttribute, std::string> 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<VertexAttribute, std::string> GetTextureCoordinate(
"\n");
}
TUSDZ_LOG_I("get tex\n");
// TODO: allow float2?
if (primvar.get_type_id() !=
value::TypeTraits<std::vector<value::texcoord2f>>::type_id()) {
@@ -1148,8 +1152,10 @@ nonstd::expected<VertexAttribute, std::string> GetTextureCoordinate(
primvar.get_type_name() + "\n");
}
TUSDZ_LOG_I("flatten_with_indices\n");
std::vector<value::texcoord2f> 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<VertexAttribute, std::string> GetTextureCoordinate(
}
TUSDZ_LOG_I("texcoord. " << name << ", " << uvs.size());
DCOUT("texcoord " << name << " : " << uvs);
vattr.format = VertexAttributeFormat::Vec2;
@@ -1175,6 +1182,7 @@ nonstd::expected<VertexAttribute, std::string> 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<value::texcoord2f> 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");
}
}

View File

@@ -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<T> *dest, con
return false;
}
TUSDZ_LOG_I("get_value");
std::vector<T> value;
if (_attr.get_value<std::vector<T>>(t, &value, tinterp)) {
uint32_t elementSize = _attr.metas().elementSize.value_or(1);
TUSDZ_LOG_I("elementSize" << elementSize);
// Get indices at specified time
std::vector<int32_t> indices;
@@ -273,9 +276,11 @@ bool GeomPrimvar::flatten_with_indices(const double t, std::vector<T> *dest, con
} else {
_ts_indices.get(&indices, t, tinterp);
}
TUSDZ_LOG_I("indices.size " << indices.size());
std::vector<T> 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()

View File

@@ -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")

4
web/bootstrap-linux-debug.sh Executable file
View File

@@ -0,0 +1,4 @@
rm -rf build_debug
mkdir build_debug
emcmake cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_VERBOSE_MAKEFILE=1 -Bbuild_debug

View File

@@ -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> 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> 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) {

View File

@@ -82,11 +82,11 @@
The scene loads a USD model and can play both synthetic and USD-embedded animations.
</p>
<p>
<strong>Current file:</strong> <span id="currentFile">suzanne.usdc</span>
<strong>Current file:</strong> <span id="currentFile"></span>
</p>
<div id="file-controls">
<input type="file" id="fileInput" accept=".usd,.usda,.usdc,.usdz">
<button class="button" onclick="document.getElementById('fileInput').click()">Load USD File</button>
<button class="button" onclick="document.getElementById('fileInput').click()">Load USD File(Not working)</button>
<button class="button" onclick="window.reloadDefaultModel()">Reset to Default</button>
</div>
<div id="animation-info" style="display:none;">
@@ -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
? ` <span style="color: #6bb6ff; font-weight: bold;">[${trackLabels.join(',')}]</span>`
: '';
// Get animation type information if available
let typeInfo = '';
if (animationInfos && animationInfos[index]) {
@@ -135,7 +163,7 @@
}
}
item.innerHTML = `<strong>${index}:</strong> ${anim.name || 'Unnamed'} - ${anim.duration.toFixed(2)}s, ${anim.tracks.length} tracks${typeInfo}`;
item.innerHTML = `<strong>${index}:</strong> ${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);
};

View File

@@ -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<THREE.AnimationClip>} 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

View File

@@ -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');