WIP: MaterialX addon #104594

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

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
6 changed files with 92 additions and 136 deletions
Showing only changes of commit 2b3e80a3a2 - Show all commits

View File

@ -11,7 +11,7 @@ from . import (
register_classes, unregister_classes = bpy.utils.register_classes_factory([ register_classes, unregister_classes = bpy.utils.register_classes_factory([
ui.MATERIAL_OP_new_mx_node_tree, ui.MATERIAL_OP_new_mx_node_tree,
ui.MATERIAL_OP_duplicate_mx_node_tree, ui.MATERIAL_OP_duplicate_mx_node_tree,
ui.MATERIAL_OP_convert_shader_to_mx, ui.MATERIAL_OP_convert_to_materialx,
ui.MATERIAL_OP_duplicate_mat_mx_node_tree, ui.MATERIAL_OP_duplicate_mat_mx_node_tree,
ui.MATERIAL_OP_link_mx_node_tree, ui.MATERIAL_OP_link_mx_node_tree,
ui.MATERIAL_OP_unlink_mx_node_tree, ui.MATERIAL_OP_unlink_mx_node_tree,
@ -24,8 +24,8 @@ register_classes, unregister_classes = bpy.utils.register_classes_factory([
ui.MATERIAL_OP_invoke_popup_shader_nodes, ui.MATERIAL_OP_invoke_popup_shader_nodes,
ui.MATERIAL_OP_remove_node, ui.MATERIAL_OP_remove_node,
ui.MATERIAL_OP_disconnect_node, ui.MATERIAL_OP_disconnect_node,
ui.MATERIAL_OP_export_mx_file, ui.MATERIAL_OP_export_file,
ui.MATERIAL_OP_export_mx_console, ui.MATERIAL_OP_export_console,
ui.MATERIAL_PT_tools, ui.MATERIAL_PT_tools,
ui.MATERIAL_PT_dev, ui.MATERIAL_PT_dev,
]) ])

View File

@ -8,9 +8,9 @@ import MaterialX as mx
from ..node_tree import MxNodeTree from ..node_tree import MxNodeTree
from ..bl_nodes.output import ShaderNodeOutputMaterial from ..bl_nodes.output import ShaderNodeOutputMaterial
from ..utils import MX_LIBS_DIR from ..utils import MX_LIBS_DIR, mx_properties, get_temp_file, MaterialXProperties, with_prefix
from ..utils import logging, get_temp_file, MaterialXProperties from .. import logging
log = logging.Log('material.properties') log = logging.Log('material.properties')
@ -18,7 +18,29 @@ class MaterialProperties(MaterialXProperties):
bl_type = bpy.types.Material bl_type = bpy.types.Material
def update_mx_node_tree(self, context): def update_mx_node_tree(self, context):
self.update() # 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) mx_node_tree: bpy.props.PointerProperty(type=MxNodeTree, update=update_mx_node_tree)
@ -33,8 +55,8 @@ class MaterialProperties(MaterialXProperties):
node.bl_idname == ShaderNodeOutputMaterial.__name__ and node.bl_idname == ShaderNodeOutputMaterial.__name__ and
node.is_active_output), None) node.is_active_output), None)
def export(self, obj: bpy.types.Object) -> [mx.Document, None]: def export(self, obj: bpy.types.Object, check_mx_node_tree=True) -> [mx.Document, None]:
if self.mx_node_tree: if check_mx_node_tree and self.mx_node_tree:
return self.mx_node_tree.export() return self.mx_node_tree.export()
material = self.id_data material = self.id_data
@ -51,19 +73,7 @@ class MaterialProperties(MaterialXProperties):
return doc return doc
def update(self, is_depsgraph=False): def convert_to_materialx(self, obj: bpy.types.Object = None):
"""
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 mat = self.id_data
output_node = self.output_node output_node = self.output_node
if not output_node: if not output_node:
@ -95,6 +105,7 @@ class MaterialProperties(MaterialXProperties):
mx.readFromXmlFile(doc, str(mtlx_file), searchPath=search_path) mx.readFromXmlFile(doc, str(mtlx_file), searchPath=search_path)
mx_node_tree.import_(doc, mtlx_file) mx_node_tree.import_(doc, mtlx_file)
self.mx_node_tree = mx_node_tree self.mx_node_tree = mx_node_tree
except Exception as e: except Exception as e:
log.error(traceback.format_exc(), mtlx_file) log.error(traceback.format_exc(), mtlx_file)
return False return False
@ -102,16 +113,6 @@ class MaterialProperties(MaterialXProperties):
return True 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(( register, unregister = bpy.utils.register_classes_factory((
MaterialProperties, MaterialProperties,
)) ))

View File

@ -58,13 +58,13 @@ class MATERIAL_OP_duplicate_mx_node_tree(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class MATERIAL_OP_convert_shader_to_mx(bpy.types.Operator): class MATERIAL_OP_convert_to_materialx(bpy.types.Operator):
"""Converts standard shader node tree to MaterialX node tree for selected material""" """Converts standard shader node tree to MaterialX node tree for selected material"""
bl_idname = utils.with_prefix('material_convert_shader_to_mx') bl_idname = utils.with_prefix('material_convert_to_materialx')
bl_label = "Convert to MaterialX" bl_label = "Convert to MaterialX"
def execute(self, context): def execute(self, context):
if not mx_properties(context.material).convert_shader_to_mx(context.object): if not mx_properties(context.material).convert_to_materialx(context.object):
return {'CANCELLED'} return {'CANCELLED'}
return {"FINISHED"} return {"FINISHED"}
@ -131,12 +131,12 @@ class MATERIAL_PT_materialx(bpy.types.Panel):
if mat_materialx.mx_node_tree: if mat_materialx.mx_node_tree:
row.prop(mat_materialx.mx_node_tree, 'name', text="") 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_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_duplicate_mx_node_tree.bl_idname, icon='DUPLICATE')
row.operator(MATERIAL_OP_unlink_mx_node_tree.bl_idname, icon='X') row.operator(MATERIAL_OP_unlink_mx_node_tree.bl_idname, icon='X')
else: else:
row.operator(MATERIAL_OP_convert_shader_to_mx.bl_idname, icon='FILE_TICK', text="Convert") 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="") row.operator(MATERIAL_OP_new_mx_node_tree.bl_idname, icon='ADD', text="")
@ -404,15 +404,15 @@ class MATERIAL_PT_materialx_surfaceshader(MATERIAL_PT_materialx_output):
class MATERIAL_PT_materialx_displacementshader(MATERIAL_PT_materialx_output): class MATERIAL_PT_materialx_displacementshader(MATERIAL_PT_materialx_output):
bl_idname = utils.with_prefix('MATERIAL_PT_materialx_sdisplacementshader', '_', True) bl_idname = utils.with_prefix('MATERIAL_PT_materialx_displacementshader', '_', True)
bl_label = "Displacement Shader" bl_label = "Displacement Shader"
bl_options = {'DEFAULT_CLOSED'} bl_options = {'DEFAULT_CLOSED'}
out_key = 'displacementshader' out_key = 'displacementshader'
class MATERIAL_OP_export_mx_file(bpy.types.Operator, ExportHelper): class MATERIAL_OP_export_file(bpy.types.Operator, ExportHelper):
bl_idname = utils.with_prefix('material_export_mx_file') bl_idname = utils.with_prefix('material_export_file')
bl_label = "Export MaterialX" bl_label = "Export MaterialX"
bl_description = "Export material as MaterialX node tree to .mtlx file" bl_description = "Export material as MaterialX node tree to .mtlx file"
@ -465,7 +465,7 @@ class MATERIAL_OP_export_mx_file(bpy.types.Operator, ExportHelper):
def execute(self, context): def execute(self, context):
materialx_prop = mx_properties(context.material) materialx_prop = mx_properties(context.material)
if not materialx_prop.convert_shader_to_mx(): if not materialx_prop.convert_to_materialx():
return {'CANCELLED'} return {'CANCELLED'}
doc = mx_properties(context.material).export(None) doc = mx_properties(context.material).export(None)
@ -476,7 +476,7 @@ class MATERIAL_OP_export_mx_file(bpy.types.Operator, ExportHelper):
self.filepath = str(Path(self.filepath).parent / context.material.name_full / Path(self.filepath).name) self.filepath = str(Path(self.filepath).parent / context.material.name_full / Path(self.filepath).name)
utils.export_mx_to_file(doc, self.filepath, utils.export_mx_to_file(doc, self.filepath,
mx_node_tree=materialx_prop.mx_node_tree, mx_node_tree=None,
is_export_deps=self.is_export_deps, is_export_deps=self.is_export_deps,
is_export_textures=self.is_export_textures, is_export_textures=self.is_export_textures,
texture_dir_name=self.texture_dir_name, texture_dir_name=self.texture_dir_name,
@ -498,13 +498,13 @@ class MATERIAL_OP_export_mx_file(bpy.types.Operator, ExportHelper):
row.prop(self, 'texture_dir_name', text='') row.prop(self, 'texture_dir_name', text='')
class MATERIAL_OP_export_mx_console(bpy.types.Operator): class MATERIAL_OP_export_console(bpy.types.Operator):
bl_idname = utils.with_prefix('material_export_mx_console') bl_idname = utils.with_prefix('material_export_console')
bl_label = "Export MaterialX to Console" bl_label = "Export to Console"
bl_description = "Export material as MaterialX node tree to console" bl_description = "Export material as MaterialX node tree to console"
def execute(self, context): def execute(self, context):
doc = mx_properties(context.material).export(context.object) doc = mx_properties(context.material).export(context.object, False)
if not doc: if not doc:
return {'CANCELLED'} return {'CANCELLED'}
@ -526,10 +526,23 @@ class MATERIAL_PT_tools(bpy.types.Panel):
return tree and tree.bl_idname == bpy.types.ShaderNodeTree.__name__ return tree and tree.bl_idname == bpy.types.ShaderNodeTree.__name__
def draw(self, context): def draw(self, context):
mat_materialx = mx_properties(context.material)
layout = self.layout layout = self.layout
layout.operator(MATERIAL_OP_convert_shader_to_mx.bl_idname, icon='FILE_TICK') row = layout.row(align=True)
layout.operator(MATERIAL_OP_export_mx_file.bl_idname, text="Export MaterialX to file", icon='EXPORT') 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, text="Export MaterialX to file", icon='EXPORT')
class MATERIAL_PT_dev(bpy.types.Panel): class MATERIAL_PT_dev(bpy.types.Panel):
@ -546,4 +559,4 @@ class MATERIAL_PT_dev(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.operator(MATERIAL_OP_export_mx_console.bl_idname) layout.operator(MATERIAL_OP_export_console.bl_idname)

View File

@ -14,8 +14,6 @@ log = logging.Log('node_tree')
NODE_LAYER_SEPARATION_WIDTH = 280 NODE_LAYER_SEPARATION_WIDTH = 280
NODE_LAYER_SHIFT_X = 30 NODE_LAYER_SHIFT_X = 30
NODE_LAYER_SHIFT_Y = 100 NODE_LAYER_SHIFT_Y = 100
AREA_TO_UPDATE = 'PROPERTIES'
REGION_TO_UPDATE = 'WINDOW'
class MxNodeTree(bpy.types.ShaderNodeTree): class MxNodeTree(bpy.types.ShaderNodeTree):
@ -251,17 +249,7 @@ class MxNodeTree(bpy.types.ShaderNodeTree):
self.update_() self.update_()
def update_(self): def update_(self):
for material in bpy.data.materials: utils.update_ui()
if utils.mx_properties(material).mx_node_tree and \
utils.mx_properties(material).mx_node_tree.name == self.name:
utils.mx_properties(material).update()
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == AREA_TO_UPDATE:
for region in area.regions:
if region.type == REGION_TO_UPDATE:
region.tag_redraw()
# We have to call self.update_links via bpy.app.timers.register # We have to call self.update_links via bpy.app.timers.register
# to have slight delay after self.update(). It'll be called once # to have slight delay after self.update(). It'll be called once

View File

@ -50,7 +50,7 @@ class NODES_OP_import_file(bpy.types.Operator, ImportHelper):
class NODES_OP_export_file(bpy.types.Operator, ExportHelper): class NODES_OP_export_file(bpy.types.Operator, ExportHelper):
bl_idname = utils.with_prefix('nodes_export_file') bl_idname = utils.with_prefix('nodes_export_file')
bl_label = "Export MaterialX" bl_label = "Export to File"
bl_description = "Export MaterialX node tree to .mtlx file" bl_description = "Export MaterialX node tree to .mtlx file"
# region properties # region properties
@ -137,7 +137,7 @@ class NODES_OP_export_file(bpy.types.Operator, ExportHelper):
class NODES_OP_export_console(bpy.types.Operator): class NODES_OP_export_console(bpy.types.Operator):
bl_idname = utils.with_prefix('nodes_export_console') bl_idname = utils.with_prefix('nodes_export_console')
bl_label = "Export MaterialX to Console" bl_label = "Export to Console"
bl_description = "Export MaterialX node tree to console" bl_description = "Export MaterialX node tree to console"
def execute(self, context): def execute(self, context):
@ -183,7 +183,7 @@ class NODES_PT_tools(bpy.types.Panel):
layout.operator(NODES_OP_create_basic_nodes.bl_idname, icon='ADD') 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_import_file.bl_idname, icon='IMPORT')
layout.operator(NODES_OP_export_file.bl_idname, icon='EXPORT', text='Export MaterialX to file') layout.operator(NODES_OP_export_file.bl_idname, icon='EXPORT')
class NODES_PT_dev(bpy.types.Panel): class NODES_PT_dev(bpy.types.Panel):

View File

@ -30,12 +30,6 @@ MATLIB_URL = "https://api.matlib.gpuopen.com/api"
TEMP_FOLDER = "bl-materialx" TEMP_FOLDER = "bl-materialx"
SUPPORTED_FORMATS = {".png", ".jpeg", ".jpg", ".hdr", ".tga", ".bmp"}
DEFAULT_FORMAT = ".hdr"
BLENDER_DEFAULT_FORMAT = "HDR"
BLENDER_DEFAULT_COLOR_MODE = "RGB"
READONLY_IMAGE_FORMATS = {".dds"} # blender can read these formats, but can't write
os.environ['MATERIALX_SEARCH_PATH'] = str(MX_LIBS_DIR) os.environ['MATERIALX_SEARCH_PATH'] = str(MX_LIBS_DIR)
@ -323,13 +317,13 @@ def export_mx_to_file(doc, filepath, *, mx_node_tree=None, is_export_deps=False,
dest_path = texture_dir / f"{source_path.stem}{source_path.suffix}" dest_path = texture_dir / f"{source_path.stem}{source_path.suffix}"
shutil.copy(source_path, dest_path) shutil.copy(source_path, dest_path)
log(f"Export file {source_path} to {dest_path}: completed successfuly") log(f"Export file {source_path} to {dest_path}: completed successfully")
rel_dest_path = dest_path.relative_to(root_dir) rel_dest_path = dest_path.relative_to(root_dir)
mx_input.setValue(str(rel_dest_path), mx_input.getType()) mx_input.setValue(str(rel_dest_path), mx_input.getType())
mx.writeToXmlFile(doc, filepath) mx.writeToXmlFile(doc, filepath)
log(f"Export MaterialX to {filepath}: completed successfuly") log(f"Export MaterialX to {filepath}: completed successfully")
def temp_dir(): def temp_dir():
@ -354,13 +348,14 @@ def get_temp_file(suffix, name=None, is_rand=False):
return temp_dir() / name return temp_dir() / name
def cache_image_file(image: bpy.types.Image, cache_check=True): SUPPORTED_FORMATS = {".png", ".jpeg", ".jpg", ".hdr", ".tga", ".bmp"}
SUPPORTED_FORMATS = {".png", ".jpeg", ".jpg", ".hdr", ".tga", ".bmp"} DEFAULT_FORMAT = ".hdr"
DEFAULT_FORMAT = ".hdr" BLENDER_DEFAULT_FORMAT = "HDR"
BLENDER_DEFAULT_FORMAT = "HDR" BLENDER_DEFAULT_COLOR_MODE = "RGB"
BLENDER_DEFAULT_COLOR_MODE = "RGB" READONLY_IMAGE_FORMATS = {".dds"} # blender can read these formats, but can't write
READONLY_IMAGE_FORMATS = {".dds"} # blender can read these formats, but can't write
def cache_image_file(image: bpy.types.Image, cache_check=True):
image_path = Path(image.filepath_from_user()) image_path = Path(image.filepath_from_user())
if not image.packed_file and image.source != 'GENERATED': if not image.packed_file and image.source != 'GENERATED':
if not image_path.is_file(): if not image_path.is_file():
@ -401,6 +396,23 @@ def cache_image_file(image: bpy.types.Image, cache_check=True):
return temp_path return temp_path
def cache_image_file_path(image_path, cache_check=True):
if image_path.suffix.lower() in SUPPORTED_FORMATS:
return image_path
if cache_check:
temp_path = get_temp_file(DEFAULT_FORMAT, image_path.name)
if temp_path.is_file():
return temp_path
image = bpy.data.images.load(str(image_path))
try:
return cache_image_file(image, cache_check)
finally:
bpy.data.images.remove(image)
def pass_node_reroute(link): def pass_node_reroute(link):
while isinstance(link.from_node, bpy.types.NodeReroute): while isinstance(link.from_node, bpy.types.NodeReroute):
if not link.from_node.inputs[0].links: if not link.from_node.inputs[0].links:
@ -418,61 +430,3 @@ def update_ui(area_type='PROPERTIES', region_type='WINDOW'):
for region in area.regions: for region in area.regions:
if region.type == region_type: if region.type == region_type:
region.tag_redraw() region.tag_redraw()
def cache_image_file(image: bpy.types.Image, cache_check=True):
image_path = Path(image.filepath_from_user())
if not image.packed_file and image.source != 'GENERATED':
if not image_path.is_file():
log.warn("Image is missing", image, image_path)
return None
image_suffix = image_path.suffix.lower()
if image_suffix in SUPPORTED_FORMATS and\
f".{image.file_format.lower()}" in SUPPORTED_FORMATS and not image.is_dirty:
return image_path
if image_suffix in READONLY_IMAGE_FORMATS:
return image_path
temp_path = get_temp_file(DEFAULT_FORMAT, image_path.stem)
if cache_check and image.source != 'GENERATED' and temp_path.is_file():
return temp_path
scene = bpy.context.scene
user_format = scene.render.image_settings.file_format
user_color_mode = scene.render.image_settings.color_mode
# in some scenes the color_mode is undefined
# we can read it but unable to assign back, so switch it to 'RGB' if color_mode isn't selected
if not user_color_mode:
user_color_mode = 'RGB'
scene.render.image_settings.file_format = BLENDER_DEFAULT_FORMAT
scene.render.image_settings.color_mode = BLENDER_DEFAULT_COLOR_MODE
try:
image.save_render(filepath=str(temp_path))
finally:
scene.render.image_settings.file_format = user_format
scene.render.image_settings.color_mode = user_color_mode
return temp_path
def cache_image_file_path(image_path, cache_check=True):
if image_path.suffix.lower() in SUPPORTED_FORMATS:
return image_path
if cache_check:
temp_path = get_temp_file(DEFAULT_FORMAT, image_path.name)
if temp_path.is_file():
return temp_path
image = bpy.data.images.load(str(image_path))
try:
return cache_image_file(image, cache_check)
finally:
bpy.data.images.remove(image)