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.
18 changed files with 366 additions and 1983 deletions
Showing only changes of commit e9826b3008 - Show all commits

View File

@ -25,10 +25,8 @@ import bpy
from . import ( from . import (
preferences, preferences,
node_tree,
nodes, nodes,
matlib, ui,
material,
utils, utils,
) )
@ -37,7 +35,6 @@ log = logging.Log("__init__")
register_classes, unregister_classes = bpy.utils.register_classes_factory([ register_classes, unregister_classes = bpy.utils.register_classes_factory([
node_tree.MxNodeTree,
preferences.AddonPreferences, preferences.AddonPreferences,
]) ])
@ -47,8 +44,7 @@ def register():
register_classes() register_classes()
nodes.register() nodes.register()
material.register() ui.register()
matlib.register()
def unregister(): def unregister():
@ -56,7 +52,6 @@ def unregister():
utils.clear_temp_dir() utils.clear_temp_dir()
matlib.unregister() ui.unregister()
material.unregister()
nodes.unregister() nodes.unregister()
unregister_classes() unregister_classes()

View File

@ -9,6 +9,7 @@ import MaterialX as mx
from .. import utils from .. import utils
from ..utils import pass_node_reroute from ..utils import pass_node_reroute
from ..nodes import get_mx_node_cls from ..nodes import get_mx_node_cls
from ..nodes.node import MxNode
from .. import logging from .. import logging
log = logging.Log("bl_nodes.node_parser") log = logging.Log("bl_nodes.node_parser")
@ -354,7 +355,11 @@ class NodeParser:
if not link: if not link:
return None return None
return self._export_node(link.from_node, link.from_socket.identifier, link.to_socket) if isinstance(link.from_node, MxNode):
mx_node = link.from_node.compute(link.from_socket.name, doc=self.doc)
return mx_node
return self._export_node(link.from_node, link.from_socket.name, link.to_socket)
def get_input_value(self, in_key): def get_input_value(self, in_key):
""" Returns linked node or default socket value """ """ Returns linked node or default socket value """

View File

@ -1,7 +1,8 @@
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2022, AMD # Copyright 2022, AMD
import MaterialX as mx
from .node_parser import NodeParser, Id from .node_parser import NodeParser, Id, log
class ShaderNodeOutputMaterial(NodeParser): class ShaderNodeOutputMaterial(NodeParser):
@ -15,14 +16,12 @@ class ShaderNodeOutputMaterial(NodeParser):
if surface is None: if surface is None:
return None return None
if surface.type == 'BSDF': linked_input_type = surface.getType() if isinstance(surface, mx.Node) else surface.type
surface = self.create_node('surface', 'surfaceshader', {
'bsdf': surface, if linked_input_type != 'surfaceshader':
}) log.warn("Incorrect node tree to export: output node doesn't have correct input")
elif surface.type == 'EDF':
surface = self.create_node('surface', 'surfaceshader', { return None
'edf': surface,
})
result = self.create_node('surfacematerial', 'material', { result = self.create_node('surfacematerial', 'material', {
'surfaceshader': surface, 'surfaceshader': surface,

View File

@ -2,6 +2,7 @@
# Copyright 2022, AMD # Copyright 2022, AMD
import math import math
import MaterialX as mx
from .node_parser import NodeParser from .node_parser import NodeParser
from . import log from . import log
@ -15,6 +16,9 @@ def enabled(val):
if val is None: if val is None:
return False return False
if isinstance(val, mx.Node):
return True
if isinstance(val.data, float) and math.isclose(val.data, 0.0): if isinstance(val.data, float) and math.isclose(val.data, 0.0):
return False return False

View File

@ -1,41 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2022, AMD
import bpy
from . import (
ui,
properties
)
register_classes, unregister_classes = bpy.utils.register_classes_factory([
ui.MATERIAL_OP_new_mx_node_tree,
ui.MATERIAL_OP_duplicate_mx_node_tree,
ui.MATERIAL_OP_convert_to_materialx,
ui.MATERIAL_OP_duplicate_mat_mx_node_tree,
ui.MATERIAL_OP_link_mx_node_tree,
ui.MATERIAL_OP_unlink_mx_node_tree,
ui.MATERIAL_MT_mx_node_tree,
ui.MATERIAL_PT_materialx,
ui.MATERIAL_PT_materialx_surfaceshader,
ui.MATERIAL_PT_materialx_displacementshader,
ui.MATERIAL_OP_link_mx_node,
ui.MATERIAL_OP_invoke_popup_input_nodes,
ui.MATERIAL_OP_invoke_popup_shader_nodes,
ui.MATERIAL_OP_remove_node,
ui.MATERIAL_OP_disconnect_node,
ui.MATERIAL_OP_export_file,
ui.MATERIAL_OP_export_console,
ui.MATERIAL_PT_tools,
ui.MATERIAL_PT_dev,
])
def register():
properties.register()
register_classes()
def unregister():
properties.unregister()
unregister_classes()

View File

@ -1,130 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2022, AMD
import traceback
import bpy
import MaterialX as mx
from ..node_tree import MxNodeTree
from ..bl_nodes.output import ShaderNodeOutputMaterial
from ..utils import MX_LIBS_DIR, mx_properties, get_temp_file, MaterialXProperties, with_prefix
from .. import logging
log = logging.Log('material.properties')
class MaterialProperties(MaterialXProperties):
bl_type = bpy.types.Material
def update_mx_node_tree(self, context):
# trying to show MaterialX area with node tree or Shader area
material = self.id_data
mx_node_tree = mx_properties(material).mx_node_tree
if not mx_node_tree:
return
screen = context.screen
if not hasattr(screen, 'areas'):
return
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.ui_type not in (MxNodeTree.bl_idname, 'ShaderNodeTree'):
continue
space = next(s for s in area.spaces if s.type == 'NODE_EDITOR')
if space.pin or space.shader_type != 'OBJECT':
continue
area.ui_type = MxNodeTree.bl_idname
space.node_tree = mx_node_tree
mx_node_tree: bpy.props.PointerProperty(type=MxNodeTree, update=update_mx_node_tree)
@property
def output_node(self):
material = self.id_data
if not material.node_tree:
return None
return next((node for node in material.node_tree.nodes if
node.bl_idname == ShaderNodeOutputMaterial.__name__ and
node.is_active_output), None)
def export(self, obj: bpy.types.Object, check_mx_node_tree=True) -> [mx.Document, None]:
if check_mx_node_tree and self.mx_node_tree:
return self.mx_node_tree.export()
material = self.id_data
output_node = self.output_node
if not output_node:
return None
doc = mx.createDocument()
node_parser = ShaderNodeOutputMaterial(doc, material, output_node, obj)
if not node_parser.export():
return None
return doc
def get_materialx_data(self, obj: bpy.types.Object):
doc = self.export(obj)
if not doc:
return None, None
mat = self.id_data
mtlx_file = get_temp_file(".mtlx", f'{mat.name}_{self.mx_node_tree.name if self.mx_node_tree else ""}')
mx.writeToXmlFile(doc, str(mtlx_file))
return mtlx_file, doc
def convert_to_materialx(self, obj: bpy.types.Object = None):
mat = self.id_data
output_node = self.output_node
if not output_node:
log.warn("Incorrect node tree to export: output node doesn't exist")
return False
mx_node_tree = bpy.data.node_groups.new(f"MX_{mat.name}", type=MxNodeTree.bl_idname)
if obj:
doc = self.export(obj)
else:
doc = mx.createDocument()
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)
return False
mtlx_file = get_temp_file(".mtlx",
f'{mat.name}_{self.mx_node_tree.name if self.mx_node_tree else ""}')
mx.writeToXmlFile(doc, str(mtlx_file))
search_path = mx.FileSearchPath(str(mtlx_file.parent))
search_path.append(str(MX_LIBS_DIR))
try:
mx.readFromXmlFile(doc, str(mtlx_file), searchPath=search_path)
mx_node_tree.import_(doc, mtlx_file)
self.mx_node_tree = mx_node_tree
except Exception as e:
log.error(traceback.format_exc(), mtlx_file)
return False
return True
register, unregister = bpy.utils.register_classes_factory((
MaterialProperties,
))

View File

@ -1,542 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2022, AMD
from pathlib import Path
import MaterialX as mx
import bpy
from bpy_extras.io_utils import ExportHelper
from ..node_tree import MxNodeTree, NODE_LAYER_SEPARATION_WIDTH
from ..nodes.node import is_mx_node_valid
from .. import utils
from ..utils import pass_node_reroute, title_str, mx_properties
from ..preferences import addon_preferences
from ..utils import logging
log = logging.Log(tag='material.ui')
class MATERIAL_OP_new_mx_node_tree(bpy.types.Operator):
"""Create new MaterialX node tree for selected material"""
bl_idname = utils.with_prefix('material_new_mx_node_tree')
bl_label = "New"
def execute(self, context):
mat = context.material
mx_node_tree = bpy.data.node_groups.new(f"MX_{mat.name}", type=MxNodeTree.bl_idname)
mx_node_tree.create_basic_nodes()
mx_properties(mat).mx_node_tree = mx_node_tree
return {"FINISHED"}
class MATERIAL_OP_duplicate_mat_mx_node_tree(bpy.types.Operator):
"""Create duplicates of Material and MaterialX node tree for selected material"""
bl_idname = utils.with_prefix('material_duplicate_mat_mx_node_tree')
bl_label = ""
def execute(self, context):
bpy.ops.material.new()
bpy.ops.materialx.material_duplicate_mx_node_tree()
return {"FINISHED"}
class MATERIAL_OP_duplicate_mx_node_tree(bpy.types.Operator):
"""Create duplicate of MaterialX node tree for selected material"""
bl_idname = utils.with_prefix('material_duplicate_mx_node_tree')
bl_label = ""
def execute(self, context):
mat = context.object.active_material
mx_node_tree = mx_properties(mat).mx_node_tree
if mx_node_tree:
mx_properties(mat).mx_node_tree = mx_node_tree.copy()
return {"FINISHED"}
class MATERIAL_OP_convert_to_materialx(bpy.types.Operator):
"""Converts standard shader node tree to MaterialX node tree for selected material"""
bl_idname = utils.with_prefix('material_convert_to_materialx')
bl_label = "Convert to MaterialX"
def execute(self, context):
if not mx_properties(context.material).convert_to_materialx(context.object):
return {'CANCELLED'}
return {"FINISHED"}
class MATERIAL_OP_link_mx_node_tree(bpy.types.Operator):
"""Link MaterialX node tree to selected material"""
bl_idname = utils.with_prefix('material_link_mx_node_tree')
bl_label = ""
mx_node_tree_name: bpy.props.StringProperty(default="")
def execute(self, context):
mx_properties(context.material).mx_node_tree = bpy.data.node_groups[self.mx_node_tree_name]
return {"FINISHED"}
class MATERIAL_OP_unlink_mx_node_tree(bpy.types.Operator):
"""Unlink MaterialX node tree from selected material"""
bl_idname = utils.with_prefix('material_unlink_mx_node_tree')
bl_label = ""
def execute(self, context):
mx_properties(context.material).mx_node_tree = None
return {"FINISHED"}
class MATERIAL_MT_mx_node_tree(bpy.types.Menu):
bl_idname = utils.with_prefix('MATERIAL_MT_mx_node_tree', '_', True)
bl_label = "MX Nodetree"
def draw(self, context):
layout = self.layout
node_groups = bpy.data.node_groups
for ng in node_groups:
if ng.bl_idname != utils.with_prefix('MxNodeTree'):
continue
row = layout.row()
row.enabled = bool(ng.output_node)
op = row.operator(MATERIAL_OP_link_mx_node_tree.bl_idname,
text=ng.name, icon='MATERIAL')
op.mx_node_tree_name = ng.name
class MATERIAL_PT_materialx(bpy.types.Panel):
bl_idname = utils.with_prefix("MATERIAL_PT_materialx", '_', True)
bl_label = "MaterialX"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "material"
@classmethod
def poll(cls, context):
return context.material
def draw(self, context):
mat_materialx = mx_properties(context.material)
layout = self.layout
row = layout.row(align=True)
row.menu(MATERIAL_MT_mx_node_tree.bl_idname, text="", icon='MATERIAL')
if mat_materialx.mx_node_tree:
row.prop(mat_materialx.mx_node_tree, 'name', text="")
row.operator(MATERIAL_OP_convert_to_materialx.bl_idname, icon='FILE_TICK', text="")
row.operator(MATERIAL_OP_duplicate_mx_node_tree.bl_idname, icon='DUPLICATE')
row.operator(MATERIAL_OP_unlink_mx_node_tree.bl_idname, icon='X')
else:
row.operator(MATERIAL_OP_convert_to_materialx.bl_idname, icon='FILE_TICK', text="Convert")
row.operator(MATERIAL_OP_new_mx_node_tree.bl_idname, icon='ADD', text="")
class MATERIAL_OP_link_mx_node(bpy.types.Operator):
"""Link MaterialX node"""
bl_idname = utils.with_prefix('material_link_mx_node')
bl_label = ""
new_node_name: bpy.props.StringProperty()
input_num: bpy.props.IntProperty()
current_node_name: bpy.props.StringProperty()
def execute(self, context):
layout = self.layout
node_tree = mx_properties(context.material).mx_node_tree
current_node = mx_properties(context.material).mx_node_tree.nodes[self.current_node_name]
input = current_node.inputs[self.input_num]
link = next((link for link in input.links), None) if input.is_linked else None
linked_node_name = link.from_node.bl_idname if link else None
if linked_node_name:
if linked_node_name != self.new_node_name:
bpy.ops.materialx.material_remove_node(input_node_name=link.from_node.name)
else:
return {"FINISHED"}
new_node = node_tree.nodes.new(self.new_node_name)
new_node.location = (current_node.location[0] - NODE_LAYER_SEPARATION_WIDTH,
current_node.location[1])
node_tree.links.new(new_node.outputs[0], current_node.inputs[self.input_num])
return {"FINISHED"}
class MATERIAL_OP_invoke_popup_input_nodes(bpy.types.Operator):
"""Open panel with nodes to link"""
bl_idname = utils.with_prefix('material_invoke_popup_input_nodes')
bl_label = ""
input_num: bpy.props.IntProperty()
current_node_name: bpy.props.StringProperty()
def execute(self, context):
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=600)
def draw(self, context):
from ..nodes import mx_node_classes
MAX_COLUMN_ITEMS = 34
split = self.layout.split()
cat = ""
i = 0
col = None
for cls in mx_node_classes:
if cls.category in ("PBR", "material"):
continue
if not col or i >= MAX_COLUMN_ITEMS:
i = 0
col = split.column()
col.emboss = 'PULLDOWN_MENU'
if cat != cls.category:
cat = cls.category
col.label(text=title_str(cat), icon='NODE')
i += 1
row = col.row()
row.alignment = 'LEFT'
op = row.operator(MATERIAL_OP_link_mx_node.bl_idname, text=cls.bl_label)
op.new_node_name = cls.bl_idname
op.input_num = self.input_num
op.current_node_name = self.current_node_name
i += 1
input = mx_properties(context.material).mx_node_tree.nodes[self.current_node_name].inputs[self.input_num]
if input.is_linked:
link = input.links[0]
col = split.column()
col.emboss = 'PULLDOWN_MENU'
col.label(text="Link")
row = col.row()
row.alignment = 'LEFT'
op = row.operator(MATERIAL_OP_remove_node.bl_idname)
op.input_node_name = link.from_node.name
row = col.row()
row.alignment = 'LEFT'
op = row.operator(MATERIAL_OP_disconnect_node.bl_idname)
op.output_node_name = link.to_node.name
op.input_num = self.input_num
class MATERIAL_OP_invoke_popup_shader_nodes(bpy.types.Operator):
"""Open panel with shader nodes to link"""
bl_idname = utils.with_prefix('material_invoke_popup_shader_nodes')
bl_label = ""
input_num: bpy.props.IntProperty()
new_node_name: bpy.props.StringProperty()
def execute(self, context):
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=300)
def draw(self, context):
from ..nodes import mx_node_classes
split = self.layout.split()
col = split.column()
col.emboss = 'PULLDOWN_MENU'
col.label(text="PBR", icon='NODE')
output_node = mx_properties(context.material).mx_node_tree.output_node
for cls in mx_node_classes:
if cls.category != "PBR":
continue
row = col.row()
row.alignment = 'LEFT'
op = row.operator(MATERIAL_OP_link_mx_node.bl_idname, text=cls.bl_label)
op.new_node_name = cls.bl_idname
op.input_num = self.input_num
op.current_node_name = output_node.name
input = output_node.inputs[self.input_num]
if input.is_linked:
link = input.links[0]
col = split.column()
col.emboss = 'PULLDOWN_MENU'
col.label(text="Link")
row = col.row()
row.alignment = 'LEFT'
op = row.operator(MATERIAL_OP_remove_node.bl_idname)
op.input_node_name = link.from_node.name
row = col.row()
row.alignment = 'LEFT'
op = row.operator(MATERIAL_OP_disconnect_node.bl_idname)
op.output_node_name = link.to_node.name
op.input_num = self.input_num
class MATERIAL_OP_remove_node(bpy.types.Operator):
"""Remove linked node"""
bl_idname = utils.with_prefix('material_remove_node')
bl_label = "Remove"
input_node_name: bpy.props.StringProperty()
def remove_nodes(self, context, node):
for input in node.inputs:
if input.is_linked:
for link in input.links:
self.remove_nodes(context, link.from_node)
mx_properties(context.material).mx_node_tree.nodes.remove(node)
def execute(self, context):
node_tree = mx_properties(context.material).mx_node_tree
input_node = node_tree.nodes[self.input_node_name]
self.remove_nodes(context, input_node)
return {'FINISHED'}
class MATERIAL_OP_disconnect_node(bpy.types.Operator):
"""Disconnect linked node"""
bl_idname = utils.with_prefix('material_disconnect_node')
bl_label = "Disconnect"
output_node_name: bpy.props.StringProperty()
input_num: bpy.props.IntProperty()
def execute(self, context):
node_tree = mx_properties(context.material).mx_node_tree
output_node = node_tree.nodes[self.output_node_name]
links = output_node.inputs[self.input_num].links
link = next((link for link in links), None)
if link:
node_tree.links.remove(link)
return {'FINISHED'}
class MATERIAL_PT_materialx_output(bpy.types.Panel):
bl_label = ""
bl_parent_id = MATERIAL_PT_materialx.bl_idname
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
out_key = ""
@classmethod
def poll(cls, context):
return bool(mx_properties(context.material).mx_node_tree)
def draw(self, context):
layout = self.layout
node_tree = mx_properties(context.material).mx_node_tree
output_node = node_tree.output_node
if not output_node:
layout.label(text="No output node")
return
input = output_node.inputs[self.out_key]
link = next((link for link in input.links if link.is_valid), None)
split = layout.split(factor=0.4)
row = split.row(align=True)
row.alignment = 'RIGHT'
row.label(text='Surface')
row = split.row(align=True)
box = row.box()
box.scale_x = 0.7
box.scale_y = 0.5
op = box.operator(MATERIAL_OP_invoke_popup_shader_nodes.bl_idname, icon='HANDLETYPE_AUTO_CLAMP_VEC')
op.input_num = output_node.inputs.find(self.out_key)
if link and is_mx_node_valid(link.from_node):
row.prop(link.from_node, 'name', text="")
else:
box = row.box()
box.scale_y = 0.5
box.label(text='None')
row.label(icon='BLANK1')
if not link:
return
if not is_mx_node_valid(link.from_node):
layout.label(text="Unsupported node")
return
link = pass_node_reroute(link)
if not link:
return
layout.separator()
link.from_node.draw_node_view(context, layout)
class MATERIAL_PT_materialx_surfaceshader(MATERIAL_PT_materialx_output):
bl_idname = utils.with_prefix('MATERIAL_PT_materialx_surfaceshader', '_', True)
bl_label = "Surface Shader"
out_key = 'surfaceshader'
class MATERIAL_PT_materialx_displacementshader(MATERIAL_PT_materialx_output):
bl_idname = utils.with_prefix('MATERIAL_PT_materialx_displacementshader', '_', True)
bl_label = "Displacement Shader"
bl_options = {'DEFAULT_CLOSED'}
out_key = 'displacementshader'
class MATERIAL_OP_export_file(bpy.types.Operator, ExportHelper):
bl_idname = utils.with_prefix('material_export_file')
bl_label = "Export to File"
bl_description = "Export material as MaterialX node tree to .mtlx file"
# region properties
filename_ext = ".mtlx"
filepath: bpy.props.StringProperty(
name="File Path",
description="File path used for exporting material as MaterialX node tree to .mtlx file",
maxlen=1024,
subtype="FILE_PATH"
)
filter_glob: bpy.props.StringProperty(
default="*.mtlx",
options={'HIDDEN'},
)
is_export_deps: bpy.props.BoolProperty(
name="Include dependencies",
description="Export used MaterialX dependencies",
default=False
)
is_export_textures: bpy.props.BoolProperty(
name="Export textures",
description="Export bound textures to corresponded folder",
default=True
)
is_clean_texture_folder: bpy.props.BoolProperty(
name="Сlean texture folder",
description="Сlean texture folder before export",
default=False
)
texture_dir_name: bpy.props.StringProperty(
name="Folder name",
description="Texture folder name used for exporting files",
default='textures',
maxlen=1024,
)
# endregion
def execute(self, context):
materialx_prop = mx_properties(context.material)
doc = materialx_prop.export(None, False)
if not doc:
return {'CANCELLED'}
utils.export_mx_to_file(doc, self.filepath,
mx_node_tree=None,
# is_export_deps=self.is_export_deps,
is_export_textures=self.is_export_textures,
texture_dir_name=self.texture_dir_name)
return {'FINISHED'}
def draw(self, context):
# self.layout.prop(self, 'is_export_deps')
col = self.layout.column(align=False)
col.prop(self, 'is_export_textures')
row = col.row()
row.enabled = self.is_export_textures
row.prop(self, 'texture_dir_name', text='')
class MATERIAL_OP_export_console(bpy.types.Operator):
bl_idname = utils.with_prefix('material_export_console')
bl_label = "Export to Console"
bl_description = "Export material as MaterialX node tree to console"
def execute(self, context):
doc = mx_properties(context.material).export(context.object, False)
if not doc:
return {'CANCELLED'}
print(mx.writeToXmlString(doc))
return {'FINISHED'}
class MATERIAL_PT_tools(bpy.types.Panel):
bl_idname = utils.with_prefix('MATERIAL_PT_tools', '_', True)
bl_label = "MaterialX Tools"
bl_space_type = "NODE_EDITOR"
bl_region_type = "UI"
bl_category = "Tool"
@classmethod
def poll(cls, context):
tree = context.space_data.edit_tree
return tree and tree.bl_idname == bpy.types.ShaderNodeTree.__name__
def draw(self, context):
mat_materialx = mx_properties(context.material)
layout = self.layout
row = layout.row(align=True)
row.menu(MATERIAL_MT_mx_node_tree.bl_idname, text="", icon='MATERIAL')
if mat_materialx.mx_node_tree:
row.prop(mat_materialx.mx_node_tree, 'name', text="")
row.operator(MATERIAL_OP_convert_to_materialx.bl_idname, icon='FILE_TICK', text="")
row.operator(MATERIAL_OP_duplicate_mx_node_tree.bl_idname, icon='DUPLICATE')
row.operator(MATERIAL_OP_unlink_mx_node_tree.bl_idname, icon='X')
else:
row.operator(MATERIAL_OP_convert_to_materialx.bl_idname, icon='FILE_TICK', text="Convert")
row.operator(MATERIAL_OP_new_mx_node_tree.bl_idname, icon='ADD', text="")
layout.operator(MATERIAL_OP_export_file.bl_idname, icon='EXPORT')
class MATERIAL_PT_dev(bpy.types.Panel):
bl_idname = utils.with_prefix('MATERIAL_PT_dev', '_', True)
bl_label = "Dev"
bl_parent_id = MATERIAL_PT_tools.bl_idname
bl_space_type = "NODE_EDITOR"
bl_region_type = "UI"
@classmethod
def poll(cls, context):
preferences = addon_preferences()
return preferences.dev_tools if preferences else True
def draw(self, context):
layout = self.layout
layout.operator(MATERIAL_OP_export_console.bl_idname)

View File

@ -1,34 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2022, AMD
import bpy
from . import ui, properties
register_properties, unregister_properties = bpy.utils.register_classes_factory(
[
properties.MatlibProperties,
properties.WindowManagerProperties,
]
)
register_ui, unregister_ui = bpy.utils.register_classes_factory(
[
ui.MATLIB_PT_matlib,
ui.MATLIB_PT_matlib_tools,
ui.MATLIB_OP_load_materials,
ui.MATLIB_OP_load_package,
ui.MATLIB_OP_import_material,
ui.MATERIAL_OP_matlib_clear_search,
]
)
def register():
register_properties()
register_ui()
def unregister():
unregister_ui()
unregister_properties()

View File

@ -1,433 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2022, AMD
import requests
import weakref
from dataclasses import dataclass, field
import shutil
from pathlib import Path
import zipfile
import json
import threading
from concurrent import futures
import bpy.utils.previews
from ..utils import logging, update_ui, MATLIB_DIR, MATLIB_URL
log = logging.Log('matlib.manager')
def download_file(url, path, cache_check=True):
if cache_check and path.is_file():
return path
log("download_file", f"{url=}, {path=}")
path.parent.mkdir(parents=True, exist_ok=True)
with requests.get(url, stream=True) as response:
with open(path, 'wb') as f:
shutil.copyfileobj(response.raw, f)
log("download_file", "done")
return path
def download_file_callback(url, path, update_callback, cache_check=True):
if cache_check and path.is_file():
return None
log("download_file_callback", f"{url=}, {path=}")
path.parent.mkdir(parents=True, exist_ok=True)
path_raw = path.with_suffix(".raw")
size = 0
with requests.get(url, stream=True) as response:
with open(path_raw, 'wb') as f:
if update_callback:
for chunk in response.iter_content(chunk_size=8192):
size += len(chunk)
update_callback(size)
f.write(chunk)
path_raw.rename(path)
log("download_file_callback", "done")
return path
def request_json(url, params, path, cache_check=True):
if cache_check and path and path.is_file():
with open(path) as json_file:
return json.load(json_file)
log("request_json", f"{url=}, {params=}, {path=}")
response = requests.get(url, params=params)
res_json = response.json()
if path:
save_json(res_json, path)
log("request_json", "done")
return res_json
def save_json(json_obj, path):
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w') as outfile:
json.dump(json_obj, outfile)
@dataclass(init=False)
class Render:
id: str
author: str = field(init=False, default=None)
image: str = field(init=False, default=None)
image_url: str = field(init=False, default=None)
image_path: Path = field(init=False, default=None)
thumbnail: str = field(init=False, default=None)
thumbnail_url: str = field(init=False, default=None)
thumbnail_path: Path = field(init=False, default=None)
thumbnail_icon_id: int = field(init=False, default=None)
def __init__(self, id, material):
self.id = id
self.material = weakref.ref(material)
@property
def cache_dir(self):
return self.material().cache_dir
def get_info(self, cache_chek=True):
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']
self.image = json_data['image']
self.image_url = json_data['image_url']
self.thumbnail = json_data['thumbnail']
self.thumbnail_url = json_data['thumbnail_url']
def get_image(self, cache_check=True):
self.image_path = download_file(self.image_url,
self.cache_dir / self.image, cache_check)
def get_thumbnail(self, cache_check=True):
self.thumbnail_path = download_file(self.thumbnail_url,
self.cache_dir / self.thumbnail, cache_check)
def thumbnail_load(self, pcoll):
thumb = pcoll.get(self.thumbnail)
if not thumb:
thumb = pcoll.load(self.thumbnail, str(self.thumbnail_path), 'IMAGE')
self.thumbnail_icon_id = thumb.icon_id
@dataclass(init=False)
class Package:
id: str
author: str = field(init=False, default=None)
label: str = field(init=False, default=None)
file: str = field(init=False, default=None)
file_url: str = field(init=False, default=None)
size_str: str = field(init=False, default=None)
def __init__(self, id, material):
self.id = id
self.material = weakref.ref(material)
self.size_load = None
@property
def cache_dir(self):
return self.material().cache_dir / f"P-{self.id[:8]}"
@property
def file_path(self):
return self.cache_dir / self.file
@property
def has_file(self):
return self.file_path.is_file()
def get_info(self, cache_check=True):
json_data = request_json(f"{MATLIB_URL}/packages/{self.id}", None,
self.cache_dir / "info.json", cache_check)
self.author = json_data['author']
self.file = json_data['file']
self.file_url = json_data['file_url']
self.label = json_data['label']
self.size_str = json_data['size']
def download(self, cache_check=True):
def callback(size):
self.size_load = size
update_ui()
download_file_callback(self.file_url, self.file_path, callback, cache_check)
def unzip(self, path=None, cache_check=True):
if not path:
path = self.cache_dir / "package"
if path.is_dir() and not cache_check:
shutil.rmtree(path, ignore_errors=True)
if not path.is_dir():
with zipfile.ZipFile(self.file_path) as z:
z.extractall(path=path)
mtlx_file = next(path.glob("**/*.mtlx"))
return mtlx_file
@property
def size(self):
n, b = self.size_str.split(" ")
size = float(n)
if b == "MB":
size *= 1048576 # 2 ** 20
elif b == "KB":
size *= 1024 # 2 ** 10
elif b == "GB":
size *= 2 ** 30
return int(size)
def __lt__(self, other):
return self.size < other.size
@dataclass
class Category:
id: str
title: str = field(init=False, default=None)
@property
def cache_dir(self):
return MATLIB_DIR
def get_info(self, use_cache=True):
if not self.id:
return
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']
def __lt__(self, other):
return self.title < other.title
@dataclass(init=False)
class Material:
id: str
author: str
title: str
description: str
category: Category
status: str
renders: list[Render]
packages: list[Package]
def __init__(self, mat_json):
self.id = mat_json['id']
self.author = mat_json['author']
self.title = mat_json['title']
self.description = mat_json['description']
self.category = Category(mat_json['category'])
self.status = mat_json['status']
self.renders = []
for id in mat_json['renders_order']:
self.renders.append(Render(id, self))
self.packages = []
for id in mat_json['packages']:
self.packages.append(Package(id, self))
save_json(mat_json, self.cache_dir / "info.json")
def __lt__(self, other):
return self.title.lower() < other.title.lower()
@property
def cache_dir(self):
return MATLIB_DIR / f"M-{self.id[:8]}"
@classmethod
def get_materials(cls):
offset = 0
limit = 500
while True:
res_json = request_json(f"{MATLIB_URL}/materials", {'limit': limit, 'offset': offset}, None)
count = res_json['count']
for mat_json in res_json['results']:
mat = Material(mat_json)
if not mat.packages or not mat.category.id:
continue
yield mat
offset += limit
if offset >= count:
break
@classmethod
def get_materials_cache(cls):
for f in MATLIB_DIR.glob("M-*/info.json"):
with open(f) as json_file:
mat_json = json.load(json_file)
yield Material(mat_json)
class Manager:
def __init__(self):
self.materials = None
self.categories = None
self.pcoll = None
self.load_thread = None
self.package_executor = None
self.status = ""
self.is_synced = None
def __del__(self):
# bpy.utils.previews.remove(self.pcoll)
pass
def set_status(self, msg):
self.status = msg
update_ui()
@property
def materials_list(self):
# required for thread safe purposes
return list(manager.materials.values())
@property
def categories_list(self):
# required for thread safe purposes
return list(manager.categories.values())
def check_load_materials(self, reset=False):
# required is not None condition to prevent further update if no material is found at first time.
if self.materials is not None and not reset:
return True
if reset and self.pcoll:
bpy.utils.previews.remove(self.pcoll)
self.materials = {}
self.categories = {}
self.pcoll = bpy.utils.previews.new()
def category_load(cat):
cat.get_info()
self.categories[cat.id] = cat
def material_load(mat, is_cached):
for render in mat.renders:
render.get_info()
render.get_thumbnail()
render.thumbnail_load(self.pcoll)
for package in mat.packages:
package.get_info()
self.materials[mat.id] = mat
self.set_status(f"Syncing {len(self.materials)} {'cached' if is_cached else 'online'} materials...")
def load():
self.is_synced = False
self.set_status("Start syncing...")
with futures.ThreadPoolExecutor() as executor:
try:
#
# getting cached materials
#
materials = {mat.id: mat for mat in Material.get_materials_cache()}
categories = {mat.category.id: mat.category for mat in materials.values()}
# loading categories
category_loaders = [executor.submit(category_load, cat)
for cat in categories.values()]
for future in futures.as_completed(category_loaders):
future.result()
# updating category for cached materials
for mat in materials.values():
mat.category.get_info()
# loading cached materials
material_loaders = [executor.submit(material_load, mat, True)
for mat in materials.values()]
for future in futures.as_completed(material_loaders):
future.result()
#
# getting and syncing with online materials
#
online_materials = {mat.id: mat for mat in Material.get_materials()}
# loading new categories
new_categories = {}
for mat in online_materials.values():
cat = mat.category
if cat.id not in categories and cat.id not in new_categories:
new_categories[cat.id] = cat
category_loaders = [executor.submit(category_load, cat)
for cat in new_categories.values()]
for future in futures.as_completed(category_loaders):
future.result()
# updating categories for online materials
for mat in online_materials.values():
mat.category.get_info()
# loading online materials
material_loaders = [executor.submit(material_load, mat, False)
for mat in online_materials.values()]
for future in futures.as_completed(material_loaders):
future.result()
self.set_status(f"Syncing {len(self.materials)} materials completed")
except requests.exceptions.RequestException as err:
executor.shutdown(wait=True, cancel_futures=True)
self.set_status(f"Connection error. Synced {len(self.materials)} materials")
log.error(err)
finally:
self.is_synced = True
self.load_thread = threading.Thread(target=load, daemon=True)
self.load_thread.start()
return False
def load_package(self, package):
package.size_load = 0
def package_load():
try:
package.download()
except requests.exceptions.RequestException as err:
log.error(err)
package.size_load = None
update_ui()
if not self.package_executor:
self.package_executor = futures.ThreadPoolExecutor()
self.package_executor.submit(package_load)
manager = Manager()

View File

@ -1,130 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2022, AMD
import bpy
from ..matlib.manager import manager
from ..utils import MaterialXProperties
class MatlibProperties(bpy.types.PropertyGroup):
def get_materials(self) -> dict:
materials = {}
search_str = self.search.strip().lower()
materials_list = manager.materials_list
for mat in materials_list:
if search_str not in mat.title.lower():
continue
if not (mat.category.id == self.category_id or self.category_id == 'ALL'):
continue
materials[mat.id] = mat
return materials
def get_materials_prop(self, context):
materials = []
for i, mat in enumerate(sorted(self.get_materials().values())):
description = mat.title
if mat.description:
description += f"\n{mat.description}"
description += f"\nCategory: {mat.category.title}\nAuthor: {mat.author}"
icon_id = mat.renders[0].thumbnail_icon_id if mat.renders else 'MATERIAL'
materials.append((mat.id, mat.title, description, icon_id, i))
return materials
def get_categories_prop(self, context):
categories = []
if manager.categories is None:
return categories
categories += [('ALL', "All Categories", "Show materials for all categories")]
categories_list = manager.categories_list
categories += ((cat.id, cat.title, f"Show materials with category {cat.title}")
for cat in sorted(categories_list))
return categories
def get_packages_prop(self, context):
packages = []
mat = self.material
if not mat:
return packages
for i, p in enumerate(sorted(mat.packages)):
description = f"Package: {p.label} ({p.size_str})\nAuthor: {p.author}"
if p.has_file:
description += "\nReady to import"
icon_id = 'RADIOBUT_ON' if p.has_file else 'RADIOBUT_OFF'
packages.append((p.id, f"{p.label} ({p.size_str})", description, icon_id, i))
return packages
def update_material(self, context):
mat = self.material
if mat:
self.package_id = min(mat.packages).id
def update_category(self, context):
materials = self.get_materials()
if not materials:
return
mat = min(materials.values())
self.material_id = mat.id
self.package_id = min(mat.packages).id
def update_search(self, context):
materials = self.get_materials()
if not materials or self.material_id in materials:
return
mat = min(materials.values())
self.material_id = mat.id
self.package_id = min(mat.packages).id
material_id: bpy.props.EnumProperty(
name="Material",
description="Select material",
items=get_materials_prop,
update=update_material,
)
category_id: bpy.props.EnumProperty(
name="Category",
description="Select materials category",
items=get_categories_prop,
update=update_category,
)
search: bpy.props.StringProperty(
name="Search",
description="Search materials by title",
update=update_search,
)
package_id: bpy.props.EnumProperty(
name="Package",
description="Selected material package",
items=get_packages_prop,
)
@property
def material(self):
return manager.materials.get(self.material_id)
@property
def package(self):
mat = self.material
if not mat:
return None
return next((p for p in mat.packages if p.id == self.package_id), None)
class WindowManagerProperties(MaterialXProperties):
bl_type = bpy.types.WindowManager
matlib: bpy.props.PointerProperty(type=MatlibProperties)

View File

@ -1,189 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2022, AMD
import traceback
import textwrap
import MaterialX as mx
import bpy
from .. import utils
from ..node_tree import MxNodeTree
from .manager import manager
from ..utils import logging
log = logging.Log('matlib.ui')
class MATERIAL_OP_matlib_clear_search(bpy.types.Operator):
"""Create new MaterialX node tree for selected material"""
bl_idname = utils.with_prefix("matlib_clear_search")
bl_label = ""
def execute(self, context):
utils.mx_properties(context.window_manager).matlib.search = ''
return {"FINISHED"}
class MATLIB_OP_load_materials(bpy.types.Operator):
"""Load materials"""
bl_idname = utils.with_prefix("matlib_load")
bl_label = "Reload Library"
def execute(self, context):
manager.check_load_materials(reset=True)
return {"FINISHED"}
class MATLIB_OP_import_material(bpy.types.Operator):
"""Import Material Package to material"""
bl_idname = utils.with_prefix("matlib_import_material")
bl_label = "Import Material Package"
def execute(self, context):
matlib_prop = utils.mx_properties(context.window_manager).matlib
package = matlib_prop.package
mtlx_file = package.unzip()
# getting/creating MxNodeTree
bl_material = context.material
mx_node_tree = utils.mx_properties(bl_material).mx_node_tree
if not mx_node_tree:
mx_node_tree = bpy.data.node_groups.new(f"MX_{bl_material.name}",
type=MxNodeTree.bl_idname)
utils.mx_properties(bl_material).mx_node_tree = mx_node_tree
log(f"Reading: {mtlx_file}")
doc = mx.createDocument()
search_path = mx.FileSearchPath(str(mtlx_file.parent))
search_path.append(str(utils.MX_LIBS_DIR))
try:
mx.readFromXmlFile(doc, str(mtlx_file), searchPath=search_path)
mx_node_tree.import_(doc, mtlx_file)
except Exception as e:
log.error(traceback.format_exc(), mtlx_file)
return {'CANCELLED'}
return {"FINISHED"}
class MATLIB_OP_load_package(bpy.types.Operator):
"""Download material package"""
bl_idname = utils.with_prefix("matlib_load_package")
bl_label = "Download Package"
def execute(self, context):
matlib_prop = utils.mx_properties(context.window_manager).matlib
manager.load_package(matlib_prop.package)
return {"FINISHED"}
class MATLIB_PT_matlib(bpy.types.Panel):
bl_idname = utils.with_prefix("MATLIB_PT_matlib", '_', True)
bl_label = "MaterialX Library"
bl_context = "material"
bl_region_type = 'WINDOW'
bl_space_type = 'PROPERTIES'
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.material
def draw(self, context):
layout = self.layout
matlib_prop = utils.mx_properties(context.window_manager).matlib
manager.check_load_materials()
# category
layout.prop(matlib_prop, 'category_id')
# search
row = layout.row(align=True)
row.prop(matlib_prop, 'search', text="", icon='VIEWZOOM')
if matlib_prop.search:
row.operator(MATERIAL_OP_matlib_clear_search.bl_idname, icon='X')
# materials
col = layout.column(align=True)
materials = matlib_prop.get_materials()
if not materials:
col.label(text="Start syncing..." if not manager.materials else "No materials found")
return
row = col.row()
row.alignment = 'RIGHT'
row.label(text=f"{len(materials)} materials")
col.template_icon_view(matlib_prop, 'material_id', show_labels=True)
mat = matlib_prop.material
if not mat:
return
# other material renders
if len(mat.renders) > 1:
grid = col.grid_flow(align=True)
for i, render in enumerate(mat.renders):
if i % 6 == 0:
row = grid.row()
row.alignment = 'CENTER'
row.template_icon(render.thumbnail_icon_id, scale=5)
# material title
row = col.row()
row.alignment = 'CENTER'
row.label(text=mat.title)
# material description
col = layout.column(align=True)
if mat.description:
for line in textwrap.wrap(mat.description, 60):
col.label(text=line)
col = layout.column(align=True)
col.label(text=f"Category: {mat.category.title}")
col.label(text=f"Author: {mat.author}")
# packages
package = matlib_prop.package
if not package:
return
layout.prop(matlib_prop, 'package_id', icon='DOCUMENTS')
row = layout.row()
if package.has_file:
row.operator(MATLIB_OP_import_material.bl_idname, icon='IMPORT')
else:
if package.size_load is None:
row.operator(MATLIB_OP_load_package.bl_idname, icon='IMPORT')
else:
percent = min(100, int(package.size_load * 100 / package.size))
row.operator(MATLIB_OP_load_package.bl_idname, icon='IMPORT',
text=f"Downloading Package...{percent}%")
row.enabled = False
class MATLIB_PT_matlib_tools(bpy.types.Panel):
bl_label = "Tools"
bl_context = "material"
bl_region_type = 'WINDOW'
bl_space_type = 'PROPERTIES'
bl_parent_id = utils.with_prefix('MATLIB_PT_matlib', '_', True)
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
col = layout.column()
col.label(text=manager.status)
row = col.row()
row.enabled = bool(manager.is_synced)
row.operator(MATLIB_OP_load_materials.bl_idname, icon='FILE_REFRESH')

View File

@ -1,268 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2022, AMD
import MaterialX as mx
import bpy
from . import utils
from . import logging
log = logging.Log('node_tree')
NODE_LAYER_SEPARATION_WIDTH = 280
NODE_LAYER_SHIFT_X = 30
NODE_LAYER_SHIFT_Y = 100
class MxNodeTree(bpy.types.ShaderNodeTree):
"""
MaterialX NodeTree
"""
bl_label = "MaterialX"
bl_icon = "NODE_MATERIAL"
bl_idname = utils.with_prefix("MxNodeTree")
_do_update = True
@property
def output_node(self):
return next((node for node in self.nodes
if node.bl_idname == utils.with_prefix('MxNode_STD_surfacematerial')), None)
@property
def output_node_volume(self):
return next((node for node in self.nodes
if node.bl_idname == utils.with_prefix('MxNode_STD_volumematerial')), None)
def no_update_call(self, op, *args, **kwargs):
"""This function prevents call of self.update() during calling our function"""
if not self._do_update:
return op(*args, **kwargs)
self._do_update = False
try:
return op(*args, **kwargs)
finally:
self._do_update = True
def export(self) -> mx.Document:
output_node = self.output_node
if not output_node:
return None
doc = mx.createDocument()
surfacematerial = output_node.compute(0, doc=doc)
if not surfacematerial:
return None
return doc
def import_(self, doc: mx.Document, file_path):
def prepare_for_import():
surfacematerial = next(
(n for n in doc.getNodes() if n.getCategory() == 'surfacematerial'), None)
if surfacematerial:
return
mat = doc.getMaterials()[0]
sr = mat.getShaderRefs()[0]
doc.removeMaterial(mat.getName())
node_name = sr.getName()
if not node_name.startswith("SR_"):
node_name = f"SR_{node_name}"
node = doc.addNode(sr.getNodeString(), node_name, 'surfaceshader')
for sr_input in sr.getBindInputs():
input = node.addInput(sr_input.getName(), sr_input.getType())
ng_name = sr_input.getNodeGraphString()
if ng_name:
input.setAttribute('nodegraph', ng_name)
input.setAttribute('output', sr_input.getOutputString())
else:
input.setValue(sr_input.getValue())
surfacematerial = doc.addNode('surfacematerial', mat.getName(), 'material')
input = surfacematerial.addInput('surfaceshader', node.getType())
input.setNodeName(node.getName())
def do_import():
from .nodes import get_mx_node_cls
self.nodes.clear()
def import_node(mx_node, mx_output_name=None, look_nodedef=True):
mx_nodegraph = mx_node.getParent()
node_path = mx_node.getNamePath()
file_prefix = utils.get_file_prefix(mx_node, file_path)
if node_path in self.nodes:
return self.nodes[node_path]
try:
MxNode_cls, data_type = get_mx_node_cls(mx_node)
except KeyError as e:
if not look_nodedef:
log.warn(e)
return None
# looking for nodedef and switching to another nodegraph defined in doc
nodedef = next(nd for nd in doc.getNodeDefs()
if nd.getNodeString() == mx_node.getCategory() and
nd.getType() == mx_node.getType())
new_mx_nodegraph = next(ng for ng in doc.getNodeGraphs()
if ng.getNodeDefString() == nodedef.getName())
mx_output = new_mx_nodegraph.getOutput(mx_output_name)
node_name = mx_output.getNodeName()
new_mx_node = new_mx_nodegraph.getNode(node_name)
return import_node(new_mx_node, None, False)
node = self.nodes.new(MxNode_cls.bl_idname)
node.name = node_path
node.data_type = data_type
nodedef = node.nodedef
for mx_input in mx_node.getInputs():
input_name = mx_input.getName()
nd_input = nodedef.getInput(input_name)
if nd_input.getAttribute('uniform') == 'true':
node.set_param_value(input_name, utils.parse_value(
node, mx_input.getValue(), mx_input.getType(), file_prefix))
continue
if input_name not in node.inputs:
log.error(f"Incorrect input name '{input_name}' for node {node}")
continue
val = mx_input.getValue()
if val is not None:
node.set_input_value(input_name, utils.parse_value(
node, val, mx_input.getType(), file_prefix))
continue
node_name = mx_input.getNodeName()
if node_name:
new_mx_node = mx_nodegraph.getNode(node_name)
if not new_mx_node:
log.error(f"Couldn't find node '{node_name}' in nodegraph '{mx_nodegraph.getNamePath()}'")
continue
new_node = import_node(new_mx_node)
out_name = mx_input.getAttribute('output')
if len(new_node.nodedef.getOutputs()) > 1 and out_name:
new_node_output = new_node.outputs[out_name]
else:
new_node_output = new_node.outputs[0]
self.links.new(new_node_output, node.inputs[input_name])
continue
new_nodegraph_name = mx_input.getAttribute('nodegraph')
if new_nodegraph_name:
mx_output_name = mx_input.getAttribute('output')
new_mx_nodegraph = mx_nodegraph.getNodeGraph(new_nodegraph_name)
mx_output = new_mx_nodegraph.getOutput(mx_output_name)
node_name = mx_output.getNodeName()
new_mx_node = new_mx_nodegraph.getNode(node_name)
new_node = import_node(new_mx_node, mx_output_name)
if not new_node:
continue
out_name = mx_output.getAttribute('output')
if len(new_node.nodedef.getOutputs()) > 1 and out_name:
new_node_output = new_node.outputs[out_name]
else:
new_node_output = new_node.outputs[0]
self.links.new(new_node_output, node.inputs[input_name])
continue
node.check_ui_folders()
return node
mx_node = next(n for n in doc.getNodes() if n.getCategory() == 'surfacematerial')
output_node = import_node(mx_node, 0)
if not output_node:
return
# arranging nodes by layers
layer = {output_node}
layer_index = 0
layers = {}
while layer:
new_layer = set()
for node in layer:
layers[node] = layer_index
for inp in node.inputs:
for link in inp.links:
new_layer.add(link.from_node)
layer = new_layer
layer_index += 1
node_layers = [[] for _ in range(max(layers.values()) + 1)]
for node in self.nodes:
node_layers[layers[node]].append(node)
# placing nodes by layers
loc_x = 0
for i, nodes in enumerate(node_layers):
loc_y = 0
for node in nodes:
node.location = (loc_x, loc_y)
loc_y -= NODE_LAYER_SHIFT_Y
loc_x -= NODE_LAYER_SHIFT_X
loc_x -= NODE_LAYER_SEPARATION_WIDTH
prepare_for_import()
self.no_update_call(do_import)
self.update_()
def create_basic_nodes(self, node_name='PBR_standard_surface'):
""" Reset basic node tree structure using scene or USD file as an input """
def create_nodes():
self.nodes.clear()
mat_node = self.nodes.new(utils.with_prefix('MxNode_STD_surfacematerial'))
node = self.nodes.new(utils.with_prefix(f'MxNode_{node_name}'))
node.location = (mat_node.location[0] - NODE_LAYER_SEPARATION_WIDTH,
mat_node.location[1])
self.links.new(node.outputs[0], mat_node.inputs[0])
self.no_update_call(create_nodes)
self.update_()
# this is called from Blender
def update(self):
if not self._do_update:
return
self.update_()
def update_(self):
utils.update_ui()
# We have to call self.update_links via bpy.app.timers.register
# to have slight delay after self.update(). It'll be called once
bpy.app.timers.register(self.update_links)
# this is called from Blender
def update_links(self):
for link in self.links:
socket_from_type = link.from_socket.node.nodedef.getOutput(link.from_socket.name).getType()
socket_to_type = link.to_socket.node.nodedef.getInput(link.to_socket.name).getType()
if socket_to_type != socket_from_type:
link.is_valid = False
continue
link.is_valid = True

View File

@ -7,7 +7,7 @@ import bpy
import nodeitems_utils import nodeitems_utils
import sys import sys
from . import node, categories, generate_node_classes, ui from . import node, categories, generate_node_classes
from .. import utils from .. import utils
@ -29,21 +29,12 @@ register_sockets, unregister_sockets = bpy.utils.register_classes_factory([
node.MxNodeInputSocket, node.MxNodeInputSocket,
node.MxNodeOutputSocket, node.MxNodeOutputSocket,
]) ])
register_ui, unregister_ui = bpy.utils.register_classes_factory([
ui.NODES_OP_import_file,
ui.NODES_OP_export_file,
ui.NODES_OP_export_console,
ui.NODES_OP_create_basic_nodes,
ui.NODES_PT_tools,
ui.NODES_PT_dev,
])
register_nodes, unregister_nodes = bpy.utils.register_classes_factory(mx_node_classes) register_nodes, unregister_nodes = bpy.utils.register_classes_factory(mx_node_classes)
def register(): def register():
register_sockets() register_sockets()
register_ui()
register_nodes() register_nodes()
nodeitems_utils.register_node_categories(utils.with_prefix("MX_NODES"), categories.get_node_categories()) nodeitems_utils.register_node_categories(utils.with_prefix("MX_NODES"), categories.get_node_categories())
@ -53,7 +44,6 @@ def unregister():
nodeitems_utils.unregister_node_categories(utils.with_prefix("MX_NODES")) nodeitems_utils.unregister_node_categories(utils.with_prefix("MX_NODES"))
unregister_nodes() unregister_nodes()
unregister_ui()
unregister_sockets() unregister_sockets()

View File

@ -1,6 +1,8 @@
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2022, AMD # Copyright 2022, AMD
import bpy
from collections import defaultdict from collections import defaultdict
from nodeitems_utils import NodeCategory, NodeItem from nodeitems_utils import NodeCategory, NodeItem
@ -11,7 +13,7 @@ from ..utils import title_str, code_str, with_prefix
class MxNodeCategory(NodeCategory): class MxNodeCategory(NodeCategory):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.space_data.tree_type == with_prefix('MxNodeTree') return context.space_data.tree_type == 'ShaderNodeTree'
def get_node_categories(): def get_node_categories():

View File

@ -130,8 +130,6 @@ def generate_property_code(mx_param, category):
val_str = f'"{val}"' if isinstance(val, str) else str(val) val_str = f'"{val}"' if isinstance(val, str) else str(val)
prop_attr_strings.append(f"{name}={val_str}") prop_attr_strings.append(f"{name}={val_str}")
prop_attr_strings.append("update=MxNode.update_prop")
if mx_type == 'filename' and category in ("texture2d", "texture3d"): if mx_type == 'filename' and category in ("texture2d", "texture3d"):
prop_attr_strings.insert(0, "type=bpy.types.Image") prop_attr_strings.insert(0, "type=bpy.types.Image")

View File

@ -111,9 +111,29 @@ class MxNode(bpy.types.ShaderNode):
def _input_prop_name(self, name): def _input_prop_name(self, name):
return f"nd_{self.data_type}_in_{name}" return f"nd_{self.data_type}_in_{name}"
def update_prop(self, context): def update(self):
bpy.app.timers.register(self.mark_invalid_links)
def mark_invalid_links(self):
if not is_mx_node_valid(self):
return
nodetree = self.id_data nodetree = self.id_data
nodetree.update_()
if not (hasattr(nodetree, 'links')):
return
for link in nodetree.links:
if hasattr(link.from_socket.node, 'nodedef') and hasattr(link.to_socket.node, 'nodedef'):
socket_from_type = link.from_socket.node.nodedef.getOutput(link.from_socket.name).getType()
socket_to_type = link.to_socket.node.nodedef.getInput(link.to_socket.name).getType()
if socket_to_type != socket_from_type:
link.is_valid = False
continue
link.is_valid = True
def update_data_type(self, context): def update_data_type(self, context):
# updating names for inputs and outputs # updating names for inputs and outputs
@ -124,23 +144,20 @@ class MxNode(bpy.types.ShaderNode):
self.outputs[i].name = nd_output.getName() self.outputs[i].name = nd_output.getName()
def init(self, context): def init(self, context):
def init_(): nodedef = self.nodedef
nodedef = self.nodedef
for nd_input in utils.get_nodedef_inputs(nodedef, False): for nd_input in utils.get_nodedef_inputs(nodedef, False):
self.create_input(nd_input) self.create_input(nd_input)
for nd_output in nodedef.getOutputs(): for nd_output in nodedef.getOutputs():
self.create_output(nd_output) self.create_output(nd_output)
if self._ui_folders: if self._ui_folders:
self.update_ui_folders(context) self.update_ui_folders(context)
nodetree = self.id_data
nodetree.no_update_call(init_)
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
is_prop_area = context.area.type == 'PROPERTIES' is_prop_area = context.area.type == 'PROPERTIES'
if len(self._data_types) > 1: if len(self._data_types) > 1:
layout1 = layout layout1 = layout
if is_prop_area: if is_prop_area:
@ -163,6 +180,7 @@ class MxNode(bpy.types.ShaderNode):
for nd_input in utils.get_nodedef_inputs(nodedef, True): for nd_input in utils.get_nodedef_inputs(nodedef, True):
f = nd_input.getAttribute('uifolder') f = nd_input.getAttribute('uifolder')
if f and not getattr(self, self._folder_prop_name(f)): if f and not getattr(self, self._folder_prop_name(f)):
continue continue
@ -185,84 +203,10 @@ class MxNode(bpy.types.ShaderNode):
layout1 = layout1.column() layout1 = layout1.column()
layout1.prop(self, self._input_prop_name(name)) layout1.prop(self, self._input_prop_name(name))
def draw_node_view(self, context, layout):
from ..material.ui import MATERIAL_OP_invoke_popup_input_nodes
layout.use_property_split = True
layout.use_property_decorate = True
self.draw_buttons(context, layout)
for i, socket_in in enumerate(self.inputs):
nd = self.nodedef
uiname = utils.get_attr(nd.getInput(socket_in.name), 'uiname',
utils.title_str(nd.getInput(socket_in.name).getName()))
if socket_in.is_linked:
link = next((link for link in socket_in.links if link.is_valid), None)
if not link:
continue
link = utils.pass_node_reroute(link)
if not link or isinstance(link.from_node, bpy.types.NodeReroute):
continue
split = layout.split(factor=0.4)
split_1 = split.split(factor=0.4)
row = split_1.row()
row.use_property_split = False
row.use_property_decorate = False
row.alignment = 'LEFT'
row.prop(socket_in, "show_expanded",
icon="DISCLOSURE_TRI_DOWN" if socket_in.show_expanded else "DISCLOSURE_TRI_RIGHT",
icon_only=True, emboss=False)
row = split_1.row()
row.alignment = 'RIGHT'
row.label(text=uiname)
row = split.row(align=True)
row.use_property_decorate = False
box = row.box()
box.scale_x = 0.7
box.scale_y = 0.5
box.emboss = 'NONE_OR_STATUS'
op = box.operator(MATERIAL_OP_invoke_popup_input_nodes.bl_idname, icon='HANDLETYPE_AUTO_CLAMP_VEC')
op.input_num = i
op.current_node_name = self.name
row.prop(link.from_node, 'name', text='')
row.label(icon='BLANK1')
if socket_in.show_expanded:
link.from_node.draw_node_view(context, layout)
else:
mx_input = self.nodedef.getInput(socket_in.name)
f = mx_input.getAttribute('uifolder')
is_draw = True
if f:
if not getattr(self, self._folder_prop_name(f)):
is_draw = False
if is_draw:
split = layout.split(factor=0.4)
row = split.row(align=True)
row.alignment = 'RIGHT'
row.label(text=uiname)
row = split.row(align=True)
box = row.box()
box.scale_x = 0.7
box.scale_y = 0.5
op = box.operator(MATERIAL_OP_invoke_popup_input_nodes.bl_idname,
icon='HANDLETYPE_AUTO_CLAMP_VEC')
op.input_num = i
op.current_node_name = self.name
socket_in.draw(context, row, self, '')
# COMPUTE FUNCTION # COMPUTE FUNCTION
def compute(self, out_key, **kwargs): def compute(self, out_key, **kwargs):
from ..bl_nodes.node_parser import NodeItem
log("compute", self, out_key) log("compute", self, out_key)
doc = kwargs['doc'] doc = kwargs['doc']
@ -287,7 +231,7 @@ class MxNode(bpy.types.ShaderNode):
nd_input = self.get_nodedef_input(in_key) nd_input = self.get_nodedef_input(in_key)
nd_type = nd_input.getType() nd_type = nd_input.getType()
if isinstance(val, mx.Node): if isinstance(val, (mx.Node, NodeItem)):
mx_input = mx_node.addInput(nd_input.getName(), nd_type) mx_input = mx_node.addInput(nd_input.getName(), nd_type)
utils.set_param_value(mx_input, val, nd_type) utils.set_param_value(mx_input, val, nd_type)
continue continue
@ -346,6 +290,7 @@ class MxNode(bpy.types.ShaderNode):
def get_input_link(self, in_key: [str, int], **kwargs): def get_input_link(self, in_key: [str, int], **kwargs):
"""Returns linked parsed node or None if nothing is linked or not link is not valid""" """Returns linked parsed node or None if nothing is linked or not link is not valid"""
from ..bl_nodes import node_parser
socket_in = self.inputs[in_key] socket_in = self.inputs[in_key]
if not socket_in.links: if not socket_in.links:
@ -360,11 +305,26 @@ class MxNode(bpy.types.ShaderNode):
if not link: if not link:
return None return None
if not is_mx_node_valid(link.from_node): if isinstance(link.from_node, MxNode):
log.warn(f"Ignoring unsupported node {link.from_node.bl_idname}", link.from_node, link.from_node.id_data) if not is_mx_node_valid(link.from_node):
log.warn(f"Ignoring unsupported node {link.from_node.bl_idname}", link.from_node,
link.from_node.id_data)
return None
return self._compute_node(link.from_node, link.from_socket.name, **kwargs)
NodeParser_cls = node_parser.NodeParser.get_node_parser_cls(link.from_node.bl_idname)
if not NodeParser_cls:
log.warn(f"Ignoring unsupported node {link.from_node.bl_idname}", link.from_node, self.material)
return None return None
return self._compute_node(link.from_node, link.from_socket.name, **kwargs) output_type = NodeParser_cls.get_output_type(link.to_socket)
node_parser_cls = NodeParser_cls(node_parser.Id(), kwargs['doc'], None, link.from_node, None,
link.from_socket.name, output_type, {})
node_item = node_parser_cls.export()
return node_item
def get_input_value(self, in_key: [str, int], **kwargs): def get_input_value(self, in_key: [str, int], **kwargs):
node = self.get_input_link(in_key, **kwargs) node = self.get_input_link(in_key, **kwargs)
@ -393,7 +353,7 @@ class MxNode(bpy.types.ShaderNode):
@classmethod @classmethod
def poll(cls, tree): def poll(cls, tree):
return tree.bl_idname == utils.with_prefix('MxNodeTree') return tree.bl_idname == 'ShaderNodeTree'
def update_ui_folders(self, context): def update_ui_folders(self, context):
for i, nd_input in enumerate(utils.get_nodedef_inputs(self.nodedef, False)): for i, nd_input in enumerate(utils.get_nodedef_inputs(self.nodedef, False)):
@ -401,9 +361,6 @@ class MxNode(bpy.types.ShaderNode):
if f: if f:
self.inputs[i].hide = not getattr(self, self._folder_prop_name(f)) self.inputs[i].hide = not getattr(self, self._folder_prop_name(f))
nodetree = self.id_data
nodetree.update_()
def check_ui_folders(self): def check_ui_folders(self):
if not self._ui_folders: if not self._ui_folders:
return return

View File

@ -2,23 +2,23 @@
# Copyright 2022, AMD # Copyright 2022, AMD
from pathlib import Path from pathlib import Path
import traceback
import traceback
import MaterialX as mx import MaterialX as mx
import bpy import bpy
from bpy_extras.io_utils import ImportHelper, ExportHelper from bpy_extras.io_utils import ImportHelper, ExportHelper
from ..node_tree import MxNodeTree from . import utils
from .. import utils from .utils import import_materialx_from_file, export
from ..preferences import addon_preferences from .preferences import addon_preferences
from ..utils import logging from .utils import logging
log = logging.Log('nodes.ui') log = logging.Log(tag='ui')
class NODES_OP_import_file(bpy.types.Operator, ImportHelper): class MATERIALX_OP_import_file(bpy.types.Operator, ImportHelper):
bl_idname = utils.with_prefix('nodes_import_file') bl_idname = utils.with_prefix('materialx_import_file')
bl_label = "Import from File" bl_label = "Import from File"
bl_description = "Import MaterialX node tree from .mtlx file" bl_description = "Import MaterialX node tree from .mtlx file"
@ -39,7 +39,7 @@ class NODES_OP_import_file(bpy.types.Operator, ImportHelper):
search_path.append(str(utils.MX_LIBS_DIR)) search_path.append(str(utils.MX_LIBS_DIR))
try: try:
mx.readFromXmlFile(doc, str(mtlx_file)) mx.readFromXmlFile(doc, str(mtlx_file))
mx_node_tree.import_(doc, mtlx_file) import_materialx_from_file(mx_node_tree, doc, mtlx_file)
except Exception as e: except Exception as e:
log.error(traceback.format_exc(), mtlx_file) log.error(traceback.format_exc(), mtlx_file)
@ -48,17 +48,17 @@ class NODES_OP_import_file(bpy.types.Operator, ImportHelper):
return {'FINISHED'} return {'FINISHED'}
class NODES_OP_export_file(bpy.types.Operator, ExportHelper): class MATERIALX_OP_export_file(bpy.types.Operator, ExportHelper):
bl_idname = utils.with_prefix('nodes_export_file') bl_idname = utils.with_prefix('materialx_export_file')
bl_label = "Export to File" bl_label = "Export to File"
bl_description = "Export MaterialX node tree to .mtlx file" bl_description = "Export material as MaterialX node tree to .mtlx file"
# region properties # region properties
filename_ext = ".mtlx" filename_ext = ".mtlx"
filepath: bpy.props.StringProperty( filepath: bpy.props.StringProperty(
name="File Path", name="File Path",
description="File path used for exporting MaterialX node tree to .mtlx file", description="File path used for exporting material as MaterialX node tree to .mtlx file",
maxlen=1024, maxlen=1024,
subtype="FILE_PATH" subtype="FILE_PATH"
) )
@ -72,27 +72,30 @@ class NODES_OP_export_file(bpy.types.Operator, ExportHelper):
default=False default=False
) )
is_export_textures: bpy.props.BoolProperty( is_export_textures: bpy.props.BoolProperty(
name="Export bound textures", name="Export textures",
description="Export bound textures to corresponded folder", description="Export bound textures to corresponded folder",
default=True default=True
) )
is_clean_texture_folder: bpy.props.BoolProperty(
name="Сlean texture folder",
description="Сlean texture folder before export",
default=False
)
texture_dir_name: bpy.props.StringProperty( texture_dir_name: bpy.props.StringProperty(
name="Texture folder name", name="Folder name",
description="Texture folder name used for exporting files", description="Texture folder name used for exporting files",
default='textures', default='textures',
maxlen=1024 maxlen=1024,
) )
# endregion # endregion
def execute(self, context): def execute(self, context):
mx_node_tree = context.space_data.edit_tree doc = export(context.material, None)
doc = mx_node_tree.export()
if not doc: if not doc:
log.warn("Incorrect node tree to export", mx_node_tree)
return {'CANCELLED'} return {'CANCELLED'}
utils.export_mx_to_file(doc, self.filepath, utils.export_mx_to_file(doc, self.filepath,
mx_node_tree=mx_node_tree, mx_node_tree=None,
# is_export_deps=self.is_export_deps, # is_export_deps=self.is_export_deps,
is_export_textures=self.is_export_textures, is_export_textures=self.is_export_textures,
texture_dir_name=self.texture_dir_name) texture_dir_name=self.texture_dir_name)
@ -109,68 +112,47 @@ class NODES_OP_export_file(bpy.types.Operator, ExportHelper):
row.enabled = self.is_export_textures row.enabled = self.is_export_textures
row.prop(self, 'texture_dir_name', text='') row.prop(self, 'texture_dir_name', text='')
@staticmethod
def enabled(context):
return bool(context.space_data.edit_tree.output_node)
class MATERIALX_OP_export_console(bpy.types.Operator):
class NODES_OP_export_console(bpy.types.Operator): bl_idname = utils.with_prefix('materialx_export_console')
bl_idname = utils.with_prefix('nodes_export_console')
bl_label = "Export to Console" bl_label = "Export to Console"
bl_description = "Export MaterialX node tree to console" bl_description = "Export material as MaterialX node tree to console"
def execute(self, context): def execute(self, context):
mx_node_tree = context.space_data.edit_tree doc = export(context.material, context.object)
doc = mx_node_tree.export()
if not doc: if not doc:
log.warn("Incorrect node tree to export", mx_node_tree)
return {'CANCELLED'} return {'CANCELLED'}
print(mx.writeToXmlString(doc)) print(mx.writeToXmlString(doc))
return {'FINISHED'} return {'FINISHED'}
@staticmethod
def enabled(context):
return bool(context.space_data.edit_tree.output_node)
class MATERIALX_PT_tools(bpy.types.Panel):
class NODES_OP_create_basic_nodes(bpy.types.Operator): bl_idname = utils.with_prefix('MATERIALX_PT_tools', '_', True)
bl_idname = utils.with_prefix("nodes_create_basic_nodes")
bl_label = "Create Basic Nodes"
bl_description = "Create basic MaterialX nodes"
def execute(self, context):
mx_node_tree = context.space_data.edit_tree
mx_node_tree.create_basic_nodes()
return {'FINISHED'}
class NODES_PT_tools(bpy.types.Panel):
bl_idname = utils.with_prefix('NODES_PT_tools', '_', True)
bl_label = "MaterialX Tools" bl_label = "MaterialX Tools"
bl_space_type = 'NODE_EDITOR' bl_space_type = "NODE_EDITOR"
bl_region_type = 'UI' bl_region_type = "UI"
bl_category = "Tool" bl_category = "Tool"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
tree = context.space_data.edit_tree tree = context.space_data.edit_tree
return tree and tree.bl_idname == MxNodeTree.bl_idname
return tree and tree.bl_idname == 'ShaderNodeTree'
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.operator(NODES_OP_create_basic_nodes.bl_idname, icon='ADD') layout.operator(MATERIALX_OP_import_file.bl_idname, icon='IMPORT')
layout.operator(NODES_OP_import_file.bl_idname, icon='IMPORT') layout.operator(MATERIALX_OP_export_file.bl_idname, icon='EXPORT')
layout.operator(NODES_OP_export_file.bl_idname, icon='EXPORT')
class NODES_PT_dev(bpy.types.Panel): class MATERIALX_PT_dev(bpy.types.Panel):
bl_idname = utils.with_prefix('NODES_PT_dev', '_', True) bl_idname = utils.with_prefix('MATERIALX_PT_dev', '_', True)
bl_parent_id = NODES_PT_tools.bl_idname
bl_label = "Dev" bl_label = "Dev"
bl_space_type = 'NODE_EDITOR' bl_parent_id = MATERIALX_PT_tools.bl_idname
bl_region_type = 'UI' bl_space_type = "NODE_EDITOR"
bl_region_type = "UI"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -179,5 +161,13 @@ class NODES_PT_dev(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.operator(MATERIALX_OP_export_console.bl_idname)
layout.operator(NODES_OP_export_console.bl_idname)
register, unregister = bpy.utils.register_classes_factory([
MATERIALX_OP_import_file,
MATERIALX_OP_export_file,
MATERIALX_OP_export_console,
MATERIALX_PT_tools,
MATERIALX_PT_dev,
])

View File

@ -31,25 +31,9 @@ MATLIB_URL = "https://api.matlib.gpuopen.com/api"
TEMP_FOLDER = "bl-materialx" TEMP_FOLDER = "bl-materialx"
NODE_LAYER_SEPARATION_WIDTH = 280
class MaterialXProperties(bpy.types.PropertyGroup): NODE_LAYER_SHIFT_X = 30
bl_type = None NODE_LAYER_SHIFT_Y = 100
@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):
@ -66,6 +50,8 @@ def code_str(val):
def set_param_value(mx_param, val, nd_type, nd_output=None): def set_param_value(mx_param, val, nd_type, nd_output=None):
from .bl_nodes.node_parser import NodeItem
if isinstance(val, mx.Node): if isinstance(val, mx.Node):
param_nodegraph = mx_param.getParent().getParent() param_nodegraph = mx_param.getParent().getParent()
val_nodegraph = val.getParent() val_nodegraph = val.getParent()
@ -107,12 +93,20 @@ def set_param_value(mx_param, val, nd_type, nd_output=None):
else: else:
mx_param.setValueString(str(val)) mx_param.setValueString(str(val))
elif hasattr(val, 'data') and isinstance(val.data, mx.Node):
set_param_value(mx_param, val.data, nd_type, nd_output)
else: else:
mx_type = getattr(mx, title_str(nd_type), None) mx_type = getattr(mx, title_str(nd_type), None)
if mx_type: if mx_type:
val = mx_type(val) val = mx_type(val.data) if isinstance(val, NodeItem) else mx_type(val)
elif nd_type == 'float' and isinstance(val, tuple):
val = val[0] elif nd_type == 'float':
if isinstance(val, NodeItem):
val = val.data
if isinstance(val, tuple):
val = val[0]
mx_param.setValue(val) mx_param.setValue(val)
@ -446,15 +440,13 @@ def update_ui(area_type='PROPERTIES', region_type='WINDOW'):
def update_materialx_data(depsgraph, materialx_data): def update_materialx_data(depsgraph, materialx_data):
from .node_tree import MxNodeTree
if not depsgraph.updates: if not depsgraph.updates:
return return
for mx_node_tree in (upd.id for upd in depsgraph.updates if isinstance(upd.id, MxNodeTree)): for node_tree in (upd.id for upd in depsgraph.updates if isinstance(upd.id, bpy.types.ShaderNodeTree)):
for material in bpy.data.materials: for material in bpy.data.materials:
if material.materialx.mx_node_tree and material.materialx.mx_node_tree.name == mx_node_tree.name: if material.node_tree and material.node_tree.name == node_tree.name:
doc = material.materialx.export(None) doc = export(material, None)
if not doc: if not doc:
# log.warn("MX export failed", mat) # log.warn("MX export failed", mat)
continue continue
@ -463,7 +455,7 @@ def update_materialx_data(depsgraph, materialx_data):
if not matx_data: if not matx_data:
mx_file = get_temp_file(".mtlx", mx_file = get_temp_file(".mtlx",
f'{material.name}{material.materialx.mx_node_tree.name if material.materialx.mx_node_tree else ""}', f'{material.name}{material.node_tree.name if material.node_tree else ""}',
False) False)
mx.writeToXmlFile(doc, str(mx_file)) mx.writeToXmlFile(doc, str(mx_file))
@ -472,3 +464,221 @@ def update_materialx_data(depsgraph, materialx_data):
materialx_data.append((material.name, str(mx_file), surfacematerial.getName())) materialx_data.append((material.name, str(mx_file), surfacematerial.getName()))
else: else:
mx.writeToXmlFile(doc, str(matx_data[1])) mx.writeToXmlFile(doc, str(matx_data[1]))
def import_materialx_from_file(node_tree, doc: mx.Document, file_path):
def prepare_for_import():
surfacematerial = next(
(n for n in doc.getNodes() if n.getCategory() == 'surfacematerial'), None)
if surfacematerial:
return
mat = doc.getMaterials()[0]
sr = mat.getShaderRefs()[0]
doc.removeMaterial(mat.getName())
node_name = sr.getName()
if not node_name.startswith("SR_"):
node_name = f"SR_{node_name}"
node = doc.addNode(sr.getNodeString(), node_name, 'surfaceshader')
for sr_input in sr.getBindInputs():
input = node.addInput(sr_input.getName(), sr_input.getType())
ng_name = sr_input.getNodeGraphString()
if ng_name:
input.setAttribute('nodegraph', ng_name)
input.setAttribute('output', sr_input.getOutputString())
else:
input.setValue(sr_input.getValue())
surfacematerial = doc.addNode('surfacematerial', mat.getName(), 'material')
input = surfacematerial.addInput('surfaceshader', node.getType())
input.setNodeName(node.getName())
def do_import():
from .nodes import get_mx_node_cls
node_tree.nodes.clear()
def import_node(mx_node, mx_output_name=None, look_nodedef=True):
mx_nodegraph = mx_node.getParent()
node_path = mx_node.getNamePath()
file_prefix = get_file_prefix(mx_node, file_path)
if node_path in node_tree.nodes:
return node_tree.nodes[node_path]
try:
MxNode_cls, data_type = get_mx_node_cls(mx_node)
except KeyError as e:
if not look_nodedef:
log.warn(e)
return None
# looking for nodedef and switching to another nodegraph defined in doc
nodedef = next(nd for nd in doc.getNodeDefs()
if nd.getNodeString() == mx_node.getCategory() and
nd.getType() == mx_node.getType())
new_mx_nodegraph = next(ng for ng in doc.getNodeGraphs()
if ng.getNodeDefString() == nodedef.getName())
mx_output = new_mx_nodegraph.getOutput(mx_output_name)
node_name = mx_output.getNodeName()
new_mx_node = new_mx_nodegraph.getNode(node_name)
return import_node(new_mx_node, None, False)
node = node_tree.nodes.new(MxNode_cls.bl_idname)
node.name = node_path
node.data_type = data_type
nodedef = node.nodedef
for mx_input in mx_node.getInputs():
input_name = mx_input.getName()
nd_input = nodedef.getInput(input_name)
if nd_input.getAttribute('uniform') == 'true':
node.set_param_value(input_name, parse_value(
node, mx_input.getValue(), mx_input.getType(), file_prefix))
continue
if input_name not in node.inputs:
log.error(f"Incorrect input name '{input_name}' for node {node}")
continue
val = mx_input.getValue()
if val is not None:
node.set_input_value(input_name, parse_value(
node, val, mx_input.getType(), file_prefix))
continue
node_name = mx_input.getNodeName()
if node_name:
new_mx_node = mx_nodegraph.getNode(node_name)
if not new_mx_node:
log.error(f"Couldn't find node '{node_name}' in nodegraph '{mx_nodegraph.getNamePath()}'")
continue
new_node = import_node(new_mx_node)
out_name = mx_input.getAttribute('output')
if len(new_node.nodedef.getOutputs()) > 1 and out_name:
new_node_output = new_node.outputs[out_name]
else:
new_node_output = new_node.outputs[0]
node_tree.links.new(new_node_output, node.inputs[input_name])
continue
new_nodegraph_name = mx_input.getAttribute('nodegraph')
if new_nodegraph_name:
mx_output_name = mx_input.getAttribute('output')
new_mx_nodegraph = mx_nodegraph.getNodeGraph(new_nodegraph_name)
mx_output = new_mx_nodegraph.getOutput(mx_output_name)
node_name = mx_output.getNodeName()
new_mx_node = new_mx_nodegraph.getNode(node_name)
new_node = import_node(new_mx_node, mx_output_name)
if not new_node:
continue
out_name = mx_output.getAttribute('output')
if len(new_node.nodedef.getOutputs()) > 1 and out_name:
new_node_output = new_node.outputs[out_name]
else:
new_node_output = new_node.outputs[0]
node_tree.links.new(new_node_output, node.inputs[input_name])
continue
node.check_ui_folders()
return node
mx_node = next(n for n in doc.getNodes() if n.getCategory() == 'surfacematerial')
output_node = import_node(mx_node, 0)
if not output_node:
return
# arranging nodes by layers
layer = {output_node}
layer_index = 0
layers = {}
while layer:
new_layer = set()
for node in layer:
layers[node] = layer_index
for inp in node.inputs:
for link in inp.links:
new_layer.add(link.from_node)
layer = new_layer
layer_index += 1
node_layers = [[] for _ in range(max(layers.values()) + 1)]
for node in node_tree.nodes:
node_layers[layers[node]].append(node)
# placing nodes by layers
loc_x = 0
for i, nodes in enumerate(node_layers):
loc_y = 0
for node in nodes:
node.location = (loc_x, loc_y)
loc_y -= NODE_LAYER_SHIFT_Y
loc_x -= NODE_LAYER_SHIFT_X
loc_x -= NODE_LAYER_SEPARATION_WIDTH
prepare_for_import()
do_import()
def export(material, obj: bpy.types.Object) -> [mx.Document, None]:
from .bl_nodes.output import ShaderNodeOutputMaterial
from .nodes.node import MxNode
output_node = get_output_node(material)
if not output_node:
return None
doc = mx.createDocument()
if isinstance(output_node, MxNode):
mx_node = output_node.compute('out', doc=doc)
return doc
node_parser = ShaderNodeOutputMaterial(doc, material, output_node, obj)
if not node_parser.export():
return None
return doc
def get_materialx_data(material, obj: bpy.types.Object):
doc = export(obj)
if not doc:
return None, None
mtlx_file = get_temp_file(".mtlx", f'{material.name}_{material.node_tree.name if material.node_tree else ""}')
mx.writeToXmlFile(doc, str(mtlx_file))
return mtlx_file, doc
def get_output_node(material):
if not material.node_tree:
return None
bl_output_node = next((node for node in material.node_tree.nodes if
node.bl_idname == 'ShaderNodeOutputMaterial' and
node.is_active_output and node.inputs['Surface'].links), None)
if bl_output_node:
return bl_output_node
mx_output_node = next((node for node in material.node_tree.nodes if
node.bl_idname == with_prefix('MxNode_STD_surfacematerial') and
node.inputs['surfaceshader'].links), None)
return mx_output_node