from __future__ import annotations
from pathlib import Path
import numpy as np
from PIL import Image
from .pyvertexvectors.polygon_pbr_material import get_pbr_presets_root
[docs]
def _save_rgb(path: Path, arr: np.ndarray) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
arr_u8 = np.clip(arr, 0, 255).astype(np.uint8)
Image.fromarray(arr_u8, mode='RGB').save(path)
[docs]
def _save_gray(path: Path, arr: np.ndarray) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
arr_u8 = np.clip(arr, 0, 255).astype(np.uint8)
Image.fromarray(arr_u8, mode='L').save(path)
[docs]
def _normal_from_height(height: np.ndarray, strength: float = 2.0) -> np.ndarray:
h = height.astype(np.float32) / 255.0
gy, gx = np.gradient(h)
nx = -gx * strength
ny = -gy * strength
nz = np.ones_like(nx)
norm = np.sqrt(nx * nx + ny * ny + nz * nz) + 1e-8
nx /= norm
ny /= norm
nz /= norm
return np.stack([
(nx * 0.5 + 0.5) * 255.0,
(ny * 0.5 + 0.5) * 255.0,
(nz * 0.5 + 0.5) * 255.0,
], axis=-1)
[docs]
def _make_concrete(base_dir: Path, size: int = 256) -> None:
rng = np.random.default_rng(42)
noise = rng.normal(0.0, 18.0, (size, size))
speckle = rng.normal(0.0, 25.0, (size, size))
mask = rng.random((size, size)) < 0.07
noise += speckle * mask
gray = 140.0 + noise
albedo = np.stack([gray * 1.02, gray, gray * 0.97], axis=-1)
normal = _normal_from_height(gray, strength=3.0)
ao = np.full((size, size), 200.0)
rough = np.full((size, size), 220.0)
# metallic: 0.2 baseline (50/255) for non-metallic surfaces
metallic = np.full((size, size), 50.0)
orm = np.stack([ao, rough, metallic], axis=-1)
folder = base_dir / 'concrete'
_save_rgb(folder / 'albedo.png', albedo)
_save_rgb(folder / 'normal.png', normal)
_save_rgb(folder / 'orm.png', orm)
[docs]
def _make_asphalt(base_dir: Path, size: int = 256) -> None:
rng = np.random.default_rng(999)
base = 52.0 + rng.normal(0.0, 10.0, (size, size))
crack = np.zeros((size, size), dtype=np.float32)
for _ in range(8):
x0 = rng.integers(0, size)
y0 = rng.integers(0, size)
x1 = (x0 + rng.integers(-120, 120)) % size
y1 = (y0 + rng.integers(-120, 120)) % size
n = 250
xs = np.linspace(x0, x1, n).astype(int) % size
ys = np.linspace(y0, y1, n).astype(int) % size
crack[ys, xs] = 1.0
crack_blur = crack.copy()
for _ in range(3):
crack_blur = (
crack_blur
+ np.roll(crack_blur, 1, axis=0)
+ np.roll(crack_blur, -1, axis=0)
+ np.roll(crack_blur, 1, axis=1)
+ np.roll(crack_blur, -1, axis=1)
) / 5.0
height = base - crack_blur * 25.0
albedo_gray = base - crack_blur * 15.0
albedo = np.stack([albedo_gray, albedo_gray, albedo_gray], axis=-1)
normal = _normal_from_height(height, strength=5.0)
ao = 180.0 + crack_blur * 40.0
rough = 235.0 - crack_blur * 30.0
# metallic: 0.2 baseline (50/255) for non-metallic asphalt surface
metallic = np.full((size, size), 50.0)
orm = np.stack([ao, rough, metallic], axis=-1)
emissive = np.zeros((size, size), dtype=np.float32)
stripe = (np.abs((np.arange(size) - size // 2)) < 2).astype(np.float32)
emissive += stripe[None, :] * 90.0
folder = base_dir / 'asphalt'
_save_rgb(folder / 'albedo.png', albedo)
_save_rgb(folder / 'normal.png', normal)
_save_rgb(folder / 'orm.png', orm)
_save_gray(folder / 'emissive.png', emissive)
[docs]
def generate_builtin_polygon_pbr_presets(base_dir: Path | None = None) -> Path:
"""Generate packaged polygon PBR preset textures and return the target directory."""
target = get_pbr_presets_root(base_dir)
_make_concrete(target)
_make_rusted_metal(target)
_make_asphalt(target)
return target
[docs]
def main() -> None:
target = generate_builtin_polygon_pbr_presets()
print(f'PBR presets generated in: {target}')
if __name__ == '__main__':
main()