Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/materials_processor/dcc/blender/recreator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Recreate generic material graphs as Blender shader networks."""

import logging
import math
from typing import List

from materials_processor.core.graph import NodeInfo
Expand Down Expand Up @@ -101,6 +102,26 @@ def _apply_parameters(self, node, parameters):
node.inputs["Strength"].default_value = float(val) if isinstance(val, (int, float)) else 1.0
continue

if node_type == "ShaderNodeValue" and blender_name == "value":
value_socket = next((socket for socket in node.outputs if socket.name == "Value"), None)
if value_socket and hasattr(value_socket, "default_value"):
value_socket.default_value = float(val) if isinstance(val, (int, float)) else 0.0
continue

if node_type == "ShaderNodeMapping" and blender_name in {"Location", "Rotation", "Scale"}:
if hasattr(node, "inputs") and blender_name in node.inputs:
socket = node.inputs[blender_name]
try:
if blender_name == "Rotation":
socket.default_value = (0.0, 0.0, math.radians(float(val)))
else:
values = list(val) if isinstance(val, list) else [val]
z_default = 1.0 if blender_name == "Scale" else 0.0
socket.default_value = tuple((values + [z_default])[:3])
except Exception as exc:
logger.warning("Failed to set mapping parameter '%s' on node '%s': %s", blender_name, node.name, exc)
continue

# Default socket value assignment
if hasattr(node, "inputs") and blender_name in node.inputs:
socket = node.inputs[blender_name]
Expand Down
80 changes: 67 additions & 13 deletions src/materials_processor/dcc/blender/traverser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Traverse Blender shader node networks."""

import logging
import math

logger = logging.getLogger(__name__)

Expand All @@ -12,13 +13,30 @@
bpy = None


def _socket_parameter_name(socket, *, is_output=False, node=None):
"""Return a stable Blender parameter key for sockets with ambiguous names."""
node_type = getattr(node, "bl_idname", None)
if is_output and node_type == "ShaderNodeMapping" and socket.name == "Vector":
return "Vector Output"
return socket.name


def _socket_generic_type(socket, *, is_output=False, node=None):
"""Return the closest generic parameter type for a Blender socket."""
node_type = getattr(node, "bl_idname", None)
if node_type == "ShaderNodeMapping":
if socket.name in {"Vector", "Location", "Scale"}:
return "vector2"
if socket.name == "Rotation":
return "float1"
if is_output and node_type == "ShaderNodeTexCoord" and socket.name == "UV":
return "vector2"

socket_type = socket.type.lower()
if socket_type == "value":
return "float1"
if socket_type == "vector":
if is_output and getattr(node, "bl_idname", None) == "ShaderNodeUVMap":
if is_output and node_type == "ShaderNodeUVMap":
return "vector2"
return "vector3"
if socket_type == "rgba":
Expand All @@ -28,6 +46,34 @@ def _socket_generic_type(socket, *, is_output=False, node=None):
return "float1"


def _socket_default_value(socket, node):
"""Return a JSON-friendly socket value, normalizing Blender mapping semantics."""
val = socket.default_value
node_type = getattr(node, "bl_idname", None)
if node_type == "ShaderNodeMapping":
if socket.name in {"Vector", "Location", "Scale"}:
return list(val)[:2]
if socket.name == "Rotation":
try:
rotation_values = list(val)
except TypeError:
try:
rotation_values = [val[0], val[1], val[2]]
except (TypeError, IndexError):
rotation_values = [0.0, 0.0, val]
z_rotation = rotation_values[2] if len(rotation_values) > 2 else 0.0
return math.degrees(z_rotation)

# Convert math-types like Vector, Color, RGBA, and Blender arrays to standard lists.
if hasattr(val, "copy") or isinstance(val, (list, tuple, bytes, set)):
return list(val)
if type(val).__name__ in ("Vector", "Color", "bpy_prop_array"):
return list(val)
if not isinstance(val, str) and hasattr(val, "__iter__"):
return list(val)
return val


def _resolve_blender_image_path(image):
"""Resolve Blender image paths relative to the current blend file when possible."""
filepath = getattr(image, "filepath", "")
Expand Down Expand Up @@ -148,15 +194,15 @@ def _detect_node_connections(node, parent_node, material_name):
"node_path": f"/mat/{material_name}/{node.name}",
"node_type": node.bl_idname,
"node_index": 0,
"parm_name": output_socket.name,
"parm_name": _socket_parameter_name(output_socket, is_output=True, node=node),
"data_type": output_socket.type,
},
"output": {
"node_name": parent_node.name,
"node_path": f"/mat/{material_name}/{parent_node.name}",
"node_type": parent_node.bl_idname,
"node_index": 0,
"parm_name": link.to_socket.name,
"parm_name": _socket_parameter_name(link.to_socket, node=parent_node),
"data_type": link.to_socket.type,
}
}
Expand Down Expand Up @@ -186,17 +232,10 @@ def _convert_parms_to_dict(node):
if not hasattr(socket, "default_value"):
continue

val = socket.default_value
# Convert math-types like Vector, Color, RGBA, and Blender arrays to standard lists.
if hasattr(val, "copy") or isinstance(val, (list, tuple, bytes, set)):
val = list(val)
elif type(val).__name__ in ("Vector", "Color", "bpy_prop_array"):
val = list(val)
elif not isinstance(val, str) and hasattr(val, "__iter__"):
val = list(val)
val = _socket_default_value(socket, node)

parms["input"].append({
"generic_name": socket.name,
"generic_name": _socket_parameter_name(socket, node=node),
"value": val,
"type": _socket_generic_type(socket, node=node),
"direction": "input",
Expand All @@ -217,6 +256,21 @@ def _convert_parms_to_dict(node):
"type": "string1",
"direction": "input"
})
elif node.bl_idname == "ShaderNodeTexCoord":
parms["input"].append({
"generic_name": "uv_map",
"value": "",
"type": "string1",
"direction": "input"
})
elif node.bl_idname == "ShaderNodeValue":
value_socket = next((socket for socket in node.outputs if socket.name == "Value"), None)
parms["input"].append({
"generic_name": "value",
"value": getattr(value_socket, "default_value", 0.0),
"type": "float1",
"direction": "input"
})
elif node.bl_idname == "ShaderNodeNormalMap":
strength_val = 1.0
if hasattr(node, "inputs") and "Strength" in node.inputs:
Expand All @@ -231,7 +285,7 @@ def _convert_parms_to_dict(node):
# Node outputs mapped as output parameters
for socket in node.outputs:
parms["output"].append({
"generic_name": socket.name,
"generic_name": _socket_parameter_name(socket, is_output=True, node=node),
"value": None,
"type": _socket_generic_type(socket, is_output=True, node=node),
"direction": "output",
Expand Down
35 changes: 35 additions & 0 deletions src/materials_processor/mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@
'ND_image_color3': 'GENERIC::image',
'ND_normalmap_vector3': 'GENERIC::normalmap',
'ND_geompropvalue_vector2': 'GENERIC::uvmap',
'ND_place2d_vector2': 'GENERIC::mapping',
'ND_constant_float': 'GENERIC::value',
'ND_separate3_color3': 'GENERIC::separate_color',
'ND_colorcorrect_color3': 'GENERIC::color_correct',
'ND_range_float': 'GENERIC::range',
Expand All @@ -285,6 +287,8 @@
'ND_image_color3': 'GENERIC::image',
'ND_normalmap_vector3': 'GENERIC::normalmap',
'ND_geompropvalue_vector2': 'GENERIC::uvmap',
'ND_place2d_vector2': 'GENERIC::mapping',
'ND_constant_float': 'GENERIC::value',
'ND_separate3_color3': 'GENERIC::separate_color',
'ND_colorcorrect_color3': 'GENERIC::color_correct',
'ND_range_float': 'GENERIC::range',
Expand Down Expand Up @@ -313,7 +317,10 @@
'blender_shader_nodes': {
'ShaderNodeBsdfPrincipled': 'GENERIC::standard_surface',
'ShaderNodeTexImage': 'GENERIC::image',
'ShaderNodeTexCoord': 'GENERIC::uvmap',
'ShaderNodeUVMap': 'GENERIC::uvmap',
'ShaderNodeMapping': 'GENERIC::mapping',
'ShaderNodeValue': 'GENERIC::value',
'ShaderNodeSeparateColor': 'GENERIC::separate_color',
'ShaderNodeNormalMap': 'GENERIC::normalmap',
'ShaderNodeBump': 'GENERIC::displacement',
Expand Down Expand Up @@ -498,6 +505,19 @@ def convert_generic(node_type: str,
'default': 'default',
'out': 'vector',
},
'ND_place2d_vector2': {
'texcoord': 'texcoord',
'pivot': 'pivot',
'scale': 'scale',
'rotate': 'rotate',
'offset': 'offset',
'operationorder': 'operationorder',
'out': 'out',
},
'ND_constant_float': {
'value': 'value',
'out': 'out',
},
'ND_separate3_color3': {
'in': 'rgb',
'outr': 'r',
Expand Down Expand Up @@ -726,6 +746,21 @@ def convert_generic(node_type: str,
'uv_map': 'uv_map',
'UV': 'vector',
},
'ShaderNodeTexCoord': {
'uv_map': 'uv_map',
'UV': 'vector',
},
'ShaderNodeMapping': {
'Vector': 'texcoord',
'Location': 'offset',
'Rotation': 'rotate',
'Scale': 'scale',
'Vector Output': 'out',
},
'ShaderNodeValue': {
'value': 'value',
'Value': 'out',
},
'ShaderNodeSeparateColor': {
'Color': 'rgb',
'Red': 'r',
Expand Down
16 changes: 14 additions & 2 deletions src/materials_processor/usd/graph_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,19 @@ def _coerce_usd_value(value, generic_type):
"""Shape JSON-friendly parameter values for USD's typed attribute setters."""
if isinstance(value, (list, tuple)) and len(value) == 1:
return value[0]
if generic_type in {'float2', 'float3', 'float4', 'color3', 'rgba3', 'color4', 'rgba4', 'xyzw3'}:
if generic_type in {
'float2',
'float3',
'float4',
'vector2',
'vector3',
'vector4',
'color3',
'rgba3',
'color4',
'rgba4',
'xyzw3',
}:
if isinstance(value, (list, tuple)):
return tuple(value)
return value
Expand Down Expand Up @@ -160,7 +172,7 @@ def _apply_parameters(self, shader, node_type, parameters):

parm_new_name = parm_new_name[0]
val = _coerce_usd_value(param.value, param.generic_type)
if not val:
if val is None or val == "":
continue

val_type = _ATTRIB_TYPE_CASTERS.get(param.generic_type)
Expand Down
14 changes: 14 additions & 0 deletions src/materials_processor/usd/mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@
'openpbr': 'ND_geompropvalue_vector2',
},
},
'GENERIC::mapping': {
'prim_type': 'Shader',
'info_id': {
'mtlx': 'ND_place2d_vector2',
'openpbr': 'ND_place2d_vector2',
},
},
'GENERIC::value': {
'prim_type': 'Shader',
'info_id': {
'mtlx': 'ND_constant_float',
'openpbr': 'ND_constant_float',
},
},
'GENERIC::separate_color': {
'prim_type': 'Shader',
'info_id': {
Expand Down
Loading
Loading