feat(PBR): extract Blender PBR properties and apply in 3D viewer
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>
This commit is contained in:
@@ -4,17 +4,75 @@ Usage:
|
||||
blender --background --python catalog_assets.py -- <blend_path>
|
||||
|
||||
Outputs a single JSON line to stdout:
|
||||
{"materials": ["Mat1", "Mat2", ...], "node_groups": ["NG1", ...]}
|
||||
{"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:
|
||||
@@ -27,7 +85,9 @@ def main() -> None:
|
||||
|
||||
bpy.ops.wm.open_mainfile(filepath=blend_path)
|
||||
|
||||
materials = [m.name for m in bpy.data.materials if m.asset_data is not None]
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user