WIP: MaterialX addon #104594
@ -23,13 +23,16 @@ ADDON_ALIAS = "materialx"
|
||||
|
||||
import bpy
|
||||
|
||||
from . import preferences
|
||||
from . import node_tree
|
||||
from . import nodes
|
||||
from . import matlib
|
||||
from . import (
|
||||
preferences,
|
||||
node_tree,
|
||||
nodes,
|
||||
matlib,
|
||||
material,
|
||||
)
|
||||
|
||||
from . import logging
|
||||
log = logging.Log("")
|
||||
log = logging.Log("__init__")
|
||||
|
||||
|
||||
def register():
|
||||
@ -38,11 +41,13 @@ def register():
|
||||
bpy.utils.register_class(node_tree.MxNodeTree)
|
||||
nodes.register()
|
||||
matlib.register()
|
||||
material.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
log("unregister")
|
||||
matlib.unregister()
|
||||
nodes.unregister()
|
||||
material.unregister()
|
||||
bpy.utils.unregister_class(node_tree.MxNodeTree)
|
||||
bpy.utils.unregister_class(preferences.AddonPreferences)
|
||||
|
61
materialx/material/__init__.py
Normal file
61
materialx/material/__init__.py
Normal file
@ -0,0 +1,61 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright 2022, AMD
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
class MATERIALX_Panel(bpy.types.Panel):
|
||||
bl_space_type = 'PROPERTIES'
|
||||
bl_region_type = 'WINDOW'
|
||||
bl_context = 'render'
|
||||
|
||||
|
||||
class MATERIALX_ChildPanel(bpy.types.Panel):
|
||||
bl_space_type = 'PROPERTIES'
|
||||
bl_region_type = 'WINDOW'
|
||||
bl_parent_id = ''
|
||||
|
||||
|
||||
from . import (
|
||||
ui,
|
||||
properties
|
||||
)
|
||||
|
||||
register_classes, unregister_classes = bpy.utils.register_classes_factory([
|
||||
ui.MATERIAL_PT_context,
|
||||
ui.MATERIAL_PT_preview,
|
||||
ui.MATERIAL_OP_new_mx_node_tree,
|
||||
ui.MATERIAL_OP_duplicate_mx_node_tree,
|
||||
ui.MATERIAL_OP_convert_shader_to_mx,
|
||||
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_material,
|
||||
ui.MATERIAL_PT_material_settings_surface,
|
||||
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_PT_material_settings_displacement,
|
||||
ui.MATERIAL_PT_output_surface,
|
||||
ui.MATERIAL_PT_output_displacement,
|
||||
ui.MATERIAL_PT_output_volume,
|
||||
ui.MATERIAL_OP_export_mx_file,
|
||||
ui.MATERIAL_OP_export_mx_console,
|
||||
ui.MATERIAL_PT_tools,
|
||||
ui.MATERIAL_PT_dev,
|
||||
])
|
||||
|
||||
|
||||
def register():
|
||||
ui.register()
|
||||
properties.register()
|
||||
register_classes()
|
||||
|
||||
|
||||
def unregister():
|
||||
ui.unregister()
|
||||
properties.unregister()
|
||||
unregister_classes()
|
119
materialx/material/properties.py
Normal file
119
materialx/material/properties.py
Normal file
@ -0,0 +1,119 @@
|
||||
# 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 ..utils import MX_LIBS_DIR
|
||||
|
||||
from ..utils import logging, get_temp_file, MaterialXProperties
|
||||
log = logging.Log('material.properties')
|
||||
|
||||
|
||||
class MaterialProperties(MaterialXProperties):
|
||||
bl_type = bpy.types.Material
|
||||
|
||||
def update_mx_node_tree(self, context):
|
||||
self.update()
|
||||
|
||||
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
|
||||
# TODO add implementation
|
||||
# node.bl_idname == ShaderNodeOutputMaterial.__name__ and
|
||||
node.is_active_output), None)
|
||||
|
||||
def export(self, obj: bpy.types.Object) -> [mx.Document, None]:
|
||||
if 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()
|
||||
|
||||
# TODO add implementation
|
||||
# node_parser = ShaderNodeOutputMaterial(doc, material, output_node, obj)
|
||||
# if not node_parser.export():
|
||||
# return None
|
||||
|
||||
return doc
|
||||
|
||||
def update(self, is_depsgraph=False):
|
||||
"""
|
||||
Main update callback function, which notifies that material was updated from both:
|
||||
depsgraph or MaterialX node tree
|
||||
"""
|
||||
if is_depsgraph and self.mx_node_tree:
|
||||
return
|
||||
|
||||
material = self.id_data
|
||||
# usd_node_tree.material_update(material)
|
||||
# ViewportEngineScene.material_update(material)
|
||||
|
||||
def convert_shader_to_mx(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()
|
||||
|
||||
# TODO add implementation
|
||||
# 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
|
||||
|
||||
|
||||
def depsgraph_update(depsgraph):
|
||||
if not depsgraph.updates:
|
||||
return
|
||||
|
||||
# Undo operation sends modified object with other stuff (scene, collection, etc...)
|
||||
mat = next((upd.id for upd in depsgraph.updates if isinstance(upd.id, bpy.types.Material)), None)
|
||||
if mat:
|
||||
mat.hdusd.update(True)
|
||||
|
||||
|
||||
register, unregister = bpy.utils.register_classes_factory((
|
||||
MaterialProperties,
|
||||
))
|
774
materialx/material/ui.py
Normal file
774
materialx/material/ui.py
Normal file
@ -0,0 +1,774 @@
|
||||
# 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 . import MATERIALX_Panel, MATERIALX_ChildPanel
|
||||
from ..node_tree import MxNodeTree, NODE_LAYER_SEPARATION_WIDTH
|
||||
from ..nodes.node import is_mx_node_valid
|
||||
from .. import utils
|
||||
from ..utils import pass_node_reroute, title_str, mx_properties
|
||||
|
||||
from ..utils import logging
|
||||
log = logging.Log(tag='material.ui')
|
||||
|
||||
|
||||
class MATERIAL_PT_context(MATERIALX_Panel):
|
||||
bl_label = ""
|
||||
bl_context = "material"
|
||||
bl_options = {'HIDE_HEADER'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if context.active_object and context.active_object.type == 'GPENCIL':
|
||||
return False
|
||||
else:
|
||||
return context.material or context.object
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
material = context.material
|
||||
object = context.object
|
||||
slot = context.material_slot
|
||||
space = context.space_data
|
||||
|
||||
if object:
|
||||
is_sortable = len(object.material_slots) > 1
|
||||
rows = 1
|
||||
if is_sortable:
|
||||
rows = 4
|
||||
|
||||
row = layout.row()
|
||||
|
||||
row.template_list("MATERIAL_UL_matslots", "", object, "material_slots", object,
|
||||
"active_material_index", rows=rows)
|
||||
|
||||
col = row.column(align=True)
|
||||
col.operator("object.material_slot_add", icon='ADD', text="")
|
||||
col.operator("object.material_slot_remove", icon='REMOVE', text="")
|
||||
|
||||
col.menu("MATERIAL_MT_context_menu", icon='DOWNARROW_HLT', text="")
|
||||
|
||||
if is_sortable:
|
||||
col.separator()
|
||||
|
||||
col.operator("object.material_slot_move", icon='TRIA_UP', text="").direction = 'UP'
|
||||
col.operator("object.material_slot_move", icon='TRIA_DOWN', text="").direction = 'DOWN'
|
||||
|
||||
if object.mode == 'EDIT':
|
||||
row = layout.row(align=True)
|
||||
row.operator("object.material_slot_assign", text="Assign")
|
||||
row.operator("object.material_slot_select", text="Select")
|
||||
row.operator("object.material_slot_deselect", text="Deselect")
|
||||
|
||||
split = layout.split(factor=0.65)
|
||||
|
||||
if object:
|
||||
split.template_ID(object, "active_material", new=utils.with_prefix("material_duplicate_mat_mx_node_tree"))
|
||||
row = split.row()
|
||||
|
||||
if slot:
|
||||
row.prop(slot, "link", text="")
|
||||
else:
|
||||
row.label()
|
||||
elif material:
|
||||
split.template_ID(space, "pin_id")
|
||||
split.separator()
|
||||
|
||||
|
||||
class MATERIAL_PT_preview(MATERIALX_Panel):
|
||||
bl_label = "Preview"
|
||||
bl_context = "material"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.material
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.template_preview(context.material)
|
||||
|
||||
|
||||
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_shader_to_mx(bpy.types.Operator):
|
||||
"""Converts standard shader node tree to MaterialX node tree for selected material"""
|
||||
bl_idname = utils.with_prefix("material_convert_shader_to_mx")
|
||||
bl_label = "Convert to MaterialX"
|
||||
|
||||
def execute(self, context):
|
||||
if not mx_properties(context.material).convert_shader_to_mx(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 = "MATERIAL_MT_mx_node_tree"
|
||||
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_material(MATERIALX_Panel):
|
||||
bl_label = ""
|
||||
bl_context = "material"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.material
|
||||
|
||||
def draw(self, context):
|
||||
mat_materialx = mx_properties(context.material)
|
||||
layout = self.layout
|
||||
|
||||
split = layout.row(align=True).split(factor=0.4)
|
||||
row = split.column()
|
||||
row.alignment = 'RIGHT'
|
||||
row.label(text="MaterialX")
|
||||
row = split.row()
|
||||
row = row.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_shader_to_mx.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_shader_to_mx.bl_idname, icon='FILE_TICK', text="Convert")
|
||||
row.operator(MATERIAL_OP_new_mx_node_tree.bl_idname, icon='ADD', text="")
|
||||
|
||||
def draw_header(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text=f"Material: {context.material.name}")
|
||||
|
||||
|
||||
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_material_settings_surface(MATERIALX_ChildPanel):
|
||||
bl_label = "surfaceshader"
|
||||
bl_parent_id = 'MATERIAL_PT_material'
|
||||
|
||||
@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.bl_label]
|
||||
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.bl_label)
|
||||
|
||||
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:
|
||||
layout.label(text="No input node")
|
||||
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_material_settings_displacement(MATERIALX_ChildPanel):
|
||||
bl_label = "displacementshader"
|
||||
bl_parent_id = 'MATERIAL_PT_material'
|
||||
|
||||
@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.bl_label]
|
||||
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='Displacement')
|
||||
|
||||
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.bl_label)
|
||||
|
||||
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:
|
||||
layout.label(text="No input node")
|
||||
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_output_node(MATERIALX_ChildPanel):
|
||||
bl_label = ""
|
||||
bl_parent_id = 'MATERIAL_PT_material'
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return not bool(mx_properties(context.material).mx_node_tree)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
node_tree = context.material.node_tree
|
||||
|
||||
output_node = mx_properties(context.material).output_node
|
||||
if not output_node:
|
||||
layout.label(text="No output node")
|
||||
return
|
||||
|
||||
input = output_node.inputs[self.bl_label]
|
||||
layout.template_node_view(node_tree, output_node, input)
|
||||
|
||||
|
||||
class MATERIAL_PT_output_surface(MATERIAL_PT_output_node):
|
||||
bl_label = "Surface"
|
||||
|
||||
|
||||
class MATERIAL_PT_output_displacement(MATERIAL_PT_output_node):
|
||||
bl_label = "Displacement"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
|
||||
class MATERIAL_PT_output_volume(MATERIAL_PT_output_node):
|
||||
bl_label = "Volume"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
|
||||
class MATERIAL_OP_export_mx_file(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = utils.with_prefix("material_export_mx_file")
|
||||
bl_label = "Export MaterialX"
|
||||
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 bound 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
|
||||
)
|
||||
is_clean_deps_folders: bpy.props.BoolProperty(
|
||||
name="Сlean dependencies folders",
|
||||
description="Сlean MaterialX dependencies folders 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,
|
||||
)
|
||||
is_create_new_folder: bpy.props.BoolProperty(
|
||||
name="Create new folder",
|
||||
description="Create new folder for material",
|
||||
default=True
|
||||
)
|
||||
# endregion
|
||||
|
||||
def execute(self, context):
|
||||
materialx_prop = mx_properties(context.material)
|
||||
|
||||
if not materialx_prop.convert_shader_to_mx():
|
||||
return {'CANCELLED'}
|
||||
|
||||
doc = mx_properties(context.material).export(None)
|
||||
if not doc:
|
||||
return {'CANCELLED'}
|
||||
|
||||
if self.is_create_new_folder:
|
||||
self.filepath = str(Path(self.filepath).parent / context.material.name_full / Path(self.filepath).name)
|
||||
|
||||
utils.export_mx_to_file(doc, self.filepath,
|
||||
mx_node_tree=materialx_prop.mx_node_tree,
|
||||
is_export_deps=self.is_export_deps,
|
||||
is_export_textures=self.is_export_textures,
|
||||
texture_dir_name=self.texture_dir_name,
|
||||
is_clean_texture_folder=self.is_clean_texture_folder,
|
||||
is_clean_deps_folders=self.is_clean_deps_folders)
|
||||
|
||||
bpy.data.node_groups.remove(materialx_prop.mx_node_tree)
|
||||
return {'FINISHED'}
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.prop(self, 'is_create_new_folder')
|
||||
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_mx_console(bpy.types.Operator):
|
||||
bl_idname = utils.with_prefix("material_export_mx_console")
|
||||
bl_label = "Export MaterialX to Console"
|
||||
bl_description = "Export material as MaterialX node tree to console"
|
||||
|
||||
def execute(self, context):
|
||||
doc = mx_properties(context.material).export(context.object)
|
||||
if not doc:
|
||||
return {'CANCELLED'}
|
||||
|
||||
print(mx.writeToXmlString(doc))
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class MATERIAL_PT_tools(MATERIALX_Panel):
|
||||
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):
|
||||
layout = self.layout
|
||||
|
||||
layout.operator(MATERIAL_OP_convert_shader_to_mx.bl_idname, icon='FILE_TICK')
|
||||
layout.operator(MATERIAL_OP_export_mx_file.bl_idname, text="Export MaterialX to file", icon='EXPORT')
|
||||
|
||||
|
||||
class MATERIAL_PT_dev(MATERIALX_ChildPanel):
|
||||
bl_label = "Dev"
|
||||
bl_parent_id = 'MATERIAL_PT_tools'
|
||||
bl_space_type = "NODE_EDITOR"
|
||||
bl_region_type = "UI"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return config.show_dev_settings
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.operator(MATERIAL_OP_export_mx_console.bl_idname)
|
||||
|
||||
|
||||
def depsgraph_update(depsgraph):
|
||||
context = bpy.context
|
||||
mx_node_tree = None
|
||||
if hasattr(context, 'object') and context.object and context.object.active_material:
|
||||
mx_node_tree = mx_properties(context.object.active_material).mx_node_tree
|
||||
|
||||
# trying to show MaterialX area with node tree or Shader area
|
||||
screen = context.screen
|
||||
if not hasattr(screen, 'areas'):
|
||||
return
|
||||
|
||||
bpy.types.NODE_HT_header.remove(update_material_ui)
|
||||
|
||||
for window in context.window_manager.windows:
|
||||
for area in window.screen.areas:
|
||||
if not mx_node_tree:
|
||||
if area.ui_type != utils.with_prefix('MxNodeTree'):
|
||||
continue
|
||||
|
||||
area.ui_type = 'ShaderNodeTree'
|
||||
continue
|
||||
|
||||
if area.ui_type not in (utils.with_prefix('MxNodeTree'), '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 = utils.with_prefix('MxNodeTree')
|
||||
space.node_tree = mx_node_tree
|
||||
|
||||
mx_node_tree.update_links()
|
||||
|
||||
bpy.types.NODE_HT_header.append(update_material_ui)
|
||||
|
||||
|
||||
# update for material ui according to MaterialX nodetree header changes
|
||||
def update_material_ui(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return
|
||||
|
||||
mat = obj.active_material
|
||||
if not mat:
|
||||
return
|
||||
|
||||
space = context.space_data
|
||||
if space.tree_type != utils.with_prefix('MxNodeTree'):
|
||||
return
|
||||
|
||||
ui_mx_node_tree = mx_properties(mat).mx_node_tree
|
||||
editor_node_tree = space.node_tree
|
||||
|
||||
if editor_node_tree != ui_mx_node_tree and not space.pin and editor_node_tree:
|
||||
mx_properties(mat).mx_node_tree = editor_node_tree
|
||||
|
||||
|
||||
def register():
|
||||
# set update for material ui according to MaterialX nodetree header changes
|
||||
bpy.types.NODE_HT_header.append(update_material_ui)
|
||||
|
||||
|
||||
def unregister():
|
||||
# remove update for material ui according to MaterialX nodetree header changes
|
||||
bpy.types.NODE_HT_header.remove(update_material_ui)
|
@ -186,10 +186,7 @@ class MxNode(bpy.types.ShaderNode):
|
||||
layout1.prop(self, self._input_prop_name(name))
|
||||
|
||||
def draw_node_view(self, context, layout):
|
||||
return
|
||||
|
||||
# TODO: enable implementation
|
||||
from ...ui.material import USDHYDRA_MATERIAL_OP_invoke_popup_input_nodes
|
||||
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)
|
||||
@ -228,7 +225,7 @@ class MxNode(bpy.types.ShaderNode):
|
||||
box.scale_y = 0.5
|
||||
box.emboss = 'NONE_OR_STATUS'
|
||||
|
||||
op = box.operator(USDHYDRA_MATERIAL_OP_invoke_popup_input_nodes.bl_idname, icon='HANDLETYPE_AUTO_CLAMP_VEC')
|
||||
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
|
||||
|
||||
@ -257,7 +254,7 @@ class MxNode(bpy.types.ShaderNode):
|
||||
box.scale_x = 0.7
|
||||
box.scale_y = 0.5
|
||||
|
||||
op = box.operator(USDHYDRA_MATERIAL_OP_invoke_popup_input_nodes.bl_idname,
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user