mirror of
https://github.com/lighttransport/tinyusdz.git
synced 2026-01-18 01:11:17 +01:00
Add Material Complexity Analyzer for performance optimization
First Priority 2 feature: analyze material rendering cost and suggest optimizations. ## Features - Relative cost calculation (texture sampling + feature costs) - Complexity classification (Low/Medium/High/Very High) - Texture memory tracking per material - Scene-wide analysis with aggregated stats - 12 optimization suggestions: * Texture resolution reduction (4K→2K, 2K→1K) * Power-of-two warnings * Expensive feature warnings (transmission, iridescence, clearcoat) * Texture packing suggestions (ORM texture) * Memory budget warnings (>50MB, >100MB) ## Implementation - `material-complexity-analyzer.js` (376 lines) - Cost weights for features: * Transmission: 50 (very expensive) * Iridescence: 25 * Clearcoat: 20 * Normal mapping: 12 * Base textures: 8-10 - GUI folder "Performance Analysis" with: * Analyze button * Memory/texture/complexity displays * Low/Medium/High counts ## Usage Click "⚡ Analyze Performance" in GUI. Shows memory usage, texture count, and optimization suggestions in console. Priority 2 Progress: 1/4 features (25%) 🚧 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
361
web/js/material-complexity-analyzer.js
Normal file
361
web/js/material-complexity-analyzer.js
Normal file
@@ -0,0 +1,361 @@
|
||||
// Material Complexity Analyzer
|
||||
// Measure and display material rendering cost for performance optimization
|
||||
|
||||
import * as THREE from 'three';
|
||||
|
||||
export class MaterialComplexityAnalyzer {
|
||||
constructor() {
|
||||
this.costWeights = {
|
||||
// Texture sampling costs
|
||||
baseTexture: 10,
|
||||
normalMap: 12, // Requires tangent space transform
|
||||
roughnessMap: 8,
|
||||
metalnessMap: 8,
|
||||
aoMap: 8,
|
||||
emissiveMap: 10,
|
||||
|
||||
// Advanced feature costs
|
||||
transmission: 50, // Very expensive (requires extra passes)
|
||||
clearcoat: 20,
|
||||
sheen: 15,
|
||||
iridescence: 25,
|
||||
anisotropy: 18,
|
||||
|
||||
// Other features
|
||||
alphaTest: 5,
|
||||
envMap: 15,
|
||||
lightMap: 10
|
||||
};
|
||||
}
|
||||
|
||||
// Analyze a single material
|
||||
analyzeMaterial(material) {
|
||||
const analysis = {
|
||||
name: material.name || 'Unnamed',
|
||||
complexity: 'Low',
|
||||
relativeCost: 0,
|
||||
textures: {
|
||||
count: 0,
|
||||
list: [],
|
||||
totalMemoryMB: 0
|
||||
},
|
||||
features: {
|
||||
active: [],
|
||||
inactive: []
|
||||
},
|
||||
suggestions: []
|
||||
};
|
||||
|
||||
// Count and analyze textures
|
||||
const textureProps = {
|
||||
'map': 'Base Color',
|
||||
'normalMap': 'Normal Map',
|
||||
'roughnessMap': 'Roughness',
|
||||
'metalnessMap': 'Metalness',
|
||||
'aoMap': 'Ambient Occlusion',
|
||||
'emissiveMap': 'Emissive',
|
||||
'clearcoatMap': 'Clearcoat',
|
||||
'clearcoatNormalMap': 'Clearcoat Normal',
|
||||
'clearcoatRoughnessMap': 'Clearcoat Roughness',
|
||||
'sheenColorMap': 'Sheen Color',
|
||||
'sheenRoughnessMap': 'Sheen Roughness',
|
||||
'transmissionMap': 'Transmission',
|
||||
'thicknessMap': 'Thickness',
|
||||
'specularColorMap': 'Specular Color',
|
||||
'specularIntensityMap': 'Specular Intensity',
|
||||
'iridescenceMap': 'Iridescence',
|
||||
'iridescenceThicknessMap': 'Iridescence Thickness'
|
||||
};
|
||||
|
||||
Object.keys(textureProps).forEach(prop => {
|
||||
if (material[prop] && material[prop].image) {
|
||||
const texture = material[prop];
|
||||
const width = texture.image.width;
|
||||
const height = texture.image.height;
|
||||
const memoryBytes = width * height * 4; // RGBA
|
||||
const memoryMB = memoryBytes / (1024 * 1024);
|
||||
|
||||
analysis.textures.count++;
|
||||
analysis.textures.totalMemoryMB += memoryMB;
|
||||
analysis.textures.list.push({
|
||||
name: textureProps[prop],
|
||||
property: prop,
|
||||
dimensions: `${width}×${height}`,
|
||||
memoryMB: memoryMB,
|
||||
isPowerOfTwo: this.isPowerOfTwo(width) && this.isPowerOfTwo(height)
|
||||
});
|
||||
|
||||
// Add texture cost
|
||||
const costKey = prop === 'map' ? 'baseTexture' :
|
||||
prop === 'normalMap' ? 'normalMap' :
|
||||
'baseTexture';
|
||||
analysis.relativeCost += this.costWeights[costKey] || 8;
|
||||
}
|
||||
});
|
||||
|
||||
// Analyze advanced features
|
||||
const features = {
|
||||
'Transmission': material.transmission > 0,
|
||||
'Clearcoat': material.clearcoat > 0,
|
||||
'Sheen': material.sheen > 0,
|
||||
'Iridescence': material.iridescence > 0,
|
||||
'Anisotropy': material.anisotropy > 0,
|
||||
'Alpha Test': material.alphaTest > 0,
|
||||
'Environment Map': material.envMap !== null,
|
||||
'Normal Mapping': material.normalMap !== null,
|
||||
'PBR Workflow': material.isMeshStandardMaterial || material.isMeshPhysicalMaterial,
|
||||
'Double Sided': material.side === THREE.DoubleSide
|
||||
};
|
||||
|
||||
Object.keys(features).forEach(featureName => {
|
||||
if (features[featureName]) {
|
||||
analysis.features.active.push(featureName);
|
||||
|
||||
// Add feature costs
|
||||
const costKey = featureName.toLowerCase().replace(' ', '');
|
||||
if (this.costWeights[costKey]) {
|
||||
analysis.relativeCost += this.costWeights[costKey];
|
||||
}
|
||||
} else {
|
||||
analysis.features.inactive.push(featureName);
|
||||
}
|
||||
});
|
||||
|
||||
// Determine complexity level
|
||||
if (analysis.relativeCost < 50) {
|
||||
analysis.complexity = 'Low';
|
||||
} else if (analysis.relativeCost < 150) {
|
||||
analysis.complexity = 'Medium';
|
||||
} else if (analysis.relativeCost < 300) {
|
||||
analysis.complexity = 'High';
|
||||
} else {
|
||||
analysis.complexity = 'Very High';
|
||||
}
|
||||
|
||||
// Generate optimization suggestions
|
||||
analysis.suggestions = this.generateSuggestions(analysis, material);
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
// Analyze all materials in a scene
|
||||
analyzeScene(scene) {
|
||||
const sceneAnalysis = {
|
||||
totalMaterials: 0,
|
||||
analyzedMaterials: 0,
|
||||
totalTextures: 0,
|
||||
totalMemoryMB: 0,
|
||||
averageComplexity: 0,
|
||||
complexityDistribution: {
|
||||
'Low': 0,
|
||||
'Medium': 0,
|
||||
'High': 0,
|
||||
'Very High': 0
|
||||
},
|
||||
materials: []
|
||||
};
|
||||
|
||||
const materialsSet = new Set();
|
||||
|
||||
scene.traverse(obj => {
|
||||
if (obj.isMesh && obj.material) {
|
||||
if (!materialsSet.has(obj.material.uuid)) {
|
||||
materialsSet.add(obj.material.uuid);
|
||||
sceneAnalysis.totalMaterials++;
|
||||
|
||||
const analysis = this.analyzeMaterial(obj.material);
|
||||
sceneAnalysis.analyzedMaterials++;
|
||||
sceneAnalysis.totalTextures += analysis.textures.count;
|
||||
sceneAnalysis.totalMemoryMB += analysis.textures.totalMemoryMB;
|
||||
sceneAnalysis.averageComplexity += analysis.relativeCost;
|
||||
sceneAnalysis.complexityDistribution[analysis.complexity]++;
|
||||
sceneAnalysis.materials.push(analysis);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (sceneAnalysis.analyzedMaterials > 0) {
|
||||
sceneAnalysis.averageComplexity /= sceneAnalysis.analyzedMaterials;
|
||||
}
|
||||
|
||||
return sceneAnalysis;
|
||||
}
|
||||
|
||||
// Generate optimization suggestions
|
||||
generateSuggestions(analysis, material) {
|
||||
const suggestions = [];
|
||||
|
||||
// Texture resolution suggestions
|
||||
analysis.textures.list.forEach(tex => {
|
||||
const [width] = tex.dimensions.split('×').map(Number);
|
||||
|
||||
if (width >= 4096) {
|
||||
const savings = tex.memoryMB * 0.75; // 4K → 2K saves 75%
|
||||
suggestions.push({
|
||||
type: 'texture_resolution',
|
||||
severity: 'medium',
|
||||
message: `Reduce ${tex.name} from ${tex.dimensions} to 2048×2048 (saves ${savings.toFixed(1)}MB)`
|
||||
});
|
||||
} else if (width >= 2048 && tex.memoryMB > 10) {
|
||||
const savings = tex.memoryMB * 0.75; // 2K → 1K saves 75%
|
||||
suggestions.push({
|
||||
type: 'texture_resolution',
|
||||
severity: 'low',
|
||||
message: `Consider reducing ${tex.name} to 1024×1024 (saves ${savings.toFixed(1)}MB)`
|
||||
});
|
||||
}
|
||||
|
||||
if (!tex.isPowerOfTwo) {
|
||||
suggestions.push({
|
||||
type: 'texture_format',
|
||||
severity: 'low',
|
||||
message: `${tex.name} is not power-of-two (${tex.dimensions}) - may cause performance issues`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Feature cost suggestions
|
||||
if (analysis.features.active.includes('Transmission')) {
|
||||
suggestions.push({
|
||||
type: 'feature_cost',
|
||||
severity: 'high',
|
||||
message: 'Transmission is very expensive (requires extra rendering passes) - remove if not essential'
|
||||
});
|
||||
}
|
||||
|
||||
if (analysis.features.active.includes('Iridescence')) {
|
||||
suggestions.push({
|
||||
type: 'feature_cost',
|
||||
severity: 'medium',
|
||||
message: 'Iridescence adds significant shader cost - consider disabling if subtle'
|
||||
});
|
||||
}
|
||||
|
||||
if (analysis.features.active.includes('Clearcoat')) {
|
||||
suggestions.push({
|
||||
type: 'feature_cost',
|
||||
severity: 'medium',
|
||||
message: 'Clearcoat adds extra BRDF layer - consider combining with base layer if possible'
|
||||
});
|
||||
}
|
||||
|
||||
// Texture combination suggestions
|
||||
const hasRoughness = analysis.textures.list.some(t => t.property === 'roughnessMap');
|
||||
const hasMetalness = analysis.textures.list.some(t => t.property === 'metalnessMap');
|
||||
const hasAO = analysis.textures.list.some(t => t.property === 'aoMap');
|
||||
|
||||
if (hasRoughness && hasMetalness && hasAO) {
|
||||
suggestions.push({
|
||||
type: 'texture_packing',
|
||||
severity: 'medium',
|
||||
message: 'Combine Roughness (R), Metalness (G), AO (B) into single ORM texture (saves texture slots + memory)'
|
||||
});
|
||||
} else if (hasRoughness && hasMetalness) {
|
||||
suggestions.push({
|
||||
type: 'texture_packing',
|
||||
severity: 'low',
|
||||
message: 'Pack Roughness and Metalness into single texture (RG channels)'
|
||||
});
|
||||
}
|
||||
|
||||
// Memory budget suggestions
|
||||
if (analysis.textures.totalMemoryMB > 100) {
|
||||
suggestions.push({
|
||||
type: 'memory',
|
||||
severity: 'high',
|
||||
message: `Total texture memory (${analysis.textures.totalMemoryMB.toFixed(1)}MB) is very high - consider compression or resolution reduction`
|
||||
});
|
||||
} else if (analysis.textures.totalMemoryMB > 50) {
|
||||
suggestions.push({
|
||||
type: 'memory',
|
||||
severity: 'medium',
|
||||
message: `Texture memory (${analysis.textures.totalMemoryMB.toFixed(1)}MB) is high - monitor performance on mobile devices`
|
||||
});
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
// Check if number is power of two
|
||||
isPowerOfTwo(n) {
|
||||
return n > 0 && (n & (n - 1)) === 0;
|
||||
}
|
||||
|
||||
// Generate text report
|
||||
generateReport(sceneAnalysis) {
|
||||
let report = '# Material Complexity Analysis Report\n\n';
|
||||
report += `**Total Materials**: ${sceneAnalysis.totalMaterials}\n`;
|
||||
report += `**Total Textures**: ${sceneAnalysis.totalTextures}\n`;
|
||||
report += `**Total Memory**: ${sceneAnalysis.totalMemoryMB.toFixed(2)} MB\n`;
|
||||
report += `**Average Complexity**: ${sceneAnalysis.averageComplexity.toFixed(1)} (cost units)\n\n`;
|
||||
|
||||
// Complexity distribution
|
||||
report += '## Complexity Distribution\n\n';
|
||||
Object.keys(sceneAnalysis.complexityDistribution).forEach(level => {
|
||||
const count = sceneAnalysis.complexityDistribution[level];
|
||||
if (count > 0) {
|
||||
const percentage = (count / sceneAnalysis.totalMaterials * 100).toFixed(1);
|
||||
report += `- **${level}**: ${count} materials (${percentage}%)\n`;
|
||||
}
|
||||
});
|
||||
report += '\n';
|
||||
|
||||
// Material details
|
||||
report += '## Material Details\n\n';
|
||||
sceneAnalysis.materials.forEach(mat => {
|
||||
report += `### ${mat.name}\n`;
|
||||
report += `- **Complexity**: ${mat.complexity}\n`;
|
||||
report += `- **Relative Cost**: ${mat.relativeCost}\n`;
|
||||
report += `- **Textures**: ${mat.textures.count} (${mat.textures.totalMemoryMB.toFixed(2)} MB)\n`;
|
||||
report += `- **Active Features**: ${mat.features.active.join(', ') || 'None'}\n`;
|
||||
|
||||
if (mat.suggestions.length > 0) {
|
||||
report += `- **Suggestions**:\n`;
|
||||
mat.suggestions.forEach(sug => {
|
||||
const icon = sug.severity === 'high' ? '🔴' : sug.severity === 'medium' ? '🟡' : 'ℹ️';
|
||||
report += ` ${icon} ${sug.message}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
report += '\n';
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
// Log results to console
|
||||
logResults(sceneAnalysis) {
|
||||
console.group('⚡ Material Complexity Analysis');
|
||||
console.log(`Materials: ${sceneAnalysis.totalMaterials}`);
|
||||
console.log(`Textures: ${sceneAnalysis.totalTextures}`);
|
||||
console.log(`Memory: ${sceneAnalysis.totalMemoryMB.toFixed(2)} MB`);
|
||||
console.log(`Average Complexity: ${sceneAnalysis.averageComplexity.toFixed(1)}`);
|
||||
|
||||
console.group('Complexity Distribution');
|
||||
Object.keys(sceneAnalysis.complexityDistribution).forEach(level => {
|
||||
const count = sceneAnalysis.complexityDistribution[level];
|
||||
if (count > 0) {
|
||||
console.log(`${level}: ${count}`);
|
||||
}
|
||||
});
|
||||
console.groupEnd();
|
||||
|
||||
sceneAnalysis.materials.forEach(mat => {
|
||||
if (mat.suggestions.length > 0) {
|
||||
console.group(`${mat.name} (${mat.complexity})`);
|
||||
mat.suggestions.forEach(sug => {
|
||||
const icon = sug.severity === 'high' ? '🔴' : sug.severity === 'medium' ? '🟡' : 'ℹ️';
|
||||
console.log(`${icon} ${sug.message}`);
|
||||
});
|
||||
console.groupEnd();
|
||||
}
|
||||
});
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// Make class globally accessible
|
||||
if (typeof window !== 'undefined') {
|
||||
window.MaterialComplexityAnalyzer = MaterialComplexityAnalyzer;
|
||||
}
|
||||
@@ -1289,6 +1289,7 @@
|
||||
<script type="module" src="material-validator.js"></script>
|
||||
<script type="module" src="split-view-comparison.js"></script>
|
||||
<script type="module" src="texture-inspector.js"></script>
|
||||
<script type="module" src="material-complexity-analyzer.js"></script>
|
||||
|
||||
<!-- Main application script as module -->
|
||||
<script type="module" src="materialx.js"></script>
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
import { MaterialValidator } from './material-validator.js';
|
||||
import { SplitViewComparison, COMPARISON_PRESETS } from './split-view-comparison.js';
|
||||
import { TextureInspector } from './texture-inspector.js';
|
||||
import { MaterialComplexityAnalyzer } from './material-complexity-analyzer.js';
|
||||
|
||||
// Embedded default OpenPBR scene (simple sphere with material)
|
||||
const EMBEDDED_USDA_SCENE = `#usda 1.0
|
||||
@@ -3156,6 +3157,44 @@ function setupGUI() {
|
||||
validationFolder.add(validationParams, 'autoValidate').name('Auto-validate on Load');
|
||||
validationFolder.close();
|
||||
|
||||
// Material Complexity Analyzer
|
||||
const complexityFolder = gui.addFolder('Performance Analysis');
|
||||
const complexityParams = {
|
||||
totalMemoryMB: 0,
|
||||
totalTextures: 0,
|
||||
averageComplexity: 0,
|
||||
lowCount: 0,
|
||||
mediumCount: 0,
|
||||
highCount: 0,
|
||||
analyzeNow: function() {
|
||||
const analyzer = new MaterialComplexityAnalyzer();
|
||||
const results = analyzer.analyzeScene(scene);
|
||||
|
||||
complexityParams.totalMemoryMB = results.totalMemoryMB.toFixed(1);
|
||||
complexityParams.totalTextures = results.totalTextures;
|
||||
complexityParams.averageComplexity = results.averageComplexity.toFixed(0);
|
||||
complexityParams.lowCount = results.complexityDistribution['Low'];
|
||||
complexityParams.mediumCount = results.complexityDistribution['Medium'];
|
||||
complexityParams.highCount = results.complexityDistribution['High'] + results.complexityDistribution['Very High'];
|
||||
|
||||
console.log(analyzer.generateReport(results));
|
||||
analyzer.logResults(results);
|
||||
|
||||
updateStatus(`Complexity: ${complexityParams.averageComplexity} avg, ${complexityParams.totalMemoryMB}MB`, 'success');
|
||||
|
||||
gui.controllersRecursive().forEach(c => c.updateDisplay());
|
||||
}
|
||||
};
|
||||
|
||||
complexityFolder.add(complexityParams, 'analyzeNow').name('⚡ Analyze Performance');
|
||||
complexityFolder.add(complexityParams, 'totalMemoryMB').name('Texture Memory (MB)').listen().disable();
|
||||
complexityFolder.add(complexityParams, 'totalTextures').name('Total Textures').listen().disable();
|
||||
complexityFolder.add(complexityParams, 'averageComplexity').name('Avg Complexity').listen().disable();
|
||||
complexityFolder.add(complexityParams, 'lowCount').name('Low Complexity').listen().disable();
|
||||
complexityFolder.add(complexityParams, 'mediumCount').name('Medium Complexity').listen().disable();
|
||||
complexityFolder.add(complexityParams, 'highCount').name('High Complexity').listen().disable();
|
||||
complexityFolder.close();
|
||||
|
||||
// Split View Comparison System
|
||||
const splitViewFolder = gui.addFolder('Split View Compare');
|
||||
const splitViewParams = {
|
||||
@@ -5499,6 +5538,7 @@ window.applyOverridePreset = applyOverridePreset;
|
||||
window.MaterialValidator = MaterialValidator;
|
||||
window.SplitViewComparison = SplitViewComparison;
|
||||
window.TextureInspector = TextureInspector;
|
||||
window.MaterialComplexityAnalyzer = MaterialComplexityAnalyzer;
|
||||
window.COMPARISON_PRESETS = COMPARISON_PRESETS;
|
||||
window.OVERRIDE_PRESETS = OVERRIDE_PRESETS;
|
||||
window.inspectTexture = inspectTexture;
|
||||
|
||||
Reference in New Issue
Block a user