d843162e5f
Extract Base Color, Metallic, Roughness, Transmission, IOR from Blender asset library materials via catalog_assets.py. Store in catalog JSON and serve via /api/asset-libraries/pbr-map endpoint. Frontend viewers apply PBR properties to Three.js MeshStandardMaterial using hex color strings (avoiding Three.js ColorManagement sRGB/linear issues). Key fixes: - RLS bypass for material alias lookup in pbr-map endpoint - pbrMap empty guard prevents premature grey fallback in viewers - Cache-Control: no-cache on pbr-map requests to avoid stale data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
104 lines
3.1 KiB
Python
104 lines
3.1 KiB
Python
"""Blender headless script: extract asset catalog from a .blend library file.
|
|
|
|
Usage:
|
|
blender --background --python catalog_assets.py -- <blend_path>
|
|
|
|
Outputs a single JSON line to stdout:
|
|
{"materials": [{"name": "Mat1", "base_color": [R,G,B], "metallic": 0.0, ...}, ...],
|
|
"node_groups": ["NG1", ...]}
|
|
|
|
Only assets marked via Blender's asset system (asset_data is not None) are included.
|
|
PBR properties are extracted from the Principled BSDF node of each material.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import sys
|
|
import traceback
|
|
|
|
|
|
def _linear_to_srgb(v: float) -> float:
|
|
"""Convert a single linear color channel to sRGB."""
|
|
if v <= 0.0031308:
|
|
return v * 12.92
|
|
return 1.055 * math.pow(v, 1.0 / 2.4) - 0.055
|
|
|
|
|
|
def _extract_pbr(mat) -> dict:
|
|
"""Extract PBR properties from a Blender material's Principled BSDF node.
|
|
|
|
Returns dict with: name, base_color (sRGB), metallic, roughness, transmission, ior.
|
|
Falls back to diffuse_color if no Principled BSDF found.
|
|
"""
|
|
entry = {
|
|
"name": mat.name,
|
|
"base_color": [0.5, 0.5, 0.5],
|
|
"metallic": 0.0,
|
|
"roughness": 0.5,
|
|
"transmission": 0.0,
|
|
"ior": 1.45,
|
|
}
|
|
|
|
# Find Principled BSDF node
|
|
bsdf = None
|
|
if mat.use_nodes and mat.node_tree:
|
|
for node in mat.node_tree.nodes:
|
|
if node.type == "BSDF_PRINCIPLED":
|
|
bsdf = node
|
|
break
|
|
|
|
if bsdf:
|
|
# Base Color — convert linear → sRGB
|
|
bc = bsdf.inputs["Base Color"].default_value
|
|
entry["base_color"] = [
|
|
round(_linear_to_srgb(bc[0]), 4),
|
|
round(_linear_to_srgb(bc[1]), 4),
|
|
round(_linear_to_srgb(bc[2]), 4),
|
|
]
|
|
entry["metallic"] = round(bsdf.inputs["Metallic"].default_value, 4)
|
|
entry["roughness"] = round(bsdf.inputs["Roughness"].default_value, 4)
|
|
# Transmission Weight (Blender 4.0+) or Transmission (older)
|
|
for tx_name in ("Transmission Weight", "Transmission"):
|
|
if tx_name in bsdf.inputs:
|
|
entry["transmission"] = round(bsdf.inputs[tx_name].default_value, 4)
|
|
break
|
|
if "IOR" in bsdf.inputs:
|
|
entry["ior"] = round(bsdf.inputs["IOR"].default_value, 4)
|
|
else:
|
|
# Fallback: use viewport diffuse_color (already sRGB)
|
|
dc = mat.diffuse_color
|
|
entry["base_color"] = [round(dc[0], 4), round(dc[1], 4), round(dc[2], 4)]
|
|
|
|
return entry
|
|
|
|
|
|
def main() -> None:
|
|
argv = sys.argv
|
|
if "--" not in argv:
|
|
print(json.dumps({"error": "No blend path provided after --"}))
|
|
sys.exit(1)
|
|
|
|
blend_path = argv[argv.index("--") + 1]
|
|
|
|
import bpy # type: ignore[import]
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=blend_path)
|
|
|
|
materials = [
|
|
_extract_pbr(m) for m in bpy.data.materials if m.asset_data is not None
|
|
]
|
|
node_groups = [ng.name for ng in bpy.data.node_groups if ng.asset_data is not None]
|
|
|
|
catalog = {"materials": materials, "node_groups": node_groups}
|
|
print(json.dumps(catalog))
|
|
|
|
|
|
try:
|
|
main()
|
|
except SystemExit:
|
|
raise
|
|
except Exception:
|
|
traceback.print_exc()
|
|
sys.exit(1)
|