WIP: MaterialX addon #104594

Closed
Bogdan Nagirniak wants to merge 34 commits from BogdanNagirniak/blender-addons:materialx-addon into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
16 changed files with 1104 additions and 81 deletions
Showing only changes of commit 8c3745c394 - Show all commits

View 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))

View 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

View 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 *

View 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

View 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

View 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()

View 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

View 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

View 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

View 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

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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']

View File

@ -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

View File

@ -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)