Source code for axe_usd.usd.material_model
"""Shared data helpers for USD material processing."""
from dataclasses import dataclass
import logging
import re
from pathlib import Path, PurePosixPath
from typing import Dict, Mapping, Optional
from .types import MaterialTextureDict
SLOT_ALIASES = {
"height": "displacement",
}
_LOGGER = logging.getLogger(__name__)
[docs]
def normalize_slot_name(
slot: str, slot_aliases: Mapping[str, str] = SLOT_ALIASES
) -> str:
normalized = slot.strip().lower()
return slot_aliases.get(normalized, normalized)
[docs]
def normalize_material_dict(
material_dict: MaterialTextureDict,
logger: Optional[logging.Logger] = None,
slot_aliases: Mapping[str, str] = SLOT_ALIASES,
) -> MaterialTextureDict:
active_logger = logger or _LOGGER
normalized: MaterialTextureDict = {}
for slot, info in material_dict.items():
normalized_slot = normalize_slot_name(slot, slot_aliases=slot_aliases)
if normalized_slot in normalized and normalized_slot != slot:
active_logger.debug(
"Overriding texture slot '%s' with '%s'.", normalized_slot, slot
)
normalized[normalized_slot] = info
return normalized
[docs]
def normalize_asset_path(path: str) -> str:
if not path:
return path
path_str = str(path)
is_absolute = Path(path_str).is_absolute()
# On non-Windows, Path("C:/...") is not absolute. Check for drive letter.
if not is_absolute and re.match(r"^[a-zA-Z]:", path_str):
is_absolute = True
normalized = path_str.replace("\\", "/")
if normalized.startswith("./") or normalized.startswith("../"):
return normalized
if is_absolute:
return normalized
# For relative paths, prepend ./ if safe (not already containing protocol/drive)
return f"./{normalized}"
[docs]
def apply_texture_format_override(path: str, override: Optional[str]) -> str:
normalized = normalize_asset_path(path)
if not override:
return normalized
ext = override if override.startswith(".") else f".{override}"
prefix = "./" if normalized.startswith("./") else ""
working = normalized[2:] if prefix else normalized
posix_path = PurePosixPath(working)
if posix_path.suffix:
posix_path = posix_path.with_suffix(ext)
else:
posix_path = PurePosixPath(f"{posix_path}{ext}")
return f"{prefix}{posix_path.as_posix()}"
[docs]
def is_transmissive_material(
material_name: str,
tokens: Optional[tuple[str, ...]] = None,
) -> bool:
if not material_name:
return False
active_tokens = tokens or ("glass", "glas")
lower_name = material_name.lower()
return any(token in lower_name for token in active_tokens)
[docs]
@dataclass(frozen=True)
class TextureFormatOverrides:
overrides: Dict[str, str]
[docs]
@classmethod
def from_mapping(
cls, texture_format_overrides: Optional[Mapping[str, str]]
) -> "TextureFormatOverrides":
if not texture_format_overrides:
return cls({})
normalized: Dict[str, str] = {}
for key, value in texture_format_overrides.items():
normalized[key.lower()] = value
return cls(normalized)
[docs]
def for_renderer(self, renderer: str) -> Optional[str]:
return self.overrides.get(renderer)