from __future__ import annotations
import json
from dataclasses import dataclass, field
from pathlib import Path
[docs]
def _float_pair(values, default: tuple[float, float]) -> tuple[float, float]:
if not isinstance(values, (list, tuple)) or len(values) != 2:
return default
return float(values[0]), float(values[1])
[docs]
def _float_vec3(values, default: tuple[float, float, float]) -> tuple[float, float, float]:
if not isinstance(values, (list, tuple)) or len(values) != 3:
return default
return float(values[0]), float(values[1]), float(values[2])
[docs]
def _float_vec4(values, default: tuple[float, float, float, float]) -> tuple[float, float, float, float]:
if not isinstance(values, (list, tuple)) or len(values) != 4:
return default
return float(values[0]), float(values[1]), float(values[2]), float(values[3])
@dataclass
[docs]
class PolygonPBRMaterial:
"""Optional PBR-like material for filled polygon rendering.
The material blends texture details (albedo, normal, ORM, emissive) with global
control factors (metallic_factor, roughness_factor, etc.) to achieve intuitive
per-pixel variation and global override capability.
Key shading formula:
- metallic: blends ORM texture metallic channel with metallic_factor
- factor=0 → fully controlled by texture
- factor=1 → forces fully metallic everywhere
- 0<factor<1 → smooth interpolation enables artistic control
- roughness: scales ORM roughness channel by roughness_factor
- normal: applies normalScale to detail intensity from normal texture
"""
[docs]
albedo_texture: str = ''
[docs]
normal_texture: str = ''
[docs]
orm_texture: str = ''
[docs]
emissive_texture: str = ''
[docs]
base_color_factor: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0)
"""Metallic blending control [0..1].
0 = texture determines metallic appearance.
1 = surface fully metallic. Use >0 to add metallic character to any surface."""
[docs]
roughness_factor: float = 1.0
"""Surface roughness multiplier [0.04..1.0].
Scales the ORM texture roughness channel. Values <1 = smoother, >1 = rougher.
Interacts with ORM texture; pure texture control requires factor=1."""
[docs]
normal_scale: float = 1.0
"""Normal map intensity [0..∞].
Controls detail relief from normal texture. 0 = flat, 1 = full strength."""
[docs]
occlusion_strength: float = 1.0
"""Ambient occlusion intensity [0..1].
Scales ORM texture AO channel into shadow; 0 = no shadows, 1 = full."""
[docs]
emissive_factor: tuple[float, float, float] = (0.0, 0.0, 0.0)
"""Emissive RGB [0..∞]. Additive glow color. Blends with emissive texture if present."""
[docs]
uv_scale: tuple[float, float] = (100.0, 100.0)
"""Texture tiling scale. Larger = more repetitions."""
[docs]
uv_offset: tuple[float, float] = (0.0, 0.0)
"""Texture coordinate translation for animation or alignment."""
[docs]
cushion_strength: float = 0.0
"""Cushion/pillow effect strength [0..2].
Tilts surface normals near polygon edges to simulate a raised border.
0 = flat, 1 = moderate pillow, 2 = strong 3D-like bevel."""
[docs]
_version: int = field(default=2, init=False, repr=False)
@property
[docs]
def has_any_texture(self) -> bool:
return any((self.albedo_texture, self.normal_texture, self.orm_texture, self.emissive_texture))
[docs]
def to_dict(self) -> dict:
return {
'version': self._version,
'enabled': bool(self.enabled),
'albedo_texture': str(self.albedo_texture or ''),
'normal_texture': str(self.normal_texture or ''),
'orm_texture': str(self.orm_texture or ''),
'emissive_texture': str(self.emissive_texture or ''),
'base_color_factor': list(self.base_color_factor),
'metallic_factor': float(self.metallic_factor),
'roughness_factor': float(self.roughness_factor),
'normal_scale': float(self.normal_scale),
'occlusion_strength': float(self.occlusion_strength),
'emissive_factor': list(self.emissive_factor),
'uv_scale': list(self.uv_scale),
'uv_offset': list(self.uv_offset),
'preset_name': str(self.preset_name or ''),
'cushion_strength': float(self.cushion_strength),
}
@classmethod
[docs]
def from_dict(cls, data: dict | None) -> 'PolygonPBRMaterial':
if not isinstance(data, dict):
return cls()
return cls(
enabled=bool(data.get('enabled', False)),
albedo_texture=str(data.get('albedo_texture', '') or ''),
normal_texture=str(data.get('normal_texture', '') or ''),
orm_texture=str(data.get('orm_texture', '') or ''),
emissive_texture=str(data.get('emissive_texture', '') or ''),
base_color_factor=_float_vec4(data.get('base_color_factor'), (1.0, 1.0, 1.0, 1.0)),
metallic_factor=float(data.get('metallic_factor', 0.0)),
roughness_factor=float(data.get('roughness_factor', 1.0)),
normal_scale=float(data.get('normal_scale', 1.0)),
occlusion_strength=float(data.get('occlusion_strength', 1.0)),
emissive_factor=_float_vec3(data.get('emissive_factor'), (0.0, 0.0, 0.0)),
uv_scale=_float_pair(data.get('uv_scale'), (100.0, 100.0)),
uv_offset=_float_pair(data.get('uv_offset'), (0.0, 0.0)),
preset_name=str(data.get('preset_name', '') or ''),
cushion_strength=float(data.get('cushion_strength', 0.0)),
)
[docs]
def to_json(self) -> str:
return json.dumps(self.to_dict(), separators=(',', ':'))
@classmethod
[docs]
def from_json(cls, raw: str | None) -> 'PolygonPBRMaterial':
if not raw:
return cls()
try:
return cls.from_dict(json.loads(raw))
except Exception:
return cls()
[docs]
def get_pbr_presets_root(base_dir: Path | None = None) -> Path:
"""Return the expected root directory for built-in polygon PBR presets."""
if base_dir is None:
# .../wolfhece/pyvertexvectors -> wolfhece package root
base_dir = Path(__file__).resolve().parent.parent
return base_dir / 'data' / 'pbr_presets'
[docs]
def get_official_polygon_pbr_preset_names() -> list[str]:
"""Return the two official presets highlighted in the UI."""
return [
'Official Pie - Glass Dashboard',
'Official Bar - Frosted Bars',
]
[docs]
def _make_material(
*,
root: Path,
preset_name: str,
folder: str,
metallic: float,
roughness: float,
normal_scale: float,
occlusion_strength: float,
emissive_factor: tuple[float, float, float],
uv_scale: tuple[float, float],
base_color_factor: tuple[float, float, float, float],
with_emissive: bool = False,
) -> PolygonPBRMaterial:
return PolygonPBRMaterial(
enabled=True,
albedo_texture=str((root / folder / 'albedo.png').resolve()),
normal_texture=str((root / folder / 'normal.png').resolve()),
orm_texture=str((root / folder / 'orm.png').resolve()),
emissive_texture=str((root / folder / 'emissive.png').resolve()) if with_emissive else '',
metallic_factor=metallic,
roughness_factor=roughness,
normal_scale=normal_scale,
occlusion_strength=occlusion_strength,
emissive_factor=emissive_factor,
uv_scale=uv_scale,
uv_offset=(0.0, 0.0),
base_color_factor=base_color_factor,
preset_name=preset_name,
)
[docs]
def get_builtin_polygon_pbr_presets(base_dir: Path | None = None) -> dict[str, PolygonPBRMaterial]:
"""Return the built-in polygon PBR preset catalogue."""
root = get_pbr_presets_root(base_dir)
presets: dict[str, PolygonPBRMaterial] = {}
# Official presets first.
presets['Official Pie - Glass Dashboard'] = _make_material(
root=root,
preset_name='Official Pie - Glass Dashboard',
folder='concrete',
metallic=0.0,
roughness=0.12,
normal_scale=0.15,
occlusion_strength=0.8,
emissive_factor=(0.0, 0.0, 0.0),
uv_scale=(18.0, 18.0),
base_color_factor=(0.78, 0.9, 1.0, 1.0),
)
presets['Official Bar - Frosted Bars'] = _make_material(
root=root,
preset_name='Official Bar - Frosted Bars',
folder='concrete',
metallic=0.0,
roughness=0.18,
normal_scale=0.12,
occlusion_strength=0.85,
emissive_factor=(0.0, 0.0, 0.0),
uv_scale=(24.0, 24.0),
base_color_factor=(0.7, 0.88, 1.0, 1.0),
)
# Pie modern styles.
presets['Pie - Neon Ring'] = _make_material(
root=root,
preset_name='Pie - Neon Ring',
folder='asphalt',
metallic=0.15,
roughness=0.25,
normal_scale=0.2,
occlusion_strength=1.0,
emissive_factor=(0.0, 0.85, 0.95),
uv_scale=(14.0, 14.0),
base_color_factor=(0.16, 0.2, 0.28, 1.0),
with_emissive=True,
)
presets['Pie - Soft Clay'] = _make_material(
root=root,
preset_name='Pie - Soft Clay',
folder='concrete',
metallic=0.0,
roughness=0.85,
normal_scale=0.05,
occlusion_strength=0.9,
emissive_factor=(0.0, 0.0, 0.0),
uv_scale=(28.0, 28.0),
base_color_factor=(0.95, 0.84, 0.78, 1.0),
)
presets['Pie - Satin Metal'] = _make_material(
root=root,
preset_name='Pie - Satin Metal',
folder='rusted_metal',
metallic=0.55,
roughness=0.35,
normal_scale=0.2,
occlusion_strength=0.95,
emissive_factor=(0.0, 0.0, 0.0),
uv_scale=(16.0, 16.0),
base_color_factor=(0.63, 0.71, 0.79, 1.0),
)
presets['Pie - Carbon UI'] = _make_material(
root=root,
preset_name='Pie - Carbon UI',
folder='asphalt',
metallic=0.2,
roughness=0.6,
normal_scale=0.35,
occlusion_strength=1.0,
emissive_factor=(0.0, 0.0, 0.0),
uv_scale=(10.0, 10.0),
base_color_factor=(0.18, 0.18, 0.2, 1.0),
)
presets['Pie - Warm Gradient Pro'] = _make_material(
root=root,
preset_name='Pie - Warm Gradient Pro',
folder='concrete',
metallic=0.05,
roughness=0.45,
normal_scale=0.1,
occlusion_strength=0.85,
emissive_factor=(0.08, 0.03, 0.02),
uv_scale=(22.0, 22.0),
base_color_factor=(0.98, 0.62, 0.5, 1.0),
)
# Bar modern styles.
presets['Bar - Pulse Tech'] = _make_material(
root=root,
preset_name='Bar - Pulse Tech',
folder='asphalt',
metallic=0.25,
roughness=0.3,
normal_scale=0.2,
occlusion_strength=1.0,
emissive_factor=(0.55, 0.1, 0.8),
uv_scale=(26.0, 26.0),
base_color_factor=(0.2, 0.16, 0.34, 1.0),
with_emissive=True,
)
presets['Bar - Brushed Aluminum'] = _make_material(
root=root,
preset_name='Bar - Brushed Aluminum',
folder='rusted_metal',
metallic=0.8,
roughness=0.42,
normal_scale=0.25,
occlusion_strength=0.9,
emissive_factor=(0.0, 0.0, 0.0),
uv_scale=(20.0, 20.0),
base_color_factor=(0.74, 0.76, 0.8, 1.0),
)
presets['Bar - Minimal Matte'] = _make_material(
root=root,
preset_name='Bar - Minimal Matte',
folder='concrete',
metallic=0.0,
roughness=0.92,
normal_scale=0.03,
occlusion_strength=0.8,
emissive_factor=(0.0, 0.0, 0.0),
uv_scale=(35.0, 35.0),
base_color_factor=(0.58, 0.58, 0.58, 1.0),
)
presets['Bar - Night City'] = _make_material(
root=root,
preset_name='Bar - Night City',
folder='asphalt',
metallic=0.35,
roughness=0.28,
normal_scale=0.22,
occlusion_strength=1.0,
emissive_factor=(0.45, 0.1, 0.35),
uv_scale=(24.0, 24.0),
base_color_factor=(0.12, 0.18, 0.28, 1.0),
with_emissive=True,
)
presets['Bar - Earth Analytics'] = _make_material(
root=root,
preset_name='Bar - Earth Analytics',
folder='concrete',
metallic=0.05,
roughness=0.72,
normal_scale=0.12,
occlusion_strength=1.0,
emissive_factor=(0.0, 0.0, 0.0),
uv_scale=(30.0, 30.0),
base_color_factor=(0.54, 0.48, 0.36, 1.0),
)
return presets