Add IBL Contribution Analyzer for diffuse/specular split debugging

- Created ibl-contribution-analyzer.js with IBLContributionAnalyzer class
- 4 visualization modes:
  * Full IBL (diffuse + specular combined)
  * Diffuse Only (force non-metallic, high roughness)
  * Specular Only (force metallic, low roughness)
  * No IBL (disable environment map)

- Analysis features:
  * Analyze single material or entire scene
  * Estimate diffuse vs specular contribution
  * Identify dominant contribution type
  * Track materials with/without IBL

- Scene-level statistics:
  * Count materials with IBL
  * Average envMapIntensity, metalness, roughness
  * Contribution breakdown (diffuse/specular/balanced)

- GUI integration:
  * Visualization mode dropdown
  * Analyze Scene button
  * Live statistics display (materials, intensity, contributions)
  * Export Report button (markdown format)
  * Reset to Original button

- Report generation:
  * Markdown format with material details
  * Per-material contribution estimates
  * Dominant contribution classification

Useful for debugging IBL issues, balancing lighting, and understanding
how materials respond to environment lighting.

Priority 2 feature (3/4 complete)
This commit is contained in:
Syoyo Fujita
2025-11-21 03:18:26 +09:00
parent 7c6ba2a11f
commit ea200c4459
3 changed files with 398 additions and 0 deletions

View File

@@ -0,0 +1,314 @@
// IBL Contribution Analyzer
// Split Image-Based Lighting into diffuse and specular contributions for debugging
import * as THREE from 'three';
export class IBLContributionAnalyzer {
constructor(scene, renderer) {
this.scene = scene;
this.renderer = renderer;
this.originalMaterials = new Map();
this.currentMode = 'full'; // 'full', 'diffuse', 'specular', 'none'
}
// Set visualization mode
setMode(mode) {
this.currentMode = mode;
this.applyMode();
}
// Apply current mode to all materials
applyMode() {
this.scene.traverse(obj => {
if (obj.isMesh && obj.material) {
// Store original material if not already stored
if (!this.originalMaterials.has(obj.uuid)) {
this.originalMaterials.set(obj.uuid, {
envMapIntensity: obj.material.envMapIntensity !== undefined ? obj.material.envMapIntensity : 1.0,
metalness: obj.material.metalness !== undefined ? obj.material.metalness : 0.0,
roughness: obj.material.roughness !== undefined ? obj.material.roughness : 1.0
});
}
const original = this.originalMaterials.get(obj.uuid);
switch (this.currentMode) {
case 'full':
// Restore full IBL contribution
if (obj.material.envMapIntensity !== undefined) {
obj.material.envMapIntensity = original.envMapIntensity;
}
if (obj.material.metalness !== undefined) {
obj.material.metalness = original.metalness;
}
if (obj.material.roughness !== undefined) {
obj.material.roughness = original.roughness;
}
break;
case 'diffuse':
// Show only diffuse IBL (force non-metallic, high roughness)
if (obj.material.metalness !== undefined) {
obj.material.metalness = 0.0;
}
if (obj.material.roughness !== undefined) {
obj.material.roughness = 1.0;
}
if (obj.material.envMapIntensity !== undefined) {
obj.material.envMapIntensity = original.envMapIntensity;
}
break;
case 'specular':
// Show only specular IBL (force metallic, reduce roughness)
if (obj.material.metalness !== undefined) {
obj.material.metalness = 1.0;
}
if (obj.material.roughness !== undefined) {
// Use original roughness but clamp to see clearer reflections
obj.material.roughness = Math.min(original.roughness, 0.3);
}
if (obj.material.envMapIntensity !== undefined) {
obj.material.envMapIntensity = original.envMapIntensity;
}
break;
case 'none':
// Disable IBL entirely
if (obj.material.envMapIntensity !== undefined) {
obj.material.envMapIntensity = 0.0;
}
break;
}
obj.material.needsUpdate = true;
}
});
}
// Reset to original materials
reset() {
this.scene.traverse(obj => {
if (obj.isMesh && this.originalMaterials.has(obj.uuid)) {
const original = this.originalMaterials.get(obj.uuid);
if (obj.material.envMapIntensity !== undefined) {
obj.material.envMapIntensity = original.envMapIntensity;
}
if (obj.material.metalness !== undefined) {
obj.material.metalness = original.metalness;
}
if (obj.material.roughness !== undefined) {
obj.material.roughness = original.roughness;
}
obj.material.needsUpdate = true;
}
});
this.originalMaterials.clear();
this.currentMode = 'full';
}
// Analyze IBL contribution for a single material
analyzeMaterial(material) {
const analysis = {
hasEnvMap: material.envMap !== null,
envMapIntensity: material.envMapIntensity !== undefined ? material.envMapIntensity : 1.0,
metalness: material.metalness !== undefined ? material.metalness : 0.0,
roughness: material.roughness !== undefined ? material.roughness : 1.0,
estimatedDiffuseContribution: 0,
estimatedSpecularContribution: 0,
dominantContribution: 'none'
};
if (!analysis.hasEnvMap) {
analysis.dominantContribution = 'none';
return analysis;
}
// Estimate contributions based on material properties
// These are rough approximations of the actual BRDF integration
// Diffuse contribution: stronger when non-metallic, affected by roughness
const diffuseFactor = (1.0 - analysis.metalness) * analysis.envMapIntensity;
analysis.estimatedDiffuseContribution = diffuseFactor;
// Specular contribution: stronger when metallic or low roughness
const specularFactor = (analysis.metalness + (1.0 - analysis.roughness) * 0.5) * analysis.envMapIntensity;
analysis.estimatedSpecularContribution = specularFactor;
// Determine dominant contribution
if (analysis.estimatedDiffuseContribution > analysis.estimatedSpecularContribution * 1.2) {
analysis.dominantContribution = 'diffuse';
} else if (analysis.estimatedSpecularContribution > analysis.estimatedDiffuseContribution * 1.2) {
analysis.dominantContribution = 'specular';
} else {
analysis.dominantContribution = 'balanced';
}
return analysis;
}
// Analyze entire scene
analyzeScene(scene) {
const sceneAnalysis = {
totalMaterials: 0,
materialsWithIBL: 0,
averageEnvMapIntensity: 0,
averageMetalness: 0,
averageRoughness: 0,
contributionBreakdown: {
diffuseDominant: 0,
specularDominant: 0,
balanced: 0,
noIBL: 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.materials.push({
name: obj.material.name || 'Unnamed',
...analysis
});
if (analysis.hasEnvMap) {
sceneAnalysis.materialsWithIBL++;
sceneAnalysis.averageEnvMapIntensity += analysis.envMapIntensity;
sceneAnalysis.averageMetalness += analysis.metalness;
sceneAnalysis.averageRoughness += analysis.roughness;
switch (analysis.dominantContribution) {
case 'diffuse':
sceneAnalysis.contributionBreakdown.diffuseDominant++;
break;
case 'specular':
sceneAnalysis.contributionBreakdown.specularDominant++;
break;
case 'balanced':
sceneAnalysis.contributionBreakdown.balanced++;
break;
}
} else {
sceneAnalysis.contributionBreakdown.noIBL++;
}
}
}
});
// Calculate averages
if (sceneAnalysis.materialsWithIBL > 0) {
sceneAnalysis.averageEnvMapIntensity /= sceneAnalysis.materialsWithIBL;
sceneAnalysis.averageMetalness /= sceneAnalysis.materialsWithIBL;
sceneAnalysis.averageRoughness /= sceneAnalysis.materialsWithIBL;
}
return sceneAnalysis;
}
// Generate text report
generateReport(sceneAnalysis) {
let report = '# IBL Contribution Analysis Report\n\n';
report += `**Total Materials**: ${sceneAnalysis.totalMaterials}\n`;
report += `**Materials with IBL**: ${sceneAnalysis.materialsWithIBL}\n`;
if (sceneAnalysis.materialsWithIBL > 0) {
report += `**Average EnvMap Intensity**: ${sceneAnalysis.averageEnvMapIntensity.toFixed(2)}\n`;
report += `**Average Metalness**: ${sceneAnalysis.averageMetalness.toFixed(2)}\n`;
report += `**Average Roughness**: ${sceneAnalysis.averageRoughness.toFixed(2)}\n\n`;
// Contribution breakdown
report += '## Contribution Breakdown\n\n';
const breakdown = sceneAnalysis.contributionBreakdown;
if (breakdown.diffuseDominant > 0) {
const pct = (breakdown.diffuseDominant / sceneAnalysis.materialsWithIBL * 100).toFixed(1);
report += `- **Diffuse Dominant**: ${breakdown.diffuseDominant} materials (${pct}%)\n`;
}
if (breakdown.specularDominant > 0) {
const pct = (breakdown.specularDominant / sceneAnalysis.materialsWithIBL * 100).toFixed(1);
report += `- **Specular Dominant**: ${breakdown.specularDominant} materials (${pct}%)\n`;
}
if (breakdown.balanced > 0) {
const pct = (breakdown.balanced / sceneAnalysis.materialsWithIBL * 100).toFixed(1);
report += `- **Balanced**: ${breakdown.balanced} materials (${pct}%)\n`;
}
if (breakdown.noIBL > 0) {
report += `- **No IBL**: ${breakdown.noIBL} materials\n`;
}
report += '\n';
// Material details
report += '## Material Details\n\n';
sceneAnalysis.materials.forEach(mat => {
if (mat.hasEnvMap) {
report += `### ${mat.name}\n`;
report += `- **EnvMap Intensity**: ${mat.envMapIntensity.toFixed(2)}\n`;
report += `- **Metalness**: ${mat.metalness.toFixed(2)}\n`;
report += `- **Roughness**: ${mat.roughness.toFixed(2)}\n`;
report += `- **Estimated Diffuse**: ${mat.estimatedDiffuseContribution.toFixed(2)}\n`;
report += `- **Estimated Specular**: ${mat.estimatedSpecularContribution.toFixed(2)}\n`;
report += `- **Dominant**: ${mat.dominantContribution}\n\n`;
}
});
} else {
report += '\n**No materials with IBL found in scene.**\n';
}
return report;
}
// Log results to console
logResults(sceneAnalysis) {
console.group('🌍 IBL Contribution Analysis');
console.log(`Total Materials: ${sceneAnalysis.totalMaterials}`);
console.log(`Materials with IBL: ${sceneAnalysis.materialsWithIBL}`);
if (sceneAnalysis.materialsWithIBL > 0) {
console.log(`Avg EnvMap Intensity: ${sceneAnalysis.averageEnvMapIntensity.toFixed(2)}`);
console.log(`Avg Metalness: ${sceneAnalysis.averageMetalness.toFixed(2)}`);
console.log(`Avg Roughness: ${sceneAnalysis.averageRoughness.toFixed(2)}`);
console.group('Contribution Breakdown');
const breakdown = sceneAnalysis.contributionBreakdown;
if (breakdown.diffuseDominant > 0) {
console.log(`Diffuse Dominant: ${breakdown.diffuseDominant}`);
}
if (breakdown.specularDominant > 0) {
console.log(`Specular Dominant: ${breakdown.specularDominant}`);
}
if (breakdown.balanced > 0) {
console.log(`Balanced: ${breakdown.balanced}`);
}
if (breakdown.noIBL > 0) {
console.log(`No IBL: ${breakdown.noIBL}`);
}
console.groupEnd();
console.group('Material Details');
sceneAnalysis.materials.forEach(mat => {
if (mat.hasEnvMap) {
console.log(`${mat.name}: Diffuse=${mat.estimatedDiffuseContribution.toFixed(2)}, Specular=${mat.estimatedSpecularContribution.toFixed(2)}, Dominant=${mat.dominantContribution}`);
}
});
console.groupEnd();
}
console.groupEnd();
}
}
// Make class globally accessible
if (typeof window !== 'undefined') {
window.IBLContributionAnalyzer = IBLContributionAnalyzer;
}

View File

@@ -1291,6 +1291,7 @@
<script type="module" src="texture-inspector.js"></script>
<script type="module" src="material-complexity-analyzer.js"></script>
<script type="module" src="reference-materials.js"></script>
<script type="module" src="ibl-contribution-analyzer.js"></script>
<!-- Main application script as module -->
<script type="module" src="materialx.js"></script>

View File

@@ -55,6 +55,7 @@ import {
getReferencesByCategory,
getCategories
} from './reference-materials.js';
import { IBLContributionAnalyzer } from './ibl-contribution-analyzer.js';
// Embedded default OpenPBR scene (simple sphere with material)
const EMBEDDED_USDA_SCENE = `#usda 1.0
@@ -3284,6 +3285,87 @@ function setupGUI() {
referenceFolder.add(referenceParams, 'applyToAll').name('Apply to All Materials');
referenceFolder.close();
// IBL Contribution Analyzer
const iblFolder = gui.addFolder('IBL Contribution');
const iblParams = {
mode: 'full',
materialsWithIBL: 0,
avgIntensity: 0,
diffuseDominant: 0,
specularDominant: 0,
balanced: 0,
analyzeNow: function() {
if (!window.iblAnalyzer) {
window.iblAnalyzer = new IBLContributionAnalyzer(scene, renderer);
}
const results = window.iblAnalyzer.analyzeScene(scene);
// Update display values
iblParams.materialsWithIBL = results.materialsWithIBL;
iblParams.avgIntensity = parseFloat(results.averageEnvMapIntensity.toFixed(2));
iblParams.diffuseDominant = results.contributionBreakdown.diffuseDominant;
iblParams.specularDominant = results.contributionBreakdown.specularDominant;
iblParams.balanced = results.contributionBreakdown.balanced;
// Update GUI displays
iblFolder.controllers.forEach(c => c.updateDisplay());
// Log to console
window.iblAnalyzer.logResults(results);
updateStatus(`IBL Analysis: ${results.materialsWithIBL} materials with IBL`, 'success');
},
exportReport: function() {
if (!window.iblAnalyzer) {
window.iblAnalyzer = new IBLContributionAnalyzer(scene, renderer);
}
const results = window.iblAnalyzer.analyzeScene(scene);
const report = window.iblAnalyzer.generateReport(results);
// Download report
const blob = new Blob([report], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'ibl-analysis-report.md';
a.click();
URL.revokeObjectURL(url);
updateStatus('IBL analysis report exported', 'success');
},
reset: function() {
if (window.iblAnalyzer) {
window.iblAnalyzer.reset();
iblParams.mode = 'full';
iblFolder.controllers.forEach(c => c.updateDisplay());
updateStatus('IBL mode reset to full', 'success');
}
}
};
iblFolder.add(iblParams, 'mode', {
'Full IBL (Diffuse + Specular)': 'full',
'Diffuse Only': 'diffuse',
'Specular Only': 'specular',
'No IBL': 'none'
}).name('Visualization Mode').onChange(mode => {
if (!window.iblAnalyzer) {
window.iblAnalyzer = new IBLContributionAnalyzer(scene, renderer);
}
window.iblAnalyzer.setMode(mode);
updateStatus(`IBL mode: ${mode}`, 'success');
});
iblFolder.add(iblParams, 'analyzeNow').name('Analyze Scene');
iblFolder.add(iblParams, 'materialsWithIBL').name('Materials w/ IBL').listen().disable();
iblFolder.add(iblParams, 'avgIntensity').name('Avg Intensity').listen().disable();
iblFolder.add(iblParams, 'diffuseDominant').name('Diffuse Dominant').listen().disable();
iblFolder.add(iblParams, 'specularDominant').name('Specular Dominant').listen().disable();
iblFolder.add(iblParams, 'balanced').name('Balanced').listen().disable();
iblFolder.add(iblParams, 'exportReport').name('Export Report');
iblFolder.add(iblParams, 'reset').name('Reset to Original');
iblFolder.close();
// Split View Comparison System
const splitViewFolder = gui.addFolder('Split View Compare');
const splitViewParams = {
@@ -5628,6 +5710,7 @@ window.MaterialValidator = MaterialValidator;
window.SplitViewComparison = SplitViewComparison;
window.TextureInspector = TextureInspector;
window.MaterialComplexityAnalyzer = MaterialComplexityAnalyzer;
window.IBLContributionAnalyzer = IBLContributionAnalyzer;
window.REFERENCE_MATERIALS = REFERENCE_MATERIALS;
window.applyReferenceMaterial = applyReferenceMaterial;
window.getReferencesByCategory = getReferencesByCategory;