mirror of
https://github.com/lighttransport/tinyusdz.git
synced 2026-01-18 01:11:17 +01:00
Add delayed texture loading with progress bar for faster initial scene render
- Add TextureLoadingManager class for queuing and progressive texture loading - Add texture progress bar UI (bottom overlay with green theme) - Modify buildSceneWithProgress to use delayed texture mode - Scene renders immediately without textures, then textures load in background - Support textureLoadingManager option in material conversion functions - Update TinyUSDZMaterialX.js to support delayed loading for OpenPBR materials - Textures load with concurrency control (2 at a time) and browser yields 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -183,10 +183,42 @@
|
||||
|
||||
#progress-bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
width: 100%;
|
||||
background: linear-gradient(90deg, #2196F3, #4CAF50);
|
||||
border-radius: 8px;
|
||||
transition: width 0.3s ease-out;
|
||||
transform-origin: left center;
|
||||
transform: scaleX(0);
|
||||
/* Use will-change to hint compositor for independent animation */
|
||||
will-change: transform;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Animated stripe overlay to show activity during sync WASM execution */
|
||||
#progress-bar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255,255,255,0.3) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: progress-shimmer 1.5s ease-in-out infinite;
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
|
||||
@keyframes progress-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* Hide shimmer when progress is complete */
|
||||
#progress-bar.complete::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#progress-percentage {
|
||||
@@ -267,6 +299,75 @@
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Texture Loading Progress Bar (bottom overlay) */
|
||||
#texture-progress-container {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
display: none;
|
||||
z-index: 9999;
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
#texture-progress-container.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#texture-progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#texture-progress-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
#texture-progress-count {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#texture-progress-bar-wrapper {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
height: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
#texture-progress-bar {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: linear-gradient(90deg, #4CAF50, #8BC34A);
|
||||
border-radius: 4px;
|
||||
transform-origin: left center;
|
||||
transform: scaleX(0);
|
||||
will-change: transform;
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
#texture-progress-details {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
min-height: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Help overlay */
|
||||
#help-overlay {
|
||||
position: absolute;
|
||||
@@ -556,6 +657,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Texture Loading Progress (appears after initial scene render) -->
|
||||
<div id="texture-progress-container">
|
||||
<div id="texture-progress-header">
|
||||
<span id="texture-progress-title">Loading Textures</span>
|
||||
<span id="texture-progress-count">0/0</span>
|
||||
</div>
|
||||
<div id="texture-progress-bar-wrapper">
|
||||
<div id="texture-progress-bar"></div>
|
||||
</div>
|
||||
<div id="texture-progress-details"></div>
|
||||
</div>
|
||||
|
||||
<!-- Help Overlay -->
|
||||
<div id="help-overlay">
|
||||
<kbd>Drag & Drop</kbd> USD files to load |
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import { TinyUSDZLoader } from 'tinyusdz/TinyUSDZLoader.js';
|
||||
import { TinyUSDZLoaderUtils } from 'tinyusdz/TinyUSDZLoaderUtils.js';
|
||||
import { TinyUSDZLoaderUtils, TextureLoadingManager } from 'tinyusdz/TinyUSDZLoaderUtils.js';
|
||||
import { setTinyUSDZ as setMaterialXTinyUSDZ } from 'tinyusdz/TinyUSDZMaterialX.js';
|
||||
import { OpenPBRMaterial } from './OpenPBRMaterial.js';
|
||||
|
||||
@@ -17,8 +17,9 @@ const CAMERA_PADDING = 1.2;
|
||||
|
||||
// Sample models for testing
|
||||
const SAMPLE_MODELS = [
|
||||
'assets/mtlx-normalmap-multi.usdz',
|
||||
'assets/multi-mesh-test.usda'
|
||||
//'assets/mtlx-normalmap-multi.usdz',
|
||||
//'assets/multi-mesh-test.usda'
|
||||
'assets/WesternDesertTown2-mtlx.usdz'
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
@@ -43,7 +44,8 @@ const sceneState = {
|
||||
materials: [],
|
||||
textureCount: 0,
|
||||
meshCount: 0,
|
||||
upAxis: 'Y'
|
||||
upAxis: 'Y',
|
||||
textureLoadingManager: null // For delayed texture loading
|
||||
};
|
||||
|
||||
// Settings
|
||||
@@ -350,6 +352,92 @@ function resetProgressStages() {
|
||||
});
|
||||
progressState.currentStage = null;
|
||||
progressState.stageProgress = {};
|
||||
|
||||
// Reset progress bar to 0
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
progressBar.style.transform = 'scaleX(0)';
|
||||
progressBar.classList.remove('complete');
|
||||
document.getElementById('progress-percentage').textContent = '0%';
|
||||
|
||||
// Reset throttle timer
|
||||
lastProgressUpdate = 0;
|
||||
}
|
||||
|
||||
// Track last update time for throttling
|
||||
let lastProgressUpdate = 0;
|
||||
const PROGRESS_UPDATE_INTERVAL = 50; // ms - throttle updates to allow browser to paint
|
||||
|
||||
/**
|
||||
* Force browser to schedule a repaint.
|
||||
* This uses a technique of reading layout properties which forces a reflow,
|
||||
* combined with requestAnimationFrame queueing.
|
||||
*/
|
||||
function forceRepaint() {
|
||||
// Force layout reflow by reading offsetHeight
|
||||
void document.body.offsetHeight;
|
||||
|
||||
// Queue a microtask to potentially allow compositor to paint
|
||||
queueMicrotask(() => {
|
||||
void document.body.offsetHeight;
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Texture Progress UI Functions
|
||||
// ============================================================================
|
||||
|
||||
function showTextureProgress() {
|
||||
const container = document.getElementById('texture-progress-container');
|
||||
if (container) {
|
||||
container.classList.add('visible');
|
||||
// Reset progress bar
|
||||
const bar = document.getElementById('texture-progress-bar');
|
||||
if (bar) bar.style.transform = 'scaleX(0)';
|
||||
updateTextureProgressUI({ loaded: 0, total: 0, percentage: 0, currentTexture: null });
|
||||
}
|
||||
}
|
||||
|
||||
function hideTextureProgress() {
|
||||
const container = document.getElementById('texture-progress-container');
|
||||
if (container) container.classList.remove('visible');
|
||||
}
|
||||
|
||||
function updateTextureProgressUI(info) {
|
||||
const { loaded, total, failed, percentage, currentTexture, isComplete } = info;
|
||||
|
||||
// Update count
|
||||
const countEl = document.getElementById('texture-progress-count');
|
||||
if (countEl) {
|
||||
const failedText = failed > 0 ? ` (${failed} failed)` : '';
|
||||
countEl.textContent = `${loaded}/${total}${failedText}`;
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
const bar = document.getElementById('texture-progress-bar');
|
||||
if (bar) {
|
||||
bar.style.transform = `scaleX(${percentage / 100})`;
|
||||
}
|
||||
|
||||
// Update details
|
||||
const detailsEl = document.getElementById('texture-progress-details');
|
||||
if (detailsEl) {
|
||||
if (isComplete) {
|
||||
detailsEl.textContent = failed > 0
|
||||
? `Complete with ${failed} failures`
|
||||
: 'All textures loaded';
|
||||
} else if (currentTexture) {
|
||||
detailsEl.textContent = `Loading: ${currentTexture}`;
|
||||
} else {
|
||||
detailsEl.textContent = 'Starting texture loading...';
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-hide when complete after a delay
|
||||
if (isComplete) {
|
||||
setTimeout(() => {
|
||||
hideTextureProgress();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgressUI(progress) {
|
||||
@@ -357,16 +445,33 @@ function updateProgressUI(progress) {
|
||||
const stage = progress.stage;
|
||||
const message = progress.message || '';
|
||||
|
||||
// Update progress bar
|
||||
document.getElementById('progress-bar').style.width = `${percentage}%`;
|
||||
// Throttle updates to give browser time to repaint
|
||||
const now = performance.now();
|
||||
if (now - lastProgressUpdate < PROGRESS_UPDATE_INTERVAL && percentage < 100) {
|
||||
// Skip this update, but still record memory
|
||||
recordMemoryPoint(stage, percentage);
|
||||
return;
|
||||
}
|
||||
lastProgressUpdate = now;
|
||||
|
||||
// Update progress bar using transform (can be animated by compositor)
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
progressBar.style.transform = `scaleX(${percentage / 100})`;
|
||||
|
||||
// Add 'complete' class when done to stop shimmer animation
|
||||
if (percentage >= 100 || stage === 'complete') {
|
||||
progressBar.classList.add('complete');
|
||||
}
|
||||
|
||||
// Update percentage text
|
||||
document.getElementById('progress-percentage').textContent = `${percentage}%`;
|
||||
|
||||
// Update stage text
|
||||
const stageLabels = {
|
||||
'downloading': 'Downloading file...',
|
||||
'parsing': 'Parsing USD...',
|
||||
'building': 'Building meshes...',
|
||||
'textures': 'Decoding textures...',
|
||||
'parsing': 'Parsing USD (WASM)...',
|
||||
'building': 'Building Three.js scene...',
|
||||
'textures': 'Processing textures...',
|
||||
'materials': 'Converting materials...',
|
||||
'complete': 'Complete!'
|
||||
};
|
||||
@@ -378,6 +483,12 @@ function updateProgressUI(progress) {
|
||||
|
||||
// Record memory at each progress callback
|
||||
recordMemoryPoint(stage, percentage);
|
||||
|
||||
// Force browser repaint attempt
|
||||
forceRepaint();
|
||||
|
||||
// Log progress for debugging (helps verify callbacks are being called)
|
||||
console.log(`[Progress] ${stage}: ${percentage}% - ${message}`);
|
||||
}
|
||||
|
||||
function updateStageIcons(currentStage) {
|
||||
@@ -635,6 +746,14 @@ function cleanupScene() {
|
||||
sceneState.meshCount = 0;
|
||||
sceneState.textureCount = 0;
|
||||
|
||||
// Abort and reset texture loading manager
|
||||
if (sceneState.textureLoadingManager) {
|
||||
sceneState.textureLoadingManager.abort();
|
||||
sceneState.textureLoadingManager.reset();
|
||||
sceneState.textureLoadingManager = null;
|
||||
}
|
||||
hideTextureProgress();
|
||||
|
||||
// Clear WASM memory
|
||||
if (loaderState.nativeLoader) {
|
||||
try {
|
||||
@@ -697,7 +816,12 @@ function disposeMaterial(material) {
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Build Three.js scene from USD with detailed progress reporting
|
||||
* Build Three.js scene from USD with detailed progress reporting.
|
||||
* This function handles the JS layer processing (Three.js mesh/material/texture creation)
|
||||
* which CAN show real-time progress since it's async JavaScript (not blocked by WASM).
|
||||
*
|
||||
* Uses delayed texture loading mode: scene renders first without textures,
|
||||
* then textures load in background with separate progress bar.
|
||||
*/
|
||||
async function buildSceneWithProgress(usd, onProgress) {
|
||||
const rootNode = usd.getDefaultRootNode();
|
||||
@@ -709,32 +833,43 @@ async function buildSceneWithProgress(usd, onProgress) {
|
||||
sceneState.textureCount = 0;
|
||||
sceneState.materials = [];
|
||||
|
||||
// Create texture loading manager for delayed texture loading
|
||||
sceneState.textureLoadingManager = new TextureLoadingManager();
|
||||
|
||||
// Get mesh/material counts from native loader for accurate progress
|
||||
const totalMeshes = usd.numMeshes ? usd.numMeshes() : 0;
|
||||
const totalMaterials = usd.numMaterials ? usd.numMaterials() : 0;
|
||||
const totalTextures = usd.numTextures ? usd.numTextures() : 0;
|
||||
let meshesBuilt = 0;
|
||||
|
||||
// Phase 1: Building meshes with per-mesh progress
|
||||
// Phase 1: Building Three.js meshes with per-mesh progress
|
||||
// This is JS layer processing - progress WILL update in real-time!
|
||||
// Using delayed texture mode - textures are queued, not loaded immediately
|
||||
onProgress({
|
||||
stage: 'building',
|
||||
percentage: 50,
|
||||
message: `Building meshes (0/${totalMeshes})...`
|
||||
message: `Building Three.js meshes (0/${totalMeshes})...`
|
||||
});
|
||||
|
||||
// Allow initial progress to render
|
||||
await new Promise(r => requestAnimationFrame(r));
|
||||
|
||||
const threeRoot = await TinyUSDZLoaderUtils.buildThreeNode(
|
||||
rootNode,
|
||||
null,
|
||||
usd,
|
||||
{
|
||||
// Callback for per-mesh progress (if supported)
|
||||
onMeshBuilt: (meshName, meshIndex) => {
|
||||
meshesBuilt++;
|
||||
const progress = 50 + (meshesBuilt / Math.max(1, totalMeshes)) * 20;
|
||||
// Enable delayed texture loading - textures will be queued
|
||||
textureLoadingManager: sceneState.textureLoadingManager,
|
||||
|
||||
// Progress callback - called during JS layer mesh building
|
||||
// This WILL show real-time updates because buildThreeNode yields to browser
|
||||
onProgress: (info) => {
|
||||
// Map buildThreeNode progress (0-100%) to our range (50-80%)
|
||||
const mappedPercentage = 50 + (info.percentage * 0.3);
|
||||
onProgress({
|
||||
stage: 'building',
|
||||
percentage: Math.min(70, progress),
|
||||
message: `Building mesh ${meshesBuilt}/${totalMeshes}: ${meshName || 'mesh'}`
|
||||
percentage: Math.min(80, mappedPercentage),
|
||||
message: info.message
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -743,14 +878,20 @@ async function buildSceneWithProgress(usd, onProgress) {
|
||||
// Phase 2: Count meshes and materials from built scene
|
||||
onProgress({
|
||||
stage: 'materials',
|
||||
percentage: 70,
|
||||
message: `Processing materials (0/${totalMaterials})...`
|
||||
percentage: 80,
|
||||
message: `Counting materials...`
|
||||
});
|
||||
|
||||
// Yield to show materials stage
|
||||
await new Promise(r => requestAnimationFrame(r));
|
||||
|
||||
const materialSet = new Set();
|
||||
let meshIndex = 0;
|
||||
threeRoot.traverse((obj) => {
|
||||
if (obj.isMesh) {
|
||||
sceneState.meshCount++;
|
||||
meshIndex++;
|
||||
|
||||
if (obj.material) {
|
||||
if (Array.isArray(obj.material)) {
|
||||
obj.material.forEach(m => materialSet.add(m));
|
||||
@@ -763,13 +904,39 @@ async function buildSceneWithProgress(usd, onProgress) {
|
||||
|
||||
sceneState.materials = Array.from(materialSet);
|
||||
|
||||
// Count textures from materials
|
||||
sceneState.materials.forEach(mat => {
|
||||
const textureProps = ['map', 'normalMap', 'roughnessMap', 'metalnessMap', 'emissiveMap', 'aoMap', 'alphaMap'];
|
||||
// Phase 3: Count textures from materials with progress
|
||||
onProgress({
|
||||
stage: 'textures',
|
||||
percentage: 85,
|
||||
message: `Counting textures (0/${sceneState.materials.length} materials)...`
|
||||
});
|
||||
|
||||
// Yield to show textures stage
|
||||
await new Promise(r => requestAnimationFrame(r));
|
||||
|
||||
const textureProps = ['map', 'normalMap', 'roughnessMap', 'metalnessMap', 'emissiveMap', 'aoMap', 'alphaMap'];
|
||||
let materialIndex = 0;
|
||||
|
||||
for (const mat of sceneState.materials) {
|
||||
materialIndex++;
|
||||
|
||||
textureProps.forEach(prop => {
|
||||
if (mat[prop]) sceneState.textureCount++;
|
||||
});
|
||||
});
|
||||
|
||||
// Update progress every few materials
|
||||
if (materialIndex % 5 === 0 || materialIndex === sceneState.materials.length) {
|
||||
const texProgress = 85 + (materialIndex / sceneState.materials.length) * 10;
|
||||
onProgress({
|
||||
stage: 'textures',
|
||||
percentage: Math.min(95, texProgress),
|
||||
message: `Processing materials (${materialIndex}/${sceneState.materials.length}), ${sceneState.textureCount} textures found`
|
||||
});
|
||||
|
||||
// Yield periodically to allow UI updates
|
||||
await new Promise(r => requestAnimationFrame(r));
|
||||
}
|
||||
}
|
||||
|
||||
// Report final counts
|
||||
onProgress({
|
||||
@@ -989,6 +1156,42 @@ async function loadUSDWithProgress(source, isFile = false) {
|
||||
|
||||
hideProgress();
|
||||
|
||||
// Force a render frame BEFORE starting texture loading
|
||||
// This ensures the scene without textures is displayed first
|
||||
await new Promise(r => requestAnimationFrame(r));
|
||||
threeState.renderer.render(threeState.scene, threeState.camera);
|
||||
|
||||
// Start delayed texture loading if there are queued textures
|
||||
if (sceneState.textureLoadingManager && sceneState.textureLoadingManager.total > 0) {
|
||||
const texManager = sceneState.textureLoadingManager;
|
||||
const totalTextures = texManager.total;
|
||||
|
||||
console.log(`[Progress Demo] Starting delayed texture loading: ${totalTextures} textures queued`);
|
||||
showTextureProgress();
|
||||
|
||||
// Start texture loading with progress callbacks
|
||||
// This runs asynchronously - scene is already visible and interactive
|
||||
texManager.startLoading({
|
||||
onProgress: updateTextureProgressUI,
|
||||
onTextureLoaded: (material, mapProperty, texture) => {
|
||||
// Material is automatically updated by the manager
|
||||
// Force re-render to show the new texture
|
||||
material.needsUpdate = true;
|
||||
},
|
||||
concurrency: 2, // Load 2 textures at a time
|
||||
yieldInterval: 16 // Yield to browser every frame (~60fps)
|
||||
}).then(status => {
|
||||
console.log(`[Progress Demo] Texture loading complete: ${status.loaded}/${status.total} loaded, ${status.failed} failed`);
|
||||
|
||||
// Update texture count in model info
|
||||
sceneState.textureCount = status.loaded;
|
||||
document.getElementById('texture-count').textContent = sceneState.textureCount;
|
||||
}).catch(err => {
|
||||
console.error('[Progress Demo] Texture loading error:', err);
|
||||
hideTextureProgress();
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load USD:', error);
|
||||
updateStatus(`Error: ${error.message}`);
|
||||
|
||||
@@ -6,15 +6,264 @@ import { LoaderUtils } from "three"
|
||||
import { convertOpenPBRToMeshPhysicalMaterialLoaded } from './TinyUSDZMaterialX.js';
|
||||
import { decodeEXR as decodeEXRWithFallback } from './EXRDecoder.js';
|
||||
|
||||
/**
|
||||
* TextureLoadingManager - Manages delayed/progressive texture loading.
|
||||
*
|
||||
* This allows the scene to render immediately with basic materials,
|
||||
* while textures load in the background with progress reporting.
|
||||
*
|
||||
* Usage:
|
||||
* const manager = new TextureLoadingManager();
|
||||
*
|
||||
* // Queue texture tasks during material setup
|
||||
* manager.queueTexture(material, 'map', textureId, usdScene);
|
||||
*
|
||||
* // Start loading after scene is rendered
|
||||
* await manager.startLoading({
|
||||
* onProgress: (info) => console.log(`${info.loaded}/${info.total} textures`),
|
||||
* concurrency: 2, // Load 2 textures at a time
|
||||
* yieldInterval: 16 // Yield to browser every 16ms
|
||||
* });
|
||||
*/
|
||||
class TextureLoadingManager {
|
||||
constructor() {
|
||||
this.queue = []; // Pending texture tasks
|
||||
this.loaded = 0; // Number of loaded textures
|
||||
this.failed = 0; // Number of failed textures
|
||||
this.total = 0; // Total textures to load
|
||||
this.isLoading = false; // Loading in progress
|
||||
this.aborted = false; // Loading was aborted
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a texture to be loaded later
|
||||
* @param {THREE.Material} material - Target material
|
||||
* @param {string} mapProperty - Property name (e.g., 'map', 'normalMap')
|
||||
* @param {number} textureId - USD texture ID
|
||||
* @param {Object} usdScene - USD scene/loader instance
|
||||
* @param {Object} options - Additional options (e.g., normalScale)
|
||||
*/
|
||||
queueTexture(material, mapProperty, textureId, usdScene, options = {}) {
|
||||
this.queue.push({
|
||||
material,
|
||||
mapProperty,
|
||||
textureId,
|
||||
usdScene,
|
||||
options,
|
||||
status: 'pending'
|
||||
});
|
||||
this.total = this.queue.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current loading status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
total: this.total,
|
||||
loaded: this.loaded,
|
||||
failed: this.failed,
|
||||
pending: this.total - this.loaded - this.failed,
|
||||
percentage: this.total > 0 ? (this.loaded / this.total) * 100 : 0,
|
||||
isLoading: this.isLoading,
|
||||
isComplete: this.loaded + this.failed >= this.total && !this.isLoading
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort loading
|
||||
*/
|
||||
abort() {
|
||||
this.aborted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset manager for new loading session
|
||||
*/
|
||||
reset() {
|
||||
this.queue = [];
|
||||
this.loaded = 0;
|
||||
this.failed = 0;
|
||||
this.total = 0;
|
||||
this.isLoading = false;
|
||||
this.aborted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start loading queued textures with progress reporting
|
||||
* @param {Object} options - Loading options
|
||||
* @param {Function} options.onProgress - Progress callback ({loaded, total, percentage, currentTexture})
|
||||
* @param {Function} options.onTextureLoaded - Called when each texture loads (material, mapProperty, texture)
|
||||
* @param {number} options.concurrency - Number of concurrent loads (default: 1)
|
||||
* @param {number} options.yieldInterval - ms between browser yields (default: 16)
|
||||
* @returns {Promise<Object>} - Final status {loaded, failed, total}
|
||||
*/
|
||||
async startLoading(options = {}) {
|
||||
const {
|
||||
onProgress = null,
|
||||
onTextureLoaded = null,
|
||||
concurrency = 1,
|
||||
yieldInterval = 16
|
||||
} = options;
|
||||
|
||||
if (this.isLoading) {
|
||||
console.warn('TextureLoadingManager: Already loading');
|
||||
return this.getStatus();
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.aborted = false;
|
||||
let lastYieldTime = performance.now();
|
||||
|
||||
// Report initial progress
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
loaded: 0,
|
||||
total: this.total,
|
||||
percentage: 0,
|
||||
currentTexture: null
|
||||
});
|
||||
}
|
||||
|
||||
// Yield to allow initial render without textures
|
||||
await new Promise(r => requestAnimationFrame(r));
|
||||
|
||||
// Process queue with concurrency control
|
||||
const pendingTasks = [...this.queue];
|
||||
const activeTasks = new Set();
|
||||
|
||||
const loadTexture = async (task) => {
|
||||
if (this.aborted) return;
|
||||
|
||||
task.status = 'loading';
|
||||
const { material, mapProperty, textureId, usdScene, options: taskOptions } = task;
|
||||
|
||||
try {
|
||||
const texture = await TinyUSDZLoaderUtils.getTextureFromUSD(usdScene, textureId);
|
||||
|
||||
if (texture && !this.aborted) {
|
||||
material[mapProperty] = texture;
|
||||
|
||||
// Apply special options (e.g., normal map scale)
|
||||
if (taskOptions.normalScale && mapProperty === 'normalMap' && material.normalScale) {
|
||||
material.normalScale.set(taskOptions.normalScale, taskOptions.normalScale);
|
||||
}
|
||||
|
||||
material.needsUpdate = true;
|
||||
task.status = 'loaded';
|
||||
this.loaded++;
|
||||
|
||||
if (onTextureLoaded) {
|
||||
onTextureLoaded(material, mapProperty, texture);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to load texture ${textureId} for ${mapProperty}:`, err.message);
|
||||
task.status = 'failed';
|
||||
this.failed++;
|
||||
}
|
||||
|
||||
// Report progress
|
||||
if (onProgress && !this.aborted) {
|
||||
onProgress({
|
||||
loaded: this.loaded,
|
||||
failed: this.failed,
|
||||
total: this.total,
|
||||
percentage: (this.loaded / this.total) * 100,
|
||||
currentTexture: `${mapProperty} (${textureId})`
|
||||
});
|
||||
}
|
||||
|
||||
// Yield to browser periodically
|
||||
const now = performance.now();
|
||||
if (now - lastYieldTime >= yieldInterval) {
|
||||
lastYieldTime = now;
|
||||
await new Promise(r => requestAnimationFrame(r));
|
||||
}
|
||||
};
|
||||
|
||||
// Process with concurrency limit
|
||||
while (pendingTasks.length > 0 || activeTasks.size > 0) {
|
||||
if (this.aborted) break;
|
||||
|
||||
// Start new tasks up to concurrency limit
|
||||
while (pendingTasks.length > 0 && activeTasks.size < concurrency) {
|
||||
const task = pendingTasks.shift();
|
||||
const promise = loadTexture(task).then(() => {
|
||||
activeTasks.delete(promise);
|
||||
});
|
||||
activeTasks.add(promise);
|
||||
}
|
||||
|
||||
// Wait for at least one task to complete
|
||||
if (activeTasks.size > 0) {
|
||||
await Promise.race(activeTasks);
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
// Final progress report
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
loaded: this.loaded,
|
||||
failed: this.failed,
|
||||
total: this.total,
|
||||
percentage: 100,
|
||||
currentTexture: null,
|
||||
isComplete: true
|
||||
});
|
||||
}
|
||||
|
||||
return this.getStatus();
|
||||
}
|
||||
}
|
||||
|
||||
// Export the manager class
|
||||
export { TextureLoadingManager };
|
||||
|
||||
class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
|
||||
// Static reference to TinyUSDZ WASM module for EXR fallback
|
||||
static _tinyusdz = null;
|
||||
|
||||
// Yield interval for UI updates (ms)
|
||||
static YIELD_INTERVAL_MS = 16; // ~60fps
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Yield to browser to allow UI repaint during long-running async operations.
|
||||
* Uses requestAnimationFrame for optimal frame timing.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static yieldToUI() {
|
||||
return new Promise(resolve => {
|
||||
// Use requestAnimationFrame for smoother updates
|
||||
// Falls back to setTimeout if RAF is not available
|
||||
if (typeof requestAnimationFrame === 'function') {
|
||||
requestAnimationFrame(() => resolve());
|
||||
} else {
|
||||
setTimeout(resolve, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditional yield - only yields if enough time has passed since last yield
|
||||
* @param {Object} state - State object with lastYieldTime property
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async maybeYieldToUI(state) {
|
||||
const now = performance.now();
|
||||
if (!state.lastYieldTime || (now - state.lastYieldTime) >= this.YIELD_INTERVAL_MS) {
|
||||
state.lastYieldTime = now;
|
||||
await this.yieldToUI();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set TinyUSDZ WASM module for EXR decoding fallback
|
||||
* @param {Object} tinyusdz - TinyUSDZ WASM module instance
|
||||
@@ -275,33 +524,47 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
// - [x] clearcoat -> clearcoat
|
||||
// - [x] clearcoatRoughness -> clearcoatRoughness
|
||||
// - [x] specularColor -> specular
|
||||
// - [x] roughness -> roughness
|
||||
// - [x] roughness -> roughness
|
||||
// - [x] metallic -> metalness
|
||||
// - [x] emissiveColor -> emissive
|
||||
// - [x] opacity -> opacity (TODO: map to .transmission?)
|
||||
// - [x] occlusion -> aoMap
|
||||
// - [x] normal -> normalMap
|
||||
// - [x] displacement -> displacementMap
|
||||
static convertUsdMaterialToMeshPhysicalMaterial(usdMaterial, usdScene) {
|
||||
//
|
||||
// Options:
|
||||
// - textureLoadingManager: TextureLoadingManager instance for delayed texture loading
|
||||
// If provided, textures are queued instead of loaded immediately
|
||||
//
|
||||
static convertUsdMaterialToMeshPhysicalMaterial(usdMaterial, usdScene, options = {}) {
|
||||
const material = new THREE.MeshPhysicalMaterial();
|
||||
const loader = new THREE.TextureLoader();
|
||||
const textureManager = options.textureLoadingManager || null;
|
||||
|
||||
// Helper to load texture immediately or queue for later
|
||||
const loadOrQueueTexture = (mapProperty, textureId, textureOptions = {}) => {
|
||||
if (textureManager) {
|
||||
// Delayed mode: queue texture for later loading
|
||||
textureManager.queueTexture(material, mapProperty, textureId, usdScene, textureOptions);
|
||||
} else {
|
||||
// Immediate mode: load texture now (original behavior)
|
||||
this.getTextureFromUSD(usdScene, textureId).then((texture) => {
|
||||
material[mapProperty] = texture;
|
||||
material.needsUpdate = true;
|
||||
}).catch((err) => {
|
||||
console.error(`failed to load ${mapProperty} texture:`, err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Diffuse color and texture
|
||||
material.color = new THREE.Color(0.18, 0.18, 0.18);
|
||||
if (Object.prototype.hasOwnProperty.call(usdMaterial, 'diffuseColor')) {
|
||||
const color = usdMaterial.diffuseColor;
|
||||
material.color = new THREE.Color(color[0], color[1], color[2]);
|
||||
//console.log("diffuseColor:", material.color);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(usdMaterial, 'diffuseColorTextureId')) {
|
||||
this.getTextureFromUSD(usdScene, usdMaterial.diffuseColorTextureId).then((texture) => {
|
||||
//console.log("gettex");
|
||||
material.map = texture;
|
||||
material.needsUpdate = true;
|
||||
}).catch((err) => {
|
||||
console.error("failed to load texture. uri not exists or Cross-Site origin header is not set in the web server?", err);
|
||||
});
|
||||
loadOrQueueTexture('map', usdMaterial.diffuseColorTextureId);
|
||||
}
|
||||
|
||||
// IOR
|
||||
@@ -334,12 +597,7 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
material.specularColor = new THREE.Color(color[0], color[1], color[2]);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(usdMaterial, 'specularColorTextureId')) {
|
||||
this.getTextureFromUSD(usdScene, usdMaterial.specularColorTextureId).then((texture) => {
|
||||
material.specularColorMap = texture;
|
||||
material.needsUpdate = true;
|
||||
}).catch((err) => {
|
||||
console.error("failed to load specular color texture", err);
|
||||
});
|
||||
loadOrQueueTexture('specularColorMap', usdMaterial.specularColorTextureId);
|
||||
}
|
||||
} else {
|
||||
material.metalness = 0.0;
|
||||
@@ -347,12 +605,7 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
material.metalness = usdMaterial.metallic;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(usdMaterial, 'metallicTextureId')) {
|
||||
this.getTextureFromUSD(usdScene, usdMaterial.metallicTextureId).then((texture) => {
|
||||
material.metalnessMap = texture;
|
||||
material.needsUpdate = true;
|
||||
}).catch((err) => {
|
||||
console.error("failed to load metallic texture", err);
|
||||
});
|
||||
loadOrQueueTexture('metalnessMap', usdMaterial.metallicTextureId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,12 +615,7 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
material.roughness = usdMaterial.roughness;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(usdMaterial, 'roughnessTextureId')) {
|
||||
this.getTextureFromUSD(usdScene, usdMaterial.roughnessTextureId).then((texture) => {
|
||||
material.roughnessMap = texture;
|
||||
material.needsUpdate = true;
|
||||
}).catch((err) => {
|
||||
console.error("failed to load roughness texture", err);
|
||||
});
|
||||
loadOrQueueTexture('roughnessMap', usdMaterial.roughnessTextureId);
|
||||
}
|
||||
|
||||
// Emissive
|
||||
@@ -376,12 +624,7 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
material.emissive = new THREE.Color(color[0], color[1], color[2]);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(usdMaterial, 'emissiveColorTextureId')) {
|
||||
this.getTextureFromUSD(usdScene, usdMaterial.emissiveColorTextureId).then((texture) => {
|
||||
material.emissiveMap = texture;
|
||||
material.needsUpdate = true;
|
||||
}).catch((err) => {
|
||||
console.error("failed to load emissive texture", err);
|
||||
});
|
||||
loadOrQueueTexture('emissiveMap', usdMaterial.emissiveColorTextureId);
|
||||
}
|
||||
|
||||
// Opacity
|
||||
@@ -393,46 +636,22 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
}
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(usdMaterial, 'opacityTextureId')) {
|
||||
this.getTextureFromUSD(usdScene, usdMaterial.opacityTextureId).then((texture) => {
|
||||
material.alphaMap = texture;
|
||||
// FIXME. disable opacity texture for a while.
|
||||
// transparent = true will create completely transparent material for some reason.
|
||||
//material.transparent = true;
|
||||
material.needsUpdate = true;
|
||||
}).catch((err) => {
|
||||
console.error("failed to load opacity texture", err);
|
||||
});
|
||||
loadOrQueueTexture('alphaMap', usdMaterial.opacityTextureId);
|
||||
}
|
||||
|
||||
// Ambient Occlusion
|
||||
if (Object.prototype.hasOwnProperty.call(usdMaterial, 'occlusionTextureId')) {
|
||||
this.getTextureFromUSD(usdScene, usdMaterial.occlusionTextureId).then((texture) => {
|
||||
material.aoMap = texture;
|
||||
material.needsUpdate = true;
|
||||
}).catch((err) => {
|
||||
console.error("failed to load occlusion texture", err);
|
||||
});
|
||||
loadOrQueueTexture('aoMap', usdMaterial.occlusionTextureId);
|
||||
}
|
||||
|
||||
// Normal Map
|
||||
if (Object.prototype.hasOwnProperty.call(usdMaterial, 'normalTextureId')) {
|
||||
this.getTextureFromUSD(usdScene, usdMaterial.normalTextureId).then((texture) => {
|
||||
material.normalMap = texture;
|
||||
material.needsUpdate = true;
|
||||
}).catch((err) => {
|
||||
console.error("failed to load normal texture", err);
|
||||
});
|
||||
loadOrQueueTexture('normalMap', usdMaterial.normalTextureId);
|
||||
}
|
||||
|
||||
// Displacement Map
|
||||
if (Object.prototype.hasOwnProperty.call(usdMaterial, 'displacementTextureId')) {
|
||||
this.getTextureFromUSD(usdScene, usdMaterial.displacementTextureId).then((texture) => {
|
||||
material.displacementMap = texture;
|
||||
material.displacementScale = 1.0;
|
||||
material.needsUpdate = true;
|
||||
}).catch((err) => {
|
||||
console.error("failed to load displacement texture", err);
|
||||
});
|
||||
loadOrQueueTexture('displacementMap', usdMaterial.displacementTextureId, { displacementScale: 1.0 });
|
||||
}
|
||||
|
||||
return material;
|
||||
@@ -546,7 +765,7 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
if (parsedMaterial && parsedMaterial.hasUsdPreviewSurface) {
|
||||
// Extract surfaceShader data from the JSON structure
|
||||
const shaderData = parsedMaterial.surfaceShader || parsedMaterial;
|
||||
return this.convertUsdMaterialToMeshPhysicalMaterial(shaderData, usdScene);
|
||||
return this.convertUsdMaterialToMeshPhysicalMaterial(shaderData, usdScene, options);
|
||||
}
|
||||
return this.createDefaultMaterial();
|
||||
}
|
||||
@@ -556,7 +775,8 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
const material = await convertOpenPBRToMeshPhysicalMaterialLoaded(parsedMaterial, usdScene, {
|
||||
envMap: options.envMap || null,
|
||||
envMapIntensity: options.envMapIntensity || 1.0,
|
||||
textureCache: options.textureCache || new Map()
|
||||
textureCache: options.textureCache || new Map(),
|
||||
textureLoadingManager: options.textureLoadingManager || null
|
||||
});
|
||||
|
||||
// Apply sideness based on USD doubleSided attribute
|
||||
@@ -661,7 +881,8 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
// Extract surfaceShader data from the JSON structure
|
||||
// The JSON format nests shader properties under surfaceShader
|
||||
const shaderData = parsedMaterial.surfaceShader || parsedMaterial;
|
||||
return this.convertUsdMaterialToMeshPhysicalMaterial(shaderData, usdScene);
|
||||
// Pass options through to support textureLoadingManager
|
||||
return this.convertUsdMaterialToMeshPhysicalMaterial(shaderData, usdScene, options);
|
||||
}
|
||||
|
||||
return this.createDefaultMaterial();
|
||||
@@ -772,7 +993,8 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
envMap: options.envMap || null,
|
||||
envMapIntensity: options.envMapIntensity || 1.0,
|
||||
textureCache: options.textureCache || new Map(),
|
||||
doubleSided: geometry.userData['doubleSided']
|
||||
doubleSided: geometry.userData['doubleSided'],
|
||||
textureLoadingManager: options.textureLoadingManager || null
|
||||
});
|
||||
|
||||
// Store material metadata for UI and reloading
|
||||
@@ -842,7 +1064,8 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
envMap: options.envMap || null,
|
||||
envMapIntensity: options.envMapIntensity || 1.0,
|
||||
textureCache: options.textureCache || new Map(),
|
||||
doubleSided: geometry.userData['doubleSided']
|
||||
doubleSided: geometry.userData['doubleSided'],
|
||||
textureLoadingManager: options.textureLoadingManager || null
|
||||
});
|
||||
|
||||
material.envMap = options.envMap || null;
|
||||
@@ -910,14 +1133,30 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count total meshes in USD hierarchy
|
||||
* @private
|
||||
*/
|
||||
static _countMeshes(usdNode) {
|
||||
let count = usdNode.nodeType === 'mesh' ? 1 : 0;
|
||||
if (usdNode.children) {
|
||||
for (const child of usdNode.children) {
|
||||
count += this._countMeshes(child);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Supported options:
|
||||
// - 'overrideMaterial' : Override usd material with defaultMtl.
|
||||
// - 'onProgress' : Progress callback (info) => void
|
||||
// info: { stage: 'building', percentage: number, message: string }
|
||||
// info: { stage: 'building'|'textures', percentage: number, message: string }
|
||||
// - '_progressState' : Internal state for progress tracking (auto-created)
|
||||
|
||||
/**
|
||||
* Build a Three.js scene graph from a USD node hierarchy
|
||||
* Includes browser yields to allow UI updates during scene building.
|
||||
*
|
||||
* @param {Object} usdNode - USD node from TinyUSDZLoader
|
||||
* @param {THREE.Material} defaultMtl - Default material to use
|
||||
* @param {Object} usdScene - USD scene object (TinyUSDZLoaderNative)
|
||||
@@ -931,18 +1170,24 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
// Initialize progress tracking on first call (root node)
|
||||
if (!options._progressState) {
|
||||
const totalNodes = this._countNodes(usdNode);
|
||||
const totalMeshes = this._countMeshes(usdNode);
|
||||
options._progressState = {
|
||||
processedNodes: 0,
|
||||
totalNodes: totalNodes
|
||||
processedMeshes: 0,
|
||||
totalNodes: totalNodes,
|
||||
totalMeshes: totalMeshes,
|
||||
lastYieldTime: 0
|
||||
};
|
||||
// Report initial progress
|
||||
if (options.onProgress) {
|
||||
options.onProgress({
|
||||
stage: 'building',
|
||||
percentage: 0,
|
||||
message: `Building scene (0/${totalNodes} nodes)...`
|
||||
message: `Building scene (0/${totalMeshes} meshes)...`
|
||||
});
|
||||
}
|
||||
// Initial yield to show progress UI
|
||||
await this.yieldToUI();
|
||||
}
|
||||
|
||||
var node = new THREE.Group();
|
||||
@@ -953,39 +1198,43 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
// intermediate xform node
|
||||
// Apply the USD local transform matrix to the Three.js node
|
||||
const matrix = this.toMatrix4(usdNode.localMatrix);
|
||||
//console.log(" Applied localMatrix:", {
|
||||
// matrix: matrix});
|
||||
|
||||
// Decompose the matrix into position, rotation, and scale
|
||||
// This is necessary for Three.js to properly handle the transform
|
||||
node.applyMatrix4(matrix);
|
||||
|
||||
// Log transform for debugging
|
||||
//console.log(" Applied xform matrix:", {
|
||||
// position: [node.position.x, node.position.y, node.position.z],
|
||||
// rotation: [node.rotation.x, node.rotation.y, node.rotation.z],
|
||||
// scale: [node.scale.x, node.scale.y, node.scale.z]
|
||||
//});
|
||||
|
||||
} else if (usdNode.nodeType == 'mesh') {
|
||||
|
||||
// contentId is the mesh ID in the USD scene.
|
||||
const mesh = usdScene.getMesh(usdNode.contentId);
|
||||
|
||||
// Update progress before building mesh
|
||||
if (options._progressState && options.onProgress) {
|
||||
const { processedMeshes, totalMeshes } = options._progressState;
|
||||
const percentage = (processedMeshes / Math.max(1, totalMeshes)) * 100;
|
||||
options.onProgress({
|
||||
stage: 'building',
|
||||
percentage: percentage,
|
||||
message: `Building mesh ${processedMeshes + 1}/${totalMeshes}: ${usdNode.primName}`
|
||||
});
|
||||
}
|
||||
|
||||
// Yield to browser before heavy mesh setup
|
||||
await this.maybeYieldToUI(options._progressState);
|
||||
|
||||
const threeMesh = await this.setupMesh(mesh, defaultMtl, usdScene, options);
|
||||
node = threeMesh;
|
||||
|
||||
// Increment mesh counter after building
|
||||
if (options._progressState) {
|
||||
options._progressState.processedMeshes++;
|
||||
}
|
||||
|
||||
// Apply transform to mesh nodes as well
|
||||
// Mesh nodes can also have transforms in USD
|
||||
if (usdNode.localMatrix) {
|
||||
const matrix = this.toMatrix4(usdNode.localMatrix);
|
||||
node.applyMatrix4(matrix);
|
||||
|
||||
//console.log(" Applied mesh matrix:", {
|
||||
// position: [node.position.x, node.position.y, node.position.z],
|
||||
// rotation: [node.rotation.x, node.rotation.y, node.rotation.z],
|
||||
// scale: [node.scale.x, node.scale.y, node.scale.z]
|
||||
//});
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -1003,16 +1252,9 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
// Update progress after processing this node
|
||||
if (options._progressState) {
|
||||
options._progressState.processedNodes++;
|
||||
const { processedNodes, totalNodes } = options._progressState;
|
||||
const percentage = (processedNodes / totalNodes) * 100;
|
||||
|
||||
if (options.onProgress) {
|
||||
options.onProgress({
|
||||
stage: 'building',
|
||||
percentage: percentage,
|
||||
message: `Building: ${usdNode.primName} (${processedNodes}/${totalNodes})`
|
||||
});
|
||||
}
|
||||
// Yield periodically to allow UI updates
|
||||
await this.maybeYieldToUI(options._progressState);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(usdNode, 'children')) {
|
||||
|
||||
@@ -435,8 +435,9 @@ async function convertOpenPBRToMeshPhysicalMaterialLoaded(materialData, usdScene
|
||||
return material;
|
||||
}
|
||||
|
||||
// Texture cache
|
||||
// Texture cache and delayed loading manager
|
||||
const textureCache = options.textureCache || new Map();
|
||||
const textureManager = options.textureLoadingManager || null;
|
||||
|
||||
// Helper to apply parameter with optional texture
|
||||
const applyParam = async (paramName, paramValue, group = null) => {
|
||||
@@ -456,14 +457,22 @@ async function convertOpenPBRToMeshPhysicalMaterialLoaded(materialData, usdScene
|
||||
if (usdScene && hasTexture(paramValue)) {
|
||||
const texMapName = OPENPBR_TEXTURE_MAP[paramName];
|
||||
if (texMapName) {
|
||||
const texture = await loadTextureFromUSD(usdScene, getTextureId(paramValue), textureCache);
|
||||
if (texture) {
|
||||
material[texMapName] = texture;
|
||||
material.userData.textures[texMapName] = {
|
||||
textureId: getTextureId(paramValue),
|
||||
texture: texture
|
||||
};
|
||||
material.needsUpdate = true;
|
||||
const textureId = getTextureId(paramValue);
|
||||
|
||||
// If textureLoadingManager is provided, queue texture for later loading
|
||||
if (textureManager) {
|
||||
textureManager.queueTexture(material, texMapName, textureId, usdScene);
|
||||
} else {
|
||||
// Load immediately (original behavior)
|
||||
const texture = await loadTextureFromUSD(usdScene, textureId, textureCache);
|
||||
if (texture) {
|
||||
material[texMapName] = texture;
|
||||
material.userData.textures[texMapName] = {
|
||||
textureId: textureId,
|
||||
texture: texture
|
||||
};
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -521,10 +530,15 @@ async function convertOpenPBRToMeshPhysicalMaterialLoaded(materialData, usdScene
|
||||
}
|
||||
// Load emission texture
|
||||
if (usdScene && hasTexture(flat.emission_color)) {
|
||||
const texture = await loadTextureFromUSD(usdScene, getTextureId(flat.emission_color), textureCache);
|
||||
if (texture) {
|
||||
material.emissiveMap = texture;
|
||||
material.userData.textures.emissiveMap = { textureId: getTextureId(flat.emission_color), texture };
|
||||
const textureId = getTextureId(flat.emission_color);
|
||||
if (textureManager) {
|
||||
textureManager.queueTexture(material, 'emissiveMap', textureId, usdScene);
|
||||
} else {
|
||||
const texture = await loadTextureFromUSD(usdScene, textureId, textureCache);
|
||||
if (texture) {
|
||||
material.emissiveMap = texture;
|
||||
material.userData.textures.emissiveMap = { textureId, texture };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,11 +555,17 @@ async function convertOpenPBRToMeshPhysicalMaterialLoaded(materialData, usdScene
|
||||
material.transparent = opacityValue < 1.0;
|
||||
}
|
||||
if (usdScene && hasTexture(opacityParam)) {
|
||||
const texture = await loadTextureFromUSD(usdScene, getTextureId(opacityParam), textureCache);
|
||||
if (texture) {
|
||||
material.alphaMap = texture;
|
||||
material.transparent = true;
|
||||
material.userData.textures.alphaMap = { textureId: getTextureId(opacityParam), texture };
|
||||
const textureId = getTextureId(opacityParam);
|
||||
// For alpha maps, we need to set transparent=true even in delayed mode
|
||||
material.transparent = true;
|
||||
if (textureManager) {
|
||||
textureManager.queueTexture(material, 'alphaMap', textureId, usdScene);
|
||||
} else {
|
||||
const texture = await loadTextureFromUSD(usdScene, textureId, textureCache);
|
||||
if (texture) {
|
||||
material.alphaMap = texture;
|
||||
material.userData.textures.alphaMap = { textureId, texture };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,11 +573,17 @@ async function convertOpenPBRToMeshPhysicalMaterialLoaded(materialData, usdScene
|
||||
// Normal map
|
||||
const normalParam = flat.normal !== undefined ? flat.normal : flat.geometry_normal;
|
||||
if (normalParam !== undefined && usdScene && hasTexture(normalParam)) {
|
||||
const texture = await loadTextureFromUSD(usdScene, getTextureId(normalParam), textureCache);
|
||||
if (texture) {
|
||||
material.normalMap = texture;
|
||||
material.normalScale = new THREE.Vector2(1, 1);
|
||||
material.userData.textures.normalMap = { textureId: getTextureId(normalParam), texture };
|
||||
const textureId = getTextureId(normalParam);
|
||||
// Initialize normalScale even in delayed mode
|
||||
material.normalScale = new THREE.Vector2(1, 1);
|
||||
if (textureManager) {
|
||||
textureManager.queueTexture(material, 'normalMap', textureId, usdScene);
|
||||
} else {
|
||||
const texture = await loadTextureFromUSD(usdScene, textureId, textureCache);
|
||||
if (texture) {
|
||||
material.normalMap = texture;
|
||||
material.userData.textures.normalMap = { textureId, texture };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -622,10 +648,15 @@ async function convertOpenPBRToMeshPhysicalMaterialLoaded(materialData, usdScene
|
||||
material.emissive = createColor(emissionColor);
|
||||
}
|
||||
if (usdScene && hasTexture(pbr.emission.emission_color)) {
|
||||
const texture = await loadTextureFromUSD(usdScene, getTextureId(pbr.emission.emission_color), textureCache);
|
||||
if (texture) {
|
||||
material.emissiveMap = texture;
|
||||
material.userData.textures.emissiveMap = { textureId: getTextureId(pbr.emission.emission_color), texture };
|
||||
const textureId = getTextureId(pbr.emission.emission_color);
|
||||
if (textureManager) {
|
||||
textureManager.queueTexture(material, 'emissiveMap', textureId, usdScene);
|
||||
} else {
|
||||
const texture = await loadTextureFromUSD(usdScene, textureId, textureCache);
|
||||
if (texture) {
|
||||
material.emissiveMap = texture;
|
||||
material.userData.textures.emissiveMap = { textureId, texture };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pbr.emission.emission_luminance !== undefined) {
|
||||
@@ -643,22 +674,32 @@ async function convertOpenPBRToMeshPhysicalMaterialLoaded(materialData, usdScene
|
||||
material.transparent = opacityValue < 1.0;
|
||||
}
|
||||
if (usdScene && hasTexture(opacityParam)) {
|
||||
const texture = await loadTextureFromUSD(usdScene, getTextureId(opacityParam), textureCache);
|
||||
if (texture) {
|
||||
material.alphaMap = texture;
|
||||
material.transparent = true;
|
||||
material.userData.textures.alphaMap = { textureId: getTextureId(opacityParam), texture };
|
||||
const textureId = getTextureId(opacityParam);
|
||||
material.transparent = true;
|
||||
if (textureManager) {
|
||||
textureManager.queueTexture(material, 'alphaMap', textureId, usdScene);
|
||||
} else {
|
||||
const texture = await loadTextureFromUSD(usdScene, textureId, textureCache);
|
||||
if (texture) {
|
||||
material.alphaMap = texture;
|
||||
material.userData.textures.alphaMap = { textureId, texture };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const normalParam = pbr.geometry.normal !== undefined ? pbr.geometry.normal : pbr.geometry.geometry_normal;
|
||||
if (normalParam !== undefined && usdScene && hasTexture(normalParam)) {
|
||||
const texture = await loadTextureFromUSD(usdScene, getTextureId(normalParam), textureCache);
|
||||
if (texture) {
|
||||
material.normalMap = texture;
|
||||
material.normalScale = new THREE.Vector2(1, 1);
|
||||
material.userData.textures.normalMap = { textureId: getTextureId(normalParam), texture };
|
||||
const textureId = getTextureId(normalParam);
|
||||
material.normalScale = new THREE.Vector2(1, 1);
|
||||
if (textureManager) {
|
||||
textureManager.queueTexture(material, 'normalMap', textureId, usdScene);
|
||||
} else {
|
||||
const texture = await loadTextureFromUSD(usdScene, textureId, textureCache);
|
||||
if (texture) {
|
||||
material.normalMap = texture;
|
||||
material.userData.textures.normalMap = { textureId, texture };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user