mirror of
https://github.com/lighttransport/tinyusdz.git
synced 2026-01-18 01:11:17 +01:00
Add texture loading patterns and OpenPBRMaterial support
- Add sync/Loaded variants for material conversion functions: - convertOpenPBRToMeshPhysicalMaterial (immediate return) - convertOpenPBRToMeshPhysicalMaterialLoaded (waits for textures) - convertToOpenPBRMaterial (immediate return) - convertToOpenPBRMaterialLoaded (waits for textures) - Add full texture support for OpenPBRMaterial custom shader: - map, roughnessMap, metalnessMap, emissiveMap, normalMap, aoMap - Add HDR/EXR texture format detection and decoding: - Magic byte detection in getMimeType() - TinyUSDZ HDR decoder (faster) with Three.js fallback - Three.js EXR decoder with TinyUSDZ fallback - Update MaterialX documentation: - Three.js/WebGL integration section - Supported parameters and texture maps tables - MaterialX NodeGraph and node shader documentation - Complete USDA examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
434
doc/materialx.md
434
doc/materialx.md
@@ -326,6 +326,440 @@ Based on Blender 4.5 development:
|
||||
- MaterialX 1.39.0+ includes OpenPBR Surface
|
||||
- MaterialX 1.39.1 added Standard Surface ↔ OpenPBR translation graphs
|
||||
|
||||
## Three.js / WebGL MaterialX Integration
|
||||
|
||||
TinyUSDZ provides JavaScript APIs for converting OpenPBR/MaterialX materials to Three.js materials. Two material implementations are available:
|
||||
|
||||
### Material Implementations
|
||||
|
||||
| Implementation | Class | Use Case |
|
||||
|---------------|-------|----------|
|
||||
| **MeshPhysicalMaterial** | `THREE.MeshPhysicalMaterial` | Standard Three.js PBR material, broad compatibility |
|
||||
| **OpenPBRMaterial** | Custom `ShaderMaterial` | Full OpenPBR BRDF with Oren-Nayar diffuse, coat IOR, fuzz layer |
|
||||
|
||||
### MeshPhysicalMaterial Conversion
|
||||
|
||||
Converts OpenPBR parameters to Three.js MeshPhysicalMaterial properties.
|
||||
|
||||
#### Supported Parameters
|
||||
|
||||
| OpenPBR Parameter | MeshPhysicalMaterial Property | Notes |
|
||||
|------------------|------------------------------|-------|
|
||||
| `base_color` | `color` | Diffuse/albedo color |
|
||||
| `base_metalness` | `metalness` | Metallic factor (0-1) |
|
||||
| `specular_roughness` | `roughness` | Surface roughness (0-1) |
|
||||
| `specular_ior` | `ior` | Index of refraction |
|
||||
| `specular_color` | `specularColor` | Specular tint |
|
||||
| `specular_anisotropy` | `anisotropy` | Anisotropic stretching |
|
||||
| `transmission_weight` | `transmission` | Transmission factor |
|
||||
| `transmission_color` | `attenuationColor` | Transmission color |
|
||||
| `coat_weight` | `clearcoat` | Clearcoat intensity |
|
||||
| `coat_roughness` | `clearcoatRoughness` | Clearcoat roughness |
|
||||
| `sheen_weight` / `fuzz_weight` | `sheen` | Sheen intensity |
|
||||
| `sheen_color` / `fuzz_color` | `sheenColor` | Sheen tint |
|
||||
| `sheen_roughness` / `fuzz_roughness` | `sheenRoughness` | Sheen roughness |
|
||||
| `thin_film_weight` | `iridescence` | Iridescence intensity |
|
||||
| `thin_film_thickness` | `iridescenceThicknessRange` | Film thickness |
|
||||
| `thin_film_ior` | `iridescenceIOR` | Film IOR |
|
||||
| `emission_color` | `emissive` | Emission color |
|
||||
| `emission_luminance` | `emissiveIntensity` | Emission strength |
|
||||
| `geometry_opacity` / `opacity` | `opacity` | Alpha value |
|
||||
| `geometry_normal` / `normal` | `normalMap` | Normal map texture |
|
||||
|
||||
#### Supported Texture Maps
|
||||
|
||||
| OpenPBR Texture | MeshPhysicalMaterial Map | Channel |
|
||||
|-----------------|-------------------------|---------|
|
||||
| `base_color` | `map` | RGB |
|
||||
| `specular_roughness` | `roughnessMap` | G channel |
|
||||
| `base_metalness` | `metalnessMap` | B channel |
|
||||
| `emission_color` | `emissiveMap` | RGB |
|
||||
| `geometry_normal` | `normalMap` | RGB (tangent space) |
|
||||
| `geometry_opacity` | `alphaMap` | Single channel |
|
||||
|
||||
#### API Functions
|
||||
|
||||
```javascript
|
||||
import {
|
||||
convertOpenPBRToMeshPhysicalMaterial,
|
||||
convertOpenPBRToMeshPhysicalMaterialLoaded
|
||||
} from 'tinyusdz/TinyUSDZMaterialX.js';
|
||||
|
||||
// Returns immediately, textures load in background (fire-and-forget)
|
||||
const material = convertOpenPBRToMeshPhysicalMaterial(materialData, usdScene, options);
|
||||
|
||||
// Waits for all textures to load before returning
|
||||
const material = await convertOpenPBRToMeshPhysicalMaterialLoaded(materialData, usdScene, options);
|
||||
```
|
||||
|
||||
### OpenPBRMaterial (Custom Shader)
|
||||
|
||||
Full OpenPBR BRDF implementation as a Three.js ShaderMaterial, supporting features not available in MeshPhysicalMaterial.
|
||||
|
||||
#### Additional Features vs MeshPhysicalMaterial
|
||||
|
||||
| Feature | OpenPBRMaterial | MeshPhysicalMaterial |
|
||||
|---------|----------------|---------------------|
|
||||
| Oren-Nayar Diffuse | ✅ `base_diffuse_roughness` | ❌ Lambertian only |
|
||||
| Coat Color | ✅ `coat_color` | ❌ White only |
|
||||
| Coat IOR | ✅ `coat_ior` | ❌ Fixed 1.5 |
|
||||
| Fuzz Layer | ✅ OpenPBR formulation | ⚠️ Approximated as sheen |
|
||||
| Thin Film Physics | ✅ Interference simulation | ⚠️ Simplified |
|
||||
|
||||
#### Supported Parameters
|
||||
|
||||
| OpenPBR Parameter | Uniform Name | Default |
|
||||
|------------------|--------------|---------|
|
||||
| **Base Layer** | | |
|
||||
| `base_weight` | `base_weight` | 1.0 |
|
||||
| `base_color` | `base_color` | (0.8, 0.8, 0.8) |
|
||||
| `base_metalness` | `base_metalness` | 0.0 |
|
||||
| `base_diffuse_roughness` | `base_diffuse_roughness` | 0.0 |
|
||||
| **Specular Layer** | | |
|
||||
| `specular_weight` | `specular_weight` | 1.0 |
|
||||
| `specular_color` | `specular_color` | (1.0, 1.0, 1.0) |
|
||||
| `specular_roughness` | `specular_roughness` | 0.3 |
|
||||
| `specular_ior` | `specular_ior` | 1.5 |
|
||||
| `specular_anisotropy` | `specular_anisotropy` | 0.0 |
|
||||
| `specular_rotation` | `specular_rotation` | 0.0 |
|
||||
| **Coat Layer** | | |
|
||||
| `coat_weight` | `coat_weight` | 0.0 |
|
||||
| `coat_color` | `coat_color` | (1.0, 1.0, 1.0) |
|
||||
| `coat_roughness` | `coat_roughness` | 0.0 |
|
||||
| `coat_ior` | `coat_ior` | 1.5 |
|
||||
| **Fuzz Layer** | | |
|
||||
| `fuzz_weight` | `fuzz_weight` | 0.0 |
|
||||
| `fuzz_color` | `fuzz_color` | (1.0, 1.0, 1.0) |
|
||||
| `fuzz_roughness` | `fuzz_roughness` | 0.5 |
|
||||
| **Thin Film** | | |
|
||||
| `thin_film_weight` | `thin_film_weight` | 0.0 |
|
||||
| `thin_film_thickness` | `thin_film_thickness` | 500.0 nm |
|
||||
| `thin_film_ior` | `thin_film_ior` | 1.5 |
|
||||
| **Transmission** | | |
|
||||
| `transmission_weight` | `transmission_weight` | 0.0 |
|
||||
| `transmission_color` | `transmission_color` | (1.0, 1.0, 1.0) |
|
||||
| **Emission** | | |
|
||||
| `emission_luminance` | `emission_luminance` | 0.0 |
|
||||
| `emission_color` | `emission_color` | (1.0, 1.0, 1.0) |
|
||||
| **Geometry** | | |
|
||||
| `geometry_opacity` | `geometry_opacity` | 1.0 |
|
||||
|
||||
#### Supported Texture Maps
|
||||
|
||||
| OpenPBR Texture | Property | Shader Define |
|
||||
|-----------------|----------|---------------|
|
||||
| `base_color` | `map` | `USE_MAP` |
|
||||
| `specular_roughness` | `roughnessMap` | `USE_ROUGHNESSMAP` |
|
||||
| `base_metalness` | `metalnessMap` | `USE_METALNESSMAP` |
|
||||
| `emission_color` | `emissiveMap` | `USE_EMISSIVEMAP` |
|
||||
| `geometry_normal` | `normalMap` | `USE_NORMALMAP` |
|
||||
| `ambient_occlusion` | `aoMap` | `USE_AOMAP` |
|
||||
|
||||
#### API Functions
|
||||
|
||||
```javascript
|
||||
// In materialx.js demo
|
||||
|
||||
// Returns immediately, textures load in background
|
||||
const material = convertToOpenPBRMaterial(matData, nativeLoader);
|
||||
|
||||
// Waits for all textures to load before returning
|
||||
const material = await convertToOpenPBRMaterialLoaded(matData, nativeLoader);
|
||||
```
|
||||
|
||||
### Texture Loading Patterns
|
||||
|
||||
Two loading patterns are available for both material types:
|
||||
|
||||
| Pattern | Function Suffix | Behavior | Use Case |
|
||||
|---------|----------------|----------|----------|
|
||||
| **Immediate** | (none) | Returns material immediately, textures load asynchronously | Interactive loading, progressive display |
|
||||
| **Loaded** | `Loaded` | Awaits all textures before returning | Batch rendering, screenshots |
|
||||
|
||||
#### Example: Immediate Pattern
|
||||
```javascript
|
||||
// Material appears immediately with base colors
|
||||
// Textures pop in as they load
|
||||
const material = convertOpenPBRToMeshPhysicalMaterial(data, scene);
|
||||
mesh.material = material;
|
||||
// Render loop continues, textures appear when ready
|
||||
```
|
||||
|
||||
#### Example: Loaded Pattern
|
||||
```javascript
|
||||
// Wait for complete material with all textures
|
||||
const material = await convertOpenPBRToMeshPhysicalMaterialLoaded(data, scene);
|
||||
mesh.material = material;
|
||||
// All textures are ready before first render
|
||||
```
|
||||
|
||||
### HDR/EXR Texture Support
|
||||
|
||||
Both material converters support HDR and EXR texture formats:
|
||||
|
||||
| Format | Decoder | Fallback |
|
||||
|--------|---------|----------|
|
||||
| HDR (Radiance) | TinyUSDZ WASM (faster) | Three.js HDRLoader |
|
||||
| EXR (OpenEXR) | Three.js EXRLoader | TinyUSDZ (for unsupported compression) |
|
||||
|
||||
```javascript
|
||||
// Initialize TinyUSDZ module reference for HDR/EXR fallback
|
||||
import { TinyUSDZLoaderUtils } from 'tinyusdz/TinyUSDZLoaderUtils.js';
|
||||
import { setTinyUSDZ } from 'tinyusdz/TinyUSDZMaterialX.js';
|
||||
|
||||
// After TinyUSDZ WASM initialization
|
||||
TinyUSDZLoaderUtils.setTinyUSDZ(tinyusdzModule);
|
||||
setTinyUSDZ(tinyusdzModule);
|
||||
```
|
||||
|
||||
### Usage Example
|
||||
|
||||
```javascript
|
||||
import { TinyUSDZLoader } from 'tinyusdz/TinyUSDZLoader.js';
|
||||
import { TinyUSDZLoaderUtils } from 'tinyusdz/TinyUSDZLoaderUtils.js';
|
||||
import { OpenPBRMaterial } from './OpenPBRMaterial.js';
|
||||
|
||||
// Initialize loader
|
||||
const loader = new TinyUSDZLoader();
|
||||
await loader.init();
|
||||
|
||||
// Set TinyUSDZ reference for HDR/EXR support
|
||||
TinyUSDZLoaderUtils.setTinyUSDZ(loader.native_);
|
||||
|
||||
// Load USD file
|
||||
const usd = await loader.loadAsync('model.usdz');
|
||||
|
||||
// Convert materials using TinyUSDZLoaderUtils
|
||||
const material = await TinyUSDZLoaderUtils.convertMaterial(
|
||||
materialData,
|
||||
usd,
|
||||
{
|
||||
preferredMaterialType: 'physical', // or 'openpbr'
|
||||
envMap: envTexture,
|
||||
envMapIntensity: 1.0
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## MaterialX NodeGraph and Node Shaders
|
||||
|
||||
TinyUSDZ supports MaterialX node graphs as used in USD shader networks. This section documents the supported node types and how they map to USD primitives.
|
||||
|
||||
### MaterialX Node Definition IDs
|
||||
|
||||
When MaterialX is exported from applications like Blender, shader nodes use specific `info:id` values:
|
||||
|
||||
| Node Definition ID | Description | Used By |
|
||||
|-------------------|-------------|---------|
|
||||
| `ND_open_pbr_surface_surfaceshader` | OpenPBR Surface shader | Blender 4.5+ |
|
||||
| `ND_standard_surface_surfaceshader` | Autodesk Standard Surface | Maya, older Blender |
|
||||
| `ND_UsdPreviewSurface_surfaceshader` | USD Preview Surface | Universal fallback |
|
||||
| `ND_image_color3` | Color image texture | Texture nodes |
|
||||
| `ND_image_float` | Grayscale image texture | Roughness, metalness |
|
||||
| `ND_texcoord_vector2` | UV coordinate generator | Texture coordinates |
|
||||
| `ND_normalmap` | Normal map processor | Normal mapping |
|
||||
|
||||
### USD NodeGraph Structure
|
||||
|
||||
MaterialX node graphs in USD appear as `NodeGraph` prims containing shader nodes:
|
||||
|
||||
```usda
|
||||
def NodeGraph "NG_materialx" {
|
||||
# Texture coordinate node
|
||||
def Shader "texcoord" {
|
||||
uniform token info:id = "ND_texcoord_vector2"
|
||||
int inputs:index = 0
|
||||
float2 outputs:out
|
||||
}
|
||||
|
||||
# Image texture node
|
||||
def Shader "base_color_image" {
|
||||
uniform token info:id = "ND_image_color3"
|
||||
asset inputs:file = @textures/diffuse.png@
|
||||
string inputs:filtertype = "linear"
|
||||
float2 inputs:texcoord.connect = </Material/NG_materialx/texcoord.outputs:out>
|
||||
color3f outputs:out
|
||||
}
|
||||
|
||||
# Output interface
|
||||
color3f outputs:base_color.connect = </Material/NG_materialx/base_color_image.outputs:out>
|
||||
}
|
||||
```
|
||||
|
||||
### Supported MaterialX Nodes (Three.js TSL) [W.I.P.]
|
||||
|
||||
> ⚠️ **Work in Progress**: Three.js TSL node graph processing is experimental and under active development. Not all nodes are fully tested.
|
||||
|
||||
The following MaterialX nodes are supported in the Three.js TSL (Three Shading Language) implementation:
|
||||
|
||||
#### Math Operations
|
||||
|
||||
| Node Type | MaterialX Name | Description | Inputs |
|
||||
|-----------|---------------|-------------|--------|
|
||||
| `add` | `ND_add_*` | Add two values | `in1`, `in2` |
|
||||
| `subtract` | `ND_subtract_*` | Subtract values | `in1`, `in2` |
|
||||
| `multiply` | `ND_multiply_*` | Multiply values | `in1`, `in2` |
|
||||
| `divide` | `ND_divide_*` | Divide values | `in1`, `in2` |
|
||||
| `power` | `ND_power_*` | Power function | `in1`, `in2` |
|
||||
| `clamp` | `ND_clamp_*` | Clamp to range | `in`, `low`, `high` |
|
||||
| `mix` | `ND_mix_*` | Linear interpolation | `bg`, `fg`, `mix` |
|
||||
| `remap` | `ND_remap_*` | Remap value range | `in`, `inlow`, `inhigh`, `outlow`, `outhigh` |
|
||||
| `smoothstep` | `ND_smoothstep_*` | Smooth interpolation | `low`, `high`, `in` |
|
||||
|
||||
#### Vector Operations
|
||||
|
||||
| Node Type | MaterialX Name | Description | Inputs |
|
||||
|-----------|---------------|-------------|--------|
|
||||
| `normalize` | `ND_normalize_*` | Normalize vector | `in` |
|
||||
| `dotproduct` | `ND_dotproduct_*` | Dot product | `in1`, `in2` |
|
||||
| `extract` | `ND_extract_*` | Extract component | `in`, `index` |
|
||||
| `combine2` | `ND_combine2_*` | Combine to vec2 | `in1`, `in2` |
|
||||
| `combine3` | `ND_combine3_*` | Combine to vec3 | `in1`, `in2`, `in3` |
|
||||
| `combine4` | `ND_combine4_*` | Combine to vec4 | `in1`, `in2`, `in3`, `in4` |
|
||||
|
||||
#### Texture & Geometry
|
||||
|
||||
| Node Type | MaterialX Name | Description | Inputs |
|
||||
|-----------|---------------|-------------|--------|
|
||||
| `image` | `ND_image_*` | Sample texture | `file`, `texcoord` |
|
||||
| `tiledimage` | `ND_tiledimage_*` | Tiled texture sample | `file`, `texcoord`, `uvtiling` |
|
||||
| `texcoord` | `ND_texcoord_vector2` | UV coordinates | `index` |
|
||||
| `position` | `ND_position_*` | World position | - |
|
||||
| `normal` | `ND_normal_*` | World normal | - |
|
||||
| `tangent` | `ND_tangent_*` | World tangent | - |
|
||||
|
||||
#### Color Operations
|
||||
|
||||
| Node Type | MaterialX Name | Description | Inputs |
|
||||
|-----------|---------------|-------------|--------|
|
||||
| `luminance` | `ND_luminance_*` | RGB to luminance | `in` |
|
||||
| `constant` | `ND_constant_*` | Constant value | `value` |
|
||||
|
||||
#### Conditional
|
||||
|
||||
| Node Type | MaterialX Name | Description | Inputs |
|
||||
|-----------|---------------|-------------|--------|
|
||||
| `ifgreater` | `ND_ifgreater_*` | Conditional select | `value1`, `value2`, `in1`, `in2` |
|
||||
|
||||
### Node Connection Syntax
|
||||
|
||||
In USD, MaterialX node connections use the standard connection syntax:
|
||||
|
||||
```usda
|
||||
# Connect texture coordinate to image node
|
||||
float2 inputs:texcoord.connect = </Material/NodeGraph/texcoord.outputs:out>
|
||||
|
||||
# Connect image output to shader input
|
||||
color3f inputs:base_color.connect = </Material/NodeGraph/diffuse_image.outputs:out>
|
||||
```
|
||||
|
||||
### Primary UV Set Configuration
|
||||
|
||||
TinyUSDZ supports configuring the primary UV set name (similar to OpenUSD's `USDMTLX_PRIMARY_UV_NAME`):
|
||||
|
||||
```cpp
|
||||
// C++ configuration
|
||||
tinyusdz::MtlxConfig config;
|
||||
config.primary_uv_name = "st"; // Default UV set name
|
||||
config.secondary_uv_name_prefix = "st"; // Pattern for st1, st2, etc.
|
||||
```
|
||||
|
||||
### Example: Complete MaterialX Material in USD
|
||||
|
||||
```usda
|
||||
def Material "OpenPBRMaterial" {
|
||||
token outputs:surface.connect = </OpenPBRMaterial/OpenPBRShader.outputs:surface>
|
||||
|
||||
def Shader "OpenPBRShader" {
|
||||
uniform token info:id = "ND_open_pbr_surface_surfaceshader"
|
||||
|
||||
# Base layer with texture
|
||||
color3f inputs:base_color.connect = </OpenPBRMaterial/NodeGraph.outputs:base_color>
|
||||
float inputs:base_metalness = 0.0
|
||||
|
||||
# Specular layer
|
||||
float inputs:specular_roughness.connect = </OpenPBRMaterial/NodeGraph.outputs:roughness>
|
||||
float inputs:specular_ior = 1.5
|
||||
|
||||
# Normal map
|
||||
normal3f inputs:geometry_normal.connect = </OpenPBRMaterial/NodeGraph.outputs:normal>
|
||||
|
||||
token outputs:surface
|
||||
}
|
||||
|
||||
def NodeGraph "NodeGraph" {
|
||||
# UV coordinates
|
||||
def Shader "texcoord" {
|
||||
uniform token info:id = "ND_texcoord_vector2"
|
||||
int inputs:index = 0
|
||||
float2 outputs:out
|
||||
}
|
||||
|
||||
# Base color texture
|
||||
def Shader "diffuse_tex" {
|
||||
uniform token info:id = "ND_image_color3"
|
||||
asset inputs:file = @textures/diffuse.png@
|
||||
string inputs:filtertype = "linear"
|
||||
float2 inputs:texcoord.connect = </OpenPBRMaterial/NodeGraph/texcoord.outputs:out>
|
||||
color3f outputs:out
|
||||
}
|
||||
|
||||
# Roughness texture
|
||||
def Shader "roughness_tex" {
|
||||
uniform token info:id = "ND_image_float"
|
||||
asset inputs:file = @textures/roughness.png@
|
||||
float2 inputs:texcoord.connect = </OpenPBRMaterial/NodeGraph/texcoord.outputs:out>
|
||||
float outputs:out
|
||||
}
|
||||
|
||||
# Normal map
|
||||
def Shader "normal_tex" {
|
||||
uniform token info:id = "ND_normalmap"
|
||||
asset inputs:file = @textures/normal.png@
|
||||
float2 inputs:texcoord.connect = </OpenPBRMaterial/NodeGraph/texcoord.outputs:out>
|
||||
normal3f outputs:out
|
||||
}
|
||||
|
||||
# NodeGraph outputs
|
||||
color3f outputs:base_color.connect = </OpenPBRMaterial/NodeGraph/diffuse_tex.outputs:out>
|
||||
float outputs:roughness.connect = </OpenPBRMaterial/NodeGraph/roughness_tex.outputs:out>
|
||||
normal3f outputs:normal.connect = </OpenPBRMaterial/NodeGraph/normal_tex.outputs:out>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Surface Shaders
|
||||
|
||||
TinyUSDZ supports the following MaterialX surface shader types:
|
||||
|
||||
| Shader Type | C++ Struct | info:id Value |
|
||||
|-------------|-----------|---------------|
|
||||
| OpenPBR Surface | `MtlxOpenPBRSurface` | `ND_open_pbr_surface_surfaceshader` |
|
||||
| Standard Surface | `MtlxAutodeskStandardSurface` | `ND_standard_surface_surfaceshader` |
|
||||
| USD Preview Surface | `MtlxUsdPreviewSurface` | `ND_UsdPreviewSurface_surfaceshader` |
|
||||
|
||||
### Light Shader Nodes (EDF)
|
||||
|
||||
MaterialX light shaders (Emission Distribution Functions) are also supported:
|
||||
|
||||
| Node Type | Description | Key Inputs |
|
||||
|-----------|-------------|------------|
|
||||
| `uniform_edf` | Uniform light emission | `color` |
|
||||
| `conical_edf` | Conical/spot light emission | `color`, `inner_angle`, `outer_angle` |
|
||||
| `measured_edf` | IES profile emission | `color`, `file` |
|
||||
| `light` | Light shader wrapper | `edf`, `intensity` |
|
||||
|
||||
### Implementation Status
|
||||
|
||||
| Feature | C++ Core | JavaScript/Three.js |
|
||||
|---------|----------|---------------------|
|
||||
| NodeGraph parsing | ✅ Full | ✅ Basic |
|
||||
| Surface shader conversion | ✅ OpenPBR, Standard, Preview | ✅ OpenPBR |
|
||||
| Math nodes | ⚠️ Partial | ✅ Full |
|
||||
| Texture nodes | ✅ image, tiledimage | ✅ image, tiledimage |
|
||||
| Geometry nodes | ✅ texcoord, normal | ✅ texcoord, position, normal |
|
||||
| Light nodes (EDF) | ✅ Full | ❌ Not implemented |
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Color Space Matrices
|
||||
|
||||
@@ -8,6 +8,7 @@ import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js';
|
||||
import GUI from 'three/examples/jsm/libs/lil-gui.module.min.js';
|
||||
import { TinyUSDZLoader } from 'tinyusdz/TinyUSDZLoader.js';
|
||||
import { TinyUSDZLoaderUtils } from 'tinyusdz/TinyUSDZLoaderUtils.js';
|
||||
import { setTinyUSDZ as setMaterialXTinyUSDZ } from 'tinyusdz/TinyUSDZMaterialX.js';
|
||||
import { OpenPBRMaterial } from './OpenPBRMaterial.js';
|
||||
import { OpenPBRValidator, OpenPBRGroundTruth } from './OpenPBRValidation.js';
|
||||
|
||||
@@ -526,6 +527,12 @@ async function initLoader() {
|
||||
updateStatus('Initializing TinyUSDZ WASM...');
|
||||
loaderState.loader = new TinyUSDZLoader(null, { maxMemoryLimitMB: 512 });
|
||||
await loaderState.loader.init({ useMemory64: false });
|
||||
|
||||
// Set TinyUSDZ WASM module reference for HDR/EXR texture decoding fallback
|
||||
const tinyusdzModule = loaderState.loader.native_;
|
||||
TinyUSDZLoaderUtils.setTinyUSDZ(tinyusdzModule);
|
||||
setMaterialXTinyUSDZ(tinyusdzModule);
|
||||
|
||||
updateStatus('TinyUSDZ initialized');
|
||||
}
|
||||
|
||||
@@ -1773,8 +1780,8 @@ async function convertMaterial(matData, index) {
|
||||
let material;
|
||||
|
||||
if (useOpenPBRMaterial) {
|
||||
// Create OpenPBRMaterial directly (async for texture loading)
|
||||
material = await convertToOpenPBRMaterial(matData, loaderState.nativeLoader);
|
||||
// Create OpenPBRMaterial directly (Loaded version waits for textures)
|
||||
material = await convertToOpenPBRMaterialLoaded(matData, loaderState.nativeLoader);
|
||||
material.envMap = threeState.envMap;
|
||||
material.envMapIntensity = settings.envMapIntensity;
|
||||
} else {
|
||||
@@ -1891,132 +1898,298 @@ function extractTextureId(param) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OpenPBRMaterial Conversion Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Extract value from OpenPBR parameter
|
||||
*/
|
||||
function extractOpenPBRValue(param, defaultVal) {
|
||||
if (param === undefined || param === null) return defaultVal;
|
||||
if (typeof param === 'object' && param.value !== undefined) return param.value;
|
||||
if (typeof param === 'number') return param;
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract color from OpenPBR parameter
|
||||
*/
|
||||
function extractOpenPBRColor(param, defaultVal) {
|
||||
if (!param) return new THREE.Color(...defaultVal);
|
||||
if (param.r !== undefined) return new THREE.Color(param.r, param.g, param.b);
|
||||
if (Array.isArray(param)) return new THREE.Color(...param);
|
||||
if (typeof param === 'object' && param.value) {
|
||||
if (Array.isArray(param.value)) return new THREE.Color(...param.value);
|
||||
if (param.value.r !== undefined) return new THREE.Color(param.value.r, param.value.g, param.value.b);
|
||||
}
|
||||
return new THREE.Color(...defaultVal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OpenPBR parameter has a texture
|
||||
*/
|
||||
function hasOpenPBRTexture(param) {
|
||||
return param && typeof param === 'object' && param.textureId !== undefined && param.textureId >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get texture ID from OpenPBR parameter
|
||||
*/
|
||||
function getOpenPBRTextureId(param) {
|
||||
if (!param || typeof param !== 'object') return -1;
|
||||
return param.textureId !== undefined ? param.textureId : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve actual texture ID (handles duplicate images in USDZ)
|
||||
*/
|
||||
function resolveTextureId(nativeLoader, textureId) {
|
||||
if (textureId < 0) return textureId;
|
||||
try {
|
||||
const tex = nativeLoader.getTexture(textureId);
|
||||
const texImage = nativeLoader.getImage(tex.textureImageId);
|
||||
|
||||
if (texImage.bufferId === -1 && texImage.uri) {
|
||||
const filename = texImage.uri.replace(/^\.\//, '');
|
||||
const numImages = nativeLoader.numImages();
|
||||
for (let i = 0; i < numImages; i++) {
|
||||
const altImage = nativeLoader.getImage(i);
|
||||
if (altImage.bufferId >= 0 && altImage.uri === filename) {
|
||||
const numTextures = nativeLoader.numTextures();
|
||||
for (let t = 0; t < numTextures; t++) {
|
||||
const altTex = nativeLoader.getTexture(t);
|
||||
if (altTex.textureImageId === i) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors, return original ID
|
||||
}
|
||||
return textureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create base OpenPBRMaterial with scalar/color values (no textures)
|
||||
*/
|
||||
function createBaseOpenPBRMaterial(openPBR) {
|
||||
const material = new OpenPBRMaterial({
|
||||
// Base layer
|
||||
base_weight: extractOpenPBRValue(openPBR.base_weight, 1.0),
|
||||
base_color: extractOpenPBRColor(openPBR.base_color, [0.8, 0.8, 0.8]),
|
||||
base_metalness: extractOpenPBRValue(openPBR.base_metalness, 0.0),
|
||||
base_diffuse_roughness: extractOpenPBRValue(openPBR.base_diffuse_roughness, 0.0),
|
||||
|
||||
// Specular layer
|
||||
specular_weight: extractOpenPBRValue(openPBR.specular_weight, 1.0),
|
||||
specular_color: extractOpenPBRColor(openPBR.specular_color, [1.0, 1.0, 1.0]),
|
||||
specular_roughness: extractOpenPBRValue(openPBR.specular_roughness, 0.3),
|
||||
specular_ior: extractOpenPBRValue(openPBR.specular_ior, 1.5),
|
||||
specular_anisotropy: extractOpenPBRValue(openPBR.specular_anisotropy, 0.0),
|
||||
|
||||
// Coat layer
|
||||
coat_weight: extractOpenPBRValue(openPBR.coat_weight, 0.0),
|
||||
coat_color: extractOpenPBRColor(openPBR.coat_color, [1.0, 1.0, 1.0]),
|
||||
coat_roughness: extractOpenPBRValue(openPBR.coat_roughness, 0.0),
|
||||
coat_ior: extractOpenPBRValue(openPBR.coat_ior, 1.5),
|
||||
|
||||
// Fuzz layer
|
||||
fuzz_weight: extractOpenPBRValue(openPBR.fuzz_weight || openPBR.sheen_weight, 0.0),
|
||||
fuzz_color: extractOpenPBRColor(openPBR.fuzz_color || openPBR.sheen_color, [1.0, 1.0, 1.0]),
|
||||
fuzz_roughness: extractOpenPBRValue(openPBR.fuzz_roughness || openPBR.sheen_roughness, 0.5),
|
||||
|
||||
// Thin film
|
||||
thin_film_weight: extractOpenPBRValue(openPBR.thin_film_weight, 0.0),
|
||||
thin_film_thickness: extractOpenPBRValue(openPBR.thin_film_thickness, 500.0),
|
||||
thin_film_ior: extractOpenPBRValue(openPBR.thin_film_ior, 1.5),
|
||||
|
||||
// Emission
|
||||
emission_luminance: extractOpenPBRValue(openPBR.emission_luminance, 0.0),
|
||||
emission_color: extractOpenPBRColor(openPBR.emission_color, [1.0, 1.0, 1.0]),
|
||||
|
||||
// Geometry
|
||||
geometry_opacity: extractOpenPBRValue(openPBR.geometry_opacity || openPBR.opacity, 1.0)
|
||||
});
|
||||
|
||||
// Store texture references for later management
|
||||
material.userData.textures = {};
|
||||
material.userData.materialType = 'OpenPBRMaterial';
|
||||
|
||||
return material;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert material data to OpenPBRMaterial
|
||||
* Waits for all textures to load before returning.
|
||||
* @param {Object} matData - Material data from USD
|
||||
* @param {Object} nativeLoader - TinyUSDZ native loader for texture access
|
||||
* @returns {Promise<OpenPBRMaterial>} The created material
|
||||
*/
|
||||
async function convertToOpenPBRMaterial(matData, nativeLoader = null) {
|
||||
async function convertToOpenPBRMaterialLoaded(matData, nativeLoader = null) {
|
||||
const openPBR = matData.openPBR || matData.openPBRShader || matData;
|
||||
|
||||
// Extract values with fallbacks
|
||||
const extractValue = (param, defaultVal) => {
|
||||
if (param === undefined || param === null) return defaultVal;
|
||||
if (typeof param === 'object' && param.value !== undefined) return param.value;
|
||||
if (typeof param === 'number') return param;
|
||||
return defaultVal;
|
||||
};
|
||||
|
||||
const extractColor = (param, defaultVal) => {
|
||||
if (!param) return new THREE.Color(...defaultVal);
|
||||
if (param.r !== undefined) return new THREE.Color(param.r, param.g, param.b);
|
||||
if (Array.isArray(param)) return new THREE.Color(...param);
|
||||
if (typeof param === 'object' && param.value) {
|
||||
if (Array.isArray(param.value)) return new THREE.Color(...param.value);
|
||||
if (param.value.r !== undefined) return new THREE.Color(param.value.r, param.value.g, param.value.b);
|
||||
}
|
||||
return new THREE.Color(...defaultVal);
|
||||
};
|
||||
|
||||
const material = new OpenPBRMaterial({
|
||||
// Base layer
|
||||
base_weight: extractValue(openPBR.base_weight, 1.0),
|
||||
base_color: extractColor(openPBR.base_color, [0.8, 0.8, 0.8]),
|
||||
base_metalness: extractValue(openPBR.base_metalness, 0.0),
|
||||
base_diffuse_roughness: extractValue(openPBR.base_diffuse_roughness, 0.0),
|
||||
|
||||
// Specular layer
|
||||
specular_weight: extractValue(openPBR.specular_weight, 1.0),
|
||||
specular_color: extractColor(openPBR.specular_color, [1.0, 1.0, 1.0]),
|
||||
specular_roughness: extractValue(openPBR.specular_roughness, 0.3),
|
||||
specular_ior: extractValue(openPBR.specular_ior, 1.5),
|
||||
specular_anisotropy: extractValue(openPBR.specular_anisotropy, 0.0),
|
||||
|
||||
// Coat layer
|
||||
coat_weight: extractValue(openPBR.coat_weight, 0.0),
|
||||
coat_color: extractColor(openPBR.coat_color, [1.0, 1.0, 1.0]),
|
||||
coat_roughness: extractValue(openPBR.coat_roughness, 0.0),
|
||||
coat_ior: extractValue(openPBR.coat_ior, 1.5),
|
||||
|
||||
// Fuzz layer
|
||||
fuzz_weight: extractValue(openPBR.fuzz_weight || openPBR.sheen_weight, 0.0),
|
||||
fuzz_color: extractColor(openPBR.fuzz_color || openPBR.sheen_color, [1.0, 1.0, 1.0]),
|
||||
fuzz_roughness: extractValue(openPBR.fuzz_roughness || openPBR.sheen_roughness, 0.5),
|
||||
|
||||
// Thin film
|
||||
thin_film_weight: extractValue(openPBR.thin_film_weight, 0.0),
|
||||
thin_film_thickness: extractValue(openPBR.thin_film_thickness, 500.0),
|
||||
thin_film_ior: extractValue(openPBR.thin_film_ior, 1.5),
|
||||
|
||||
// Emission
|
||||
emission_luminance: extractValue(openPBR.emission_luminance, 0.0),
|
||||
emission_color: extractColor(openPBR.emission_color, [1.0, 1.0, 1.0]),
|
||||
|
||||
// Geometry
|
||||
geometry_opacity: extractValue(openPBR.geometry_opacity || openPBR.opacity, 1.0)
|
||||
});
|
||||
|
||||
// Set color alias for compatibility
|
||||
material.uniforms.base_color.value.copy(material.uniforms.base_color.value);
|
||||
|
||||
// Load normal map texture if available
|
||||
// The normal map is in openPBR.geometry.normal (not openPBR.normal)
|
||||
const material = createBaseOpenPBRMaterial(openPBR);
|
||||
const geometrySection = openPBR.geometry || {};
|
||||
const normalParam = geometrySection.normal || openPBR.normal || openPBR.geometry_normal;
|
||||
const normalTextureId = extractTextureId(normalParam);
|
||||
if (normalTextureId >= 0 && nativeLoader) {
|
||||
|
||||
if (!nativeLoader) return material;
|
||||
|
||||
// Load base color texture (map)
|
||||
if (hasOpenPBRTexture(openPBR.base_color)) {
|
||||
try {
|
||||
// nativeLoader itself has getTexture, getImage methods - use it directly as usdScene
|
||||
// First, try to find an image with valid bufferId for the same filename
|
||||
// This works around the issue where TinyUSDZ creates duplicate images with different paths
|
||||
let actualTextureId = normalTextureId;
|
||||
const tex = nativeLoader.getTexture(normalTextureId);
|
||||
const texImage = nativeLoader.getImage(tex.textureImageId);
|
||||
|
||||
if (texImage.bufferId === -1 && texImage.uri) {
|
||||
// Try to find an alternative image with the same filename but valid bufferId
|
||||
const filename = texImage.uri.replace(/^\.\//, ''); // Remove leading ./
|
||||
const numImages = nativeLoader.numImages();
|
||||
for (let i = 0; i < numImages; i++) {
|
||||
const altImage = nativeLoader.getImage(i);
|
||||
if (altImage.bufferId >= 0 && altImage.uri === filename) {
|
||||
// Find a texture that uses this image
|
||||
const numTextures = nativeLoader.numTextures();
|
||||
for (let t = 0; t < numTextures; t++) {
|
||||
const altTex = nativeLoader.getTexture(t);
|
||||
if (altTex.textureImageId === i) {
|
||||
actualTextureId = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
const texId = resolveTextureId(nativeLoader, getOpenPBRTextureId(openPBR.base_color));
|
||||
const texture = await TinyUSDZLoaderUtils.getTextureFromUSD(nativeLoader, texId);
|
||||
if (texture) {
|
||||
material.map = texture;
|
||||
material.userData.textures.map = { textureId: texId, texture };
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to load base color texture:', err);
|
||||
}
|
||||
}
|
||||
|
||||
const texture = await TinyUSDZLoaderUtils.getTextureFromUSD(nativeLoader, actualTextureId);
|
||||
// Load roughness texture
|
||||
if (hasOpenPBRTexture(openPBR.specular_roughness)) {
|
||||
try {
|
||||
const texId = resolveTextureId(nativeLoader, getOpenPBRTextureId(openPBR.specular_roughness));
|
||||
const texture = await TinyUSDZLoaderUtils.getTextureFromUSD(nativeLoader, texId);
|
||||
if (texture) {
|
||||
material.roughnessMap = texture;
|
||||
material.userData.textures.roughnessMap = { textureId: texId, texture };
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to load roughness texture:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Load metalness texture
|
||||
if (hasOpenPBRTexture(openPBR.base_metalness)) {
|
||||
try {
|
||||
const texId = resolveTextureId(nativeLoader, getOpenPBRTextureId(openPBR.base_metalness));
|
||||
const texture = await TinyUSDZLoaderUtils.getTextureFromUSD(nativeLoader, texId);
|
||||
if (texture) {
|
||||
material.metalnessMap = texture;
|
||||
material.userData.textures.metalnessMap = { textureId: texId, texture };
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to load metalness texture:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Load emissive texture
|
||||
if (hasOpenPBRTexture(openPBR.emission_color)) {
|
||||
try {
|
||||
const texId = resolveTextureId(nativeLoader, getOpenPBRTextureId(openPBR.emission_color));
|
||||
const texture = await TinyUSDZLoaderUtils.getTextureFromUSD(nativeLoader, texId);
|
||||
if (texture) {
|
||||
material.emissiveMap = texture;
|
||||
material.userData.textures.emissiveMap = { textureId: texId, texture };
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to load emissive texture:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Load normal map
|
||||
const normalParam = geometrySection.normal || openPBR.normal || openPBR.geometry_normal;
|
||||
if (hasOpenPBRTexture(normalParam)) {
|
||||
try {
|
||||
const texId = resolveTextureId(nativeLoader, getOpenPBRTextureId(normalParam));
|
||||
const texture = await TinyUSDZLoaderUtils.getTextureFromUSD(nativeLoader, texId);
|
||||
if (texture) {
|
||||
material.normalMap = texture;
|
||||
|
||||
// Apply normal map scale if available
|
||||
const normalMapScale = extractValue(openPBR.normal_map_scale, 1.0);
|
||||
material.userData.textures.normalMap = { textureId: texId, texture };
|
||||
const normalMapScale = extractOpenPBRValue(geometrySection.normal_map_scale || openPBR.normal_map_scale, 1.0);
|
||||
if (material.uniforms.normalScale) {
|
||||
material.uniforms.normalScale.value.set(normalMapScale, normalMapScale);
|
||||
}
|
||||
|
||||
// Normal map loaded successfully
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to load normal map texture:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Store geometry_normal info in userData for UI display
|
||||
// Load AO map (if available in geometry section)
|
||||
const aoParam = geometrySection.ambient_occlusion || openPBR.ambient_occlusion;
|
||||
if (hasOpenPBRTexture(aoParam)) {
|
||||
try {
|
||||
const texId = resolveTextureId(nativeLoader, getOpenPBRTextureId(aoParam));
|
||||
const texture = await TinyUSDZLoaderUtils.getTextureFromUSD(nativeLoader, texId);
|
||||
if (texture) {
|
||||
material.aoMap = texture;
|
||||
material.userData.textures.aoMap = { textureId: texId, texture };
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to load AO texture:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Store geometry info in userData
|
||||
material.userData.geometry_normal = {
|
||||
hasTexture: normalTextureId >= 0,
|
||||
textureId: normalTextureId,
|
||||
scale: extractValue(openPBR.normal_map_scale, 1.0)
|
||||
hasTexture: hasOpenPBRTexture(normalParam),
|
||||
textureId: getOpenPBRTextureId(normalParam),
|
||||
scale: extractOpenPBRValue(geometrySection.normal_map_scale || openPBR.normal_map_scale, 1.0)
|
||||
};
|
||||
|
||||
return material;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert material data to OpenPBRMaterial (legacy pattern)
|
||||
* Returns material immediately, textures load asynchronously in background.
|
||||
* @param {Object} matData - Material data from USD
|
||||
* @param {Object} nativeLoader - TinyUSDZ native loader for texture access
|
||||
* @returns {OpenPBRMaterial} The created material (textures load asynchronously)
|
||||
*/
|
||||
function convertToOpenPBRMaterial(matData, nativeLoader = null) {
|
||||
const openPBR = matData.openPBR || matData.openPBRShader || matData;
|
||||
const material = createBaseOpenPBRMaterial(openPBR);
|
||||
const geometrySection = openPBR.geometry || {};
|
||||
|
||||
if (!nativeLoader) return material;
|
||||
|
||||
// Helper for fire-and-forget texture loading
|
||||
const loadTextureAsync = (param, mapName, onLoad = null) => {
|
||||
if (!hasOpenPBRTexture(param)) return;
|
||||
const texId = resolveTextureId(nativeLoader, getOpenPBRTextureId(param));
|
||||
TinyUSDZLoaderUtils.getTextureFromUSD(nativeLoader, texId).then((texture) => {
|
||||
if (texture) {
|
||||
material[mapName] = texture;
|
||||
material.userData.textures[mapName] = { textureId: texId, texture };
|
||||
material.needsUpdate = true;
|
||||
if (onLoad) onLoad(texture);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.warn(`Failed to load ${mapName} texture:`, err);
|
||||
});
|
||||
};
|
||||
|
||||
// Queue texture loading (fire-and-forget)
|
||||
loadTextureAsync(openPBR.base_color, 'map');
|
||||
loadTextureAsync(openPBR.specular_roughness, 'roughnessMap');
|
||||
loadTextureAsync(openPBR.base_metalness, 'metalnessMap');
|
||||
loadTextureAsync(openPBR.emission_color, 'emissiveMap');
|
||||
|
||||
// Normal map with scale
|
||||
const normalParam = geometrySection.normal || openPBR.normal || openPBR.geometry_normal;
|
||||
loadTextureAsync(normalParam, 'normalMap', () => {
|
||||
const normalMapScale = extractOpenPBRValue(geometrySection.normal_map_scale || openPBR.normal_map_scale, 1.0);
|
||||
if (material.uniforms.normalScale) {
|
||||
material.uniforms.normalScale.value.set(normalMapScale, normalMapScale);
|
||||
}
|
||||
});
|
||||
|
||||
// AO map
|
||||
const aoParam = geometrySection.ambient_occlusion || openPBR.ambient_occlusion;
|
||||
loadTextureAsync(aoParam, 'aoMap');
|
||||
|
||||
// Store geometry info in userData
|
||||
material.userData.geometry_normal = {
|
||||
hasTexture: hasOpenPBRTexture(normalParam),
|
||||
textureId: getOpenPBRTextureId(normalParam),
|
||||
scale: extractOpenPBRValue(geometrySection.normal_map_scale || openPBR.normal_map_scale, 1.0)
|
||||
};
|
||||
|
||||
return material;
|
||||
|
||||
@@ -3,14 +3,34 @@ import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js';
|
||||
import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js';
|
||||
|
||||
import { LoaderUtils } from "three"
|
||||
import { convertOpenPBRToMeshPhysicalMaterial } from './TinyUSDZMaterialX.js';
|
||||
import { convertOpenPBRToMeshPhysicalMaterialLoaded } from './TinyUSDZMaterialX.js';
|
||||
import { decodeEXR as decodeEXRWithFallback } from './EXRDecoder.js';
|
||||
|
||||
class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
|
||||
// Static reference to TinyUSDZ WASM module for EXR fallback
|
||||
static _tinyusdz = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set TinyUSDZ WASM module for EXR decoding fallback
|
||||
* @param {Object} tinyusdz - TinyUSDZ WASM module instance
|
||||
*/
|
||||
static setTinyUSDZ(tinyusdz) {
|
||||
TinyUSDZLoaderUtils._tinyusdz = tinyusdz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TinyUSDZ WASM module
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
static getTinyUSDZ() {
|
||||
return TinyUSDZLoaderUtils._tinyusdz;
|
||||
}
|
||||
|
||||
static async getDataFromURI(uri) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
@@ -105,6 +125,14 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
if (data[0] === 0x52 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x46) {
|
||||
return 'image/webp';
|
||||
}
|
||||
// EXR magic bytes: 76 2F 31 01
|
||||
if (data[0] === 0x76 && data[1] === 0x2F && data[2] === 0x31 && data[3] === 0x01) {
|
||||
return 'image/x-exr';
|
||||
}
|
||||
// HDR magic bytes: "#?" (Radiance format)
|
||||
if (data[0] === 0x23 && data[1] === 0x3F) {
|
||||
return 'image/vnd.radiance';
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
@@ -127,12 +155,18 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
|
||||
if (texImage.uri && (texImage.bufferId == -1)) {
|
||||
// Case 1: URI only
|
||||
const lowerUri = texImage.uri.toLowerCase();
|
||||
|
||||
const loader = new THREE.TextureLoader();
|
||||
|
||||
//console.log("Loading texture from URI:", texImage.uri);
|
||||
// TODO: Use HDR/EXR loader if a uri is HDR/EXR file.
|
||||
return loader.loadAsync(texImage.uri);
|
||||
if (lowerUri.endsWith('.exr')) {
|
||||
// EXR: Use EXRLoader
|
||||
return new EXRLoader().loadAsync(texImage.uri);
|
||||
} else if (lowerUri.endsWith('.hdr')) {
|
||||
// HDR: Use HDRLoader
|
||||
return new HDRLoader().loadAsync(texImage.uri);
|
||||
} else {
|
||||
// Standard image
|
||||
return new THREE.TextureLoader().loadAsync(texImage.uri);
|
||||
}
|
||||
|
||||
} else if (texImage.bufferId >= 0 && texImage.data) {
|
||||
//console.log("case 2 or 3");
|
||||
@@ -160,19 +194,59 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
return Promise.resolve(texture);
|
||||
|
||||
} else {
|
||||
//console.log("case 3");
|
||||
// Case 2: Embedded but not decoded - check format
|
||||
try {
|
||||
const blob = new Blob([texImage.data], { type: this.getMimeType(texImage) });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const mimeType = this.getMimeType(texImage);
|
||||
|
||||
const loader = new THREE.TextureLoader();
|
||||
|
||||
//console.log("blobUrl", blobUrl);
|
||||
// TODO: Use HDR/EXR loader if a uri is HDR/EXR file.
|
||||
return loader.loadAsync(blobUrl);
|
||||
// Check if HDR/EXR format - use specialized decoders
|
||||
if (mimeType === 'image/x-exr') {
|
||||
// EXR: Use TinyUSDZ fallback decoder
|
||||
const texture = this.decodeEXRFromBuffer(texImage.data, 'float16');
|
||||
if (texture) {
|
||||
texture.flipY = true;
|
||||
return Promise.resolve(texture);
|
||||
}
|
||||
// Fallback to Three.js EXRLoader with blob URL
|
||||
const blob = new Blob([texImage.data], { type: mimeType });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
return new EXRLoader().loadAsync(blobUrl).finally(() => URL.revokeObjectURL(blobUrl));
|
||||
} else if (mimeType === 'image/vnd.radiance') {
|
||||
// HDR: Use TinyUSDZ decoder (faster)
|
||||
const tinyusdz = TinyUSDZLoaderUtils._tinyusdz;
|
||||
if (tinyusdz && typeof tinyusdz.decodeHDR === 'function') {
|
||||
const uint8Array = texImage.data instanceof Uint8Array
|
||||
? texImage.data
|
||||
: new Uint8Array(texImage.data);
|
||||
const result = tinyusdz.decodeHDR(uint8Array, 'float16');
|
||||
if (result.success) {
|
||||
const texture = new THREE.DataTexture(
|
||||
result.data,
|
||||
result.width,
|
||||
result.height,
|
||||
THREE.RGBAFormat,
|
||||
THREE.HalfFloatType
|
||||
);
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.flipY = true;
|
||||
texture.needsUpdate = true;
|
||||
return Promise.resolve(texture);
|
||||
}
|
||||
}
|
||||
// Fallback to Three.js HDRLoader
|
||||
const blob = new Blob([texImage.data], { type: mimeType });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
return new HDRLoader().loadAsync(blobUrl).finally(() => URL.revokeObjectURL(blobUrl));
|
||||
} else {
|
||||
// Standard image format
|
||||
const blob = new Blob([texImage.data], { type: mimeType });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const loader = new THREE.TextureLoader();
|
||||
return loader.loadAsync(blobUrl).finally(() => URL.revokeObjectURL(blobUrl));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create Blob from texture data:", error);
|
||||
return Promise.reject(new Error("Failed to create Blob from texture data"));
|
||||
console.error("Failed to decode texture data:", error);
|
||||
return Promise.reject(new Error("Failed to decode texture data"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,8 +550,8 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the TinyUSDZMaterialX converter
|
||||
const material = await convertOpenPBRToMeshPhysicalMaterial(parsedMaterial, usdScene, {
|
||||
// Use the TinyUSDZMaterialX converter (Loaded version waits for textures)
|
||||
const material = await convertOpenPBRToMeshPhysicalMaterialLoaded(parsedMaterial, usdScene, {
|
||||
envMap: options.envMap || null,
|
||||
envMapIntensity: options.envMapIntensity || 1.0,
|
||||
textureCache: options.textureCache || new Map()
|
||||
@@ -1205,33 +1279,109 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode EXR from buffer with Three.js primary + TinyUSDZ fallback
|
||||
* @param {ArrayBuffer|Uint8Array} buffer - EXR data
|
||||
* @param {string} [outputFormat='float16'] - Output format
|
||||
* @returns {THREE.DataTexture|null}
|
||||
*/
|
||||
static decodeEXRFromBuffer(buffer, outputFormat = 'float16') {
|
||||
const result = decodeEXRWithFallback(buffer, TinyUSDZLoaderUtils._tinyusdz, {
|
||||
outputFormat,
|
||||
preferThreeJS: true,
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.warn('EXR decode failed:', result.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create Three.js DataTexture from decoded data
|
||||
const texture = new THREE.DataTexture(
|
||||
result.data,
|
||||
result.width,
|
||||
result.height,
|
||||
THREE.RGBAFormat,
|
||||
result.format === 'float16' ? THREE.HalfFloatType : THREE.FloatType
|
||||
);
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.generateMipmaps = false;
|
||||
texture.needsUpdate = true;
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode environment map from buffer (supports EXR, HDR, and standard image formats)
|
||||
* Uses Three.js EXRLoader with TinyUSDZ fallback for EXR files
|
||||
*/
|
||||
static async decodeEnvmapFromBuffer(buffer, uri) {
|
||||
try {
|
||||
const lowerUri = uri.toLowerCase();
|
||||
const mimeType = this.getMimeTypeFromExtension(this.getFileExtension(uri)) || 'application/octet-stream';
|
||||
|
||||
const blob = new Blob([buffer], { type: mimeType });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
|
||||
let texture = null;
|
||||
|
||||
try {
|
||||
if (lowerUri.endsWith('.exr')) {
|
||||
texture = await new EXRLoader().loadAsync(objectUrl);
|
||||
} else if (lowerUri.endsWith('.hdr')) {
|
||||
texture = await new HDRLoader().loadAsync(objectUrl);
|
||||
} else {
|
||||
texture = await new THREE.TextureLoader().loadAsync(objectUrl);
|
||||
if (lowerUri.endsWith('.exr')) {
|
||||
// Use EXR decoder with TinyUSDZ fallback
|
||||
texture = this.decodeEXRFromBuffer(buffer, 'float16');
|
||||
|
||||
if (!texture) {
|
||||
// Last resort: try Three.js EXRLoader with blob URL
|
||||
const blob = new Blob([buffer], { type: 'image/x-exr' });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
try {
|
||||
texture = await new EXRLoader().loadAsync(objectUrl);
|
||||
} finally {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
}
|
||||
} else if (lowerUri.endsWith('.hdr')) {
|
||||
// HDR uses TinyUSDZ decoder (faster than Three.js)
|
||||
const tinyusdz = TinyUSDZLoaderUtils._tinyusdz;
|
||||
if (tinyusdz && typeof tinyusdz.decodeHDR === 'function') {
|
||||
const uint8Array = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
||||
const result = tinyusdz.decodeHDR(uint8Array, 'float16');
|
||||
if (result.success) {
|
||||
texture = new THREE.DataTexture(
|
||||
result.data,
|
||||
result.width,
|
||||
result.height,
|
||||
THREE.RGBAFormat,
|
||||
THREE.HalfFloatType
|
||||
);
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.generateMipmaps = false;
|
||||
texture.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (texture) {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
if (!texture) {
|
||||
// Fallback to Three.js HDRLoader
|
||||
const blob = new Blob([buffer], { type: 'image/vnd.radiance' });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
try {
|
||||
texture = await new HDRLoader().loadAsync(objectUrl);
|
||||
} finally {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
} else {
|
||||
// Standard image formats
|
||||
const mimeType = this.getMimeTypeFromExtension(this.getFileExtension(uri)) || 'application/octet-stream';
|
||||
const blob = new Blob([buffer], { type: mimeType });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
try {
|
||||
texture = await new THREE.TextureLoader().loadAsync(objectUrl);
|
||||
} finally {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (texture) {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
}
|
||||
|
||||
return texture;
|
||||
@@ -1297,6 +1447,7 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
|
||||
/**
|
||||
* Load DomeLight environment map directly from file
|
||||
* Uses TinyUSDZ for HDR (faster) and Three.js + TinyUSDZ fallback for EXR
|
||||
* @param {Object} light - USD light data
|
||||
* @param {string} textureFile - Texture file path
|
||||
* @param {THREE.PMREMGenerator} pmremGenerator - PMREM generator
|
||||
@@ -1307,9 +1458,10 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
let texture = null;
|
||||
const lowerFile = textureFile.toLowerCase();
|
||||
|
||||
// Pre-check if file exists and has valid content type
|
||||
// Fetch the file data
|
||||
let response;
|
||||
try {
|
||||
const response = await fetch(textureFile, { method: 'HEAD' });
|
||||
response = await fetch(textureFile);
|
||||
if (!response.ok) {
|
||||
console.warn(`DomeLight: Texture file not accessible '${textureFile}' (HTTP ${response.status})`);
|
||||
return null;
|
||||
@@ -1325,28 +1477,71 @@ class TinyUSDZLoaderUtils extends LoaderUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
if (lowerFile.endsWith('.exr')) {
|
||||
try {
|
||||
texture = await new EXRLoader().loadAsync(textureFile);
|
||||
} catch (exrError) {
|
||||
console.warn(`DomeLight: EXR load failed for '${textureFile}' - ${exrError.message}`);
|
||||
return null;
|
||||
// Use EXR decoder with TinyUSDZ fallback
|
||||
texture = this.decodeEXRFromBuffer(buffer, 'float16');
|
||||
|
||||
if (!texture) {
|
||||
// Fallback: try Three.js EXRLoader with blob URL
|
||||
try {
|
||||
const blob = new Blob([buffer], { type: 'image/x-exr' });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
try {
|
||||
texture = await new EXRLoader().loadAsync(objectUrl);
|
||||
} finally {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
} catch (exrError) {
|
||||
console.warn(`DomeLight: EXR load failed for '${textureFile}' - ${exrError.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else if (lowerFile.endsWith('.hdr')) {
|
||||
try {
|
||||
texture = await new HDRLoader().loadAsync(textureFile);
|
||||
} catch (hdrError) {
|
||||
console.warn(`DomeLight: HDR load failed for '${textureFile}' - ${hdrError.message}`);
|
||||
return null;
|
||||
// Use TinyUSDZ HDR decoder (faster than Three.js)
|
||||
const tinyusdz = TinyUSDZLoaderUtils._tinyusdz;
|
||||
if (tinyusdz && typeof tinyusdz.decodeHDR === 'function') {
|
||||
const uint8Array = new Uint8Array(buffer);
|
||||
const result = tinyusdz.decodeHDR(uint8Array, 'float16');
|
||||
if (result.success) {
|
||||
texture = new THREE.DataTexture(
|
||||
result.data,
|
||||
result.width,
|
||||
result.height,
|
||||
THREE.RGBAFormat,
|
||||
THREE.HalfFloatType
|
||||
);
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.generateMipmaps = false;
|
||||
texture.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!texture) {
|
||||
// Fallback to Three.js HDRLoader
|
||||
try {
|
||||
const blob = new Blob([buffer], { type: 'image/vnd.radiance' });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
try {
|
||||
texture = await new HDRLoader().loadAsync(objectUrl);
|
||||
} finally {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
} catch (hdrError) {
|
||||
console.warn(`DomeLight: HDR load failed for '${textureFile}' - ${hdrError.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn(`DomeLight: Unsupported texture format for '${textureFile}'`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if texture was loaded and has valid image data
|
||||
if (!texture || !texture.image) {
|
||||
console.warn(`DomeLight: Texture loaded but has no image data for '${textureFile}'`);
|
||||
// Check if texture was loaded and has valid data
|
||||
if (!texture) {
|
||||
console.warn(`DomeLight: Failed to decode texture for '${textureFile}'`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as THREE from 'three';
|
||||
import { decodeEXR as decodeEXRWithFallback } from './EXRDecoder.js';
|
||||
|
||||
/**
|
||||
* TinyUSDZ MaterialX / OpenPBR Material Utilities
|
||||
@@ -8,12 +9,23 @@ import * as THREE from 'three';
|
||||
*
|
||||
* Key features:
|
||||
* - OpenPBR parameter extraction from JSON format (flat and grouped)
|
||||
* - Texture loading from USD scene with caching
|
||||
* - Texture loading from USD scene with caching (including EXR/HDR)
|
||||
* - Full OpenPBR to MeshPhysicalMaterial conversion
|
||||
* - Support for all OpenPBR layers: base, specular, transmission, subsurface,
|
||||
* coat, sheen, fuzz, thin_film, emission, geometry
|
||||
*/
|
||||
|
||||
// Reference to TinyUSDZ WASM module for HDR/EXR decoding
|
||||
let _tinyusdz = null;
|
||||
|
||||
/**
|
||||
* Set TinyUSDZ WASM module for texture decoding
|
||||
* @param {Object} tinyusdz - TinyUSDZ WASM module
|
||||
*/
|
||||
export function setTinyUSDZ(tinyusdz) {
|
||||
_tinyusdz = tinyusdz;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OpenPBR Parameter Mapping to Three.js MeshPhysicalMaterial
|
||||
// ============================================================================
|
||||
@@ -166,14 +178,86 @@ function getMimeType(imgData) {
|
||||
// Check magic bytes
|
||||
if (imgData.data && imgData.data.length >= 4) {
|
||||
const data = new Uint8Array(imgData.data);
|
||||
// PNG magic: 0x89 0x50 0x4E 0x47
|
||||
if (data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4E && data[3] === 0x47) return 'image/png';
|
||||
// JPEG magic: 0xFF 0xD8 0xFF
|
||||
if (data[0] === 0xFF && data[1] === 0xD8 && data[2] === 0xFF) return 'image/jpeg';
|
||||
// WEBP magic: RIFF....WEBP
|
||||
if (data[0] === 0x52 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x46) return 'image/webp';
|
||||
// EXR magic: 0x76 0x2F 0x31 0x01
|
||||
if (data[0] === 0x76 && data[1] === 0x2F && data[2] === 0x31 && data[3] === 0x01) return 'image/x-exr';
|
||||
// HDR magic: "#?" (Radiance format)
|
||||
if (data[0] === 0x23 && data[1] === 0x3F) return 'image/vnd.radiance';
|
||||
}
|
||||
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if MIME type is HDR format (EXR or HDR)
|
||||
*/
|
||||
function isHDRFormat(mimeType) {
|
||||
return mimeType === 'image/x-exr' || mimeType === 'image/vnd.radiance';
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode HDR/EXR texture data and create Three.js DataTexture
|
||||
* @param {Uint8Array|ArrayBuffer} data - Image data
|
||||
* @param {string} mimeType - MIME type
|
||||
* @returns {THREE.DataTexture|null}
|
||||
*/
|
||||
function decodeHDRTexture(data, mimeType) {
|
||||
const buffer = data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
||||
|
||||
if (mimeType === 'image/x-exr') {
|
||||
// Use EXR decoder with TinyUSDZ fallback
|
||||
const result = decodeEXRWithFallback(buffer, _tinyusdz, {
|
||||
outputFormat: 'float16',
|
||||
preferThreeJS: true,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
const texture = new THREE.DataTexture(
|
||||
result.data,
|
||||
result.width,
|
||||
result.height,
|
||||
THREE.RGBAFormat,
|
||||
result.format === 'float16' ? THREE.HalfFloatType : THREE.FloatType
|
||||
);
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.generateMipmaps = false;
|
||||
texture.flipY = true;
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
} else if (mimeType === 'image/vnd.radiance') {
|
||||
// Use TinyUSDZ HDR decoder (faster)
|
||||
if (_tinyusdz && typeof _tinyusdz.decodeHDR === 'function') {
|
||||
const uint8Array = data instanceof Uint8Array ? data : new Uint8Array(buffer);
|
||||
const result = _tinyusdz.decodeHDR(uint8Array, 'float16');
|
||||
|
||||
if (result.success) {
|
||||
const texture = new THREE.DataTexture(
|
||||
result.data,
|
||||
result.width,
|
||||
result.height,
|
||||
THREE.RGBAFormat,
|
||||
THREE.HalfFloatType
|
||||
);
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.generateMipmaps = false;
|
||||
texture.flipY = true;
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Texture Loading
|
||||
// ============================================================================
|
||||
@@ -233,11 +317,16 @@ async function loadTextureFromUSD(usdScene, textureId, cache = null) {
|
||||
texture.needsUpdate = true;
|
||||
} else {
|
||||
const mimeType = getMimeType(altImgData);
|
||||
const blob = new Blob([altImgData.data], { type: mimeType });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const loader = new THREE.TextureLoader();
|
||||
texture = await loader.loadAsync(blobUrl);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
// Check if HDR format - use specialized decoder
|
||||
if (isHDRFormat(mimeType)) {
|
||||
texture = decodeHDRTexture(altImgData.data, mimeType);
|
||||
} else {
|
||||
const blob = new Blob([altImgData.data], { type: mimeType });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const loader = new THREE.TextureLoader();
|
||||
texture = await loader.loadAsync(blobUrl);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
}
|
||||
}
|
||||
foundEmbedded = true;
|
||||
break;
|
||||
@@ -270,14 +359,20 @@ async function loadTextureFromUSD(usdScene, textureId, cache = null) {
|
||||
texture.flipY = true;
|
||||
texture.needsUpdate = true;
|
||||
} else {
|
||||
// Needs decoding - create Blob and load
|
||||
// Needs decoding - check format and decode
|
||||
const mimeType = getMimeType(imgData);
|
||||
const blob = new Blob([imgData.data], { type: mimeType });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
const loader = new THREE.TextureLoader();
|
||||
texture = await loader.loadAsync(blobUrl);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
// Check if HDR format - use specialized decoder
|
||||
if (isHDRFormat(mimeType)) {
|
||||
texture = decodeHDRTexture(imgData.data, mimeType);
|
||||
} else {
|
||||
// Standard image - use Blob and TextureLoader
|
||||
const blob = new Blob([imgData.data], { type: mimeType });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const loader = new THREE.TextureLoader();
|
||||
texture = await loader.loadAsync(blobUrl);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,13 +394,14 @@ async function loadTextureFromUSD(usdScene, textureId, cache = null) {
|
||||
|
||||
/**
|
||||
* Convert OpenPBR material data to Three.js MeshPhysicalMaterial
|
||||
* Waits for all textures to load before returning the material.
|
||||
*
|
||||
* @param {Object} materialData - OpenPBR material data from USD
|
||||
* @param {Object} usdScene - USD scene for texture loading
|
||||
* @param {Object} options - Conversion options
|
||||
* @returns {Promise<THREE.MeshPhysicalMaterial>}
|
||||
*/
|
||||
async function convertOpenPBRToMeshPhysicalMaterial(materialData, usdScene = null, options = {}) {
|
||||
async function convertOpenPBRToMeshPhysicalMaterialLoaded(materialData, usdScene = null, options = {}) {
|
||||
const material = new THREE.MeshPhysicalMaterial();
|
||||
|
||||
// Store texture references for later management
|
||||
@@ -582,6 +678,320 @@ async function convertOpenPBRToMeshPhysicalMaterial(materialData, usdScene = nul
|
||||
return material;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert OpenPBR material data to Three.js MeshPhysicalMaterial (legacy pattern)
|
||||
* Returns material immediately, textures load asynchronously in the background.
|
||||
* This matches the behavior of convertUsdMaterialToMeshPhysicalMaterial.
|
||||
*
|
||||
* @param {Object} materialData - OpenPBR material data from USD
|
||||
* @param {Object} usdScene - USD scene for texture loading
|
||||
* @param {Object} options - Conversion options
|
||||
* @returns {THREE.MeshPhysicalMaterial} Material (textures load asynchronously)
|
||||
*/
|
||||
function convertOpenPBRToMeshPhysicalMaterial(materialData, usdScene = null, options = {}) {
|
||||
const material = new THREE.MeshPhysicalMaterial();
|
||||
|
||||
// Store texture references for later management
|
||||
material.userData.textures = {};
|
||||
material.userData.materialType = 'OpenPBR';
|
||||
material.userData.openPBRData = materialData;
|
||||
|
||||
// Get OpenPBR data - support multiple formats
|
||||
let pbr = null;
|
||||
|
||||
// Check for grouped format first
|
||||
if (materialData.openPBR) {
|
||||
pbr = materialData.openPBR;
|
||||
material.userData.format = 'grouped';
|
||||
}
|
||||
// Check for flat format
|
||||
else if (materialData.base_color !== undefined ||
|
||||
materialData.base_metalness !== undefined ||
|
||||
materialData.specular_roughness !== undefined) {
|
||||
pbr = { flat: materialData };
|
||||
material.userData.format = 'flat';
|
||||
}
|
||||
// Check for openPBRShader format
|
||||
else if (materialData.openPBRShader) {
|
||||
pbr = { flat: materialData.openPBRShader };
|
||||
material.userData.format = 'flat';
|
||||
}
|
||||
|
||||
if (!pbr) {
|
||||
console.warn('No OpenPBR data found in material');
|
||||
return material;
|
||||
}
|
||||
|
||||
// Texture cache
|
||||
const textureCache = options.textureCache || new Map();
|
||||
|
||||
// Helper to apply parameter value (sync) and queue texture loading (async)
|
||||
const applyParam = (paramName, paramValue) => {
|
||||
const mapping = OPENPBR_TO_THREEJS_MAP[paramName];
|
||||
if (!mapping || !mapping.property) return;
|
||||
|
||||
const value = extractValue(paramValue);
|
||||
|
||||
// Apply scalar or color value immediately
|
||||
if (mapping.type === 'color' && Array.isArray(value)) {
|
||||
material[mapping.property] = createColor(value);
|
||||
} else if (mapping.type === 'scalar' && typeof value === 'number') {
|
||||
material[mapping.property] = value;
|
||||
}
|
||||
|
||||
// Queue texture loading (fire-and-forget)
|
||||
if (usdScene && hasTexture(paramValue)) {
|
||||
const texMapName = OPENPBR_TEXTURE_MAP[paramName];
|
||||
if (texMapName) {
|
||||
loadTextureFromUSD(usdScene, getTextureId(paramValue), textureCache).then((texture) => {
|
||||
if (texture) {
|
||||
material[texMapName] = texture;
|
||||
material.userData.textures[texMapName] = {
|
||||
textureId: getTextureId(paramValue),
|
||||
texture: texture
|
||||
};
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(`Failed to load texture for ${paramName}:`, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Process flat format
|
||||
if (pbr.flat) {
|
||||
const flat = pbr.flat;
|
||||
|
||||
// Base layer
|
||||
applyParam('base_color', flat.base_color);
|
||||
applyParam('base_metalness', flat.base_metalness);
|
||||
|
||||
// Specular layer
|
||||
applyParam('specular_roughness', flat.specular_roughness);
|
||||
applyParam('specular_ior', flat.specular_ior);
|
||||
applyParam('specular_color', flat.specular_color);
|
||||
applyParam('specular_anisotropy', flat.specular_anisotropy);
|
||||
|
||||
// Transmission
|
||||
applyParam('transmission_weight', flat.transmission_weight);
|
||||
applyParam('transmission_color', flat.transmission_color);
|
||||
|
||||
// Coat
|
||||
applyParam('coat_weight', flat.coat_weight);
|
||||
applyParam('coat_roughness', flat.coat_roughness);
|
||||
|
||||
// Sheen/Fuzz
|
||||
if (flat.sheen_weight !== undefined) {
|
||||
applyParam('sheen_weight', flat.sheen_weight);
|
||||
applyParam('sheen_color', flat.sheen_color);
|
||||
applyParam('sheen_roughness', flat.sheen_roughness);
|
||||
} else if (flat.fuzz_weight !== undefined) {
|
||||
applyParam('fuzz_weight', flat.fuzz_weight);
|
||||
applyParam('fuzz_color', flat.fuzz_color);
|
||||
applyParam('fuzz_roughness', flat.fuzz_roughness);
|
||||
}
|
||||
|
||||
// Thin film (iridescence)
|
||||
if (flat.thin_film_weight !== undefined) {
|
||||
const weight = extractValue(flat.thin_film_weight);
|
||||
if (weight > 0) {
|
||||
material.iridescence = weight;
|
||||
const thickness = extractValue(flat.thin_film_thickness) || 500;
|
||||
material.iridescenceThicknessRange = [100, thickness];
|
||||
material.iridescenceIOR = extractValue(flat.thin_film_ior) || 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
// Emission
|
||||
if (flat.emission_color !== undefined) {
|
||||
const emissionColor = extractValue(flat.emission_color);
|
||||
if (emissionColor && Array.isArray(emissionColor)) {
|
||||
material.emissive = createColor(emissionColor);
|
||||
}
|
||||
// Load emission texture (fire-and-forget)
|
||||
if (usdScene && hasTexture(flat.emission_color)) {
|
||||
loadTextureFromUSD(usdScene, getTextureId(flat.emission_color), textureCache).then((texture) => {
|
||||
if (texture) {
|
||||
material.emissiveMap = texture;
|
||||
material.userData.textures.emissiveMap = { textureId: getTextureId(flat.emission_color), texture };
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error('Failed to load emission texture:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (flat.emission_luminance !== undefined) {
|
||||
material.emissiveIntensity = extractValue(flat.emission_luminance) || 0;
|
||||
}
|
||||
|
||||
// Geometry - opacity/alpha
|
||||
const opacityParam = flat.opacity !== undefined ? flat.opacity : flat.geometry_opacity;
|
||||
if (opacityParam !== undefined) {
|
||||
const opacityValue = extractValue(opacityParam);
|
||||
if (typeof opacityValue === 'number') {
|
||||
material.opacity = opacityValue;
|
||||
material.transparent = opacityValue < 1.0;
|
||||
}
|
||||
if (usdScene && hasTexture(opacityParam)) {
|
||||
loadTextureFromUSD(usdScene, getTextureId(opacityParam), textureCache).then((texture) => {
|
||||
if (texture) {
|
||||
material.alphaMap = texture;
|
||||
material.transparent = true;
|
||||
material.userData.textures.alphaMap = { textureId: getTextureId(opacityParam), texture };
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error('Failed to load opacity texture:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Normal map
|
||||
const normalParam = flat.normal !== undefined ? flat.normal : flat.geometry_normal;
|
||||
if (normalParam !== undefined && usdScene && hasTexture(normalParam)) {
|
||||
loadTextureFromUSD(usdScene, getTextureId(normalParam), textureCache).then((texture) => {
|
||||
if (texture) {
|
||||
material.normalMap = texture;
|
||||
material.normalScale = new THREE.Vector2(1, 1);
|
||||
material.userData.textures.normalMap = { textureId: getTextureId(normalParam), texture };
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error('Failed to load normal texture:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process grouped format
|
||||
else {
|
||||
// Base layer
|
||||
if (pbr.base) {
|
||||
applyParam('base_color', pbr.base.base_color);
|
||||
applyParam('base_metalness', pbr.base.base_metalness);
|
||||
}
|
||||
|
||||
// Specular layer
|
||||
if (pbr.specular) {
|
||||
applyParam('specular_roughness', pbr.specular.specular_roughness);
|
||||
applyParam('specular_ior', pbr.specular.specular_ior);
|
||||
applyParam('specular_color', pbr.specular.specular_color);
|
||||
applyParam('specular_anisotropy', pbr.specular.specular_anisotropy);
|
||||
}
|
||||
|
||||
// Transmission
|
||||
if (pbr.transmission) {
|
||||
applyParam('transmission_weight', pbr.transmission.transmission_weight);
|
||||
applyParam('transmission_color', pbr.transmission.transmission_color);
|
||||
}
|
||||
|
||||
// Coat
|
||||
if (pbr.coat) {
|
||||
applyParam('coat_weight', pbr.coat.coat_weight);
|
||||
applyParam('coat_roughness', pbr.coat.coat_roughness);
|
||||
}
|
||||
|
||||
// Sheen
|
||||
if (pbr.sheen) {
|
||||
applyParam('sheen_weight', pbr.sheen.sheen_weight);
|
||||
applyParam('sheen_color', pbr.sheen.sheen_color);
|
||||
applyParam('sheen_roughness', pbr.sheen.sheen_roughness);
|
||||
}
|
||||
|
||||
// Fuzz
|
||||
if (pbr.fuzz) {
|
||||
applyParam('fuzz_weight', pbr.fuzz.fuzz_weight);
|
||||
applyParam('fuzz_color', pbr.fuzz.fuzz_color);
|
||||
applyParam('fuzz_roughness', pbr.fuzz.fuzz_roughness);
|
||||
}
|
||||
|
||||
// Thin film
|
||||
if (pbr.thin_film) {
|
||||
const weight = extractValue(pbr.thin_film.thin_film_weight);
|
||||
if (weight > 0) {
|
||||
material.iridescence = weight;
|
||||
const thickness = extractValue(pbr.thin_film.thin_film_thickness) || 500;
|
||||
material.iridescenceThicknessRange = [100, thickness];
|
||||
material.iridescenceIOR = extractValue(pbr.thin_film.thin_film_ior) || 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
// Emission
|
||||
if (pbr.emission) {
|
||||
const emissionColor = extractValue(pbr.emission.emission_color);
|
||||
if (emissionColor && Array.isArray(emissionColor)) {
|
||||
material.emissive = createColor(emissionColor);
|
||||
}
|
||||
if (usdScene && hasTexture(pbr.emission.emission_color)) {
|
||||
loadTextureFromUSD(usdScene, getTextureId(pbr.emission.emission_color), textureCache).then((texture) => {
|
||||
if (texture) {
|
||||
material.emissiveMap = texture;
|
||||
material.userData.textures.emissiveMap = { textureId: getTextureId(pbr.emission.emission_color), texture };
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error('Failed to load emission texture:', err);
|
||||
});
|
||||
}
|
||||
if (pbr.emission.emission_luminance !== undefined) {
|
||||
material.emissiveIntensity = extractValue(pbr.emission.emission_luminance) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Geometry
|
||||
if (pbr.geometry) {
|
||||
const opacityParam = pbr.geometry.opacity !== undefined ? pbr.geometry.opacity : pbr.geometry.geometry_opacity;
|
||||
if (opacityParam !== undefined) {
|
||||
const opacityValue = extractValue(opacityParam);
|
||||
if (typeof opacityValue === 'number') {
|
||||
material.opacity = opacityValue;
|
||||
material.transparent = opacityValue < 1.0;
|
||||
}
|
||||
if (usdScene && hasTexture(opacityParam)) {
|
||||
loadTextureFromUSD(usdScene, getTextureId(opacityParam), textureCache).then((texture) => {
|
||||
if (texture) {
|
||||
material.alphaMap = texture;
|
||||
material.transparent = true;
|
||||
material.userData.textures.alphaMap = { textureId: getTextureId(opacityParam), texture };
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error('Failed to load opacity texture:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const normalParam = pbr.geometry.normal !== undefined ? pbr.geometry.normal : pbr.geometry.geometry_normal;
|
||||
if (normalParam !== undefined && usdScene && hasTexture(normalParam)) {
|
||||
loadTextureFromUSD(usdScene, getTextureId(normalParam), textureCache).then((texture) => {
|
||||
if (texture) {
|
||||
material.normalMap = texture;
|
||||
material.normalScale = new THREE.Vector2(1, 1);
|
||||
material.userData.textures.normalMap = { textureId: getTextureId(normalParam), texture };
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error('Failed to load normal texture:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply environment map if provided
|
||||
if (options.envMap) {
|
||||
material.envMap = options.envMap;
|
||||
material.envMapIntensity = options.envMapIntensity || 1.0;
|
||||
}
|
||||
|
||||
// Set material name
|
||||
if (materialData.name) {
|
||||
material.name = materialData.name;
|
||||
}
|
||||
|
||||
return material;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TinyUSDZOpenPBR Class (Legacy compatibility)
|
||||
// ============================================================================
|
||||
@@ -646,6 +1056,7 @@ class TinyUSDZOpenPBR {
|
||||
export {
|
||||
TinyUSDZOpenPBR,
|
||||
convertOpenPBRToMeshPhysicalMaterial,
|
||||
convertOpenPBRToMeshPhysicalMaterialLoaded,
|
||||
loadTextureFromUSD,
|
||||
extractValue,
|
||||
hasTexture,
|
||||
|
||||
Reference in New Issue
Block a user