Utility_Apps/Blender/simple scripts/Export Shader Nodes/export_shader_nodes.py
2026-04-01 15:39:57 -05:00

244 lines
8.1 KiB
Python

"""
Blender Shader Node Exporter
----------------------------
Exports the active object's material shader node tree to a JSON file.
Usage:
1. Open Blender's Text Editor
2. Paste this script
3. Set EXPORT_PATH below (or leave as None to save next to your .blend file)
4. Click "Run Script"
"""
import bpy
import json
import os
# ── Config ─────────────────────────────────────────────────────────────────────
EXPORT_PATH = None # e.g. "/home/user/my_shader.json" — None = auto
MATERIAL_INDEX = 0 # which material slot to export (0 = first)
# ───────────────────────────────────────────────────────────────────────────────
class BlenderEncoder(json.JSONEncoder):
"""Handles Blender math types (Vector, Color, Euler, Matrix, etc.)
and any other iterable that the default encoder can't handle."""
def default(self, obj):
# mathutils types all support len() + indexing
try:
import mathutils
if isinstance(obj, (
mathutils.Vector,
mathutils.Color,
mathutils.Euler,
mathutils.Quaternion,
mathutils.Matrix,
)):
return list(obj)
except ImportError:
pass
# Generic fallback: anything iterable becomes a list
try:
return list(obj)
except TypeError:
pass
return super().default(obj)
def safe_value(val):
"""Recursively convert a value to something JSON-safe."""
import mathutils
if isinstance(val, (mathutils.Vector, mathutils.Color,
mathutils.Euler, mathutils.Quaternion)):
return list(val)
if isinstance(val, mathutils.Matrix):
return [list(row) for row in val]
if isinstance(val, (bool, int, float, str)) or val is None:
return val
try:
return list(val)
except TypeError:
return str(val)
def export_socket(socket):
"""Serialize a single node socket."""
data = {
"name": socket.name,
"identifier": socket.identifier,
"type": socket.type,
"is_linked": socket.is_linked,
}
# Try to capture the default value if present
if hasattr(socket, "default_value"):
data["default_value"] = safe_value(socket.default_value)
return data
def export_node(node):
"""Serialize a single shader node and all its properties."""
data = {
"name": node.name,
"label": node.label,
"type": node.type,
"bl_idname": node.bl_idname,
"location": [node.location.x, node.location.y],
"width": node.width,
"height": node.height,
"hide": node.hide,
"mute": node.mute,
"use_custom_color": node.use_custom_color,
"color": list(node.color),
"inputs": [export_socket(s) for s in node.inputs],
"outputs": [export_socket(s) for s in node.outputs],
"properties": {},
}
# ── Node-type-specific properties ──────────────────────────────────────────
prop_names = {
"ShaderNodeBsdfPrincipled": [
"distribution", "subsurface_method"
],
"ShaderNodeTexImage": [
"interpolation", "projection", "extension",
],
"ShaderNodeTexNoise": ["noise_dimensions"],
"ShaderNodeTexWave": ["wave_type", "bands_direction", "rings_direction", "wave_profile"],
"ShaderNodeTexVoronoi": ["voronoi_dimensions", "feature", "distance"],
"ShaderNodeMapping": ["vector_type"],
"ShaderNodeMath": ["operation", "use_clamp"],
"ShaderNodeVectorMath": ["operation"],
"ShaderNodeMixRGB": ["blend_type", "use_clamp"],
"ShaderNodeMix": ["blend_type", "data_type", "clamp_factor", "clamp_result", "factor_mode"],
"ShaderNodeBump": ["invert"],
"ShaderNodeNormalMap": ["space", "uv_map"],
"ShaderNodeDisplacement": ["space"],
"ShaderNodeEmission": [],
"ShaderNodeRGB": [],
"ShaderNodeValue": [],
"ShaderNodeGroup": ["node_tree"],
"ShaderNodeScript": ["mode", "script", "filepath"],
}
specific = prop_names.get(node.bl_idname, [])
for prop in specific:
try:
val = getattr(node, prop)
# Node trees: just record name
if hasattr(val, "name"):
data["properties"][prop] = val.name
elif hasattr(val, "__iter__") and not isinstance(val, str):
data["properties"][prop] = list(val)
else:
data["properties"][prop] = val
except AttributeError:
pass
# For image texture nodes, also capture the image name
if node.bl_idname == "ShaderNodeTexImage" and node.image:
data["properties"]["image_name"] = node.image.name
data["properties"]["image_filepath"] = node.image.filepath
# For value nodes, grab the output value directly
if node.bl_idname == "ShaderNodeValue":
try:
data["properties"]["value"] = node.outputs[0].default_value
except (IndexError, AttributeError):
pass
# For RGB nodes
if node.bl_idname == "ShaderNodeRGB":
try:
data["properties"]["color"] = list(node.outputs[0].default_value)
except (IndexError, AttributeError):
pass
return data
def export_link(link):
"""Serialize a single node link (edge in the graph)."""
return {
"from_node": link.from_node.name,
"from_socket": link.from_socket.identifier,
"to_node": link.to_node.name,
"to_socket": link.to_socket.identifier,
"is_muted": link.is_muted,
}
def export_node_tree(node_tree):
"""Serialize the entire node tree."""
return {
"name": node_tree.name,
"type": node_tree.type,
"nodes": [export_node(n) for n in node_tree.nodes],
"links": [export_link(l) for l in node_tree.links],
}
def export_material_shader(material):
"""Top-level: serialize a material and its shader node tree."""
if not material.use_nodes:
raise ValueError(f"Material '{material.name}' does not use nodes.")
return {
"material_name": material.name,
"blend_method": material.blend_method,
"shadow_method": getattr(material, "shadow_method", ""),
"use_backface_culling": material.use_backface_culling,
"node_tree": export_node_tree(material.node_tree),
}
def resolve_export_path(material_name):
"""Build the output file path."""
if EXPORT_PATH:
return EXPORT_PATH
blend_path = bpy.data.filepath
if blend_path:
base_dir = os.path.dirname(blend_path)
else:
base_dir = os.path.expanduser("~")
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in material_name)
return os.path.join(base_dir, f"{safe_name}_shader.json")
def main():
obj = bpy.context.active_object
if obj is None:
raise RuntimeError("No active object selected.")
if not obj.material_slots:
raise RuntimeError(f"Object '{obj.name}' has no material slots.")
if MATERIAL_INDEX >= len(obj.material_slots):
raise RuntimeError(
f"Material index {MATERIAL_INDEX} is out of range "
f"(object has {len(obj.material_slots)} slot(s))."
)
mat = obj.material_slots[MATERIAL_INDEX].material
if mat is None:
raise RuntimeError(f"Material slot {MATERIAL_INDEX} is empty.")
print(f"Exporting shader: '{mat.name}' from '{obj.name}'")
data = export_material_shader(mat)
out_path = resolve_export_path(mat.name)
with open(out_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False, cls=BlenderEncoder)
print(f"✓ Saved to: {out_path}")
print(f" Nodes : {len(data['node_tree']['nodes'])}")
print(f" Links : {len(data['node_tree']['links'])}")
main()