Add Three.js UsdLux demo with tone mapping and spectral support

- Add usdlux.html/js: Interactive Three.js demo for USD light visualization
  - Map USD light types to Three.js lights (Point, Spot, Directional, RectArea, Hemisphere)
  - Visual helpers for finite lights (spheres, disks, arrows)
  - Central sphere with MeshPhysicalMaterial for lighting preview
  - Embedded USDA scenes for quick testing
  - File dialog and drag-n-drop USD loading

- Add tone mapping options: Raw, Reinhard, ACES 1.3, ACES 2.0, AgX, Neutral
  - Custom ACES 2.0 shader with AP0/AP1 matrices, gamut mapping, highlight desaturation
  - Exposure (-3 to +3 EV) and gamma controls

- Add spectral rendering support:
  - CIE 1931 2-degree standard observer color matching functions
  - SPD to XYZ to sRGB conversion with proper gamut mapping
  - Spectral curve canvas visualization with rainbow gradient
  - Color modes: RGB (USD color), Spectral (SPD to RGB), Monochrome (single wavelength)
  - Demo SPD presets: D65, Illuminant A, LED spectra, sodium lamp
  - Blackbody spectrum generator (1000K-10000K)

- Fix RenderLight enum naming: lightType -> type, LightType -> Type, Directional -> Distant
- Improve dump-usdlux-cli.js with spectral display, transform formatting, XML output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Syoyo Fujita
2025-12-01 03:11:12 +09:00
parent e901a5029c
commit eace3f2080
6 changed files with 3062 additions and 111 deletions

View File

@@ -369,28 +369,34 @@ nonstd::expected<std::string, std::string> serializeLight(
// Light type
json << "\"type\": \"";
switch (light.lightType) {
case RenderLight::LightType::Point:
switch (light.type) {
case RenderLight::Type::Point:
json << "Point";
break;
case RenderLight::LightType::Directional:
json << "Directional";
case RenderLight::Type::Sphere:
json << "Sphere";
break;
case RenderLight::LightType::Rect:
case RenderLight::Type::Distant:
json << "Distant";
break;
case RenderLight::Type::Rect:
json << "Rect";
break;
case RenderLight::LightType::Disk:
case RenderLight::Type::Disk:
json << "Disk";
break;
case RenderLight::LightType::Cylinder:
case RenderLight::Type::Cylinder:
json << "Cylinder";
break;
case RenderLight::LightType::Dome:
case RenderLight::Type::Dome:
json << "Dome";
break;
case RenderLight::LightType::Geometry:
case RenderLight::Type::Geometry:
json << "Geometry";
break;
case RenderLight::Type::Portal:
json << "Portal";
break;
}
json << "\",";
@@ -429,18 +435,19 @@ nonstd::expected<std::string, std::string> serializeLight(
// Type-specific properties
json << "\"properties\": {";
switch (light.lightType) {
case RenderLight::LightType::Point:
case RenderLight::LightType::Disk:
switch (light.type) {
case RenderLight::Type::Point:
case RenderLight::Type::Sphere:
case RenderLight::Type::Disk:
json << "\"radius\": " << light.radius;
break;
case RenderLight::LightType::Cylinder:
case RenderLight::Type::Cylinder:
json << "\"radius\": " << light.radius << ",";
json << "\"length\": " << light.length;
break;
case RenderLight::LightType::Rect:
case RenderLight::Type::Rect:
json << "\"width\": " << light.width << ",";
json << "\"height\": " << light.height;
if (!light.textureFile.empty()) {
@@ -448,11 +455,11 @@ nonstd::expected<std::string, std::string> serializeLight(
}
break;
case RenderLight::LightType::Directional:
case RenderLight::Type::Distant:
json << "\"angle\": " << light.angle;
break;
case RenderLight::LightType::Dome:
case RenderLight::Type::Dome:
json << "\"textureFormat\": \"";
switch (light.domeTextureFormat) {
case RenderLight::DomeTextureFormat::Automatic:
@@ -475,7 +482,7 @@ nonstd::expected<std::string, std::string> serializeLight(
}
break;
case RenderLight::LightType::Geometry:
case RenderLight::Type::Geometry:
json << "\"geometryMeshId\": " << light.geometry_mesh_id << ",";
json << "\"materialSyncMode\": \"" << light.material_sync_mode << "\"";
@@ -487,6 +494,10 @@ nonstd::expected<std::string, std::string> serializeLight(
json << ",\"meshPath\": \"" << mesh.abs_path << "\"";
}
break;
case RenderLight::Type::Portal:
// Portal lights don't have special properties
break;
}
json << "}"; // Close properties

View File

@@ -8712,7 +8712,7 @@ bool RenderSceneConverter::ConvertSphereLight(
RenderLight rlight;
rlight.name = light.name;
rlight.abs_path = light_abs_path.full_path_name();
rlight.lightType = RenderLight::LightType::Point;
rlight.type = RenderLight::Type::Sphere;
// Extract common properties
if (!ExtractCommonLightProperties(env, light, &rlight)) {
@@ -8749,7 +8749,7 @@ bool RenderSceneConverter::ConvertDistantLight(
RenderLight rlight;
rlight.name = light.name;
rlight.abs_path = light_abs_path.full_path_name();
rlight.lightType = RenderLight::LightType::Directional;
rlight.type = RenderLight::Type::Distant;
// Extract common properties
if (!ExtractCommonLightProperties(env, light, &rlight)) {
@@ -8781,7 +8781,7 @@ bool RenderSceneConverter::ConvertDomeLight(
RenderLight rlight;
rlight.name = light.name;
rlight.abs_path = light_abs_path.full_path_name();
rlight.lightType = RenderLight::LightType::Dome;
rlight.type = RenderLight::Type::Dome;
// Extract common properties
if (!ExtractCommonLightProperties(env, light, &rlight)) {
@@ -8847,7 +8847,7 @@ bool RenderSceneConverter::ConvertRectLight(
RenderLight rlight;
rlight.name = light.name;
rlight.abs_path = light_abs_path.full_path_name();
rlight.lightType = RenderLight::LightType::Rect;
rlight.type = RenderLight::Type::Rect;
// Extract common properties
if (!ExtractCommonLightProperties(env, light, &rlight)) {
@@ -8900,7 +8900,7 @@ bool RenderSceneConverter::ConvertDiskLight(
RenderLight rlight;
rlight.name = light.name;
rlight.abs_path = light_abs_path.full_path_name();
rlight.lightType = RenderLight::LightType::Disk;
rlight.type = RenderLight::Type::Disk;
// Extract common properties
if (!ExtractCommonLightProperties(env, light, &rlight)) {
@@ -8932,7 +8932,7 @@ bool RenderSceneConverter::ConvertCylinderLight(
RenderLight rlight;
rlight.name = light.name;
rlight.abs_path = light_abs_path.full_path_name();
rlight.lightType = RenderLight::LightType::Cylinder;
rlight.type = RenderLight::Type::Cylinder;
// Extract common properties
if (!ExtractCommonLightProperties(env, light, &rlight)) {
@@ -8972,7 +8972,7 @@ bool RenderSceneConverter::ConvertGeometryLight(
RenderLight rlight;
rlight.name = light.name;
rlight.abs_path = light_abs_path.full_path_name();
rlight.lightType = RenderLight::LightType::Geometry;
rlight.type = RenderLight::Type::Geometry;
// Extract common properties
if (!ExtractCommonLightProperties(env, light, &rlight)) {

View File

@@ -1025,8 +1025,9 @@ json ThreeJSSceneExporter::ConvertLight(const RenderLight& light) {
light_json["intensity"] = effective_intensity;
// Map RenderLight types to Three.js light types
switch (light.lightType) {
case RenderLight::LightType::Point:
switch (light.type) {
case RenderLight::Type::Point:
case RenderLight::Type::Sphere:
light_json["type"] = "PointLight";
// Three.js PointLight doesn't have a physical radius, but we can store it in userData
if (light.radius > 0.0f) {
@@ -1038,7 +1039,7 @@ json ThreeJSSceneExporter::ConvertLight(const RenderLight& light) {
light_json["decay"] = 2.0f;
break;
case RenderLight::LightType::Directional:
case RenderLight::Type::Distant:
light_json["type"] = "DirectionalLight";
// Directional lights have uniform intensity, no attenuation
// Store angle for disk shape if specified
@@ -1050,7 +1051,7 @@ json ThreeJSSceneExporter::ConvertLight(const RenderLight& light) {
}
break;
case RenderLight::LightType::Rect:
case RenderLight::Type::Rect:
// Three.js has RectAreaLight (requires RectAreaLightUniformsLib)
light_json["type"] = "RectAreaLight";
light_json["width"] = light.width;
@@ -1065,7 +1066,7 @@ json ThreeJSSceneExporter::ConvertLight(const RenderLight& light) {
}
break;
case RenderLight::LightType::Disk:
case RenderLight::Type::Disk:
// Three.js doesn't have a disk light, use PointLight as approximation
light_json["type"] = "PointLight";
light_json["distance"] = 0.0f;
@@ -1077,7 +1078,7 @@ json ThreeJSSceneExporter::ConvertLight(const RenderLight& light) {
};
break;
case RenderLight::LightType::Cylinder:
case RenderLight::Type::Cylinder:
// Cylinder lights with shaping are like spotlights
if (light.shapingConeAngle > 0.0f) {
light_json["type"] = "SpotLight";
@@ -1112,7 +1113,7 @@ json ThreeJSSceneExporter::ConvertLight(const RenderLight& light) {
}
break;
case RenderLight::LightType::Dome:
case RenderLight::Type::Dome:
// Dome lights are environment maps, closest is HemisphereLight or PMREMGenerator
light_json["type"] = "HemisphereLight";
// HemisphereLight has groundColor (opposite hemisphere)
@@ -1146,7 +1147,7 @@ json ThreeJSSceneExporter::ConvertLight(const RenderLight& light) {
}
break;
case RenderLight::LightType::Geometry:
case RenderLight::Type::Geometry:
// Geometry lights are meshes with emissive materials
light_json["type"] = "MeshEmissive";
light_json["geometry_mesh_id"] = light.geometry_mesh_id;
@@ -1158,6 +1159,11 @@ json ThreeJSSceneExporter::ConvertLight(const RenderLight& light) {
light_json["userData"]["material_sync_mode"] = light.material_sync_mode;
}
break;
case RenderLight::Type::Portal:
// Portal lights are special lights for portals
light_json["type"] = "PortalLight";
break;
}
// Shadow properties (if shadow is enabled)
@@ -1192,7 +1198,7 @@ json ThreeJSSceneExporter::ConvertLight(const RenderLight& light) {
}
// Shaping properties for lights that support them (but aren't spotlights)
if (light.lightType != RenderLight::LightType::Cylinder &&
if (light.type != RenderLight::Type::Cylinder &&
(light.shapingConeAngle > 0.0f || light.shapingFocus != 0.0f || !light.shapingIesFile.empty())) {
light_json["userData"]["shaping"] = {
{"focus", light.shapingFocus},

View File

@@ -12,13 +12,18 @@ function parseArgs() {
const args = process.argv.slice(2);
const options = {
inputFile: null,
format: 'json', // 'json', 'yaml', or 'summary'
format: 'json', // 'json', 'yaml', 'summary', or 'xml'
outputFile: null,
lightId: null, // null means all lights
pretty: true,
verbose: false,
showMeshes: false, // Show mesh light geometries
showMaterials: false // Show material info for mesh lights
showMaterials: false, // Show material info for mesh lights
showTransform: false, // Show transform matrices
showSpectral: false, // Show spectral emission data
showAll: false, // Show all optional data
serialized: false, // Use serialized format from getLightWithFormat
colorMode: 'rgb' // 'rgb', 'hex', or 'kelvin'
};
for (let i = 0; i < args.length; i++) {
@@ -29,8 +34,8 @@ function parseArgs() {
process.exit(0);
} else if (arg === '-f' || arg === '--format') {
options.format = args[++i];
if (!['json', 'yaml', 'summary'].includes(options.format)) {
console.error('Error: Format must be "json", "yaml", or "summary"');
if (!['json', 'yaml', 'summary', 'xml'].includes(options.format)) {
console.error('Error: Format must be "json", "yaml", "summary", or "xml"');
process.exit(1);
}
} else if (arg === '-o' || arg === '--output') {
@@ -45,6 +50,24 @@ function parseArgs() {
options.showMeshes = true;
} else if (arg === '--show-materials') {
options.showMaterials = true;
} else if (arg === '--show-transform' || arg === '-t') {
options.showTransform = true;
} else if (arg === '--show-spectral' || arg === '-s') {
options.showSpectral = true;
} else if (arg === '--all' || arg === '-a') {
options.showAll = true;
options.showMeshes = true;
options.showMaterials = true;
options.showTransform = true;
options.showSpectral = true;
} else if (arg === '--serialized') {
options.serialized = true;
} else if (arg === '--color-mode') {
options.colorMode = args[++i];
if (!['rgb', 'hex', 'kelvin'].includes(options.colorMode)) {
console.error('Error: Color mode must be "rgb", "hex", or "kelvin"');
process.exit(1);
}
} else if (!options.inputFile) {
options.inputFile = arg;
}
@@ -56,6 +79,11 @@ function parseArgs() {
process.exit(1);
}
// XML format requires serialized mode
if (options.format === 'xml') {
options.serialized = true;
}
return options;
}
@@ -69,15 +97,31 @@ Arguments:
<usd-file> USD/USDA/USDC/USDZ file to load
Options:
-f, --format <format> Output format: 'json', 'yaml', or 'summary' (default: json)
-f, --format <format> Output format: 'json', 'yaml', 'summary', or 'xml' (default: json)
-o, --output <file> Write output to file instead of stdout
-l, --light <id> Dump only specific light by ID (default: all)
--no-pretty Disable pretty-printing for JSON/YAML
-v, --verbose Enable verbose logging
-a, --all Show all optional data (meshes, materials, transform, spectral)
-t, --show-transform Include transform matrices in output
-s, --show-spectral Include spectral emission data (LTE SpectralAPI)
--show-meshes Include mesh geometry data for mesh lights
--show-materials Include material data for mesh lights
-v, --verbose Enable verbose logging
--serialized Use serialized format from WASM (required for XML)
--color-mode <mode> Color display mode: 'rgb', 'hex', or 'kelvin' (default: rgb)
-h, --help Show this help message
Light Types Supported:
- Point Small spherical light source
- Sphere Spherical area light (SphereLight)
- Disk Circular area light (DiskLight)
- Rect Rectangular area light (RectLight)
- Cylinder Cylindrical area light (CylinderLight)
- Distant Directional light (DistantLight/sun)
- Dome Environment/IBL light (DomeLight)
- Geometry Mesh emissive light (GeometryLight)
- Portal Light portal
Examples:
# Dump all lights as JSON
node dump-usdlux-cli.js tests/feat/lux/04_complete_scene.usda
@@ -85,12 +129,18 @@ Examples:
# Dump as human-readable summary
node dump-usdlux-cli.js tests/feat/lux/06_mesh_lights.usda -f summary
# Dump specific light as YAML
node dump-usdlux-cli.js tests/feat/lux/04_complete_scene.usda -f yaml -l 0
# Dump specific light as YAML with all details
node dump-usdlux-cli.js tests/feat/lux/04_complete_scene.usda -f yaml -l 0 --all
# Save output to file with verbose logging
node dump-usdlux-cli.js models/scene.usdc -o lights.json -v
# Dump with spectral emission data
node dump-usdlux-cli.js tests/usda/usdlux_advanced_features.usda --show-spectral
# Export as XML (requires --serialized)
node dump-usdlux-cli.js tests/feat/lux/04_complete_scene.usda -f xml -o lights.xml
# Dump mesh lights with geometry and material info
node dump-usdlux-cli.js tests/feat/lux/06_mesh_lights.usda --show-meshes --show-materials
`);
@@ -109,104 +159,295 @@ function loadFile(filename) {
}
}
function formatSummary(lightsData, verbose = false) {
// Convert RGB to hex color
function rgbToHex(r, g, b) {
const toHex = (c) => {
const hex = Math.round(Math.max(0, Math.min(255, c * 255))).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
// Estimate color temperature from RGB (simplified Kelvin approximation)
function rgbToKelvin(r, g, b) {
// Simple heuristic based on red/blue ratio
if (b === 0) return '>10000K (warm)';
const ratio = r / b;
if (ratio > 2) return '~2700K (warm white)';
if (ratio > 1.5) return '~3500K (neutral)';
if (ratio > 1) return '~5000K (daylight)';
if (ratio > 0.7) return '~6500K (cool daylight)';
return '~10000K+ (blue sky)';
}
// Format color based on mode
function formatColor(color, mode = 'rgb') {
if (!color || color.length < 3) return 'N/A';
const [r, g, b] = color;
switch (mode) {
case 'hex':
return rgbToHex(r, g, b);
case 'kelvin':
return rgbToKelvin(r, g, b);
case 'rgb':
default:
return `RGB(${r.toFixed(3)}, ${g.toFixed(3)}, ${b.toFixed(3)})`;
}
}
// Format a 4x4 matrix for display
function formatMatrix(transform, indent = '│ ') {
if (!transform || transform.length !== 16) return `${indent}[Invalid matrix]\n`;
let output = '';
for (let row = 0; row < 4; row++) {
const values = [];
for (let col = 0; col < 4; col++) {
values.push(transform[row * 4 + col].toFixed(4).padStart(10));
}
output += `${indent}[${values.join(', ')}]\n`;
}
return output;
}
// Format spectral emission data
function formatSpectralEmission(spectral, indent = '│ ') {
if (!spectral) return '';
let output = `${indent}Interpolation: ${spectral.interpolation || 'linear'}\n`;
output += `${indent}Unit: ${spectral.unit || 'nanometers'}\n`;
if (spectral.preset && spectral.preset !== 'none') {
output += `${indent}Preset: ${spectral.preset.toUpperCase()}\n`;
}
if (spectral.samples && spectral.samples.length > 0) {
output += `${indent}Samples (${spectral.samples.length} points):\n`;
const maxSamples = 10;
const samples = spectral.samples.slice(0, maxSamples);
for (const [wavelength, value] of samples) {
output += `${indent} ${wavelength.toFixed(1)}nm: ${value.toFixed(6)}\n`;
}
if (spectral.samples.length > maxSamples) {
output += `${indent} ... and ${spectral.samples.length - maxSamples} more\n`;
}
}
return output;
}
function formatSummary(lightsData, options = {}) {
const { verbose = false, showTransform = false, showSpectral = false, colorMode = 'rgb' } = options;
let output = '';
output += `╔════════════════════════════════════════════════════════╗\n`;
output += `║ UsdLux Lights Summary ║\n`;
output += `╚════════════════════════════════════════════════════════╝\n\n`;
output += `Total Lights: ${lightsData.length}\n\n`;
output += `\n`;
output += ` ╔════════════════════════════════════════════════════════════════╗\n`;
output += ` ║ UsdLux Lights Summary ║\n`;
output += ` ╚════════════════════════════════════════════════════════════════╝\n\n`;
// Group lights by type
const byType = {};
for (const light of lightsData) {
const type = light.type || 'unknown';
if (!byType[type]) byType[type] = [];
byType[type].push(light);
}
output += ` Total Lights: ${lightsData.length}\n`;
output += ` By Type: ${Object.entries(byType).map(([t, l]) => `${t}(${l.length})`).join(', ')}\n`;
for (let i = 0; i < lightsData.length; i++) {
const light = lightsData[i];
const lightType = light.type || 'unknown';
output += `\n─ Light ${i}: ${light.name || 'Unnamed'} ──────────────\n`;
output += `│ Type: ${light.lightType}\n`;
output += `│ Path: ${light.abs_path || 'N/A'}\n`;
output += `\n ┌─── Light ${i}: ${light.name || 'Unnamed'} ${'─'.repeat(Math.max(0, 40 - (light.name || 'Unnamed').length))}\n`;
output += ` │ Type: ${lightType}\n`;
output += ` │ Path: ${light.absPath || light.abs_path || 'N/A'}\n`;
if (light.color) {
const r = light.color[0].toFixed(3);
const g = light.color[1].toFixed(3);
const b = light.color[2].toFixed(3);
output += `│ Color: RGB(${r}, ${g}, ${b})\n`;
if (light.displayName) {
output += ` │ Display Name: ${light.displayName}\n`;
}
// Color
if (light.color) {
output += ` │ Color: ${formatColor(light.color, colorMode)}\n`;
}
// Intensity & Exposure
if (light.intensity !== undefined) {
output += `│ Intensity: ${light.intensity.toFixed(2)}\n`;
output += ` │ Intensity: ${light.intensity.toFixed(3)}\n`;
}
if (light.exposure !== undefined && light.exposure !== 0) {
output += `│ Exposure: ${light.exposure.toFixed(2)} EV\n`;
output += ` │ Exposure: ${light.exposure.toFixed(2)} EV\n`;
const effectiveIntensity = light.intensity * Math.pow(2, light.exposure);
output += `│ Effective Intensity: ${effectiveIntensity.toFixed(2)}\n`;
output += ` │ Effective Intensity: ${effectiveIntensity.toFixed(3)}\n`;
}
// Color temperature
if (light.enableColorTemperature && light.colorTemperature) {
output += `│ Color Temperature: ${light.colorTemperature}K\n`;
output += ` │ Color Temperature: ${light.colorTemperature.toFixed(0)}K`;
if (light.colorTemperature < 3000) output += ' (warm)';
else if (light.colorTemperature < 5000) output += ' (neutral)';
else if (light.colorTemperature < 7000) output += ' (daylight)';
else output += ' (cool)';
output += '\n';
}
// Diffuse/Specular multipliers
if (verbose) {
if (light.diffuse !== undefined && light.diffuse !== 1.0) {
output += ` │ Diffuse Multiplier: ${light.diffuse.toFixed(3)}\n`;
}
if (light.specular !== undefined && light.specular !== 1.0) {
output += ` │ Specular Multiplier: ${light.specular.toFixed(3)}\n`;
}
if (light.normalize) {
output += ` │ Normalize: true (power normalized by area)\n`;
}
}
// Type-specific properties
if (light.lightType === 'Point' || light.lightType === 'Sphere') {
output += `\n`;
output += ` │ ── Type-Specific Properties ──\n`;
if (lightType === 'point' || lightType === 'sphere') {
if (light.radius !== undefined && light.radius > 0) {
output += `│ Radius: ${light.radius.toFixed(3)}\n`;
output += ` │ Radius: ${light.radius.toFixed(4)}\n`;
}
} else if (light.lightType === 'Directional') {
} else if (lightType === 'distant') {
if (light.angle !== undefined && light.angle > 0) {
output += `│ Angle: ${light.angle.toFixed(2)}°\n`;
output += ` │ Angular Diameter: ${light.angle.toFixed(2)}°\n`;
}
} else if (light.lightType === 'Rect') {
} else if (lightType === 'rect') {
if (light.width !== undefined && light.height !== undefined) {
output += `│ Dimensions: ${light.width.toFixed(2)} × ${light.height.toFixed(2)}\n`;
output += ` │ Dimensions: ${light.width.toFixed(3)} × ${light.height.toFixed(3)}\n`;
output += ` │ Area: ${(light.width * light.height).toFixed(4)} sq units\n`;
}
if (light.textureFile) {
output += `│ Texture: ${light.textureFile}\n`;
output += ` │ Texture: ${light.textureFile}\n`;
}
} else if (light.lightType === 'Disk') {
} else if (lightType === 'disk') {
if (light.radius !== undefined) {
output += `│ Radius: ${light.radius.toFixed(3)}\n`;
output += ` │ Radius: ${light.radius.toFixed(4)}\n`;
output += ` │ Area: ${(Math.PI * light.radius * light.radius).toFixed(4)} sq units\n`;
}
} else if (light.lightType === 'Cylinder') {
} else if (lightType === 'cylinder') {
if (light.radius !== undefined && light.length !== undefined) {
output += `│ Radius: ${light.radius.toFixed(3)}, Length: ${light.length.toFixed(3)}\n`;
output += ` │ Radius: ${light.radius.toFixed(4)}\n`;
output += ` │ Length: ${light.length.toFixed(4)}\n`;
output += ` │ Surface Area: ${(2 * Math.PI * light.radius * light.length).toFixed(4)} sq units\n`;
}
if (light.shapingConeAngle !== undefined && light.shapingConeAngle > 0) {
output += `│ Shaping Cone Angle: ${light.shapingConeAngle.toFixed(2)}°\n`;
output += `│ Cone Softness: ${(light.shapingConeSoftness || 0).toFixed(2)}\n`;
}
} else if (light.lightType === 'Dome') {
} else if (lightType === 'dome') {
if (light.textureFile) {
output += `│ Texture: ${light.textureFile}\n`;
output += ` │ Environment Map: ${light.textureFile}\n`;
}
if (light.domeTextureFormat) {
output += `│ Texture Format: ${light.domeTextureFormat}\n`;
output += ` │ Texture Format: ${light.domeTextureFormat}\n`;
}
} else if (light.lightType === 'Geometry') {
output += `│ Mesh ID: ${light.geometry_mesh_id}\n`;
if (light.geometry_mesh_name) {
output += `│ Mesh Name: ${light.geometry_mesh_name}\n`;
if (light.guideRadius !== undefined) {
output += ` │ Guide Radius: ${light.guideRadius.toExponential(2)}\n`;
}
if (light.material_sync_mode) {
output += `│ Material Sync Mode: ${light.material_sync_mode}\n`;
if (light.envmapTextureId !== undefined && light.envmapTextureId >= 0) {
output += ` │ Environment Texture ID: ${light.envmapTextureId}\n`;
}
} else if (lightType === 'geometry') {
output += ` │ Geometry Mesh ID: ${light.geometryMeshId ?? light.geometry_mesh_id ?? 'N/A'}\n`;
if (light.materialSyncMode || light.material_sync_mode) {
output += ` │ Material Sync Mode: ${light.materialSyncMode || light.material_sync_mode}\n`;
}
if (light.mesh_geometry) {
output += ` │ Mesh: ${light.mesh_geometry.primName}\n`;
output += ` │ Vertices: ${light.mesh_geometry.numVertices}\n`;
output += ` │ Faces: ${light.mesh_geometry.numFaces}\n`;
}
} else if (lightType === 'portal') {
output += ` │ (Portal light - no additional properties)\n`;
}
// Shaping properties (spotlight/IES)
const hasShaping = (light.shapingConeAngle !== undefined && light.shapingConeAngle < 90) ||
light.shapingIesFile ||
(light.shapingFocus !== undefined && light.shapingFocus !== 0);
if (hasShaping) {
output += `\n`;
output += ` │ ── Shaping (Spotlight/IES) ──\n`;
if (light.shapingConeAngle !== undefined && light.shapingConeAngle < 90) {
output += ` │ Cone Angle: ${light.shapingConeAngle.toFixed(2)}°\n`;
output += ` │ Cone Softness: ${(light.shapingConeSoftness || 0).toFixed(3)}\n`;
}
if (light.shapingFocus !== undefined && light.shapingFocus !== 0) {
output += ` │ Focus: ${light.shapingFocus.toFixed(3)}\n`;
}
if (light.shapingFocusTint && (light.shapingFocusTint[0] !== 0 || light.shapingFocusTint[1] !== 0 || light.shapingFocusTint[2] !== 0)) {
output += ` │ Focus Tint: ${formatColor(light.shapingFocusTint, colorMode)}\n`;
}
if (light.shapingIesFile) {
output += ` │ IES Profile: ${light.shapingIesFile}\n`;
if (light.shapingIesAngleScale !== undefined && light.shapingIesAngleScale !== 0) {
output += ` │ IES Angle Scale: ${light.shapingIesAngleScale.toFixed(3)}\n`;
}
if (light.shapingIesNormalize) {
output += ` │ IES Normalize: true\n`;
}
}
}
// Shadow properties
if (light.shadowEnable) {
output += ` Shadows: Enabled\n`;
if (verbose && light.shadowColor) {
const r = light.shadowColor[0].toFixed(3);
const g = light.shadowColor[1].toFixed(3);
const b = light.shadowColor[2].toFixed(3);
output += `│ Shadow Color: RGB(${r}, ${g}, ${b})\n`;
if (light.shadowEnable !== undefined) {
output += ` \n`;
output += ` │ ── Shadow ──\n`;
output += ` │ Enabled: ${light.shadowEnable ? 'Yes' : 'No'}\n`;
if (light.shadowEnable) {
if (light.shadowColor) {
const isBlack = light.shadowColor[0] === 0 && light.shadowColor[1] === 0 && light.shadowColor[2] === 0;
if (!isBlack || verbose) {
output += ` │ Color: ${formatColor(light.shadowColor, colorMode)}\n`;
}
}
if (light.shadowDistance !== undefined && light.shadowDistance > 0) {
output += ` │ Distance: ${light.shadowDistance.toFixed(2)}\n`;
}
if (light.shadowFalloff !== undefined && light.shadowFalloff > 0) {
output += ` │ Falloff: ${light.shadowFalloff.toFixed(3)}\n`;
}
if (light.shadowFalloffGamma !== undefined && light.shadowFalloffGamma !== 1.0) {
output += ` │ Falloff Gamma: ${light.shadowFalloffGamma.toFixed(3)}\n`;
}
}
}
// IES profile
if (light.shapingIesFile) {
output += ` IES Profile: ${light.shapingIesFile}\n`;
// Position/Direction
if (light.position || light.direction) {
output += ` \n`;
output += ` │ ── Spatial ──\n`;
if (light.position) {
output += ` │ Position: (${light.position[0].toFixed(3)}, ${light.position[1].toFixed(3)}, ${light.position[2].toFixed(3)})\n`;
}
if (light.direction) {
output += ` │ Direction: (${light.direction[0].toFixed(3)}, ${light.direction[1].toFixed(3)}, ${light.direction[2].toFixed(3)})\n`;
}
}
output += `└─────────────────────────────────────────────\n`;
// Transform matrix
if (showTransform && light.transform) {
output += `\n`;
output += ` │ ── Transform Matrix ──\n`;
output += formatMatrix(light.transform, ' │ ');
}
// Spectral emission (LTE SpectralAPI)
if (showSpectral && light.spectralEmission) {
output += `\n`;
output += ` │ ── Spectral Emission (LTE SpectralAPI) ──\n`;
output += formatSpectralEmission(light.spectralEmission, ' │ ');
}
output += `${'─'.repeat(60)}\n`;
}
return output;
@@ -272,6 +513,8 @@ async function dumpLights(options) {
if (!options.outputFile) {
if (options.format === 'summary') {
console.log('No lights found in scene.');
} else if (options.format === 'xml') {
console.log('<?xml version="1.0"?>\n<lights/>');
} else {
console.log(options.format === 'json' ? '[]' : 'lights: []');
}
@@ -280,6 +523,7 @@ async function dumpLights(options) {
}
const results = [];
const xmlResults = [];
// Determine which lights to dump
const lightIds = options.lightId !== null
@@ -298,41 +542,82 @@ async function dumpLights(options) {
}
try {
const result = usd.getLightWithFormat(lightId, 'json');
let lightData;
if (result.error) {
console.error(`Error getting light ${lightId}: ${result.error}`);
continue;
if (options.serialized) {
// Use serialized format
const formatType = options.format === 'xml' ? 'xml' : 'json';
const result = usd.getLightWithFormat(lightId, formatType);
if (result.error) {
console.error(`Error getting light ${lightId}: ${result.error}`);
continue;
}
if (options.format === 'xml') {
xmlResults.push(result.data);
continue;
}
lightData = JSON.parse(result.data);
} else {
// Use direct object format (more comprehensive)
lightData = usd.getLight(lightId);
if (lightData.error) {
console.error(`Error getting light ${lightId}: ${lightData.error}`);
continue;
}
}
const lightData = JSON.parse(result.data);
if (options.verbose) {
console.error(` Type: ${lightData.lightType}`);
console.error(` Type: ${lightData.type}`);
console.error(` Name: ${lightData.name || 'N/A'}`);
if (lightData.lightType === 'Geometry') {
console.error(` Mesh ID: ${lightData.geometry_mesh_id}`);
if (lightData.type === 'geometry') {
console.error(` Mesh ID: ${lightData.geometryMeshId}`);
}
if (lightData.spectralEmission) {
console.error(` Has Spectral Emission: yes`);
}
}
// Optionally fetch mesh data for geometry lights
if (options.showMeshes && lightData.lightType === 'Geometry' && lightData.geometry_mesh_id !== undefined) {
const meshId = lightData.geometry_mesh_id;
if (meshId >= 0 && meshId < usd.numMeshes()) {
if (options.showMeshes && lightData.type === 'geometry') {
const meshId = lightData.geometryMeshId ?? lightData.geometry_mesh_id;
if (meshId !== undefined && meshId >= 0 && meshId < usd.numMeshes()) {
const mesh = usd.getMesh(meshId);
lightData.mesh_geometry = {
primName: mesh.primName,
numVertices: mesh.faceVertexIndices.length,
numFaces: mesh.faceVertexCounts.length
numVertices: mesh.faceVertexIndices?.length || 0,
numFaces: mesh.faceVertexCounts?.length || 0
};
}
}
// Optionally fetch material data for mesh lights
if (options.showMaterials && lightData.lightType === 'Geometry') {
if (options.showMaterials && lightData.type === 'geometry') {
// Find material bound to the mesh if available
// This would require additional implementation
lightData.material_info = 'Material lookup not yet implemented';
const meshId = lightData.geometryMeshId ?? lightData.geometry_mesh_id;
if (meshId !== undefined && meshId >= 0 && meshId < usd.numMeshes()) {
const mesh = usd.getMesh(meshId);
if (mesh.materialId !== undefined && mesh.materialId >= 0) {
const material = usd.getMaterial(mesh.materialId);
lightData.material_info = {
id: mesh.materialId,
name: material?.name || 'Unknown'
};
}
}
}
// Filter out transform if not requested (to reduce output size)
if (!options.showTransform && !options.showAll) {
delete lightData.transform;
}
// Filter out spectral if not requested
if (!options.showSpectral && !options.showAll) {
delete lightData.spectralEmission;
}
results.push(lightData);
@@ -359,8 +644,22 @@ async function dumpLights(options) {
defaultKeyType: 'PLAIN',
defaultStringType: 'QUOTE_DOUBLE'
});
} else if (options.format === 'xml') {
// Combine XML results
output = '<?xml version="1.0" encoding="UTF-8"?>\n<lights>\n';
for (const xml of xmlResults) {
// Indent each light XML
const indented = xml.split('\n').map(line => ' ' + line).join('\n');
output += indented + '\n';
}
output += '</lights>';
} else if (options.format === 'summary') {
output = formatSummary(results, options.verbose);
output = formatSummary(results, {
verbose: options.verbose,
showTransform: options.showTransform || options.showAll,
showSpectral: options.showSpectral || options.showAll,
colorMode: options.colorMode
});
}
// Write to file or stdout

650
web/js/usdlux.html Normal file
View File

@@ -0,0 +1,650 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>TinyUSDZ UsdLux Light Demo</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
overflow: hidden;
font-family: 'Segoe UI', Arial, sans-serif;
background: #1a1a2e;
}
canvas {
display: block;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.85);
padding: 15px;
border-radius: 8px;
font-size: 13px;
max-width: 380px;
max-height: calc(100vh - 40px);
overflow-y: auto;
border: 1px solid rgba(255, 255, 255, 0.1);
}
#info h3 {
margin-top: 0;
margin-bottom: 12px;
color: #ffd700;
font-size: 16px;
}
#info p {
margin: 5px 0;
font-size: 12px;
color: #ccc;
}
#file-controls {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
#fileInput {
display: none;
}
.button {
display: inline-block;
padding: 8px 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
margin-right: 6px;
margin-bottom: 6px;
transition: all 0.2s;
}
.button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.button.secondary {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.button.danger {
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
}
#light-info {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
#light-info h4 {
margin: 0 0 10px 0;
color: #6bb6ff;
font-size: 14px;
}
.light-list {
max-height: 300px;
overflow-y: auto;
}
.light-item {
padding: 10px;
margin: 6px 0;
background: rgba(255, 255, 255, 0.08);
border-radius: 6px;
border-left: 3px solid #ffd700;
cursor: pointer;
transition: all 0.2s;
}
.light-item:hover {
background: rgba(255, 255, 255, 0.12);
}
.light-item.selected {
background: rgba(102, 126, 234, 0.3);
border-left-color: #667eea;
}
.light-item .light-name {
font-weight: bold;
color: #fff;
margin-bottom: 4px;
}
.light-item .light-type {
display: inline-block;
padding: 2px 8px;
background: rgba(255, 215, 0, 0.2);
color: #ffd700;
border-radius: 10px;
font-size: 10px;
text-transform: uppercase;
margin-left: 8px;
}
.light-item .light-details {
font-size: 11px;
color: #aaa;
margin-top: 4px;
}
.light-color-swatch {
display: inline-block;
width: 14px;
height: 14px;
border-radius: 3px;
vertical-align: middle;
margin-right: 6px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
#loadingIndicator {
display: none;
color: #ffd700;
font-weight: bold;
margin-top: 8px;
}
#loadingIndicator.active {
display: block;
}
#drop-zone {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(102, 126, 234, 0.9);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}
#drop-zone.active {
display: flex;
}
#drop-zone-content {
text-align: center;
color: white;
}
#drop-zone-content h2 {
font-size: 32px;
margin-bottom: 10px;
}
#drop-zone-content p {
font-size: 16px;
opacity: 0.8;
}
#scene-stats {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.7);
padding: 10px 15px;
border-radius: 6px;
font-size: 11px;
}
#scene-stats span {
margin-right: 15px;
}
.stat-label {
color: #888;
}
.stat-value {
color: #6bb6ff;
font-weight: bold;
}
#controls-info {
position: absolute;
bottom: 10px;
right: 10px;
color: white;
background: rgba(0, 0, 0, 0.7);
padding: 10px 15px;
border-radius: 6px;
font-size: 11px;
text-align: right;
}
#embedded-select, #tonemap-select {
width: 100%;
padding: 6px;
margin-bottom: 8px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.1);
color: white;
font-size: 12px;
}
#embedded-select option, #tonemap-select option {
background: #1a1a2e;
color: white;
}
.settings-section {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.settings-section h4 {
margin: 0 0 10px 0;
color: #95e06c;
font-size: 13px;
}
.setting-row {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.setting-row label {
flex: 0 0 80px;
font-size: 11px;
color: #888;
}
.setting-row select, .setting-row input[type="range"] {
flex: 1;
}
.setting-value {
width: 45px;
text-align: right;
font-size: 11px;
color: #6bb6ff;
margin-left: 8px;
}
input[type="range"] {
-webkit-appearance: none;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
height: 6px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: #667eea;
border-radius: 50%;
cursor: pointer;
}
.tonemap-info {
font-size: 10px;
color: #666;
margin-top: 4px;
font-style: italic;
}
#spectral-section {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
#spectral-section h4 {
margin: 0 0 10px 0;
color: #e066ff;
font-size: 13px;
}
#spectral-canvas {
width: 100%;
height: 120px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
margin-bottom: 8px;
}
#spectral-info {
font-size: 10px;
color: #aaa;
margin-top: 6px;
padding: 6px;
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
#spectral-info div {
margin-bottom: 2px;
}
#spectral-info strong {
color: #e066ff;
}
.wavelength-preview {
display: inline-block;
width: 20px;
height: 14px;
vertical-align: middle;
border-radius: 3px;
border: 1px solid rgba(255, 255, 255, 0.3);
margin-left: 8px;
}
#monochrome-controls {
display: none;
}
#monochrome-controls.visible {
display: block;
}
</style>
</head>
<body>
<div id="drop-zone">
<div id="drop-zone-content">
<h2>Drop USD File Here</h2>
<p>Supports .usd, .usda, .usdc, .usdz</p>
</div>
</div>
<div id="info">
<h3>UsdLux Light Demo</h3>
<p>
Visualize USD lights in Three.js.<br>
Drag & drop or use the button to load USD files.
</p>
<div id="file-controls">
<label for="embedded-select" style="font-size: 11px; color: #888;">Embedded Scenes:</label>
<select id="embedded-select">
<option value="basic">Basic Lights</option>
<option value="spotlight">Spotlight with IES</option>
<option value="area">Area Lights</option>
<option value="dome">Dome Light (IBL)</option>
<option value="complete" selected>Complete Scene</option>
</select>
<input type="file" id="fileInput" accept=".usd,.usda,.usdc,.usdz">
<button class="button" onclick="document.getElementById('fileInput').click()">Load USD File</button>
<button class="button secondary" onclick="window.loadEmbeddedScene()">Load Embedded</button>
<button class="button danger" onclick="window.clearLights()">Clear Lights</button>
</div>
<p>
<strong>Current:</strong> <span id="currentFile">Embedded Scene</span>
<span id="loadingIndicator">Loading...</span>
</p>
<div id="light-info">
<h4>Lights (<span id="lightCount">0</span>)</h4>
<div class="light-list" id="lightList">
<p style="color: #666; font-style: italic;">No lights loaded</p>
</div>
</div>
<div class="settings-section">
<h4>Display Settings</h4>
<div class="setting-row">
<label>Tone Map:</label>
<select id="tonemap-select">
<option value="raw">Raw (Linear)</option>
<option value="reinhard">Reinhard</option>
<option value="aces1" selected>ACES 1.3</option>
<option value="aces2">ACES 2.0</option>
<option value="agx">AgX</option>
<option value="neutral">Neutral</option>
</select>
</div>
<div class="tonemap-info" id="tonemap-info">Film-like response with highlight roll-off</div>
<div class="setting-row">
<label>Exposure:</label>
<input type="range" id="exposure-slider" min="-3" max="3" step="0.1" value="0">
<span class="setting-value" id="exposure-value">0.0 EV</span>
</div>
<div class="setting-row">
<label>Gamma:</label>
<input type="range" id="gamma-slider" min="1.0" max="3.0" step="0.1" value="2.2">
<span class="setting-value" id="gamma-value">2.2</span>
</div>
</div>
<div id="spectral-section">
<h4>Spectral Settings</h4>
<canvas id="spectral-canvas" width="340" height="120"></canvas>
<div class="setting-row">
<label>Color Mode:</label>
<select id="spectral-mode-select">
<option value="rgb" selected>RGB (USD color)</option>
<option value="spectral">Spectral (SPD to RGB)</option>
<option value="monochrome">Monochrome</option>
</select>
</div>
<div id="monochrome-controls">
<div class="setting-row">
<label>Wavelength:</label>
<input type="range" id="wavelength-slider" min="380" max="780" step="1" value="550">
<span class="setting-value" id="wavelength-value">550nm</span>
<span class="wavelength-preview" id="wavelength-preview"></span>
</div>
</div>
<div style="margin-top: 8px;">
<button class="button" style="font-size: 10px; padding: 5px 10px;" onclick="window.applyDemoSpectralData()">Apply Demo SPD</button>
</div>
<div class="setting-row" style="margin-top: 8px;">
<label>Blackbody:</label>
<input type="range" id="blackbody-slider" min="1000" max="10000" step="100" value="5500">
<span class="setting-value" id="blackbody-value">5500K</span>
</div>
<div id="spectral-info">
<span style="color: #666;">Select a light to view spectral data</span>
</div>
</div>
</div>
<div id="scene-stats">
<span><span class="stat-label">Lights:</span> <span class="stat-value" id="statLights">0</span></span>
<span><span class="stat-label">Meshes:</span> <span class="stat-value" id="statMeshes">1</span></span>
<span><span class="stat-label">FPS:</span> <span class="stat-value" id="statFPS">60</span></span>
</div>
<div id="controls-info">
<strong>Controls:</strong><br>
Left-click + drag: Rotate<br>
Right-click + drag: Pan<br>
Scroll: Zoom
</div>
<script type="module">
// Handle file upload
document.getElementById('fileInput').addEventListener('change', async (event) => {
const file = event.target.files[0];
if (!file) return;
document.getElementById('loadingIndicator').classList.add('active');
document.getElementById('currentFile').textContent = file.name;
const customEvent = new CustomEvent('loadUSDFile', { detail: { file } });
window.dispatchEvent(customEvent);
event.target.value = '';
});
// Drag and drop handling
const dropZone = document.getElementById('drop-zone');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
document.body.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
});
});
document.body.addEventListener('dragenter', () => {
dropZone.classList.add('active');
});
dropZone.addEventListener('dragleave', (e) => {
if (e.target === dropZone) {
dropZone.classList.remove('active');
}
});
dropZone.addEventListener('drop', (e) => {
dropZone.classList.remove('active');
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
const ext = file.name.toLowerCase().split('.').pop();
if (['usd', 'usda', 'usdc', 'usdz'].includes(ext)) {
document.getElementById('loadingIndicator').classList.add('active');
document.getElementById('currentFile').textContent = file.name;
const customEvent = new CustomEvent('loadUSDFile', { detail: { file } });
window.dispatchEvent(customEvent);
} else {
alert('Please drop a USD file (.usd, .usda, .usdc, .usdz)');
}
}
});
// Global functions
window.hideLoadingIndicator = function() {
document.getElementById('loadingIndicator').classList.remove('active');
};
window.updateLightList = function(lights) {
const lightList = document.getElementById('lightList');
const lightCount = document.getElementById('lightCount');
const statLights = document.getElementById('statLights');
lightCount.textContent = lights.length;
statLights.textContent = lights.length;
if (lights.length === 0) {
lightList.innerHTML = '<p style="color: #666; font-style: italic;">No lights loaded</p>';
return;
}
lightList.innerHTML = '';
lights.forEach((light, index) => {
const item = document.createElement('div');
item.className = 'light-item';
item.dataset.index = index;
const colorHex = light.color ?
`#${Math.round(light.color[0] * 255).toString(16).padStart(2, '0')}${Math.round(light.color[1] * 255).toString(16).padStart(2, '0')}${Math.round(light.color[2] * 255).toString(16).padStart(2, '0')}`
: '#ffffff';
let details = `Intensity: ${(light.intensity || 1).toFixed(2)}`;
if (light.exposure && light.exposure !== 0) {
details += ` | Exp: ${light.exposure.toFixed(1)} EV`;
}
if (light.radius && light.type !== 'distant' && light.type !== 'dome') {
details += ` | R: ${light.radius.toFixed(2)}`;
}
if (light.type === 'rect' && light.width && light.height) {
details += ` | ${light.width.toFixed(1)}x${light.height.toFixed(1)}`;
}
if (light.shapingConeAngle && light.shapingConeAngle < 90) {
details += ` | Cone: ${light.shapingConeAngle.toFixed(0)}`;
}
// Spectral indicator
const hasSpectral = light.spectralEmission &&
((light.spectralEmission.samples && light.spectralEmission.samples.length > 0) ||
(light.spectralEmission.preset && light.spectralEmission.preset !== 'none'));
const spectralBadge = hasSpectral ?
'<span style="background: linear-gradient(90deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #8b00ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-size: 9px; margin-left: 5px;">SPD</span>' : '';
item.innerHTML = `
<div class="light-name">
<span class="light-color-swatch" style="background: ${colorHex}"></span>
${light.name || 'Light ' + index}
<span class="light-type">${light.type || 'unknown'}</span>
${spectralBadge}
</div>
<div class="light-details">${details}</div>
`;
item.addEventListener('click', () => {
// Remove selected from all items
lightList.querySelectorAll('.light-item').forEach(i => i.classList.remove('selected'));
item.classList.add('selected');
// Focus camera on this light
window.focusOnLight(index);
// Select light for spectral display
if (window.selectLightForSpectral) {
window.selectLightForSpectral(index);
}
});
lightList.appendChild(item);
});
};
// Embedded scene selector
document.getElementById('embedded-select').addEventListener('change', (e) => {
window.loadEmbeddedScene(e.target.value);
});
// Tone mapping descriptions
const tonemapDescriptions = {
'raw': 'No tone mapping - linear HDR values clamped',
'reinhard': 'Simple curve, preserves detail in highlights',
'aces1': 'ACES 1.3 - Film-like response with highlight roll-off',
'aces2': 'ACES 2.0 - Improved gamut mapping and desaturation',
'agx': 'AgX - Modern filmic look, similar to Blender',
'neutral': 'Neutral - Balanced tone curve for general use'
};
// Tone mapping selector
document.getElementById('tonemap-select').addEventListener('change', (e) => {
const value = e.target.value;
window.setToneMapping(value);
document.getElementById('tonemap-info').textContent = tonemapDescriptions[value] || '';
});
// Exposure slider
document.getElementById('exposure-slider').addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
document.getElementById('exposure-value').textContent = value.toFixed(1) + ' EV';
window.setExposure(value);
});
// Gamma slider
document.getElementById('gamma-slider').addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
document.getElementById('gamma-value').textContent = value.toFixed(1);
window.setGamma(value);
});
// Spectral mode selector
document.getElementById('spectral-mode-select').addEventListener('change', (e) => {
const mode = e.target.value;
window.setSpectralMode(mode);
// Show/hide monochrome controls
const monoControls = document.getElementById('monochrome-controls');
if (mode === 'monochrome') {
monoControls.classList.add('visible');
updateWavelengthPreview(parseInt(document.getElementById('wavelength-slider').value));
} else {
monoControls.classList.remove('visible');
}
});
// Wavelength slider for monochrome mode
document.getElementById('wavelength-slider').addEventListener('input', (e) => {
const wavelength = parseInt(e.target.value);
document.getElementById('wavelength-value').textContent = wavelength + 'nm';
updateWavelengthPreview(wavelength);
window.setMonochromeWavelength(wavelength);
});
// Update wavelength preview color
function updateWavelengthPreview(wavelength) {
const rgb = window.wavelengthToRGB ? window.wavelengthToRGB(wavelength) : { r: 0.5, g: 0.5, b: 0.5 };
const preview = document.getElementById('wavelength-preview');
if (preview) {
preview.style.backgroundColor = `rgb(${Math.round(rgb.r * 255)}, ${Math.round(rgb.g * 255)}, ${Math.round(rgb.b * 255)})`;
}
}
// Initialize wavelength preview
setTimeout(() => {
updateWavelengthPreview(550);
}, 500);
// Blackbody temperature slider
document.getElementById('blackbody-slider').addEventListener('input', (e) => {
const temp = parseInt(e.target.value);
document.getElementById('blackbody-value').textContent = temp + 'K';
});
// Apply blackbody on slider change end
document.getElementById('blackbody-slider').addEventListener('change', (e) => {
const temp = parseInt(e.target.value);
if (window.applyBlackbodyToSelected) {
window.applyBlackbodyToSelected(temp);
}
});
</script>
<script type="module" src="./usdlux.js"></script>
</body>
</html>

1985
web/js/usdlux.js Normal file

File diff suppressed because it is too large Load Diff