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():
|
||||
ui.register()
|
||||
properties.register()
|
||||
register_classes()
|
||||
|
||||
|
||||
def unregister():
|
||||
ui.unregister()
|
||||
properties.unregister()
|
||||
unregister_classes()
|
||||
|
@ -7,6 +7,7 @@ import bpy
|
||||
import MaterialX as mx
|
||||
|
||||
from ..node_tree import MxNodeTree
|
||||
from ..bl_nodes.nodes import ShaderNodeOutputMaterial
|
||||
from ..utils import MX_LIBS_DIR
|
||||
|
||||
from ..utils import logging, get_temp_file, MaterialXProperties
|
||||
@ -45,10 +46,9 @@ class MaterialProperties(MaterialXProperties):
|
||||
|
||||
doc = mx.createDocument()
|
||||
|
||||
# TODO add implementation
|
||||
# node_parser = ShaderNodeOutputMaterial(doc, material, output_node, obj)
|
||||
# if not node_parser.export():
|
||||
# return None
|
||||
node_parser = ShaderNodeOutputMaterial(doc, material, output_node, obj)
|
||||
if not node_parser.export():
|
||||
return None
|
||||
|
||||
return doc
|
||||
|
||||
@ -78,10 +78,9 @@ class MaterialProperties(MaterialXProperties):
|
||||
else:
|
||||
doc = mx.createDocument()
|
||||
|
||||
# TODO add implementation
|
||||
# node_parser = ShaderNodeOutputMaterial(doc, mat, output_node, obj)
|
||||
# if not node_parser.export():
|
||||
# return False
|
||||
node_parser = ShaderNodeOutputMaterial(doc, mat, output_node, obj)
|
||||
if not node_parser.export():
|
||||
return False
|
||||
|
||||
if not doc:
|
||||
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 ..nodes.node import is_mx_node_valid
|
||||
from .. import utils
|
||||
from ..preferences import addon_preferences
|
||||
from ..utils import pass_node_reroute, title_str, mx_properties
|
||||
|
||||
from ..utils import logging
|
||||
@ -698,7 +699,7 @@ class MATERIAL_PT_dev(MATERIALX_ChildPanel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return config.show_dev_settings
|
||||
return addon_preferences().dev_tools
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@ -717,8 +718,6 @@ def depsgraph_update(depsgraph):
|
||||
if not hasattr(screen, 'areas'):
|
||||
return
|
||||
|
||||
bpy.types.NODE_HT_header.remove(update_material_ui)
|
||||
|
||||
for window in context.window_manager.windows:
|
||||
for area in window.screen.areas:
|
||||
if not mx_node_tree:
|
||||
@ -739,36 +738,3 @@ def depsgraph_update(depsgraph):
|
||||
space.node_tree = mx_node_tree
|
||||
|
||||
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
|
||||
|
||||
from ..utils import logging, update_ui, MATLIB_DIR
|
||||
from ..utils import logging, update_ui, MATLIB_DIR, MATLIB_URL
|
||||
log = logging.Log('matlib.manager')
|
||||
|
||||
URL = "https://api.matlib.gpuopen.com/api"
|
||||
|
||||
|
||||
def download_file(url, path, cache_check=True):
|
||||
if cache_check and path.is_file():
|
||||
@ -101,7 +99,7 @@ class Render:
|
||||
return self.material().cache_dir
|
||||
|
||||
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.author = json_data['author']
|
||||
@ -152,7 +150,7 @@ class Package:
|
||||
return self.file_path.is_file()
|
||||
|
||||
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.author = json_data['author']
|
||||
@ -212,7 +210,7 @@ class Category:
|
||||
if not self.id:
|
||||
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.title = json_data['title']
|
||||
@ -263,7 +261,7 @@ class Material:
|
||||
limit = 500
|
||||
|
||||
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']
|
||||
|
||||
|
@ -1,17 +1,6 @@
|
||||
# **********************************************************************
|
||||
# Copyright 2020 Advanced Micro Devices, Inc
|
||||
# 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.
|
||||
# ********************************************************************
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright 2022, AMD
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from nodeitems_utils import NodeCategory, NodeItem
|
||||
|
@ -26,10 +26,37 @@ NODE_CLASSES_DIR = ADDON_DATA_DIR / NODE_CLASSES_FOLDER
|
||||
|
||||
MATLIB_FOLDER = "matlib"
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
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()
|
||||
|
||||
|
||||
class MaterialXProperties(bpy.types.PropertyGroup):
|
||||
bl_type = None
|
||||
def cache_image_file(image: bpy.types.Image, cache_check=True):
|
||||
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
|
||||
def register(cls):
|
||||
setattr(cls.bl_type, ADDON_ALIAS, bpy.props.PointerProperty(
|
||||
name="MaterialX properties",
|
||||
description="MaterialX properties",
|
||||
type=cls,
|
||||
))
|
||||
image_suffix = image_path.suffix.lower()
|
||||
|
||||
@classmethod
|
||||
def unregister(cls):
|
||||
delattr(cls.bl_type, ADDON_ALIAS)
|
||||
if image_suffix in SUPPORTED_FORMATS and\
|
||||
f".{image.file_format.lower()}" in SUPPORTED_FORMATS and not image.is_dirty:
|
||||
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):
|
||||
return getattr(obj, ADDON_ALIAS)
|
||||
def cache_image_file_path(image_path, cache_check=True):
|
||||
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