Add Interactive PBR Theory Guide with educational tooltips

- Created pbr-theory-guide.js with PBRTheoryGuide class
- Educational tooltips explaining PBR material properties

Content Database (11 topics):
  * Material Properties: Base Color, Metalness, Roughness, IOR
  * Advanced: Transmission, Clearcoat, Sheen
  * Textures: Normal Map, AO Map
  * Theory: Energy Conservation, Fresnel Effect

Each Topic Includes:
  - Title and description
  - Physical theory explanation
  - Typical value ranges
  - Practical tips (3-5 per topic)
  - Real-world examples with values
  - Common issues and mistakes

Example Content:
  - **Base Color**: Explains albedo, metal vs dielectric values, sRGB ranges
  - **Metalness**: Binary 0/1, colored reflections for metals
  - **Roughness**: Microfacet theory, mirror to rough spectrum
  - **IOR**: Fresnel F0 relationship, material-specific values
  - **Energy Conservation**: Cannot reflect more than received
  - **Fresnel**: Grazing angle reflections (fundamental physics)

UI Features:
  - Floating tooltip panel (bottom-left corner)
  - Topic dropdown (11 topics)
  - Enable/Disable toggle
  - Export Full Guide button (markdown format)
  - Styled with examples, tips, warnings

Display Information:
  - Description and theory
  - Typical ranges (categorized)
  - Tips (practical advice)
  - Examples (real materials with values)
  - Common issues (warnings in orange)

Use Cases:
  - Learn PBR fundamentals
  - Quick reference while authoring materials
  - Understand typical value ranges
  - Avoid common mistakes
  - Educational tool for students/artists

Priority 4 feature (1/5 complete)
This commit is contained in:
Syoyo Fujita
2025-11-21 03:40:17 +09:00
parent 7cfea02e46
commit a43a67e74c
3 changed files with 508 additions and 0 deletions

View File

@@ -1296,6 +1296,7 @@
<script type="module" src="mipmap-visualizer.js"></script>
<script type="module" src="pixel-inspector.js"></script>
<script type="module" src="material-preset.js"></script>
<script type="module" src="pbr-theory-guide.js"></script>
<!-- Main application script as module -->
<script type="module" src="materialx.js"></script>

View File

@@ -60,6 +60,7 @@ import { GBufferViewer } from './gbuffer-viewer.js';
import { MipMapVisualizer } from './mipmap-visualizer.js';
import { PixelInspector } from './pixel-inspector.js';
import { MaterialPresetManager } from './material-preset.js';
import { PBRTheoryGuide } from './pbr-theory-guide.js';
// Embedded default OpenPBR scene (simple sphere with material)
const EMBEDDED_USDA_SCENE = `#usda 1.0
@@ -3772,6 +3773,77 @@ function setupGUI() {
presetFolder.add(presetParams, 'viewLibrary').name('View Library');
presetFolder.close();
// PBR Theory Guide
const theoryGuideFolder = gui.addFolder('PBR Theory Guide');
const theoryGuideParams = {
enabled: false,
currentTopic: 'baseColor',
enable: function() {
if (!window.pbrTheoryGuide) {
window.pbrTheoryGuide = new PBRTheoryGuide();
}
window.pbrTheoryGuide.enable();
theoryGuideParams.enabled = true;
// Show initial topic
window.pbrTheoryGuide.showTooltip(theoryGuideParams.currentTopic);
updateStatus('PBR Theory Guide enabled (see bottom-left panel)', 'success');
},
disable: function() {
if (window.pbrTheoryGuide) {
window.pbrTheoryGuide.disable();
theoryGuideParams.enabled = false;
updateStatus('PBR Theory Guide disabled', 'success');
}
},
exportGuide: function() {
if (!window.pbrTheoryGuide) {
window.pbrTheoryGuide = new PBRTheoryGuide();
}
const guide = window.pbrTheoryGuide.generateGuide();
const blob = new Blob([guide], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'PBR_Theory_Guide.md';
a.click();
URL.revokeObjectURL(url);
updateStatus('PBR Theory Guide exported', 'success');
}
};
theoryGuideFolder.add(theoryGuideParams, 'enabled').name('Enable Guide').onChange(value => {
if (value) {
theoryGuideParams.enable();
} else {
theoryGuideParams.disable();
}
});
theoryGuideFolder.add(theoryGuideParams, 'currentTopic', {
'Base Color': 'baseColor',
'Metalness': 'metalness',
'Roughness': 'roughness',
'IOR (Index of Refraction)': 'ior',
'Transmission': 'transmission',
'Clearcoat': 'clearcoat',
'Sheen': 'sheen',
'Normal Map': 'normalMap',
'AO Map': 'aoMap',
'Energy Conservation': 'energyConservation',
'Fresnel Effect': 'fresnel'
}).name('Topic').onChange(topic => {
if (window.pbrTheoryGuide && theoryGuideParams.enabled) {
window.pbrTheoryGuide.showTooltip(topic);
}
});
theoryGuideFolder.add(theoryGuideParams, 'exportGuide').name('Export Full Guide');
theoryGuideFolder.close();
// Split View Comparison System
const splitViewFolder = gui.addFolder('Split View Compare');
const splitViewParams = {
@@ -6124,6 +6196,7 @@ window.GBufferViewer = GBufferViewer;
window.MipMapVisualizer = MipMapVisualizer;
window.PixelInspector = PixelInspector;
window.MaterialPresetManager = MaterialPresetManager;
window.PBRTheoryGuide = PBRTheoryGuide;
window.REFERENCE_MATERIALS = REFERENCE_MATERIALS;
window.applyReferenceMaterial = applyReferenceMaterial;
window.getReferencesByCategory = getReferencesByCategory;

434
web/js/pbr-theory-guide.js Normal file
View File

@@ -0,0 +1,434 @@
// Interactive PBR Theory Guide
// Educational tooltips and explanations for PBR material properties
export class PBRTheoryGuide {
constructor() {
this.tooltipPanel = null;
this.enabled = false;
// PBR Theory content database
this.content = {
// Material Properties
baseColor: {
title: 'Base Color (Albedo)',
description: 'The inherent color of the material surface, without lighting.',
theory: 'Base color represents the percentage of light reflected at each wavelength. For metals, this should be colored (e.g., gold = yellow). For non-metals (dielectrics), use desaturated colors.',
ranges: {
metals: 'RGB 180-255 (typically bright)',
nonMetals: 'RGB 50-240 (avoid pure white/black)',
typical: 'Most surfaces: 50-200 sRGB'
},
tips: [
'Pure white (255,255,255) is unrealistic for most materials',
'Pure black (0,0,0) doesn\'t exist in nature',
'Metals should have colored base colors',
'Dielectrics (plastics, wood) should be mostly desaturated'
],
examples: {
'Charcoal': 'RGB(50, 50, 50)',
'Concrete': 'RGB(128, 128, 128)',
'White Paint': 'RGB(240, 240, 240)',
'Gold': 'RGB(255, 195, 86)',
'Copper': 'RGB(244, 162, 137)'
}
},
metalness: {
title: 'Metalness',
description: 'Whether the material is metallic (1.0) or non-metallic/dielectric (0.0).',
theory: 'Metalness controls the fundamental behavior of light interaction. Metals have no diffuse reflection - all light is reflected specularly with colored reflections. Non-metals have both diffuse and specular reflection with achromatic (white) specular.',
ranges: {
metal: '1.0 (pure metal)',
nonMetal: '0.0 (dielectric)',
avoid: '0.1-0.9 (physically incorrect)'
},
tips: [
'Should be binary: 0.0 or 1.0 (no in-between)',
'Use texture maps for metal/non-metal transitions',
'Rusty metal: Use texture with metalness map',
'Painted metal: Metalness = 0.0 (paint is dielectric)'
],
examples: {
'Metals': 'Iron, Gold, Aluminum = 1.0',
'Non-metals': 'Wood, Plastic, Stone = 0.0',
'Painted surfaces': 'Always 0.0',
'Rusty metal': 'Use metalness texture'
}
},
roughness: {
title: 'Roughness',
description: 'Surface micro-facet roughness controlling specular blur.',
theory: 'Roughness represents microscopic surface irregularities. Lower values create sharp, mirror-like reflections. Higher values create blurry, diffuse-like reflections. Based on microfacet theory (GGX/Beckmann).',
ranges: {
mirror: '0.0-0.1 (polished surfaces)',
glossy: '0.2-0.5 (plastics, glossy paint)',
matte: '0.5-0.8 (matte plastic, unfinished wood)',
rough: '0.8-1.0 (concrete, rough stone)'
},
tips: [
'Most real materials: 0.2-0.8',
'Perfect mirror (0.0) is rare in nature',
'Combine with roughness maps for variation',
'Rough metals still show reflections (just blurry)'
],
examples: {
'Polished Chrome': '0.05',
'Glossy Plastic': '0.3',
'Matte Plastic': '0.7',
'Rough Concrete': '0.9'
}
},
ior: {
title: 'IOR (Index of Refraction)',
description: 'How much light bends when entering the material.',
theory: 'IOR controls Fresnel reflectance (F0) for dielectrics. Higher IOR = stronger reflections. Related to F0 by: F0 = ((IOR-1)/(IOR+1))². Only affects dielectrics (metalness=0).',
ranges: {
common: '1.4-1.6 (most materials)',
low: '1.0-1.3 (air, water)',
high: '1.6-3.0 (glass, diamond)'
},
tips: [
'Water: 1.33',
'Most plastics: 1.4-1.6',
'Glass: 1.5-1.9',
'Diamond: 2.42',
'Only affects non-metals'
],
examples: {
'Air': '1.0',
'Water': '1.333',
'Plastic': '1.5',
'Glass': '1.5',
'Diamond': '2.42'
}
},
transmission: {
title: 'Transmission',
description: 'How much light passes through the material (transparency).',
theory: 'Transmission enables realistic glass/liquid rendering. Combines with IOR for proper refraction. Very expensive - requires extra render passes for refraction.',
ranges: {
opaque: '0.0 (no transmission)',
translucent: '0.3-0.7 (frosted glass)',
transparent: '0.9-1.0 (clear glass, water)'
},
tips: [
'Very expensive to render (screen-space refraction)',
'Use with IOR for realistic glass',
'Combine with roughness for frosted glass',
'Set transparent=true for proper blending'
],
examples: {
'Clear Glass': 'Transmission: 1.0, IOR: 1.5',
'Frosted Glass': 'Transmission: 0.8, Roughness: 0.3',
'Tinted Glass': 'Transmission: 0.9, BaseColor: tint'
}
},
clearcoat: {
title: 'Clearcoat',
description: 'Extra specular layer on top of base material (car paint, varnish).',
theory: 'Clearcoat adds a second specular BRDF layer with its own roughness. Common for automotive paint, varnished wood, and layered materials. Based on multi-layer BRDF model.',
ranges: {
none: '0.0 (no clearcoat)',
subtle: '0.3-0.6 (slight shine)',
strong: '0.8-1.0 (car paint)'
},
tips: [
'Car paint: Clearcoat=1.0, ClearcoatRoughness=0.1',
'Varnished wood: Clearcoat=0.5-0.8',
'Use separate roughness for clearcoat layer',
'Adds rendering cost (extra BRDF evaluation)'
],
examples: {
'Car Paint': 'Clearcoat: 1.0, Roughness: 0.05',
'Varnished Wood': 'Clearcoat: 0.6, Roughness: 0.2',
'Glossy Plastic': 'Clearcoat: 0.3'
}
},
sheen: {
title: 'Sheen',
description: 'Soft specular reflection at grazing angles (fabric, velvet).',
theory: 'Sheen adds a soft, colored specular lobe near 90° (grazing angles). Designed for cloth and fabric rendering. Based on Charlie sheen BRDF model.',
ranges: {
none: '0.0 (no sheen)',
subtle: '0.3-0.6 (silk)',
strong: '0.8-1.0 (velvet)'
},
tips: [
'Best for fabric materials',
'Use sheenColor for colored fabrics',
'Combine with high roughness (0.8-1.0)',
'Makes edges glow softly'
],
examples: {
'Velvet': 'Sheen: 1.0, SheenColor: fabric color',
'Silk': 'Sheen: 0.6, Roughness: 0.4',
'Cotton': 'Sheen: 0.3, Roughness: 0.8'
}
},
// Textures
normalMap: {
title: 'Normal Map',
description: 'Texture encoding surface normal variations for detail.',
theory: 'Normal maps add geometric detail without extra polygons. Encode XYZ normals in RGB channels. Blue-dominant (Z-up). Must be in linear color space (NOT sRGB).',
tips: [
'Must use linear color space (not sRGB)',
'Blue channel should dominate (Z-up)',
'Flat surface = RGB(128, 128, 255) in tangent space',
'DirectX vs OpenGL Y-axis can be flipped',
'Use normalScale to control intensity'
],
common_issues: [
'Using sRGB encoding (causes incorrect lighting)',
'Flipped Y-channel (DX vs GL)',
'Too strong (unrealistic bumps)',
'Missing tangent vectors'
]
},
aoMap: {
title: 'Ambient Occlusion Map',
description: 'Pre-baked shadows in surface crevices.',
theory: 'AO approximates how much ambient light reaches each point. Dark in crevices, bright on exposed surfaces. Multiplied with final color. Should be in linear space.',
tips: [
'Use grayscale texture (R=G=B)',
'White = fully exposed, Black = fully occluded',
'Linear color space',
'aoMapIntensity controls strength (default 1.0)',
'Complements real-time lighting (not replacement)'
],
common_issues: [
'Too dark (losing detail)',
'sRGB encoding (incorrect gamma)',
'Applied to emissive surfaces (wrong)'
]
},
// Advanced
energyConservation: {
title: 'Energy Conservation',
description: 'Material cannot reflect more light than it receives.',
theory: 'Physically correct materials obey energy conservation: diffuse + specular ≤ 1.0. Metals have no diffuse. Dielectrics balance diffuse and specular via Fresnel.',
rules: [
'Metals: baseColor controls specular color, no diffuse',
'Dielectrics: baseColor = diffuse, specular = white (F0 ~0.04)',
'Base color should never be pure white for metals',
'Total reflected light cannot exceed incident light'
],
violations: [
'Bright base color + high metalness + low roughness = too bright',
'Using colored specular on non-metals',
'Emissive values too high'
]
},
fresnel: {
title: 'Fresnel Effect',
description: 'Reflections stronger at grazing angles.',
theory: 'The Fresnel effect describes how reflectance increases at glancing angles. All materials exhibit this. F0 (reflectance at normal incidence) varies by material: ~0.04 for dielectrics, 0.5-1.0 for metals.',
observations: [
'Look at floor at your feet: dark (diffuse)',
'Look at floor far away: bright (reflective)',
'Water edge: transparent. Water horizon: mirror.',
'All materials show this (fundamental physics)'
],
tips: [
'Cannot be disabled (part of BRDF)',
'Controlled by IOR for dielectrics',
'Controlled by baseColor for metals',
'Makes materials look realistic'
]
}
};
}
// Enable theory guide
enable() {
this.enabled = true;
this.createTooltipPanel();
}
// Disable theory guide
disable() {
this.enabled = false;
this.destroyTooltipPanel();
}
// Create tooltip panel
createTooltipPanel() {
this.tooltipPanel = document.createElement('div');
this.tooltipPanel.id = 'pbr-theory-tooltip';
this.tooltipPanel.style.cssText = `
position: fixed;
bottom: 10px;
left: 10px;
max-width: 400px;
background: rgba(0, 0, 0, 0.95);
border: 2px solid #4CAF50;
border-radius: 8px;
padding: 15px;
color: #fff;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 13px;
line-height: 1.6;
z-index: 10000;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
display: none;
`;
document.body.appendChild(this.tooltipPanel);
}
// Destroy tooltip panel
destroyTooltipPanel() {
if (this.tooltipPanel && this.tooltipPanel.parentElement) {
this.tooltipPanel.parentElement.removeChild(this.tooltipPanel);
}
this.tooltipPanel = null;
}
// Show tooltip for a property
showTooltip(property) {
if (!this.enabled || !this.tooltipPanel) return;
const content = this.content[property];
if (!content) {
this.hideTooltip();
return;
}
let html = `<h3 style="margin: 0 0 10px 0; color: #4CAF50; font-size: 16px;">${content.title}</h3>`;
if (content.description) {
html += `<p style="margin: 0 0 10px 0; font-weight: bold;">${content.description}</p>`;
}
if (content.theory) {
html += `<p style="margin: 0 0 10px 0; font-style: italic; color: #aaa;">${content.theory}</p>`;
}
if (content.ranges) {
html += `<div style="margin: 10px 0;"><strong>Typical Ranges:</strong><ul style="margin: 5px 0; padding-left: 20px;">`;
Object.keys(content.ranges).forEach(key => {
html += `<li><strong>${key}:</strong> ${content.ranges[key]}</li>`;
});
html += `</ul></div>`;
}
if (content.tips) {
html += `<div style="margin: 10px 0;"><strong>Tips:</strong><ul style="margin: 5px 0; padding-left: 20px;">`;
content.tips.forEach(tip => {
html += `<li>${tip}</li>`;
});
html += `</ul></div>`;
}
if (content.examples) {
html += `<div style="margin: 10px 0;"><strong>Examples:</strong><ul style="margin: 5px 0; padding-left: 20px;">`;
Object.keys(content.examples).forEach(key => {
html += `<li><strong>${key}:</strong> ${content.examples[key]}</li>`;
});
html += `</ul></div>`;
}
if (content.rules) {
html += `<div style="margin: 10px 0;"><strong>Rules:</strong><ul style="margin: 5px 0; padding-left: 20px;">`;
content.rules.forEach(rule => {
html += `<li>${rule}</li>`;
});
html += `</ul></div>`;
}
if (content.observations) {
html += `<div style="margin: 10px 0;"><strong>Observations:</strong><ul style="margin: 5px 0; padding-left: 20px;">`;
content.observations.forEach(obs => {
html += `<li>${obs}</li>`;
});
html += `</ul></div>`;
}
if (content.common_issues) {
html += `<div style="margin: 10px 0;"><strong>⚠️ Common Issues:</strong><ul style="margin: 5px 0; padding-left: 20px; color: #ff9800;">`;
content.common_issues.forEach(issue => {
html += `<li>${issue}</li>`;
});
html += `</ul></div>`;
}
this.tooltipPanel.innerHTML = html;
this.tooltipPanel.style.display = 'block';
}
// Hide tooltip
hideTooltip() {
if (this.tooltipPanel) {
this.tooltipPanel.style.display = 'none';
}
}
// Get all available topics
getTopics() {
return Object.keys(this.content).map(key => ({
id: key,
title: this.content[key].title
}));
}
// Generate full guide as markdown
generateGuide() {
let guide = '# Interactive PBR Theory Guide\n\n';
guide += 'Complete reference for physically-based rendering material properties.\n\n';
guide += '---\n\n';
Object.keys(this.content).forEach(key => {
const content = this.content[key];
guide += `## ${content.title}\n\n`;
if (content.description) {
guide += `**Description**: ${content.description}\n\n`;
}
if (content.theory) {
guide += `**Theory**: ${content.theory}\n\n`;
}
if (content.ranges) {
guide += '### Typical Ranges\n\n';
Object.keys(content.ranges).forEach(rangeKey => {
guide += `- **${rangeKey}**: ${content.ranges[rangeKey]}\n`;
});
guide += '\n';
}
if (content.tips) {
guide += '### Tips\n\n';
content.tips.forEach(tip => {
guide += `- ${tip}\n`;
});
guide += '\n';
}
if (content.examples) {
guide += '### Examples\n\n';
Object.keys(content.examples).forEach(exKey => {
guide += `- **${exKey}**: ${content.examples[exKey]}\n`;
});
guide += '\n';
}
guide += '---\n\n';
});
return guide;
}
}
// Make class globally accessible
if (typeof window !== 'undefined') {
window.PBRTheoryGuide = PBRTheoryGuide;
}