WIP: MaterialX addon #104594
87
materialx/bl_nodes/__init__.py
Normal file
87
materialx/bl_nodes/__init__.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
# Copyright 2022, AMD
|
||||||
|
|
||||||
|
from nodeitems_utils import (
|
||||||
|
NodeCategory,
|
||||||
|
NodeItem,
|
||||||
|
register_node_categories,
|
||||||
|
unregister_node_categories,
|
||||||
|
)
|
||||||
|
from nodeitems_builtins import (
|
||||||
|
ShaderNodeCategory,
|
||||||
|
)
|
||||||
|
from .. import utils
|
||||||
|
|
||||||
|
|
||||||
|
class CompatibleShaderNodeCategory(NodeCategory):
|
||||||
|
""" Appear with an active USD plugin in Material shader editor only """
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.space_data.tree_type == 'ShaderNodeTree'
|
||||||
|
|
||||||
|
|
||||||
|
# add nodes here once they are supported
|
||||||
|
node_categories = [
|
||||||
|
CompatibleShaderNodeCategory(utils.with_prefix("SHADER_NODE_CATEGORY_INPUT", '_', True), "Input", items=[
|
||||||
|
NodeItem('ShaderNodeRGB'),
|
||||||
|
NodeItem('ShaderNodeValue'),
|
||||||
|
], ),
|
||||||
|
CompatibleShaderNodeCategory(utils.with_prefix("SHADER_NODE_CATEGORY_OUTPUT", '_', True), "Output", items=[
|
||||||
|
NodeItem('ShaderNodeOutputMaterial'),
|
||||||
|
], ),
|
||||||
|
CompatibleShaderNodeCategory(utils.with_prefix("SHADER_NODE_CATEGORY_SHADERS", '_', True), "Shader", items=[
|
||||||
|
NodeItem('ShaderNodeBsdfDiffuse'),
|
||||||
|
NodeItem('ShaderNodeBsdfGlass'),
|
||||||
|
NodeItem('ShaderNodeEmission'),
|
||||||
|
NodeItem('ShaderNodeBsdfPrincipled'),
|
||||||
|
]),
|
||||||
|
CompatibleShaderNodeCategory(utils.with_prefix("SHADER_NODE_CATEGORY_TEXTURE", '_', True), "Texture", items=[
|
||||||
|
NodeItem('ShaderNodeTexImage'),
|
||||||
|
], ),
|
||||||
|
CompatibleShaderNodeCategory(utils.with_prefix("SHADER_NODE_CATEGORY_COLOR", '_', True), "Color", items=[
|
||||||
|
NodeItem('ShaderNodeInvert'),
|
||||||
|
NodeItem('ShaderNodeMixRGB'),
|
||||||
|
], ),
|
||||||
|
CompatibleShaderNodeCategory(utils.with_prefix("SHADER_NODE_CATEGORY_CONVERTER", '_', True), "Converter", items=[
|
||||||
|
NodeItem('ShaderNodeMath'),
|
||||||
|
], ),
|
||||||
|
CompatibleShaderNodeCategory(utils.with_prefix("SHADER_NODE_CATEGORY_VECTOR", '_', True), "Vector", items=[
|
||||||
|
NodeItem('ShaderNodeNormalMap'),
|
||||||
|
], ),
|
||||||
|
CompatibleShaderNodeCategory(utils.with_prefix("SHADER_NODE_CATEGORY_LAYOUT", '_', True), "Layout", items=[
|
||||||
|
NodeItem('NodeFrame'),
|
||||||
|
NodeItem('NodeReroute'),
|
||||||
|
], ),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# some nodes are hidden from plugins by Cycles itself(like Material Output), some we could not support.
|
||||||
|
# thus we'll hide 'em all to show only selected set of supported Blender nodes
|
||||||
|
# custom HdUSD_CompatibleShaderNodeCategory will be used instead
|
||||||
|
# def hide_cycles_and_eevee_poll(method):
|
||||||
|
# @classmethod
|
||||||
|
# def func(cls, context):
|
||||||
|
# return not context.scene.render.engine == 'HdUSD' and method(context)
|
||||||
|
# return func
|
||||||
|
|
||||||
|
|
||||||
|
old_shader_node_category_poll = None
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
# hide Cycles/Eevee menu
|
||||||
|
# global old_shader_node_category_poll
|
||||||
|
# old_shader_node_category_poll = ShaderNodeCategory.poll
|
||||||
|
# ShaderNodeCategory.poll = hide_cycles_and_eevee_poll(ShaderNodeCategory.poll)
|
||||||
|
|
||||||
|
# use custom menu
|
||||||
|
register_node_categories(utils.with_prefix("NODES", '_', True), node_categories)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
# restore Cycles/Eevee menu
|
||||||
|
# if old_shader_node_category_poll and ShaderNodeCategory.poll is not old_shader_node_category_poll:
|
||||||
|
# ShaderNodeCategory.poll = old_shader_node_category_poll
|
||||||
|
|
||||||
|
# remove custom menu
|
||||||
|
unregister_node_categories(utils.with_prefix("NODES", '_', True))
|
380
materialx/bl_nodes/node_parser.py
Normal file
380
materialx/bl_nodes/node_parser.py
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
# Copyright 2022, AMD
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import MaterialX as mx
|
||||||
|
|
||||||
|
from .. import utils
|
||||||
|
from ..utils import pass_node_reroute
|
||||||
|
from ..nodes import get_mx_node_cls
|
||||||
|
from .. import logging
|
||||||
|
log = logging.Log("bl_nodes.node_parser")
|
||||||
|
|
||||||
|
|
||||||
|
OUTPUT_TYPE = {'RGBA': 'color3',
|
||||||
|
'VALUE': 'float',
|
||||||
|
'VECTOR': 'vector3'}
|
||||||
|
|
||||||
|
|
||||||
|
class Id:
|
||||||
|
def __init__(self):
|
||||||
|
self.id = 0
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
self.id += 1
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
|
||||||
|
class NodeItem:
|
||||||
|
"""This class is a wrapper used for doing operations on MaterialX nodes, floats, and tuples"""
|
||||||
|
|
||||||
|
def __init__(self, id: Id, ng: [mx.Document, mx.NodeGraph], data: [tuple, float, mx.Node]):
|
||||||
|
self.id = id
|
||||||
|
self.nodegraph = ng
|
||||||
|
self.data = data
|
||||||
|
self.nodedef = None
|
||||||
|
if isinstance(data, mx.Node):
|
||||||
|
MxNode_cls, _ = get_mx_node_cls(data)
|
||||||
|
self.nodedef = MxNode_cls.get_nodedef(self.type)
|
||||||
|
|
||||||
|
def node_item(self, value):
|
||||||
|
if isinstance(value, NodeItem):
|
||||||
|
return value
|
||||||
|
|
||||||
|
return NodeItem(self.id, self.nodegraph, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self):
|
||||||
|
if isinstance(self.data, float):
|
||||||
|
return 'float_'
|
||||||
|
elif isinstance(self.data, tuple):
|
||||||
|
return 'tuple_'
|
||||||
|
else:
|
||||||
|
return self.data.getType()
|
||||||
|
|
||||||
|
def set_input(self, name, value):
|
||||||
|
if value is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
val_data = value.data if isinstance(value, NodeItem) else value
|
||||||
|
nd_input = self.nodedef.getActiveInput(name)
|
||||||
|
input = self.data.addInput(name, nd_input.getType())
|
||||||
|
utils.set_param_value(input, val_data, input.getType())
|
||||||
|
|
||||||
|
def set_inputs(self, inputs):
|
||||||
|
for name, value in inputs.items():
|
||||||
|
self.set_input(name, value)
|
||||||
|
|
||||||
|
# MATH OPERATIONS
|
||||||
|
def _arithmetic_helper(self, other, op_node, func):
|
||||||
|
''' helper function for overridden math functions.
|
||||||
|
This simply creates an arithmetic node of rpr_type
|
||||||
|
if one of the operands has node data, else maps the function to data '''
|
||||||
|
|
||||||
|
if other is None:
|
||||||
|
if isinstance(self.data, float):
|
||||||
|
result_data = func(self.data)
|
||||||
|
elif isinstance(self.data, tuple):
|
||||||
|
result_data = tuple(map(func, self.data))
|
||||||
|
else:
|
||||||
|
result_data = self.nodegraph.addNode(op_node, f"{op_node}_{self.id()}",
|
||||||
|
self.data.getType())
|
||||||
|
input = result_data.addInput('in', self.data.getType())
|
||||||
|
utils.set_param_value(input, self.data, self.data.getType())
|
||||||
|
|
||||||
|
else:
|
||||||
|
other_data = other.data if isinstance(other, NodeItem) else other
|
||||||
|
if isinstance(self.data, (float, tuple)) and isinstance(other_data, (float, tuple)):
|
||||||
|
if isinstance(self.data, float) and isinstance(other_data, float):
|
||||||
|
result_data = func(self.data, other_data)
|
||||||
|
else:
|
||||||
|
data = self.data
|
||||||
|
|
||||||
|
# converting data or other_data to have equal length
|
||||||
|
if isinstance(data, float):
|
||||||
|
data = (data,) * len(other_data)
|
||||||
|
elif isinstance(other_data, float):
|
||||||
|
other_data = (other_data,) * len(data)
|
||||||
|
elif len(data) < len(other_data):
|
||||||
|
data = (*data, 1.0)
|
||||||
|
elif len(other_data) < len(data):
|
||||||
|
other_data = (*other_data, 1.0)
|
||||||
|
|
||||||
|
result_data = tuple(map(func, data, other_data))
|
||||||
|
|
||||||
|
else:
|
||||||
|
nd_type = self.data.getType() if isinstance(self.data, mx.Node) else \
|
||||||
|
other_data.getType()
|
||||||
|
|
||||||
|
result_data = self.nodegraph.addNode(op_node, f"{op_node}_{self.id()}", nd_type)
|
||||||
|
input1 = result_data.addInput('in1', nd_type)
|
||||||
|
utils.set_param_value(input1, self.data, nd_type)
|
||||||
|
input2 = result_data.addInput('in2', nd_type)
|
||||||
|
utils.set_param_value(input2, other_data, nd_type)
|
||||||
|
|
||||||
|
return self.node_item(result_data)
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
return self._arithmetic_helper(other, 'add', lambda a, b: a + b)
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
return self._arithmetic_helper(other, 'subtract', lambda a, b: a - b)
|
||||||
|
|
||||||
|
def __mul__(self, other):
|
||||||
|
return self._arithmetic_helper(other, 'multiply', lambda a, b: a * b)
|
||||||
|
|
||||||
|
def __truediv__(self, other):
|
||||||
|
return self._arithmetic_helper(other, 'divide',
|
||||||
|
lambda a, b: a / b if not math.isclose(b, 0.0) else 0.0)
|
||||||
|
|
||||||
|
def __mod__(self, other):
|
||||||
|
return self._arithmetic_helper(other, 'modulo', lambda a, b: a % b)
|
||||||
|
|
||||||
|
def __pow__(self, other):
|
||||||
|
return self._arithmetic_helper(other, 'power', lambda a, b: a ** b)
|
||||||
|
|
||||||
|
def __neg__(self):
|
||||||
|
return 0.0 - self
|
||||||
|
|
||||||
|
def __abs__(self):
|
||||||
|
return self._arithmetic_helper(None, 'absval', lambda a: abs(a))
|
||||||
|
|
||||||
|
def floor(self):
|
||||||
|
return self._arithmetic_helper(None, 'floor', lambda a: float(math.floor(a)))
|
||||||
|
|
||||||
|
def ceil(self):
|
||||||
|
return self._arithmetic_helper(None, 'ceil', lambda a: float(math.ceil(a)))
|
||||||
|
|
||||||
|
# right hand methods for doing something like 1.0 - Node
|
||||||
|
def __radd__(self, other):
|
||||||
|
return self + other
|
||||||
|
|
||||||
|
def __rsub__(self, other):
|
||||||
|
return self.node_item(other) - self
|
||||||
|
|
||||||
|
def __rmul__(self, other):
|
||||||
|
return self * other
|
||||||
|
|
||||||
|
def __rtruediv__(self, other):
|
||||||
|
return self.node_item(other) / self
|
||||||
|
|
||||||
|
def __rmod__(self, other):
|
||||||
|
return self.node_item(other) % self
|
||||||
|
|
||||||
|
def __rpow__(self, other):
|
||||||
|
return self.node_item(other) ** self
|
||||||
|
|
||||||
|
def dot(self, other):
|
||||||
|
dot = self._arithmetic_helper(other, 'dotproduct', lambda a, b: a * b)
|
||||||
|
if isinstance(dot.data, tuple):
|
||||||
|
dot.data = sum(dot.data)
|
||||||
|
|
||||||
|
return dot
|
||||||
|
|
||||||
|
def if_else(self, cond: str, other, if_value, else_value):
|
||||||
|
if cond == '>':
|
||||||
|
res = self._arithmetic_helper(other, 'ifgreater', lambda a, b: float(a > b))
|
||||||
|
elif cond == '>=':
|
||||||
|
res = self._arithmetic_helper(other, 'ifgreatereq', lambda a, b: float(a >= b))
|
||||||
|
elif cond == '==':
|
||||||
|
res = self._arithmetic_helper(other, 'ifequal', lambda a, b: float(a == b))
|
||||||
|
elif cond == '<':
|
||||||
|
return self.node_item(other).if_else('>', self, else_value, if_value)
|
||||||
|
elif cond == '<=':
|
||||||
|
return self.node_item(other).if_else('>=', self, else_value, if_value)
|
||||||
|
elif cond == '!=':
|
||||||
|
return self.if_else('==', other, else_value, if_value)
|
||||||
|
else:
|
||||||
|
raise ValueError("Incorrect condition:", cond)
|
||||||
|
|
||||||
|
if isinstance(res.data, float):
|
||||||
|
return if_value if res.data == 1.0 else else_value
|
||||||
|
elif isinstance(res.data, tuple):
|
||||||
|
return if_value if res.data[0] == 1.0 else else_value
|
||||||
|
else:
|
||||||
|
res.set_input('value1', if_value)
|
||||||
|
res.set_input('value2', else_value)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def min(self, other):
|
||||||
|
return self._arithmetic_helper(other, 'min', lambda a, b: min(a, b))
|
||||||
|
|
||||||
|
def max(self, other):
|
||||||
|
return self._arithmetic_helper(other, 'max', lambda a, b: max(a, b))
|
||||||
|
|
||||||
|
def clamp(self, min_val=0.0, max_val=1.0):
|
||||||
|
""" clamp data to min/max """
|
||||||
|
return self.min(max_val).max(min_val)
|
||||||
|
|
||||||
|
def sin(self):
|
||||||
|
return self._arithmetic_helper(None, 'sin', lambda a: math.sin(a))
|
||||||
|
|
||||||
|
def cos(self):
|
||||||
|
return self._arithmetic_helper(None, 'cos', lambda a: math.cos(a))
|
||||||
|
|
||||||
|
def tan(self):
|
||||||
|
return self._arithmetic_helper(None, 'tan', lambda a: math.tan(a))
|
||||||
|
|
||||||
|
def asin(self):
|
||||||
|
return self._arithmetic_helper(None, 'asin', lambda a: math.asin(a))
|
||||||
|
|
||||||
|
def acos(self):
|
||||||
|
return self._arithmetic_helper(None, 'acos', lambda a: math.acos(a))
|
||||||
|
|
||||||
|
def atan(self):
|
||||||
|
return self._arithmetic_helper(None, 'atan', lambda a: math.atan(a))
|
||||||
|
|
||||||
|
def log(self):
|
||||||
|
return self._arithmetic_helper(None, 'ln', lambda a: math.log(a))
|
||||||
|
|
||||||
|
def blend(self, value1, value2):
|
||||||
|
""" Line interpolate value between value1(0.0) and value2(1.0) by self.data as factor """
|
||||||
|
return self * value2 + (1.0 - self) * value1
|
||||||
|
|
||||||
|
|
||||||
|
class NodeParser:
|
||||||
|
"""
|
||||||
|
This is the base class that parses a blender node.
|
||||||
|
Subclasses should override only export() function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
nodegraph_path = "NG"
|
||||||
|
|
||||||
|
def __init__(self, id: Id, doc: mx.Document, material: bpy.types.Material,
|
||||||
|
node: bpy.types.Node, obj: bpy.types.Object, out_key, output_type, cached_nodes,
|
||||||
|
group_nodes=(), **kwargs):
|
||||||
|
self.id = id
|
||||||
|
self.doc = doc
|
||||||
|
self.material = material
|
||||||
|
self.node = node
|
||||||
|
self.object = obj
|
||||||
|
self.out_key = out_key
|
||||||
|
self.out_type = output_type
|
||||||
|
self.cached_nodes = cached_nodes
|
||||||
|
self.group_nodes = group_nodes
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_output_type(to_socket):
|
||||||
|
# Need to check ShaderNodeNormalMap separately because
|
||||||
|
# if has input color3 type but materialx normalmap got vector3
|
||||||
|
return 'vector3' if to_socket.node.type == 'NORMAL_MAP' else OUTPUT_TYPE.get(to_socket.type, 'color3')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_node_parser_cls(bl_idname):
|
||||||
|
""" Returns NodeParser class for node_idname or None if not found """
|
||||||
|
from . import nodes
|
||||||
|
return getattr(nodes, bl_idname, None)
|
||||||
|
|
||||||
|
# INTERNAL FUNCTIONS
|
||||||
|
def _export_node(self, node, out_key, to_socket, group_node=None):
|
||||||
|
if group_node:
|
||||||
|
if self.group_nodes:
|
||||||
|
group_nodes = self.group_nodes + (group_node,)
|
||||||
|
else:
|
||||||
|
group_nodes = (group_node,)
|
||||||
|
else:
|
||||||
|
group_nodes = self.group_nodes
|
||||||
|
|
||||||
|
# dynamically define output type of node
|
||||||
|
output_type = self.get_output_type(to_socket)
|
||||||
|
|
||||||
|
# check if this node was already parsed and cached
|
||||||
|
node_item = self.cached_nodes.get((node.name, out_key, output_type))
|
||||||
|
if node_item:
|
||||||
|
return node_item
|
||||||
|
|
||||||
|
# getting corresponded NodeParser class
|
||||||
|
NodeParser_cls = self.get_node_parser_cls(node.bl_idname)
|
||||||
|
if not NodeParser_cls:
|
||||||
|
log.warn(f"Ignoring unsupported node {node.bl_idname}", node, self.material)
|
||||||
|
self.cached_nodes[(node.name, out_key, output_type)] = None
|
||||||
|
return None
|
||||||
|
|
||||||
|
node_parser = NodeParser_cls(self.id, self.doc, self.material, node, self.object,
|
||||||
|
out_key, output_type, self.cached_nodes, group_nodes, **self.kwargs)
|
||||||
|
|
||||||
|
node_item = node_parser.export()
|
||||||
|
|
||||||
|
self.cached_nodes[(node.name, out_key, output_type)] = node_item
|
||||||
|
return node_item
|
||||||
|
|
||||||
|
def _parse_val(self, val):
|
||||||
|
"""Turn blender socket value into python's value"""
|
||||||
|
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
return float(val)
|
||||||
|
|
||||||
|
if len(val) in (3, 4):
|
||||||
|
return tuple(val)
|
||||||
|
|
||||||
|
if isinstance(val, str):
|
||||||
|
return val
|
||||||
|
|
||||||
|
raise TypeError("Unknown value type to pass to rpr", val)
|
||||||
|
|
||||||
|
def node_item(self, value):
|
||||||
|
if isinstance(value, NodeItem):
|
||||||
|
return value
|
||||||
|
|
||||||
|
nodegraph = utils.get_nodegraph_by_path(self.doc, self.nodegraph_path, True)
|
||||||
|
return NodeItem(self.id, nodegraph, value)
|
||||||
|
|
||||||
|
# HELPER FUNCTIONS
|
||||||
|
# Child classes should use them to do their export
|
||||||
|
def get_output_default(self):
|
||||||
|
""" Returns default value of output socket """
|
||||||
|
socket_out = self.node.outputs[self.out_key]
|
||||||
|
|
||||||
|
return self.node_item(self._parse_val(socket_out.default_value))
|
||||||
|
|
||||||
|
def get_input_default(self, in_key):
|
||||||
|
""" Returns default value of input socket """
|
||||||
|
|
||||||
|
socket_in = self.node.inputs[in_key]
|
||||||
|
return self.node_item(self._parse_val(socket_in.default_value))
|
||||||
|
|
||||||
|
def get_input_link(self, in_key: [str, int]):
|
||||||
|
"""Returns linked parsed node or None if nothing is linked or not link is not valid"""
|
||||||
|
|
||||||
|
socket_in = self.node.inputs[in_key]
|
||||||
|
if not socket_in.links:
|
||||||
|
return None
|
||||||
|
|
||||||
|
link = socket_in.links[0]
|
||||||
|
|
||||||
|
if not link.is_valid:
|
||||||
|
log.warn("Invalid link ignored", link, socket_in, self.node, self.material)
|
||||||
|
return None
|
||||||
|
|
||||||
|
link = pass_node_reroute(link)
|
||||||
|
if not link:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._export_node(link.from_node, link.from_socket.identifier, link.to_socket)
|
||||||
|
|
||||||
|
def get_input_value(self, in_key):
|
||||||
|
""" Returns linked node or default socket value """
|
||||||
|
|
||||||
|
val = self.get_input_link(in_key)
|
||||||
|
if val is not None:
|
||||||
|
return val
|
||||||
|
|
||||||
|
return self.get_input_default(in_key)
|
||||||
|
|
||||||
|
def create_node(self, node_name, nd_type, inputs=None):
|
||||||
|
nodegraph = utils.get_nodegraph_by_path(self.doc, self.nodegraph_path, True)
|
||||||
|
node = nodegraph.addNode(node_name, f"{node_name}_{self.id()}", nd_type)
|
||||||
|
node_item = NodeItem(self.id, nodegraph, node)
|
||||||
|
|
||||||
|
if inputs:
|
||||||
|
node_item.set_inputs(inputs)
|
||||||
|
|
||||||
|
return node_item
|
||||||
|
|
||||||
|
# EXPORT FUNCTION
|
||||||
|
def export(self) -> [NodeItem, None]:
|
||||||
|
"""Main export function which should be overridable in child classes"""
|
||||||
|
return None
|
10
materialx/bl_nodes/nodes/__init__.py
Normal file
10
materialx/bl_nodes/nodes/__init__.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
# Copyright 2022, AMD
|
||||||
|
|
||||||
|
from .input import *
|
||||||
|
from .output import *
|
||||||
|
from .shader import *
|
||||||
|
from .texture import *
|
||||||
|
from .color import *
|
||||||
|
from .converter import *
|
||||||
|
from .vector import *
|
78
materialx/bl_nodes/nodes/color.py
Normal file
78
materialx/bl_nodes/nodes/color.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
# Copyright 2022, AMD
|
||||||
|
|
||||||
|
from ..node_parser import NodeParser
|
||||||
|
|
||||||
|
from ... import logging
|
||||||
|
log = logging.Log("bl_nodes.nodes.color")
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeInvert(NodeParser):
|
||||||
|
def export(self):
|
||||||
|
fac = self.get_input_value('Fac')
|
||||||
|
color = self.get_input_value('Color')
|
||||||
|
|
||||||
|
return fac.blend(color, 1.0 - color)
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeMixRGB(NodeParser):
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
fac = self.get_input_value('Fac')
|
||||||
|
color1 = self.get_input_value('Color1')
|
||||||
|
color2 = self.get_input_value('Color2')
|
||||||
|
|
||||||
|
# these mix types are copied from cycles OSL
|
||||||
|
blend_type = self.node.blend_type
|
||||||
|
|
||||||
|
if blend_type in ('MIX', 'COLOR'):
|
||||||
|
res = fac.blend(color1, color2)
|
||||||
|
|
||||||
|
elif blend_type == 'ADD':
|
||||||
|
res = fac.blend(color1, color1 + color2)
|
||||||
|
|
||||||
|
elif blend_type == 'MULTIPLY':
|
||||||
|
res = fac.blend(color1, color1 * color2)
|
||||||
|
|
||||||
|
elif blend_type == 'SUBTRACT':
|
||||||
|
res = fac.blend(color1, color1 - color2)
|
||||||
|
|
||||||
|
elif blend_type == 'DIVIDE':
|
||||||
|
res = fac.blend(color1, color1 / color2)
|
||||||
|
|
||||||
|
elif blend_type == 'DIFFERENCE':
|
||||||
|
res = fac.blend(color1, abs(color1 - color2))
|
||||||
|
|
||||||
|
elif blend_type == 'DARKEN':
|
||||||
|
res = fac.blend(color1, color1.min(color2))
|
||||||
|
|
||||||
|
elif blend_type == 'LIGHTEN':
|
||||||
|
res = fac.blend(color1, color1.max(color2))
|
||||||
|
|
||||||
|
elif blend_type == 'VALUE':
|
||||||
|
res = color1
|
||||||
|
|
||||||
|
elif blend_type == 'SCREEN':
|
||||||
|
tm = 1.0 - fac
|
||||||
|
res = 1.0 - (tm + fac * (1.0 - color2)) * (1.0 - color1)
|
||||||
|
|
||||||
|
elif blend_type == 'SOFT_LIGHT':
|
||||||
|
tm = 1.0 - fac
|
||||||
|
scr = 1.0 - (1.0 - color2) * (1.0 - color1)
|
||||||
|
res = tm * color1 + fac * ((1.0 - color1) * color2 * color1 + color1 * scr)
|
||||||
|
|
||||||
|
elif blend_type == 'LINEAR_LIGHT':
|
||||||
|
test_val = color2 > 0.5
|
||||||
|
res = test_val.if_else(color1 + fac * (2.0 * (color2 - 0.5)),
|
||||||
|
color1 + fac * (2.0 * color2 - 1.0))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# TODO: support operations SATURATION, HUE, SCREEN, BURN, OVERLAY
|
||||||
|
log.warn("Ignoring unsupported Blend Type", blend_type, self.node, self.material,
|
||||||
|
"mix will be used")
|
||||||
|
res = fac.blend(color1, color2)
|
||||||
|
|
||||||
|
if self.node.use_clamp:
|
||||||
|
res = res.clamp()
|
||||||
|
|
||||||
|
return res
|
73
materialx/bl_nodes/nodes/converter.py
Normal file
73
materialx/bl_nodes/nodes/converter.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
# Copyright 2022, AMD
|
||||||
|
|
||||||
|
from ..node_parser import NodeParser
|
||||||
|
|
||||||
|
from ... import logging
|
||||||
|
log = logging.Log("bl_nodes.nodes.converter")
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeMath(NodeParser):
|
||||||
|
""" Map Blender operations to MaterialX definitions, see the stdlib_defs.mtlx in MaterialX """
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
op = self.node.operation
|
||||||
|
in1 = self.get_input_value(0)
|
||||||
|
# single operand operations
|
||||||
|
if op == 'SINE':
|
||||||
|
res = in1.sin()
|
||||||
|
elif op == 'COSINE':
|
||||||
|
res = in1.cos()
|
||||||
|
elif op == 'TANGENT':
|
||||||
|
res = in1.tan()
|
||||||
|
elif op == 'ARCSINE':
|
||||||
|
res = in1.asin()
|
||||||
|
elif op == 'ARCCOSINE':
|
||||||
|
res = in1.acos()
|
||||||
|
elif op == 'ARCTANGENT':
|
||||||
|
res = in1.atan()
|
||||||
|
elif op == 'LOGARITHM':
|
||||||
|
res = in1.log()
|
||||||
|
elif op == 'ABSOLUTE':
|
||||||
|
res = abs(in1)
|
||||||
|
elif op == 'FLOOR':
|
||||||
|
res = in1.floor()
|
||||||
|
elif op == 'FRACT':
|
||||||
|
res = in1 % 1.0
|
||||||
|
elif op == 'CEIL':
|
||||||
|
res = in1.ceil()
|
||||||
|
elif op == 'ROUND':
|
||||||
|
f = in1.floor()
|
||||||
|
res = (in1 % 1.0).if_else('>=', 0.5, f + 1.0, f)
|
||||||
|
|
||||||
|
else: # 2-operand operations
|
||||||
|
in2 = self.get_input_value(1)
|
||||||
|
|
||||||
|
if op == 'ADD':
|
||||||
|
res = in1 + in2
|
||||||
|
elif op == 'SUBTRACT':
|
||||||
|
res = in1 - in2
|
||||||
|
elif op == 'MULTIPLY':
|
||||||
|
res = in1 * in2
|
||||||
|
elif op == 'DIVIDE':
|
||||||
|
res = in1 / in2
|
||||||
|
elif op == 'POWER':
|
||||||
|
res = in1 ** in2
|
||||||
|
elif op == 'MINIMUM':
|
||||||
|
res = in1.min(in2)
|
||||||
|
elif op == 'MAXIMUM':
|
||||||
|
res = in1.max(in2)
|
||||||
|
|
||||||
|
else:
|
||||||
|
in3 = self.get_input_value(2)
|
||||||
|
|
||||||
|
if op == 'MULTIPLY_ADD':
|
||||||
|
res = in1 * in2 + in3
|
||||||
|
else:
|
||||||
|
log.warn("Unsupported math operation", op)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.node.use_clamp:
|
||||||
|
res = res.clamp()
|
||||||
|
|
||||||
|
return res
|
18
materialx/bl_nodes/nodes/input.py
Normal file
18
materialx/bl_nodes/nodes/input.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
# Copyright 2022, AMD
|
||||||
|
|
||||||
|
from ..node_parser import NodeParser
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeValue(NodeParser):
|
||||||
|
""" Returns float value """
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
return self.get_output_default()
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeRGB(NodeParser):
|
||||||
|
""" Returns color value """
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
return self.get_output_default()
|
31
materialx/bl_nodes/nodes/output.py
Normal file
31
materialx/bl_nodes/nodes/output.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
# Copyright 2022, AMD
|
||||||
|
|
||||||
|
from ..node_parser import NodeParser, Id
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeOutputMaterial(NodeParser):
|
||||||
|
nodegraph_path = ""
|
||||||
|
|
||||||
|
def __init__(self, doc, material, node, obj, **kwargs):
|
||||||
|
super().__init__(Id(), doc, material, node, obj, None, None, {}, **kwargs)
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
surface = self.get_input_link('Surface')
|
||||||
|
if surface is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if surface.type == 'BSDF':
|
||||||
|
surface = self.create_node('surface', 'surfaceshader', {
|
||||||
|
'bsdf': surface,
|
||||||
|
})
|
||||||
|
elif surface.type == 'EDF':
|
||||||
|
surface = self.create_node('surface', 'surfaceshader', {
|
||||||
|
'edf': surface,
|
||||||
|
})
|
||||||
|
|
||||||
|
result = self.create_node('surfacematerial', 'material', {
|
||||||
|
'surfaceshader': surface,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
266
materialx/bl_nodes/nodes/shader.py
Normal file
266
materialx/bl_nodes/nodes/shader.py
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
# Copyright 2022, AMD
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from ..node_parser import NodeParser
|
||||||
|
|
||||||
|
from ... import logging
|
||||||
|
log = logging.Log("bl_nodes.nodes.shader")
|
||||||
|
|
||||||
|
|
||||||
|
SSS_MIN_RADIUS = 0.0001
|
||||||
|
DEFAULT_WHITE_COLOR = (1.0, 1.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def enabled(val):
|
||||||
|
if val is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if isinstance(val.data, float) and math.isclose(val.data, 0.0):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if isinstance(val.data, tuple) and \
|
||||||
|
math.isclose(val.data[0], 0.0) and \
|
||||||
|
math.isclose(val.data[1], 0.0) and \
|
||||||
|
math.isclose(val.data[2], 0.0):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeBsdfPrincipled(NodeParser):
|
||||||
|
nodegraph_path = ""
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
# GETTING REQUIRED INPUTS
|
||||||
|
# Note: if some inputs are not needed they won't be taken
|
||||||
|
|
||||||
|
base_color = self.get_input_value('Base Color')
|
||||||
|
|
||||||
|
subsurface = self.get_input_value('Subsurface')
|
||||||
|
subsurface_radius = None
|
||||||
|
subsurface_color = None
|
||||||
|
if enabled(subsurface):
|
||||||
|
subsurface_radius = self.get_input_value('Subsurface Radius')
|
||||||
|
subsurface_color = self.get_input_value('Subsurface Color')
|
||||||
|
|
||||||
|
metallic = self.get_input_value('Metallic')
|
||||||
|
specular = self.get_input_value('Specular')
|
||||||
|
# specular_tint = self.get_input_value('Specular Tint')
|
||||||
|
roughness = self.get_input_value('Roughness')
|
||||||
|
|
||||||
|
anisotropic = None
|
||||||
|
anisotropic_rotation = None
|
||||||
|
if enabled(metallic):
|
||||||
|
# TODO: use Specular Tint input
|
||||||
|
anisotropic = self.get_input_value('Anisotropic')
|
||||||
|
if enabled(anisotropic):
|
||||||
|
anisotropic_rotation = self.get_input_value('Anisotropic Rotation')
|
||||||
|
# anisotropic_rotation = 0.5 - (anisotropic_rotation % 1.0)
|
||||||
|
|
||||||
|
sheen = self.get_input_value('Sheen')
|
||||||
|
# sheen_tint = None
|
||||||
|
# if enabled(sheen):
|
||||||
|
# sheen_tint = self.get_input_value('Sheen Tint')
|
||||||
|
|
||||||
|
clearcoat = self.get_input_value('Clearcoat')
|
||||||
|
clearcoat_roughness = None
|
||||||
|
if enabled(clearcoat):
|
||||||
|
clearcoat_roughness = self.get_input_value('Clearcoat Roughness')
|
||||||
|
|
||||||
|
ior = self.get_input_value('IOR')
|
||||||
|
|
||||||
|
transmission = self.get_input_value('Transmission')
|
||||||
|
transmission_roughness = None
|
||||||
|
if enabled(transmission):
|
||||||
|
transmission_roughness = self.get_input_value('Transmission Roughness')
|
||||||
|
|
||||||
|
emission = self.get_input_value('Emission')
|
||||||
|
emission_strength = self.get_input_value('Emission Strength')
|
||||||
|
|
||||||
|
alpha = self.get_input_value('Alpha')
|
||||||
|
# transparency = 1.0 - alpha
|
||||||
|
|
||||||
|
normal = self.get_input_link('Normal')
|
||||||
|
clearcoat_normal = self.get_input_link('Clearcoat Normal')
|
||||||
|
tangent = self.get_input_link('Tangent')
|
||||||
|
|
||||||
|
# CREATING STANDARD SURFACE
|
||||||
|
result = self.create_node('standard_surface', 'surfaceshader', {
|
||||||
|
'base': 1.0,
|
||||||
|
'base_color': base_color,
|
||||||
|
'diffuse_roughness': roughness,
|
||||||
|
'normal': normal,
|
||||||
|
'tangent': tangent,
|
||||||
|
})
|
||||||
|
|
||||||
|
if enabled(metallic):
|
||||||
|
result.set_input('metalness', metallic)
|
||||||
|
|
||||||
|
if enabled(specular):
|
||||||
|
result.set_inputs({
|
||||||
|
'specular': specular,
|
||||||
|
'specular_color': DEFAULT_WHITE_COLOR,
|
||||||
|
'specular_roughness': roughness,
|
||||||
|
'specular_IOR': ior,
|
||||||
|
'specular_anisotropy': anisotropic,
|
||||||
|
'specular_rotation': anisotropic_rotation,
|
||||||
|
})
|
||||||
|
|
||||||
|
if enabled(transmission):
|
||||||
|
result.set_inputs({
|
||||||
|
'transmission': transmission,
|
||||||
|
'transmission_color': DEFAULT_WHITE_COLOR,
|
||||||
|
'transmission_extra_roughness': transmission_roughness,
|
||||||
|
})
|
||||||
|
|
||||||
|
if enabled(subsurface):
|
||||||
|
result.set_inputs({
|
||||||
|
'subsurface': subsurface,
|
||||||
|
'subsurface_color': subsurface_color,
|
||||||
|
'subsurface_radius': subsurface_radius,
|
||||||
|
'subsurface_anisotropy': anisotropic,
|
||||||
|
})
|
||||||
|
|
||||||
|
if enabled(sheen):
|
||||||
|
result.set_inputs({
|
||||||
|
'sheen': sheen,
|
||||||
|
'sheen_color': DEFAULT_WHITE_COLOR,
|
||||||
|
'sheen_roughness': roughness,
|
||||||
|
})
|
||||||
|
|
||||||
|
if enabled(clearcoat):
|
||||||
|
result.set_inputs({
|
||||||
|
'coat': clearcoat,
|
||||||
|
'coat_color': DEFAULT_WHITE_COLOR,
|
||||||
|
'coat_roughness': clearcoat_roughness,
|
||||||
|
'coat_IOR': ior,
|
||||||
|
'coat_anisotropy': anisotropic,
|
||||||
|
'coat_rotation': anisotropic_rotation,
|
||||||
|
'coat_normal': clearcoat_normal,
|
||||||
|
})
|
||||||
|
|
||||||
|
if enabled(emission):
|
||||||
|
result.set_inputs({
|
||||||
|
'emission': emission_strength,
|
||||||
|
'emission_color': emission,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeBsdfDiffuse(NodeParser):
|
||||||
|
nodegraph_path = ""
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
color = self.get_input_value('Color')
|
||||||
|
roughness = self.get_input_value('Roughness')
|
||||||
|
normal = self.get_input_link('Normal')
|
||||||
|
|
||||||
|
# Also tried burley_diffuse_bsdf and oren_nayar_diffuse_bsdf here, but Blender crashes with them
|
||||||
|
# CREATING STANDARD SURFACE
|
||||||
|
result = self.create_node('standard_surface', 'surfaceshader', {
|
||||||
|
'base_color': color,
|
||||||
|
'diffuse_roughness': 1.0 - roughness,
|
||||||
|
'normal': normal,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeBsdfGlass(NodeParser):
|
||||||
|
def export(self):
|
||||||
|
color = self.get_input_value('Color')
|
||||||
|
roughness = self.get_input_value('Roughness')
|
||||||
|
ior = self.get_input_value('IOR')
|
||||||
|
normal = self.get_input_link('Normal')
|
||||||
|
|
||||||
|
# CREATING STANDARD SURFACE
|
||||||
|
result = self.create_node('standard_surface', 'surfaceshader', {
|
||||||
|
'base': 0.0,
|
||||||
|
'normal': normal,
|
||||||
|
'specular': 1.0,
|
||||||
|
'specular_color': color,
|
||||||
|
'specular_roughness': roughness,
|
||||||
|
'specular_IOR': ior,
|
||||||
|
'specular_anisotropy': 0.0,
|
||||||
|
'specular_rotation': 0.0,
|
||||||
|
'transmission': 1.0,
|
||||||
|
'transmission_color': color,
|
||||||
|
'transmission_extra_roughness': roughness,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeEmission(NodeParser):
|
||||||
|
nodegraph_path = ""
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
result = self.create_node('standard_surface', 'surfaceshader')
|
||||||
|
|
||||||
|
color = self.get_input_value('Color')
|
||||||
|
strength = self.get_input_value('Strength')
|
||||||
|
|
||||||
|
if enabled(color) and enabled(strength):
|
||||||
|
result.set_inputs({
|
||||||
|
'emission': 1.0,
|
||||||
|
'emission_color': color * strength,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeMixShader(NodeParser):
|
||||||
|
nodegraph_path = ""
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
factor = self.get_input_value(0)
|
||||||
|
shader1 = self.get_input_link(1)
|
||||||
|
shader2 = self.get_input_link(2)
|
||||||
|
|
||||||
|
if shader1 is None and shader2 is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if shader1 is None:
|
||||||
|
return shader2
|
||||||
|
|
||||||
|
if shader2 is None:
|
||||||
|
return shader1
|
||||||
|
|
||||||
|
result = self.create_node('STD_mix', 'surfaceshader', {
|
||||||
|
'fg': shader1,
|
||||||
|
'bg': shader2,
|
||||||
|
'mix': factor
|
||||||
|
})
|
||||||
|
|
||||||
|
log.warn(f"Known issue: node doesn't work correctly with {result.nodedef.getName()}", self.material, self.node)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeAddShader(NodeParser):
|
||||||
|
nodegraph_path = ""
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
shader1 = self.get_input_link(0)
|
||||||
|
shader2 = self.get_input_link(1)
|
||||||
|
|
||||||
|
if shader1 is None and shader2 is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if shader1 is None:
|
||||||
|
return shader2
|
||||||
|
|
||||||
|
if shader2 is None:
|
||||||
|
return shader1
|
||||||
|
|
||||||
|
result = self.create_node('STD_add', 'surfaceshader', {
|
||||||
|
'in1': shader1,
|
||||||
|
'in2': shader2
|
||||||
|
})
|
||||||
|
|
||||||
|
log.warn(f"Known issue: node doesn't work correctly with {result.nodedef.getName()}", self.material, self.node)
|
||||||
|
|
||||||
|
return result
|
32
materialx/bl_nodes/nodes/texture.py
Normal file
32
materialx/bl_nodes/nodes/texture.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
# Copyright 2022, AMD
|
||||||
|
|
||||||
|
from ..node_parser import NodeParser
|
||||||
|
from ...utils import cache_image_file
|
||||||
|
|
||||||
|
|
||||||
|
TEXTURE_ERROR_COLOR = (1.0, 0.0, 1.0) # following Cycles color for wrong Texture nodes
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeTexImage(NodeParser):
|
||||||
|
def export(self):
|
||||||
|
image_error_result = self.node_item(TEXTURE_ERROR_COLOR)
|
||||||
|
image = self.node.image
|
||||||
|
|
||||||
|
# TODO support UDIM Tilesets and SEQUENCE
|
||||||
|
if not image or image.source in ('TILED', 'SEQUENCE'):
|
||||||
|
return image_error_result
|
||||||
|
|
||||||
|
img_path = cache_image_file(image)
|
||||||
|
if not img_path:
|
||||||
|
return image_error_result
|
||||||
|
|
||||||
|
# TODO use Vector input for UV
|
||||||
|
uv = self.create_node('texcoord', 'vector2', {})
|
||||||
|
|
||||||
|
result = self.create_node('image', self.out_type, {
|
||||||
|
'file': img_path,
|
||||||
|
'texcoord': uv,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
33
materialx/bl_nodes/nodes/vector.py
Normal file
33
materialx/bl_nodes/nodes/vector.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
# Copyright 2022, AMD
|
||||||
|
|
||||||
|
from ..node_parser import NodeParser
|
||||||
|
|
||||||
|
from ... import logging
|
||||||
|
log = logging.Log("bl_nodes.nodes.vector")
|
||||||
|
|
||||||
|
DEFAULT_SPACE = 'OBJECT'
|
||||||
|
|
||||||
|
|
||||||
|
class ShaderNodeNormalMap(NodeParser):
|
||||||
|
def export(self):
|
||||||
|
color = self.get_input_value('Color')
|
||||||
|
strength = self.get_input_value('Strength')
|
||||||
|
space = self.node.space
|
||||||
|
|
||||||
|
if space not in ('TANGENT', 'OBJECT'):
|
||||||
|
log.warn("Ignoring unsupported Space", space, self.node, self.material,
|
||||||
|
f"{DEFAULT_SPACE} will be used")
|
||||||
|
space = DEFAULT_SPACE
|
||||||
|
|
||||||
|
if space == 'TANGENT':
|
||||||
|
log.warn("Ignoring unsupported UV Map", space, self.node, self.material,
|
||||||
|
"No UV Map will be used")
|
||||||
|
|
||||||
|
result = self.create_node('normalmap', 'vector3', {
|
||||||
|
'in': color ,
|
||||||
|
'scale': strength,
|
||||||
|
'space': space.lower(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
@ -50,12 +50,10 @@ register_classes, unregister_classes = bpy.utils.register_classes_factory([
|
|||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
ui.register()
|
|
||||||
properties.register()
|
properties.register()
|
||||||
register_classes()
|
register_classes()
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
ui.unregister()
|
|
||||||
properties.unregister()
|
properties.unregister()
|
||||||
unregister_classes()
|
unregister_classes()
|
||||||
|
@ -7,6 +7,7 @@ import bpy
|
|||||||
import MaterialX as mx
|
import MaterialX as mx
|
||||||
|
|
||||||
from ..node_tree import MxNodeTree
|
from ..node_tree import MxNodeTree
|
||||||
|
from ..bl_nodes.nodes import ShaderNodeOutputMaterial
|
||||||
from ..utils import MX_LIBS_DIR
|
from ..utils import MX_LIBS_DIR
|
||||||
|
|
||||||
from ..utils import logging, get_temp_file, MaterialXProperties
|
from ..utils import logging, get_temp_file, MaterialXProperties
|
||||||
@ -45,10 +46,9 @@ class MaterialProperties(MaterialXProperties):
|
|||||||
|
|
||||||
doc = mx.createDocument()
|
doc = mx.createDocument()
|
||||||
|
|
||||||
# TODO add implementation
|
node_parser = ShaderNodeOutputMaterial(doc, material, output_node, obj)
|
||||||
# node_parser = ShaderNodeOutputMaterial(doc, material, output_node, obj)
|
if not node_parser.export():
|
||||||
# if not node_parser.export():
|
return None
|
||||||
# return None
|
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
@ -78,10 +78,9 @@ class MaterialProperties(MaterialXProperties):
|
|||||||
else:
|
else:
|
||||||
doc = mx.createDocument()
|
doc = mx.createDocument()
|
||||||
|
|
||||||
# TODO add implementation
|
node_parser = ShaderNodeOutputMaterial(doc, mat, output_node, obj)
|
||||||
# node_parser = ShaderNodeOutputMaterial(doc, mat, output_node, obj)
|
if not node_parser.export():
|
||||||
# if not node_parser.export():
|
return False
|
||||||
# return False
|
|
||||||
|
|
||||||
if not doc:
|
if not doc:
|
||||||
log.warn("Incorrect node tree to export", mx_node_tree)
|
log.warn("Incorrect node tree to export", mx_node_tree)
|
||||||
|
@ -12,6 +12,7 @@ from . import MATERIALX_Panel, MATERIALX_ChildPanel
|
|||||||
from ..node_tree import MxNodeTree, NODE_LAYER_SEPARATION_WIDTH
|
from ..node_tree import MxNodeTree, NODE_LAYER_SEPARATION_WIDTH
|
||||||
from ..nodes.node import is_mx_node_valid
|
from ..nodes.node import is_mx_node_valid
|
||||||
from .. import utils
|
from .. import utils
|
||||||
|
from ..preferences import addon_preferences
|
||||||
from ..utils import pass_node_reroute, title_str, mx_properties
|
from ..utils import pass_node_reroute, title_str, mx_properties
|
||||||
|
|
||||||
from ..utils import logging
|
from ..utils import logging
|
||||||
@ -698,7 +699,7 @@ class MATERIAL_PT_dev(MATERIALX_ChildPanel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return config.show_dev_settings
|
return addon_preferences().dev_tools
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
@ -717,8 +718,6 @@ def depsgraph_update(depsgraph):
|
|||||||
if not hasattr(screen, 'areas'):
|
if not hasattr(screen, 'areas'):
|
||||||
return
|
return
|
||||||
|
|
||||||
bpy.types.NODE_HT_header.remove(update_material_ui)
|
|
||||||
|
|
||||||
for window in context.window_manager.windows:
|
for window in context.window_manager.windows:
|
||||||
for area in window.screen.areas:
|
for area in window.screen.areas:
|
||||||
if not mx_node_tree:
|
if not mx_node_tree:
|
||||||
@ -739,36 +738,3 @@ def depsgraph_update(depsgraph):
|
|||||||
space.node_tree = mx_node_tree
|
space.node_tree = mx_node_tree
|
||||||
|
|
||||||
mx_node_tree.update_links()
|
mx_node_tree.update_links()
|
||||||
|
|
||||||
bpy.types.NODE_HT_header.append(update_material_ui)
|
|
||||||
|
|
||||||
|
|
||||||
# update for material ui according to MaterialX nodetree header changes
|
|
||||||
def update_material_ui(self, context):
|
|
||||||
obj = context.active_object
|
|
||||||
if not obj:
|
|
||||||
return
|
|
||||||
|
|
||||||
mat = obj.active_material
|
|
||||||
if not mat:
|
|
||||||
return
|
|
||||||
|
|
||||||
space = context.space_data
|
|
||||||
if space.tree_type != utils.with_prefix('MxNodeTree'):
|
|
||||||
return
|
|
||||||
|
|
||||||
ui_mx_node_tree = mx_properties(mat).mx_node_tree
|
|
||||||
editor_node_tree = space.node_tree
|
|
||||||
|
|
||||||
if editor_node_tree != ui_mx_node_tree and not space.pin and editor_node_tree:
|
|
||||||
mx_properties(mat).mx_node_tree = editor_node_tree
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
# set update for material ui according to MaterialX nodetree header changes
|
|
||||||
bpy.types.NODE_HT_header.append(update_material_ui)
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
# remove update for material ui according to MaterialX nodetree header changes
|
|
||||||
bpy.types.NODE_HT_header.remove(update_material_ui)
|
|
||||||
|
@ -13,11 +13,9 @@ from concurrent import futures
|
|||||||
|
|
||||||
import bpy.utils.previews
|
import bpy.utils.previews
|
||||||
|
|
||||||
from ..utils import logging, update_ui, MATLIB_DIR
|
from ..utils import logging, update_ui, MATLIB_DIR, MATLIB_URL
|
||||||
log = logging.Log('matlib.manager')
|
log = logging.Log('matlib.manager')
|
||||||
|
|
||||||
URL = "https://api.matlib.gpuopen.com/api"
|
|
||||||
|
|
||||||
|
|
||||||
def download_file(url, path, cache_check=True):
|
def download_file(url, path, cache_check=True):
|
||||||
if cache_check and path.is_file():
|
if cache_check and path.is_file():
|
||||||
@ -101,7 +99,7 @@ class Render:
|
|||||||
return self.material().cache_dir
|
return self.material().cache_dir
|
||||||
|
|
||||||
def get_info(self, cache_chek=True):
|
def get_info(self, cache_chek=True):
|
||||||
json_data = request_json(f"{URL}/renders/{self.id}", None,
|
json_data = request_json(f"{MATLIB_URL}/renders/{self.id}", None,
|
||||||
self.cache_dir / f"R-{self.id[:8]}.json", cache_chek)
|
self.cache_dir / f"R-{self.id[:8]}.json", cache_chek)
|
||||||
|
|
||||||
self.author = json_data['author']
|
self.author = json_data['author']
|
||||||
@ -152,7 +150,7 @@ class Package:
|
|||||||
return self.file_path.is_file()
|
return self.file_path.is_file()
|
||||||
|
|
||||||
def get_info(self, cache_check=True):
|
def get_info(self, cache_check=True):
|
||||||
json_data = request_json(f"{URL}/packages/{self.id}", None,
|
json_data = request_json(f"{MATLIB_URL}/packages/{self.id}", None,
|
||||||
self.cache_dir / "info.json", cache_check)
|
self.cache_dir / "info.json", cache_check)
|
||||||
|
|
||||||
self.author = json_data['author']
|
self.author = json_data['author']
|
||||||
@ -212,7 +210,7 @@ class Category:
|
|||||||
if not self.id:
|
if not self.id:
|
||||||
return
|
return
|
||||||
|
|
||||||
json_data = request_json(f"{URL}/categories/{self.id}", None,
|
json_data = request_json(f"{MATLIB_URL}/categories/{self.id}", None,
|
||||||
self.cache_dir / f"C-{self.id[:8]}.json", use_cache)
|
self.cache_dir / f"C-{self.id[:8]}.json", use_cache)
|
||||||
|
|
||||||
self.title = json_data['title']
|
self.title = json_data['title']
|
||||||
@ -263,7 +261,7 @@ class Material:
|
|||||||
limit = 500
|
limit = 500
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
res_json = request_json(f"{URL}/materials", {'limit': limit, 'offset': offset}, None)
|
res_json = request_json(f"{MATLIB_URL}/materials", {'limit': limit, 'offset': offset}, None)
|
||||||
|
|
||||||
count = res_json['count']
|
count = res_json['count']
|
||||||
|
|
||||||
|
@ -1,17 +1,6 @@
|
|||||||
# **********************************************************************
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
# Copyright 2020 Advanced Micro Devices, Inc
|
# Copyright 2022, AMD
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
# ********************************************************************
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from nodeitems_utils import NodeCategory, NodeItem
|
from nodeitems_utils import NodeCategory, NodeItem
|
||||||
|
@ -26,10 +26,37 @@ NODE_CLASSES_DIR = ADDON_DATA_DIR / NODE_CLASSES_FOLDER
|
|||||||
|
|
||||||
MATLIB_FOLDER = "matlib"
|
MATLIB_FOLDER = "matlib"
|
||||||
MATLIB_DIR = ADDON_DATA_DIR / MATLIB_FOLDER
|
MATLIB_DIR = ADDON_DATA_DIR / MATLIB_FOLDER
|
||||||
|
MATLIB_URL = "https://api.matlib.gpuopen.com/api"
|
||||||
|
|
||||||
|
SUPPORTED_FORMATS = {".png", ".jpeg", ".jpg", ".hdr", ".tga", ".bmp"}
|
||||||
|
DEFAULT_FORMAT = ".hdr"
|
||||||
|
BLENDER_DEFAULT_FORMAT = "HDR"
|
||||||
|
BLENDER_DEFAULT_COLOR_MODE = "RGB"
|
||||||
|
READONLY_IMAGE_FORMATS = {".dds"} # blender can read these formats, but can't write
|
||||||
|
|
||||||
os.environ['MATERIALX_SEARCH_PATH'] = str(MX_LIBS_DIR)
|
os.environ['MATERIALX_SEARCH_PATH'] = str(MX_LIBS_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialXProperties(bpy.types.PropertyGroup):
|
||||||
|
bl_type = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(cls):
|
||||||
|
setattr(cls.bl_type, ADDON_ALIAS, bpy.props.PointerProperty(
|
||||||
|
name="MaterialX properties",
|
||||||
|
description="MaterialX properties",
|
||||||
|
type=cls,
|
||||||
|
))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def unregister(cls):
|
||||||
|
delattr(cls.bl_type, ADDON_ALIAS)
|
||||||
|
|
||||||
|
|
||||||
|
def mx_properties(obj):
|
||||||
|
return getattr(obj, ADDON_ALIAS)
|
||||||
|
|
||||||
|
|
||||||
def with_prefix(name, separator='.', upper=False):
|
def with_prefix(name, separator='.', upper=False):
|
||||||
return f"{ADDON_ALIAS.upper() if upper else ADDON_ALIAS}{separator}{name}"
|
return f"{ADDON_ALIAS.upper() if upper else ADDON_ALIAS}{separator}{name}"
|
||||||
|
|
||||||
@ -391,21 +418,59 @@ def update_ui(area_type='PROPERTIES', region_type='WINDOW'):
|
|||||||
region.tag_redraw()
|
region.tag_redraw()
|
||||||
|
|
||||||
|
|
||||||
class MaterialXProperties(bpy.types.PropertyGroup):
|
def cache_image_file(image: bpy.types.Image, cache_check=True):
|
||||||
bl_type = None
|
image_path = Path(image.filepath_from_user())
|
||||||
|
if not image.packed_file and image.source != 'GENERATED':
|
||||||
|
if not image_path.is_file():
|
||||||
|
log.warn("Image is missing", image, image_path)
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
image_suffix = image_path.suffix.lower()
|
||||||
def register(cls):
|
|
||||||
setattr(cls.bl_type, ADDON_ALIAS, bpy.props.PointerProperty(
|
|
||||||
name="MaterialX properties",
|
|
||||||
description="MaterialX properties",
|
|
||||||
type=cls,
|
|
||||||
))
|
|
||||||
|
|
||||||
@classmethod
|
if image_suffix in SUPPORTED_FORMATS and\
|
||||||
def unregister(cls):
|
f".{image.file_format.lower()}" in SUPPORTED_FORMATS and not image.is_dirty:
|
||||||
delattr(cls.bl_type, ADDON_ALIAS)
|
return image_path
|
||||||
|
|
||||||
|
if image_suffix in READONLY_IMAGE_FORMATS:
|
||||||
|
return image_path
|
||||||
|
|
||||||
|
temp_path = get_temp_file(DEFAULT_FORMAT, image_path.stem)
|
||||||
|
if cache_check and image.source != 'GENERATED' and temp_path.is_file():
|
||||||
|
return temp_path
|
||||||
|
|
||||||
|
scene = bpy.context.scene
|
||||||
|
user_format = scene.render.image_settings.file_format
|
||||||
|
user_color_mode = scene.render.image_settings.color_mode
|
||||||
|
|
||||||
|
# in some scenes the color_mode is undefined
|
||||||
|
# we can read it but unable to assign back, so switch it to 'RGB' if color_mode isn't selected
|
||||||
|
if not user_color_mode:
|
||||||
|
user_color_mode = 'RGB'
|
||||||
|
|
||||||
|
scene.render.image_settings.file_format = BLENDER_DEFAULT_FORMAT
|
||||||
|
scene.render.image_settings.color_mode = BLENDER_DEFAULT_COLOR_MODE
|
||||||
|
|
||||||
|
try:
|
||||||
|
image.save_render(filepath=str(temp_path))
|
||||||
|
finally:
|
||||||
|
scene.render.image_settings.file_format = user_format
|
||||||
|
scene.render.image_settings.color_mode = user_color_mode
|
||||||
|
|
||||||
|
return temp_path
|
||||||
|
|
||||||
|
|
||||||
def mx_properties(obj):
|
def cache_image_file_path(image_path, cache_check=True):
|
||||||
return getattr(obj, ADDON_ALIAS)
|
if image_path.suffix.lower() in SUPPORTED_FORMATS:
|
||||||
|
return image_path
|
||||||
|
|
||||||
|
if cache_check:
|
||||||
|
temp_path = get_temp_file(DEFAULT_FORMAT, image_path.name)
|
||||||
|
if temp_path.is_file():
|
||||||
|
return temp_path
|
||||||
|
|
||||||
|
image = bpy.data.images.load(str(image_path))
|
||||||
|
try:
|
||||||
|
return cache_image_file(image, cache_check)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
bpy.data.images.remove(image)
|
||||||
|
Loading…
Reference in New Issue
Block a user