WIP: MaterialX addon #104594
@ -25,10 +25,8 @@ import bpy
|
||||
|
||||
from . import (
|
||||
preferences,
|
||||
node_tree,
|
||||
nodes,
|
||||
matlib,
|
||||
material,
|
||||
ui,
|
||||
utils,
|
||||
)
|
||||
|
||||
@ -37,7 +35,6 @@ log = logging.Log("__init__")
|
||||
|
||||
|
||||
register_classes, unregister_classes = bpy.utils.register_classes_factory([
|
||||
node_tree.MxNodeTree,
|
||||
preferences.AddonPreferences,
|
||||
])
|
||||
|
||||
@ -47,8 +44,7 @@ def register():
|
||||
|
||||
register_classes()
|
||||
nodes.register()
|
||||
material.register()
|
||||
matlib.register()
|
||||
ui.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
@ -56,7 +52,6 @@ def unregister():
|
||||
|
||||
utils.clear_temp_dir()
|
||||
|
||||
matlib.unregister()
|
||||
material.unregister()
|
||||
ui.unregister()
|
||||
nodes.unregister()
|
||||
unregister_classes()
|
||||
|
@ -9,6 +9,7 @@ import MaterialX as mx
|
||||
from .. import utils
|
||||
from ..utils import pass_node_reroute
|
||||
from ..nodes import get_mx_node_cls
|
||||
from ..nodes.node import MxNode
|
||||
|
||||
from .. import logging
|
||||
log = logging.Log("bl_nodes.node_parser")
|
||||
@ -354,7 +355,11 @@ class NodeParser:
|
||||
if not link:
|
||||
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):
|
||||
""" Returns linked node or default socket value """
|
||||
|
@ -1,7 +1,8 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright 2022, AMD
|
||||
import MaterialX as mx
|
||||
|
||||
from .node_parser import NodeParser, Id
|
||||
from .node_parser import NodeParser, Id, log
|
||||
|
||||
|
||||
class ShaderNodeOutputMaterial(NodeParser):
|
||||
@ -15,14 +16,12 @@ class ShaderNodeOutputMaterial(NodeParser):
|
||||
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,
|
||||
})
|
||||
linked_input_type = surface.getType() if isinstance(surface, mx.Node) else surface.type
|
||||
|
||||
if linked_input_type != 'surfaceshader':
|
||||
log.warn("Incorrect node tree to export: output node doesn't have correct input")
|
||||
|
||||
return None
|
||||
|
||||
result = self.create_node('surfacematerial', 'material', {
|
||||
'surfaceshader': surface,
|
||||
|
@ -2,6 +2,7 @@
|
||||
# Copyright 2022, AMD
|
||||
|
||||
import math
|
||||
import MaterialX as mx
|
||||
|
||||
from .node_parser import NodeParser
|
||||
from . import log
|
||||
@ -15,6 +16,9 @@ def enabled(val):
|
||||
if val is None:
|
||||
return False
|
||||
|
||||
if isinstance(val, mx.Node):
|
||||
return True
|
||||
|
||||
if isinstance(val.data, float) and math.isclose(val.data, 0.0):
|
||||
return False
|
||||
|
||||
|
@ -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()
|
@ -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,
|
||||
))
|
@ -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)
|
@ -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()
|
@ -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()
|
@ -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)
|
@ -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')
|
@ -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
|
@ -7,7 +7,7 @@ import bpy
|
||||
import nodeitems_utils
|
||||
import sys
|
||||
|
||||
from . import node, categories, generate_node_classes, ui
|
||||
from . import node, categories, generate_node_classes
|
||||
from .. import utils
|
||||
|
||||
|
||||
@ -29,21 +29,12 @@ register_sockets, unregister_sockets = bpy.utils.register_classes_factory([
|
||||
node.MxNodeInputSocket,
|
||||
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)
|
||||
|
||||
|
||||
def register():
|
||||
register_sockets()
|
||||
register_ui()
|
||||
register_nodes()
|
||||
|
||||
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"))
|
||||
|
||||
unregister_nodes()
|
||||
unregister_ui()
|
||||
unregister_sockets()
|
||||
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright 2022, AMD
|
||||
|
||||
import bpy
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from nodeitems_utils import NodeCategory, NodeItem
|
||||
@ -11,7 +13,7 @@ from ..utils import title_str, code_str, with_prefix
|
||||
class MxNodeCategory(NodeCategory):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.tree_type == with_prefix('MxNodeTree')
|
||||
return context.space_data.tree_type == 'ShaderNodeTree'
|
||||
|
||||
|
||||
def get_node_categories():
|
||||
|
@ -130,8 +130,6 @@ def generate_property_code(mx_param, category):
|
||||
val_str = f'"{val}"' if isinstance(val, str) else str(val)
|
||||
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"):
|
||||
prop_attr_strings.insert(0, "type=bpy.types.Image")
|
||||
|
||||
|
@ -111,9 +111,29 @@ class MxNode(bpy.types.ShaderNode):
|
||||
def _input_prop_name(self, 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.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):
|
||||
# updating names for inputs and outputs
|
||||
@ -124,7 +144,6 @@ class MxNode(bpy.types.ShaderNode):
|
||||
self.outputs[i].name = nd_output.getName()
|
||||
|
||||
def init(self, context):
|
||||
def init_():
|
||||
nodedef = self.nodedef
|
||||
|
||||
for nd_input in utils.get_nodedef_inputs(nodedef, False):
|
||||
@ -136,11 +155,9 @@ class MxNode(bpy.types.ShaderNode):
|
||||
if self._ui_folders:
|
||||
self.update_ui_folders(context)
|
||||
|
||||
nodetree = self.id_data
|
||||
nodetree.no_update_call(init_)
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
is_prop_area = context.area.type == 'PROPERTIES'
|
||||
|
||||
if len(self._data_types) > 1:
|
||||
layout1 = layout
|
||||
if is_prop_area:
|
||||
@ -163,6 +180,7 @@ class MxNode(bpy.types.ShaderNode):
|
||||
|
||||
for nd_input in utils.get_nodedef_inputs(nodedef, True):
|
||||
f = nd_input.getAttribute('uifolder')
|
||||
|
||||
if f and not getattr(self, self._folder_prop_name(f)):
|
||||
continue
|
||||
|
||||
@ -185,84 +203,10 @@ class MxNode(bpy.types.ShaderNode):
|
||||
layout1 = layout1.column()
|
||||
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
|
||||
def compute(self, out_key, **kwargs):
|
||||
from ..bl_nodes.node_parser import NodeItem
|
||||
|
||||
log("compute", self, out_key)
|
||||
|
||||
doc = kwargs['doc']
|
||||
@ -287,7 +231,7 @@ class MxNode(bpy.types.ShaderNode):
|
||||
nd_input = self.get_nodedef_input(in_key)
|
||||
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)
|
||||
utils.set_param_value(mx_input, val, nd_type)
|
||||
continue
|
||||
@ -346,6 +290,7 @@ class MxNode(bpy.types.ShaderNode):
|
||||
|
||||
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"""
|
||||
from ..bl_nodes import node_parser
|
||||
|
||||
socket_in = self.inputs[in_key]
|
||||
if not socket_in.links:
|
||||
@ -360,12 +305,27 @@ class MxNode(bpy.types.ShaderNode):
|
||||
if not link:
|
||||
return None
|
||||
|
||||
if isinstance(link.from_node, MxNode):
|
||||
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)
|
||||
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
|
||||
|
||||
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):
|
||||
node = self.get_input_link(in_key, **kwargs)
|
||||
if node:
|
||||
@ -393,7 +353,7 @@ class MxNode(bpy.types.ShaderNode):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, tree):
|
||||
return tree.bl_idname == utils.with_prefix('MxNodeTree')
|
||||
return tree.bl_idname == 'ShaderNodeTree'
|
||||
|
||||
def update_ui_folders(self, context):
|
||||
for i, nd_input in enumerate(utils.get_nodedef_inputs(self.nodedef, False)):
|
||||
@ -401,9 +361,6 @@ class MxNode(bpy.types.ShaderNode):
|
||||
if f:
|
||||
self.inputs[i].hide = not getattr(self, self._folder_prop_name(f))
|
||||
|
||||
nodetree = self.id_data
|
||||
nodetree.update_()
|
||||
|
||||
def check_ui_folders(self):
|
||||
if not self._ui_folders:
|
||||
return
|
||||
|
@ -2,23 +2,23 @@
|
||||
# Copyright 2022, AMD
|
||||
|
||||
from pathlib import Path
|
||||
import traceback
|
||||
|
||||
import traceback
|
||||
import MaterialX as mx
|
||||
|
||||
import bpy
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
|
||||
from ..node_tree import MxNodeTree
|
||||
from .. import utils
|
||||
from ..preferences import addon_preferences
|
||||
from . import utils
|
||||
from .utils import import_materialx_from_file, export
|
||||
from .preferences import addon_preferences
|
||||
|
||||
from ..utils import logging
|
||||
log = logging.Log('nodes.ui')
|
||||
from .utils import logging
|
||||
log = logging.Log(tag='ui')
|
||||
|
||||
|
||||
class NODES_OP_import_file(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = utils.with_prefix('nodes_import_file')
|
||||
class MATERIALX_OP_import_file(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = utils.with_prefix('materialx_import_file')
|
||||
bl_label = "Import from 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))
|
||||
try:
|
||||
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:
|
||||
log.error(traceback.format_exc(), mtlx_file)
|
||||
@ -48,17 +48,17 @@ class NODES_OP_import_file(bpy.types.Operator, ImportHelper):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class NODES_OP_export_file(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = utils.with_prefix('nodes_export_file')
|
||||
class MATERIALX_OP_export_file(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = utils.with_prefix('materialx_export_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
|
||||
filename_ext = ".mtlx"
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
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,
|
||||
subtype="FILE_PATH"
|
||||
)
|
||||
@ -72,27 +72,30 @@ class NODES_OP_export_file(bpy.types.Operator, ExportHelper):
|
||||
default=False
|
||||
)
|
||||
is_export_textures: bpy.props.BoolProperty(
|
||||
name="Export bound textures",
|
||||
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="Texture folder name",
|
||||
name="Folder name",
|
||||
description="Texture folder name used for exporting files",
|
||||
default='textures',
|
||||
maxlen=1024
|
||||
maxlen=1024,
|
||||
)
|
||||
# endregion
|
||||
|
||||
def execute(self, context):
|
||||
mx_node_tree = context.space_data.edit_tree
|
||||
doc = mx_node_tree.export()
|
||||
doc = export(context.material, None)
|
||||
if not doc:
|
||||
log.warn("Incorrect node tree to export", mx_node_tree)
|
||||
return {'CANCELLED'}
|
||||
|
||||
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_textures=self.is_export_textures,
|
||||
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.prop(self, 'texture_dir_name', text='')
|
||||
|
||||
@staticmethod
|
||||
def enabled(context):
|
||||
return bool(context.space_data.edit_tree.output_node)
|
||||
|
||||
|
||||
class NODES_OP_export_console(bpy.types.Operator):
|
||||
bl_idname = utils.with_prefix('nodes_export_console')
|
||||
class MATERIALX_OP_export_console(bpy.types.Operator):
|
||||
bl_idname = utils.with_prefix('materialx_export_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):
|
||||
mx_node_tree = context.space_data.edit_tree
|
||||
doc = mx_node_tree.export()
|
||||
doc = export(context.material, context.object)
|
||||
if not doc:
|
||||
log.warn("Incorrect node tree to export", mx_node_tree)
|
||||
return {'CANCELLED'}
|
||||
|
||||
print(mx.writeToXmlString(doc))
|
||||
return {'FINISHED'}
|
||||
|
||||
@staticmethod
|
||||
def enabled(context):
|
||||
return bool(context.space_data.edit_tree.output_node)
|
||||
|
||||
|
||||
class NODES_OP_create_basic_nodes(bpy.types.Operator):
|
||||
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)
|
||||
class MATERIALX_PT_tools(bpy.types.Panel):
|
||||
bl_idname = utils.with_prefix('MATERIALX_PT_tools', '_', True)
|
||||
bl_label = "MaterialX Tools"
|
||||
bl_space_type = 'NODE_EDITOR'
|
||||
bl_region_type = 'UI'
|
||||
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 == MxNodeTree.bl_idname
|
||||
|
||||
return tree and tree.bl_idname == 'ShaderNodeTree'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.operator(NODES_OP_create_basic_nodes.bl_idname, icon='ADD')
|
||||
layout.operator(NODES_OP_import_file.bl_idname, icon='IMPORT')
|
||||
layout.operator(NODES_OP_export_file.bl_idname, icon='EXPORT')
|
||||
layout.operator(MATERIALX_OP_import_file.bl_idname, icon='IMPORT')
|
||||
layout.operator(MATERIALX_OP_export_file.bl_idname, icon='EXPORT')
|
||||
|
||||
|
||||
class NODES_PT_dev(bpy.types.Panel):
|
||||
bl_idname = utils.with_prefix('NODES_PT_dev', '_', True)
|
||||
bl_parent_id = NODES_PT_tools.bl_idname
|
||||
class MATERIALX_PT_dev(bpy.types.Panel):
|
||||
bl_idname = utils.with_prefix('MATERIALX_PT_dev', '_', True)
|
||||
bl_label = "Dev"
|
||||
bl_space_type = 'NODE_EDITOR'
|
||||
bl_region_type = 'UI'
|
||||
bl_parent_id = MATERIALX_PT_tools.bl_idname
|
||||
bl_space_type = "NODE_EDITOR"
|
||||
bl_region_type = "UI"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@ -179,5 +161,13 @@ class NODES_PT_dev(bpy.types.Panel):
|
||||
|
||||
def draw(self, context):
|
||||
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,
|
||||
])
|
@ -31,25 +31,9 @@ MATLIB_URL = "https://api.matlib.gpuopen.com/api"
|
||||
|
||||
TEMP_FOLDER = "bl-materialx"
|
||||
|
||||
|
||||
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)
|
||||
NODE_LAYER_SEPARATION_WIDTH = 280
|
||||
NODE_LAYER_SHIFT_X = 30
|
||||
NODE_LAYER_SHIFT_Y = 100
|
||||
|
||||
|
||||
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):
|
||||
from .bl_nodes.node_parser import NodeItem
|
||||
|
||||
if isinstance(val, mx.Node):
|
||||
param_nodegraph = mx_param.getParent().getParent()
|
||||
val_nodegraph = val.getParent()
|
||||
@ -107,11 +93,19 @@ def set_param_value(mx_param, val, nd_type, nd_output=None):
|
||||
else:
|
||||
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:
|
||||
mx_type = getattr(mx, title_str(nd_type), None)
|
||||
if mx_type:
|
||||
val = mx_type(val)
|
||||
elif nd_type == 'float' and isinstance(val, tuple):
|
||||
val = mx_type(val.data) if isinstance(val, NodeItem) else mx_type(val)
|
||||
|
||||
elif nd_type == 'float':
|
||||
if isinstance(val, NodeItem):
|
||||
val = val.data
|
||||
|
||||
if isinstance(val, tuple):
|
||||
val = val[0]
|
||||
|
||||
mx_param.setValue(val)
|
||||
@ -446,15 +440,13 @@ def update_ui(area_type='PROPERTIES', region_type='WINDOW'):
|
||||
|
||||
|
||||
def update_materialx_data(depsgraph, materialx_data):
|
||||
from .node_tree import MxNodeTree
|
||||
|
||||
if not depsgraph.updates:
|
||||
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:
|
||||
if material.materialx.mx_node_tree and material.materialx.mx_node_tree.name == mx_node_tree.name:
|
||||
doc = material.materialx.export(None)
|
||||
if material.node_tree and material.node_tree.name == node_tree.name:
|
||||
doc = export(material, None)
|
||||
if not doc:
|
||||
# log.warn("MX export failed", mat)
|
||||
continue
|
||||
@ -463,7 +455,7 @@ def update_materialx_data(depsgraph, materialx_data):
|
||||
|
||||
if not matx_data:
|
||||
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)
|
||||
|
||||
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()))
|
||||
else:
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user