WIP: MaterialX addon #104594
@ -11,7 +11,7 @@ from . import (
|
||||
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_shader_to_mx,
|
||||
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,
|
||||
@ -24,8 +24,8 @@ register_classes, unregister_classes = bpy.utils.register_classes_factory([
|
||||
ui.MATERIAL_OP_invoke_popup_shader_nodes,
|
||||
ui.MATERIAL_OP_remove_node,
|
||||
ui.MATERIAL_OP_disconnect_node,
|
||||
ui.MATERIAL_OP_export_mx_file,
|
||||
ui.MATERIAL_OP_export_mx_console,
|
||||
ui.MATERIAL_OP_export_file,
|
||||
ui.MATERIAL_OP_export_console,
|
||||
ui.MATERIAL_PT_tools,
|
||||
ui.MATERIAL_PT_dev,
|
||||
])
|
||||
|
@ -8,9 +8,9 @@ import MaterialX as mx
|
||||
|
||||
from ..node_tree import MxNodeTree
|
||||
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')
|
||||
|
||||
|
||||
@ -18,7 +18,29 @@ class MaterialProperties(MaterialXProperties):
|
||||
bl_type = bpy.types.Material
|
||||
|
||||
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)
|
||||
|
||||
@ -33,8 +55,8 @@ class MaterialProperties(MaterialXProperties):
|
||||
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:
|
||||
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
|
||||
@ -51,19 +73,7 @@ class MaterialProperties(MaterialXProperties):
|
||||
|
||||
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):
|
||||
def convert_to_materialx(self, obj: bpy.types.Object = None):
|
||||
mat = self.id_data
|
||||
output_node = self.output_node
|
||||
if not output_node:
|
||||
@ -95,6 +105,7 @@ class MaterialProperties(MaterialXProperties):
|
||||
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
|
||||
@ -102,16 +113,6 @@ class MaterialProperties(MaterialXProperties):
|
||||
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,
|
||||
))
|
||||
|
@ -58,13 +58,13 @@ class MATERIAL_OP_duplicate_mx_node_tree(bpy.types.Operator):
|
||||
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"""
|
||||
bl_idname = utils.with_prefix('material_convert_shader_to_mx')
|
||||
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_shader_to_mx(context.object):
|
||||
if not mx_properties(context.material).convert_to_materialx(context.object):
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {"FINISHED"}
|
||||
@ -131,12 +131,12 @@ class MATERIAL_PT_materialx(bpy.types.Panel):
|
||||
|
||||
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_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_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="")
|
||||
|
||||
|
||||
@ -404,15 +404,15 @@ class MATERIAL_PT_materialx_surfaceshader(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_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
out_key = 'displacementshader'
|
||||
|
||||
|
||||
class MATERIAL_OP_export_mx_file(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = utils.with_prefix('material_export_mx_file')
|
||||
class MATERIAL_OP_export_file(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = utils.with_prefix('material_export_file')
|
||||
bl_label = "Export MaterialX"
|
||||
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):
|
||||
materialx_prop = mx_properties(context.material)
|
||||
|
||||
if not materialx_prop.convert_shader_to_mx():
|
||||
if not materialx_prop.convert_to_materialx():
|
||||
return {'CANCELLED'}
|
||||
|
||||
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)
|
||||
|
||||
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_textures=self.is_export_textures,
|
||||
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='')
|
||||
|
||||
|
||||
class MATERIAL_OP_export_mx_console(bpy.types.Operator):
|
||||
bl_idname = utils.with_prefix('material_export_mx_console')
|
||||
bl_label = "Export MaterialX to Console"
|
||||
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)
|
||||
doc = mx_properties(context.material).export(context.object, False)
|
||||
if not doc:
|
||||
return {'CANCELLED'}
|
||||
|
||||
@ -526,10 +526,23 @@ class MATERIAL_PT_tools(bpy.types.Panel):
|
||||
return tree and tree.bl_idname == bpy.types.ShaderNodeTree.__name__
|
||||
|
||||
def draw(self, context):
|
||||
mat_materialx = mx_properties(context.material)
|
||||
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')
|
||||
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, text="Export MaterialX to file", icon='EXPORT')
|
||||
|
||||
|
||||
class MATERIAL_PT_dev(bpy.types.Panel):
|
||||
@ -546,4 +559,4 @@ class MATERIAL_PT_dev(bpy.types.Panel):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(MATERIAL_OP_export_mx_console.bl_idname)
|
||||
layout.operator(MATERIAL_OP_export_console.bl_idname)
|
||||
|
@ -14,8 +14,6 @@ log = logging.Log('node_tree')
|
||||
NODE_LAYER_SEPARATION_WIDTH = 280
|
||||
NODE_LAYER_SHIFT_X = 30
|
||||
NODE_LAYER_SHIFT_Y = 100
|
||||
AREA_TO_UPDATE = 'PROPERTIES'
|
||||
REGION_TO_UPDATE = 'WINDOW'
|
||||
|
||||
|
||||
class MxNodeTree(bpy.types.ShaderNodeTree):
|
||||
@ -251,17 +249,7 @@ class MxNodeTree(bpy.types.ShaderNodeTree):
|
||||
self.update_()
|
||||
|
||||
def update_(self):
|
||||
for material in bpy.data.materials:
|
||||
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()
|
||||
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
|
||||
|
@ -50,7 +50,7 @@ class NODES_OP_import_file(bpy.types.Operator, ImportHelper):
|
||||
|
||||
class NODES_OP_export_file(bpy.types.Operator, ExportHelper):
|
||||
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"
|
||||
|
||||
# region properties
|
||||
@ -137,7 +137,7 @@ class NODES_OP_export_file(bpy.types.Operator, ExportHelper):
|
||||
|
||||
class NODES_OP_export_console(bpy.types.Operator):
|
||||
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"
|
||||
|
||||
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_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):
|
||||
|
@ -30,12 +30,6 @@ MATLIB_URL = "https://api.matlib.gpuopen.com/api"
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@ -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}"
|
||||
|
||||
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)
|
||||
mx_input.setValue(str(rel_dest_path), mx_input.getType())
|
||||
|
||||
mx.writeToXmlFile(doc, filepath)
|
||||
log(f"Export MaterialX to {filepath}: completed successfuly")
|
||||
log(f"Export MaterialX to {filepath}: completed successfully")
|
||||
|
||||
|
||||
def temp_dir():
|
||||
@ -354,13 +348,14 @@ def get_temp_file(suffix, name=None, is_rand=False):
|
||||
return temp_dir() / name
|
||||
|
||||
|
||||
def cache_image_file(image: bpy.types.Image, cache_check=True):
|
||||
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
|
||||
|
||||
|
||||
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():
|
||||
@ -401,6 +396,23 @@ def cache_image_file(image: bpy.types.Image, cache_check=True):
|
||||
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):
|
||||
while isinstance(link.from_node, bpy.types.NodeReroute):
|
||||
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:
|
||||
if region.type == region_type:
|
||||
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)
|
||||
|
Loading…
Reference in New Issue
Block a user