# SPDX-License-Identifier: MIT OR GPL-3.0-or-later import random import string import time from copy import deepcopy from pathlib import Path from sys import float_info from typing import Final, Optional, TypeVar, Union import bpy from bpy.types import ( Context, Driver, FCurve, Material, Node, NodeFrame, NodeGroup, NodeGroupOutput, NodeReroute, NodeSocket, NodeSocketBool, NodeSocketColor, NodeSocketFloat, NodeSocketFloatAngle, NodeSocketFloatFactor, NodeSocketFloatPercentage, NodeSocketFloatTime, NodeSocketFloatUnsigned, NodeSocketInt, NodeSocketIntFactor, NodeSocketIntPercentage, NodeSocketIntUnsigned, NodeSocketString, NodeSocketVector, NodeSocketVectorAcceleration, NodeSocketVectorDirection, NodeSocketVectorEuler, NodeSocketVectorTranslation, NodeSocketVectorVelocity, NodeSocketVectorXYZ, NodeTree, ShaderNodeAmbientOcclusion, ShaderNodeAttribute, ShaderNodeBevel, ShaderNodeBsdfAnisotropic, ShaderNodeBsdfGlass, ShaderNodeBsdfHair, ShaderNodeBsdfHairPrincipled, ShaderNodeBsdfPrincipled, ShaderNodeBsdfRefraction, ShaderNodeBsdfToon, ShaderNodeBump, ShaderNodeClamp, ShaderNodeCustomGroup, ShaderNodeDisplacement, ShaderNodeGroup, ShaderNodeMapping, ShaderNodeMapRange, ShaderNodeMath, ShaderNodeMixRGB, ShaderNodeNormalMap, ShaderNodeOutputAOV, ShaderNodeOutputLight, ShaderNodeOutputLineStyle, ShaderNodeOutputMaterial, ShaderNodeOutputWorld, ShaderNodeScript, ShaderNodeSubsurfaceScattering, ShaderNodeTangent, ShaderNodeTexBrick, ShaderNodeTexCoord, ShaderNodeTexEnvironment, ShaderNodeTexGradient, ShaderNodeTexIES, ShaderNodeTexImage, ShaderNodeTexMagic, ShaderNodeTexNoise, ShaderNodeTexSky, ShaderNodeTexVoronoi, ShaderNodeTexWave, ShaderNodeTexWhiteNoise, ShaderNodeUVAlongStroke, ShaderNodeUVMap, ShaderNodeVectorDisplacement, ShaderNodeVectorMath, ShaderNodeVectorRotate, ShaderNodeVectorTransform, ShaderNodeVertexColor, ShaderNodeWireframe, ) from mathutils import Color, Euler, Vector from . import convert from .char import INTERNAL_NAME_PREFIX from .gl import GL_CLAMP_TO_EDGE, GL_LINEAR, GL_NEAREST, GL_REPEAT from .logger import get_logger from .version import get_addon_version from .workspace import wm_append_without_library logger = get_logger(__name__) LAST_MODIFIED_VERSION: Final = (3, 16, 1) BOOL_SOCKET_CLASSES: Final = (NodeSocketBool,) FLOAT_SOCKET_CLASSES: Final = ( NodeSocketFloat, NodeSocketFloatAngle, NodeSocketFloatFactor, NodeSocketFloatPercentage, NodeSocketFloatTime, NodeSocketFloatUnsigned, ) INT_SOCKET_CLASSES: Final = ( NodeSocketInt, NodeSocketIntFactor, NodeSocketIntPercentage, NodeSocketIntUnsigned, ) SCALAR_SOCKET_CLASSES: Final = ( *BOOL_SOCKET_CLASSES, *FLOAT_SOCKET_CLASSES, *INT_SOCKET_CLASSES, ) VECTOR_SOCKET_CLASSES: Final = ( NodeSocketVector, NodeSocketVectorAcceleration, NodeSocketVectorDirection, NodeSocketVectorEuler, NodeSocketVectorTranslation, NodeSocketVectorVelocity, NodeSocketVectorXYZ, ) COLOR_SOCKET_CLASSES: Final = (NodeSocketColor,) STRING_SOCKET_CLASSES: Final = (NodeSocketString,) MTOON1_AUTO_SETUP_GROUP_NAME: Final = "MToon" MTOON1_AUTO_SETUP_GROUP_NODE_TREE_CUSTOM_KEY: Final = ( "VRM Add-on MToon1 Auto Setup Placeholder" ) OUTLINE_GEOMETRY_GROUP_NAME: Final = "VRM Add-on MToon 1.0 Outline Geometry Revision 1" UV_GROUP_NAME: Final = "VRM Add-on MToon 1.0 UV Revision 1" UV_GROUP_UV_SCALE_X_LABEL: Final = "UV Scale X" UV_GROUP_UV_SCALE_X_DEFAULT: Final = 1 UV_GROUP_UV_SCALE_Y_LABEL: Final = "UV Scale Y" UV_GROUP_UV_SCALE_Y_DEFAULT: Final = 1 UV_GROUP_UV_OFFSET_X_LABEL: Final = "UV Offset X" UV_GROUP_UV_OFFSET_X_DEFAULT: Final = 0 UV_GROUP_UV_OFFSET_Y_LABEL: Final = "UV Offset Y" UV_GROUP_UV_OFFSET_Y_DEFAULT: Final = 0 UV_GROUP_WRAP_S_LABEL: Final = "Wrap S" UV_GROUP_WRAP_S_DEFAULT: Final = GL_REPEAT UV_GROUP_WRAP_T_LABEL: Final = "Wrap T" UV_GROUP_WRAP_T_DEFAULT: Final = GL_REPEAT UV_GROUP_IMAGE_WIDTH_LABEL: Final = "Image Width" UV_GROUP_IMAGE_HEIGHT_LABEL: Final = "Image Height" UV_ANIMATION_GROUP_NAME: Final = "VRM Add-on MToon 1.0 UV Animation Revision 1" UV_ANIMATION_GROUP_TRANSLATE_X_LABEL: Final = "Translate X" UV_ANIMATION_GROUP_TRANSLATE_X_DEFAULT: Final = 0.0 UV_ANIMATION_GROUP_TRANSLATE_Y_LABEL: Final = "Translate Y" UV_ANIMATION_GROUP_TRANSLATE_Y_DEFAULT: Final = 0.0 UV_ANIMATION_GROUP_ROTATION_LABEL: Final = "Rotation" UV_ANIMATION_GROUP_ROTATION_DEFAULT: Final = 0.0 UV_ANIMATION_GROUP_FPS_NODE_NAME: Final = "Scene.Render.Fps" UV_ANIMATION_GROUP_FPS_BASE_NODE_NAME: Final = "Scene.Render.FpsBase" UV_ANIMATION_GROUP_FRAME_CURRENT_NODE_NAME: Final = "Scene.FrameCurrent" NORMAL_GROUP_NAME: Final = "VRM Add-on MToon 1.0 Normal Revision 1" NORMAL_GROUP_SCALE_LABEL: Final = "Normal Map Texture Scale" NORMAL_GROUP_SCALE_DEFAULT: Final = 1.0 NORMAL_GROUP_IS_OUTLINE_LABEL: Final = "Is Outline" MATCAP_UV_GROUP_NAME: Final = "VRM Add-on MToon 1.0 MatCap UV Revision 1" OUTPUT_GROUP_NAME: Final = "VRM Add-on MToon 1.0 Output Revision 1" OUTPUT_GROUP_DOUBLE_SIDED_LABEL: Final = "Double Sided" OUTPUT_GROUP_TRANSPARENT_WITH_Z_WRITE_LABEL: Final = "Transparent With Z-Write" OUTPUT_GROUP_TRANSPARENT_WITH_Z_WRITE_DEFAULT: Final = False OUTPUT_GROUP_GI_EQUALIZATION_FACTOR_LABEL: Final = "GI Equalization Factor" OUTPUT_GROUP_GI_EQUALIZATION_FACTOR_DEFAULT: Final = 0.9 OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_LABEL: Final = "Render Queue Offset Number" OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_MIN: Final = -9 OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_DEFAULT: Final = 0 OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_MAX: Final = 9 OUTPUT_GROUP_BASE_COLOR_FACTOR_COLOR_LABEL: Final = "Lit Color" OUTPUT_GROUP_BASE_COLOR_FACTOR_ALPHA_LABEL: Final = "Lit Color Alpha" OUTPUT_GROUP_BASE_COLOR_FACTOR_COLOR_DEFAULT: Final = (1.0, 1.0, 1.0) OUTPUT_GROUP_BASE_COLOR_FACTOR_ALPHA_DEFAULT: Final = 1.0 OUTPUT_GROUP_SHADE_COLOR_FACTOR_LABEL: Final = "Shade Color" OUTPUT_GROUP_SHADE_COLOR_FACTOR_DEFAULT: Final = (1.0, 1.0, 1.0) OUTPUT_GROUP_SHADING_TOONY_FACTOR_LABEL: Final = "Shading Toony" OUTPUT_GROUP_SHADING_TOONY_FACTOR_DEFAULT: Final = 0.9 OUTPUT_GROUP_SHADING_SHIFT_FACTOR_LABEL: Final = "Shading Shift" OUTPUT_GROUP_SHADING_SHIFT_FACTOR_DEFAULT: Final = -0.2 OUTPUT_GROUP_SHADING_SHIFT_TEXTURE_SCALE_LABEL: Final = "Shading Shift Texture Scale" OUTPUT_GROUP_SHADING_SHIFT_TEXTURE_SCALE_DEFAULT: Final = 1.0 OUTPUT_GROUP_EMISSIVE_FACTOR_LABEL: Final = "Emissive Factor" OUTPUT_GROUP_EMISSIVE_FACTOR_DEFAULT: Final = (0, 0, 0) OUTPUT_GROUP_EMISSIVE_STRENGTH_LABEL: Final = "Emissive Strength" OUTPUT_GROUP_EMISSIVE_STRENGTH_DEFAULT: Final = 1.0 OUTPUT_GROUP_PARAMETRIC_RIM_COLOR_FACTOR_LABEL: Final = "Parametric Rim Color" OUTPUT_GROUP_PARAMETRIC_RIM_COLOR_FACTOR_DEFAULT: Final = (0, 0, 0) OUTPUT_GROUP_PARAMETRIC_RIM_FRESNEL_POWER_LABEL: Final = "Parametric Rim Fresnel Power" OUTPUT_GROUP_PARAMETRIC_RIM_FRESNEL_POWER_DEFAULT: Final = 1.0 OUTPUT_GROUP_PARAMETRIC_RIM_LIFT_FACTOR_LABEL: Final = "Parametric Rim Lift" OUTPUT_GROUP_PARAMETRIC_RIM_LIFT_FACTOR_DEFAULT: Final = 1.0 OUTPUT_GROUP_RIM_LIGHTING_MIX_FACTOR_LABEL: Final = "Rim LightingMix" OUTPUT_GROUP_RIM_LIGHTING_MIX_FACTOR_DEFAULT: Final = 0 OUTPUT_GROUP_IS_OUTLINE_LABEL: Final = NORMAL_GROUP_IS_OUTLINE_LABEL OUTPUT_GROUP_OUTLINE_LIGHTING_MIX_FACTOR_LABEL: Final = "Outline LightingMix" OUTPUT_GROUP_OUTLINE_LIGHTING_MIX_FACTOR_DEFAULT: Final = 1.0 OUTPUT_GROUP_OUTLINE_COLOR_FACTOR_LABEL: Final = "Outline Color" OUTPUT_GROUP_OUTLINE_COLOR_FACTOR_DEFAULT: Final = (0, 0, 0) OUTPUT_GROUP_OUTLINE_WIDTH_FACTOR_LABEL: Final = "Outline Width" OUTPUT_GROUP_OUTLINE_WIDTH_FACTOR_DEFAULT: Final = 0 OUTPUT_GROUP_OUTLINE_WIDTH_MODE_LABEL: Final = "Outline Width Mode" OUTPUT_GROUP_OUTLINE_WIDTH_MODE_MIN: Final = 0 OUTPUT_GROUP_OUTLINE_WIDTH_MODE_DEFAULT: Final = 0 OUTPUT_GROUP_OUTLINE_WIDTH_MODE_MAX: Final = 2 OUTPUT_GROUP_MATCAP_FACTOR_LABEL: Final = "MatCap Factor" OUTPUT_GROUP_MATCAP_FACTOR_DEFAULT: Final = (1, 1, 1) OUTPUT_GROUP_ALPHA_MODE_LABEL: Final = "Alpha Mode" OUTPUT_GROUP_ALPHA_CUTOFF_LABEL: Final = "Alpha Cutoff" OUTPUT_GROUP_ALPHA_CUTOFF_DEFAULT: Final = 0.5 SHADER_NODE_GROUP_NAMES: Final = ( MATCAP_UV_GROUP_NAME, UV_GROUP_NAME, UV_ANIMATION_GROUP_NAME, NORMAL_GROUP_NAME, OUTPUT_GROUP_NAME, ) def template_name(name: str) -> str: return INTERNAL_NAME_PREFIX + name + " Template" def backup_name(name: str, backup_suffix: str) -> str: return name.removesuffix(" Template") + backup_suffix def generate_backup_suffix() -> str: # There is a possibility of duplicates in very rare cases. To be exact, duplicates # must be regenerated if any are found. return " " + "".join( random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(8) # Be careful not to exceed a total of 64 characters ) def get_mtoon1_auto_setup_shader_node_group(context: Context) -> Optional[NodeTree]: node_group = context.blend_data.node_groups.get(MTOON1_AUTO_SETUP_GROUP_NAME) if not node_group: return None if node_group.get(MTOON1_AUTO_SETUP_GROUP_NODE_TREE_CUSTOM_KEY): return node_group return None def add_mtoon1_auto_setup_shader_node_group(context: Context) -> None: blend_path = Path(__file__).with_name("mtoon_auto_setup.blend") node_tree_path = str(blend_path) + "/NodeTree" if get_mtoon1_auto_setup_shader_node_group(context): return wm_append_without_library( context, blend_path, append_filepath=node_tree_path + "/" + MTOON1_AUTO_SETUP_GROUP_NAME, append_filename=MTOON1_AUTO_SETUP_GROUP_NAME, append_directory=node_tree_path, ) def remove_mtoon1_auto_setup_shader_node_group(context: Context) -> None: node_group = get_mtoon1_auto_setup_shader_node_group(context) if not node_group or node_group.users: return context.blend_data.node_groups.remove(node_group) def load_mtoon1_node_group( context: Context, blend_file_path: Path, node_group_name: str, node_group_type: str, node_tree_type: str, *, reset_node_groups: bool, ) -> None: from ..editor.extension import get_node_tree_extension start_time = time.perf_counter() if not blend_file_path.exists(): logger.error("File not found: %s", blend_file_path) return if not reset_node_groups: checking_node_group = context.blend_data.node_groups.get(node_group_name) if checking_node_group: if ( tuple(get_node_tree_extension(checking_node_group).addon_version) >= LAST_MODIFIED_VERSION ): return if checking_node_group.type != node_tree_type: message = ( f'Node Group "{node_group_name}" already exists' + f' with different type "{checking_node_group.type}"' ) raise TypeError(message) backup_suffix = generate_backup_suffix() template_node_group_name = template_name(node_group_name) old_template_node_group = context.blend_data.node_groups.get( template_node_group_name ) if old_template_node_group: logger.error('Node Group "%s" already exists', template_node_group_name) old_template_node_group.name = backup_name( old_template_node_group.name, backup_suffix ) node_tree_path = str(blend_file_path) + "/NodeTree" node_tree_append_result = wm_append_without_library( context, blend_file_path, append_filepath=(node_tree_path + "/" + template_node_group_name), append_filename=template_node_group_name, append_directory=node_tree_path, ) if node_tree_append_result != {"FINISHED"}: raise RuntimeError( "Failed to append MToon 1.0 template node group " + f'"{template_node_group_name}": {node_tree_append_result}' ) template_node_group = None try: template_node_group = context.blend_data.node_groups.get( template_node_group_name ) if not template_node_group: raise ValueError("No " + template_node_group_name) node_group = context.blend_data.node_groups.get(node_group_name) if not node_group: node_group = context.blend_data.node_groups.new( node_group_name, node_group_type ) clear_node_tree(node_group, clear_inputs_outputs=True) copy_node_tree(context, template_node_group, node_group) get_node_tree_extension(node_group).addon_version = get_addon_version() finally: if template_node_group: if template_node_group.users: logger.warning( 'Failed to remove "%s" with %d users while loading mtoon shader', template_node_group.name, template_node_group.users, ) else: context.blend_data.node_groups.remove(template_node_group) # Logically, it may have already been removed, so retrieve it again old_template_node_group = context.blend_data.node_groups.get( backup_name(template_node_group_name, backup_suffix) ) if old_template_node_group: old_template_node_group.name = template_node_group_name end_time = time.perf_counter() logger.debug( 'Loaded NodeTree "%s": %.9f seconds', node_group_name, end_time - start_time ) def load_mtoon1_outline_geometry_node_group( context: Context, *, reset_node_groups: bool ) -> None: if bpy.app.version < (3, 3): return load_mtoon1_node_group( context, Path(__file__).with_name("mtoon1_outline.blend"), OUTLINE_GEOMETRY_GROUP_NAME, "GeometryNodeTree", "GEOMETRY", reset_node_groups=reset_node_groups, ) def load_mtoon1_shader_node_groups( context: Context, *, reset_node_groups: bool ) -> None: for shader_node_group_name in SHADER_NODE_GROUP_NAMES: load_mtoon1_node_group( context, Path(__file__).with_name("mtoon1.blend"), shader_node_group_name, "ShaderNodeTree", "SHADER", reset_node_groups=reset_node_groups, ) setup_frame_count_driver(context) def load_mtoon1_shader( context: Context, material: Material, *, reset_node_groups: bool, ) -> None: from ..editor.extension import get_material_extension if bpy.app.version < (5, 0, 0) and not material.use_nodes: material.use_nodes = True load_mtoon1_outline_geometry_node_group( context, reset_node_groups=reset_node_groups ) load_mtoon1_shader_node_groups(context, reset_node_groups=reset_node_groups) start_time = time.perf_counter() backup_suffix = generate_backup_suffix() # Back up if there are materials with the same name as the one being appended. # In the future, this will be replaced with append(do_reuse_local_id=True). template_material_name = template_name("VRM Add-on MToon 1.0") old_material = context.blend_data.materials.get(template_material_name) if old_material: logger.error('Material "%s" already exists', template_material_name) old_material.name = backup_name(old_material.name, backup_suffix) # When appending a Material, NodeTree is also appended simultaneously. # Back up if NodeTree with the same name exists. for shader_node_group_name in SHADER_NODE_GROUP_NAMES: name = template_name(shader_node_group_name) old_template_group = context.blend_data.node_groups.get(name) if old_template_group: logger.error('Node Group "%s" already exists', name) old_template_group.name = backup_name( old_template_group.name, backup_suffix ) blend_file_path = Path(__file__).with_name("mtoon1.blend") material_path = str(blend_file_path) + "/Material" material_append_result = wm_append_without_library( context, blend_file_path, append_filepath=material_path + "/" + template_material_name, append_filename=template_material_name, append_directory=material_path, # do_reuse_local_id=True ) if material_append_result != {"FINISHED"}: raise RuntimeError( "Failed to append MToon 1.0 template material: " + f"{material_append_result}" ) template_material = None try: template_material = context.blend_data.materials.get(template_material_name) if not template_material: raise ValueError("No " + template_material_name) template_material_node_tree = template_material.node_tree material_node_tree = material.node_tree if template_material_node_tree is None: logger.error("MToon template material node tree is None") elif material_node_tree is None: logger.error("MToon copy target material node tree is None") else: copy_node_tree(context, template_material_node_tree, material_node_tree) finally: if template_material: if template_material.users: logger.warning( 'Failed to remove "%s" with %d users while loading mtoon shader', template_material.name, template_material.users, ) else: context.blend_data.materials.remove(template_material) # Remove NodeTree that was appended simultaneously when appending Material for shader_node_group_name in SHADER_NODE_GROUP_NAMES: shader_node_group_template_name = template_name(shader_node_group_name) template_group = context.blend_data.node_groups.get( shader_node_group_template_name ) if template_group: if template_group.users: logger.warning( 'Failed to remove "%s" with %d users' " while loading mtoon shader", template_group.name, template_group.users, ) else: context.blend_data.node_groups.remove(template_group) # Logically, it may have already been removed, so retrieve it again old_material = context.blend_data.materials.get( backup_name(template_material_name, backup_suffix) ) if old_material: old_material.name = template_material_name # Restore the backed up NodeTree for shader_node_group_name in SHADER_NODE_GROUP_NAMES: name = template_name(shader_node_group_name) old_template_group = context.blend_data.node_groups.get( backup_name(name, backup_suffix) ) if old_template_group: old_template_group.name = name ext = get_material_extension(material) ext.mtoon1.setup_drivers() end_time = time.perf_counter() logger.debug( 'Loaded Material "%s": %.9f seconds', material.name, end_time - start_time ) def copy_socket(from_socket: NodeSocket, to_socket: NodeSocket) -> None: if ( bpy.app.version < (5, 0) and to_socket.display_shape != from_socket.display_shape ): to_socket.display_shape = from_socket.display_shape if to_socket.enabled != from_socket.enabled: to_socket.enabled = from_socket.enabled if to_socket.hide != from_socket.hide: to_socket.hide = from_socket.hide if to_socket.hide_value != from_socket.hide_value: to_socket.hide_value = from_socket.hide_value if to_socket.link_limit != from_socket.link_limit: to_socket.link_limit = from_socket.link_limit if to_socket.name != from_socket.name: to_socket.name = from_socket.name if to_socket.show_expanded != from_socket.show_expanded: to_socket.show_expanded = from_socket.show_expanded if to_socket.type != from_socket.type: to_socket.type = from_socket.type def copy_socket_interface( from_socket: "bpy.types.NodeSocketInterface", to_socket: "bpy.types.NodeSocketInterface", ) -> None: # bpy.app.version < (4,) from bpy.types import ( NodeSocketInterfaceColor, NodeSocketInterfaceFloat, NodeSocketInterfaceFloatAngle, NodeSocketInterfaceFloatDistance, NodeSocketInterfaceFloatFactor, NodeSocketInterfaceFloatPercentage, NodeSocketInterfaceFloatTime, NodeSocketInterfaceFloatUnsigned, NodeSocketInterfaceVector, NodeSocketInterfaceVectorAcceleration, NodeSocketInterfaceVectorDirection, NodeSocketInterfaceVectorEuler, NodeSocketInterfaceVectorTranslation, NodeSocketInterfaceVectorVelocity, NodeSocketInterfaceVectorXYZ, ) if bpy.app.version >= (3, 0, 0): to_socket.attribute_domain = from_socket.attribute_domain to_socket.bl_label = from_socket.bl_label to_socket.description = from_socket.description to_socket.name = from_socket.name to_socket.hide_value = from_socket.hide_value float_classes = ( NodeSocketInterfaceFloat, NodeSocketInterfaceFloatAngle, NodeSocketInterfaceFloatDistance, NodeSocketInterfaceFloatFactor, NodeSocketInterfaceFloatPercentage, NodeSocketInterfaceFloatTime, NodeSocketInterfaceFloatUnsigned, ) color_classes = (NodeSocketInterfaceColor,) vector_classes = ( NodeSocketInterfaceVector, NodeSocketInterfaceVectorAcceleration, NodeSocketInterfaceVectorDirection, NodeSocketInterfaceVectorEuler, NodeSocketInterfaceVectorTranslation, NodeSocketInterfaceVectorVelocity, NodeSocketInterfaceVectorXYZ, ) if isinstance(from_socket, float_classes) and isinstance(to_socket, float_classes): to_socket.default_value = from_socket.default_value to_socket.min_value = from_socket.min_value to_socket.max_value = from_socket.max_value elif isinstance(from_socket, color_classes) and isinstance( to_socket, color_classes ): to_socket.default_value = deepcopy( ( from_socket.default_value[0], from_socket.default_value[1], from_socket.default_value[2], from_socket.default_value[3], ) ) elif isinstance(from_socket, vector_classes) and isinstance( to_socket, vector_classes ): to_socket.default_value = deepcopy( ( from_socket.default_value[0], from_socket.default_value[1], from_socket.default_value[2], ) ) to_socket.min_value = from_socket.min_value to_socket.max_value = from_socket.max_value def copy_socket_default_value( from_socket: NodeSocket, to_socket: NodeSocket, ) -> None: if isinstance(from_socket, SCALAR_SOCKET_CLASSES): if isinstance(to_socket, BOOL_SOCKET_CLASSES): default_bool_value = bool(from_socket.default_value) if to_socket.default_value != default_bool_value: to_socket.default_value = default_bool_value elif isinstance(to_socket, FLOAT_SOCKET_CLASSES): default_float_value = float(from_socket.default_value) if to_socket.default_value != default_float_value: to_socket.default_value = default_float_value elif isinstance(to_socket, INT_SOCKET_CLASSES): default_int_value = int(from_socket.default_value) if to_socket.default_value != default_int_value: to_socket.default_value = default_int_value elif isinstance(from_socket, VECTOR_SOCKET_CLASSES) and isinstance( to_socket, VECTOR_SOCKET_CLASSES ): default_vector_value = ( from_socket.default_value[0], from_socket.default_value[1], from_socket.default_value[2], ) if tuple(to_socket.default_value) != default_vector_value: to_socket.default_value = default_vector_value elif isinstance(from_socket, COLOR_SOCKET_CLASSES) and isinstance( to_socket, COLOR_SOCKET_CLASSES ): default_color_value = ( from_socket.default_value[0], from_socket.default_value[1], from_socket.default_value[2], from_socket.default_value[3], ) if tuple(to_socket.default_value) != default_color_value: to_socket.default_value = default_color_value elif isinstance(from_socket, STRING_SOCKET_CLASSES) and isinstance( to_socket, STRING_SOCKET_CLASSES ): default_string_value = from_socket.default_value if to_socket.default_value != default_string_value: to_socket.default_value = default_string_value def copy_node_socket_default_value( from_node: Node, to_node: Node, ) -> None: for index, from_input in enumerate(from_node.inputs): if 0 <= index < len(to_node.inputs): to_input = to_node.inputs[index] copy_socket_default_value(from_input, to_input) for index, from_output in enumerate(from_node.outputs): if 0 <= index < len(to_node.outputs): to_output = to_node.outputs[index] copy_socket_default_value(from_output, to_output) def copy_shader_node_group( context: Context, from_node: ShaderNodeGroup, to_node: ShaderNodeGroup, ) -> None: node_tree = from_node.node_tree if not node_tree: logger.error("No node_tree in ShaderNodeGroup: %s", from_node.name) return for shader_node_group_name in SHADER_NODE_GROUP_NAMES: shader_node_group_template_name = template_name(shader_node_group_name) if not node_tree.name.startswith(shader_node_group_template_name): continue group = context.blend_data.node_groups.get(shader_node_group_name) if not group: logger.error('"%s" Not Found', shader_node_group_name) continue to_node.node_tree = group return logger.error( "Importing ShaderNodeGroup doesn't be supported yet: %s", node_tree.name ) def copy_node( context: Context, from_node: Node, to_node: Node, from_to: dict[Node, Node], ) -> None: # ruff: noqa: SIM102 Disable SIM102 only in this function to improve if statement readability from_node_color = ( from_node.color[0], from_node.color[1], from_node.color[2], ) if tuple(to_node.color) != from_node_color: to_node.color = from_node_color if to_node.height != from_node.height: to_node.height = from_node.height if to_node.hide != from_node.hide: to_node.hide = from_node.hide for index, from_input in enumerate(from_node.inputs): if 0 <= index < len(to_node.inputs): to_input = to_node.inputs[index] copy_socket(from_input, to_input) if to_node.label != from_node.label: to_node.label = from_node.label from_node_location = ( from_node.location[0], from_node.location[1], ) if tuple(to_node.location) != from_node_location: to_node.location = from_node_location if to_node.mute != from_node.mute: to_node.mute = from_node.mute if to_node.name != from_node.name: to_node.name = from_node.name for index, from_output in enumerate(from_node.outputs): if 0 <= index < len(to_node.outputs): to_output = to_node.outputs[index] copy_socket(from_output, to_output) from_node_parent = from_node.parent if from_node_parent: to_node_parent = from_to.get(from_node_parent) if to_node.parent != to_node_parent: to_node.parent = to_node_parent if to_node.select: to_node.select = False if to_node.show_options != from_node.show_options: to_node.show_options = from_node.show_options if to_node.show_preview != from_node.show_preview: to_node.show_preview = from_node.show_preview if to_node.show_texture: to_node.show_texture = False if to_node.use_custom_color != from_node.use_custom_color: to_node.use_custom_color = from_node.use_custom_color if to_node.width != from_node.width: to_node.width = from_node.width if isinstance(from_node, NodeFrame) and isinstance(to_node, NodeFrame): if to_node.shrink != from_node.shrink: to_node.shrink = from_node.shrink if to_node.label_size != from_node.label_size: to_node.label_size = from_node.label_size if to_node.text != from_node.text: to_node.text = from_node.text if isinstance(from_node, NodeGroup): logger.error("Importing NodeGroup doesn't be supported yet") if isinstance(from_node, NodeGroupOutput) and isinstance(to_node, NodeGroupOutput): if to_node.is_active_output != from_node.is_active_output: to_node.is_active_output = from_node.is_active_output if isinstance(from_node, ShaderNodeWireframe) and isinstance( to_node, ShaderNodeWireframe ): if to_node.use_pixel_size != from_node.use_pixel_size: to_node.use_pixel_size = from_node.use_pixel_size if isinstance(from_node, ShaderNodeVertexColor) and isinstance( to_node, ShaderNodeVertexColor ): if to_node.layer_name != from_node.layer_name: to_node.layer_name = from_node.layer_name if isinstance(from_node, ShaderNodeVectorTransform) and isinstance( to_node, ShaderNodeVectorTransform ): if to_node.convert_from != from_node.convert_from: to_node.convert_from = from_node.convert_from if to_node.convert_to != from_node.convert_to: to_node.convert_to = from_node.convert_to if to_node.vector_type != from_node.vector_type: to_node.vector_type = from_node.vector_type if isinstance(from_node, ShaderNodeVectorRotate) and isinstance( to_node, ShaderNodeVectorRotate ): if to_node.invert != from_node.invert: to_node.invert = from_node.invert if to_node.rotation_type != from_node.rotation_type: to_node.rotation_type = from_node.rotation_type if isinstance(from_node, ShaderNodeVectorMath) and isinstance( to_node, ShaderNodeVectorMath ): if to_node.operation != from_node.operation: to_node.operation = from_node.operation if isinstance(from_node, ShaderNodeVectorDisplacement) and isinstance( to_node, ShaderNodeVectorDisplacement ): if to_node.space != from_node.space: to_node.space = from_node.space if isinstance(from_node, ShaderNodeUVMap) and isinstance(to_node, ShaderNodeUVMap): if to_node.from_instancer != from_node.from_instancer: to_node.from_instancer = from_node.from_instancer if to_node.uv_map != from_node.uv_map: to_node.uv_map = from_node.uv_map if isinstance(from_node, ShaderNodeUVAlongStroke) and isinstance( to_node, ShaderNodeUVAlongStroke ): if to_node.use_tips != from_node.use_tips: to_node.use_tips = from_node.use_tips if isinstance(from_node, ShaderNodeTexWhiteNoise) and isinstance( to_node, ShaderNodeTexWhiteNoise ): if to_node.noise_dimensions != from_node.noise_dimensions: to_node.noise_dimensions = from_node.noise_dimensions if isinstance(from_node, ShaderNodeTexWave) and isinstance( to_node, ShaderNodeTexWave ): # incomplete if to_node.bands_direction != from_node.bands_direction: to_node.bands_direction = from_node.bands_direction from_node_color_mapping_blend_color = ( from_node.color_mapping.blend_color[0], from_node.color_mapping.blend_color[1], from_node.color_mapping.blend_color[2], ) if ( tuple(to_node.color_mapping.blend_color) != from_node_color_mapping_blend_color ): to_node.color_mapping.blend_color = from_node_color_mapping_blend_color if to_node.color_mapping.blend_type != from_node.color_mapping.blend_type: to_node.color_mapping.blend_type = from_node.color_mapping.blend_type if to_node.color_mapping.brightness != from_node.color_mapping.brightness: to_node.color_mapping.brightness = from_node.color_mapping.brightness if to_node.color_mapping.color_ramp.color_mode != ( from_node.color_mapping.color_ramp.color_mode ): to_node.color_mapping.color_ramp.color_mode = ( from_node.color_mapping.color_ramp.color_mode ) if to_node.color_mapping.contrast != from_node.color_mapping.contrast: to_node.color_mapping.contrast = from_node.color_mapping.contrast if to_node.color_mapping.saturation != from_node.color_mapping.saturation: to_node.color_mapping.saturation = from_node.color_mapping.saturation if ( to_node.color_mapping.use_color_ramp != from_node.color_mapping.use_color_ramp ): to_node.color_mapping.use_color_ramp = ( from_node.color_mapping.use_color_ramp ) if to_node.rings_direction != from_node.rings_direction: to_node.rings_direction = from_node.rings_direction if to_node.wave_profile != from_node.wave_profile: to_node.wave_profile = from_node.wave_profile if to_node.wave_type != from_node.wave_type: to_node.wave_type = from_node.wave_type if isinstance(from_node, ShaderNodeTexVoronoi) and isinstance( to_node, ShaderNodeTexVoronoi ): if to_node.distance != from_node.distance: to_node.distance = from_node.distance if to_node.feature != from_node.feature: to_node.feature = from_node.feature if to_node.voronoi_dimensions != from_node.voronoi_dimensions: to_node.voronoi_dimensions = from_node.voronoi_dimensions if isinstance(from_node, ShaderNodeTexSky) and isinstance( to_node, ShaderNodeTexSky ): if to_node.ground_albedo != from_node.ground_albedo: to_node.ground_albedo = from_node.ground_albedo if to_node.sky_type != from_node.sky_type: to_node.sky_type = from_node.sky_type from_node_sun_direction = ( from_node.sun_direction[0], from_node.sun_direction[1], from_node.sun_direction[2], ) if tuple(to_node.sun_direction) != from_node_sun_direction: to_node.sun_direction = from_node_sun_direction if to_node.turbidity != from_node.turbidity: to_node.turbidity = from_node.turbidity if isinstance(from_node, ShaderNodeTexNoise) and isinstance( to_node, ShaderNodeTexNoise ): if to_node.noise_dimensions != from_node.noise_dimensions: to_node.noise_dimensions = from_node.noise_dimensions if isinstance(from_node, ShaderNodeTexMagic) and isinstance( to_node, ShaderNodeTexMagic ): if to_node.turbulence_depth != from_node.turbulence_depth: to_node.turbulence_depth = from_node.turbulence_depth if isinstance(from_node, ShaderNodeTexImage) and isinstance( to_node, ShaderNodeTexImage ): if to_node.extension != from_node.extension: to_node.extension = from_node.extension # to_node.image = from_node.image if to_node.interpolation != from_node.interpolation: to_node.interpolation = from_node.interpolation if to_node.projection != from_node.projection: to_node.projection = from_node.projection if to_node.projection_blend != from_node.projection_blend: to_node.projection_blend = from_node.projection_blend if to_node.name == "Mtoon1BaseColorTexture.Image": if not to_node.select: to_node.select = True if not to_node.show_texture: to_node.show_texture = True if isinstance(from_node, ShaderNodeTexIES) and isinstance( to_node, ShaderNodeTexIES ): if to_node.filepath != from_node.filepath: to_node.filepath = from_node.filepath if to_node.ies != from_node.ies: to_node.ies = from_node.ies if to_node.mode != from_node.mode: to_node.mode = from_node.mode if isinstance(from_node, ShaderNodeTexGradient) and isinstance( to_node, ShaderNodeTexGradient ): if to_node.gradient_type != from_node.gradient_type: to_node.gradient_type = from_node.gradient_type if isinstance(from_node, ShaderNodeTexEnvironment) and isinstance( to_node, ShaderNodeTexEnvironment ): if to_node.image != from_node.image: to_node.image = from_node.image if to_node.interpolation != from_node.interpolation: to_node.interpolation = from_node.interpolation if to_node.projection != from_node.projection: to_node.projection = from_node.projection if isinstance(from_node, ShaderNodeTexCoord) and isinstance( to_node, ShaderNodeTexCoord ): if to_node.from_instancer != from_node.from_instancer: to_node.from_instancer = from_node.from_instancer if to_node.object != from_node.object: to_node.object = from_node.object if isinstance(from_node, ShaderNodeTexBrick) and isinstance( to_node, ShaderNodeTexBrick ): if to_node.offset != from_node.offset: to_node.offset = from_node.offset if to_node.offset_frequency != from_node.offset_frequency: to_node.offset_frequency = from_node.offset_frequency if to_node.squash != from_node.squash: to_node.squash = from_node.squash if to_node.squash_frequency != from_node.squash_frequency: to_node.squash_frequency = from_node.squash_frequency if isinstance(from_node, ShaderNodeTangent) and isinstance( to_node, ShaderNodeTangent ): if to_node.axis != from_node.axis: to_node.axis = from_node.axis if to_node.direction_type != from_node.direction_type: to_node.direction_type = from_node.direction_type if to_node.uv_map != from_node.uv_map: to_node.uv_map = from_node.uv_map if isinstance(from_node, ShaderNodeSubsurfaceScattering) and isinstance( to_node, ShaderNodeSubsurfaceScattering ): if to_node.falloff != from_node.falloff: to_node.falloff = from_node.falloff if isinstance(from_node, ShaderNodeScript) and isinstance( to_node, ShaderNodeScript ): if to_node.bytecode != from_node.bytecode: to_node.bytecode = from_node.bytecode if to_node.bytecode_hash != from_node.bytecode_hash: to_node.bytecode_hash = from_node.bytecode_hash if to_node.filepath != from_node.filepath: to_node.filepath = from_node.filepath if to_node.mode != from_node.mode: to_node.mode = from_node.mode if to_node.script != from_node.script: to_node.script = from_node.script if to_node.use_auto_update != from_node.use_auto_update: to_node.use_auto_update = from_node.use_auto_update if isinstance(from_node, ShaderNodeOutputWorld) and isinstance( to_node, ShaderNodeOutputWorld ): if to_node.is_active_output != from_node.is_active_output: to_node.is_active_output = from_node.is_active_output if to_node.target != from_node.target: to_node.target = from_node.target if isinstance(from_node, ShaderNodeOutputMaterial) and isinstance( to_node, ShaderNodeOutputMaterial ): if to_node.is_active_output != from_node.is_active_output: to_node.is_active_output = from_node.is_active_output if to_node.target != from_node.target: to_node.target = from_node.target if isinstance(from_node, ShaderNodeOutputLineStyle) and isinstance( to_node, ShaderNodeOutputLineStyle ): if to_node.blend_type != from_node.blend_type: to_node.blend_type = from_node.blend_type if to_node.is_active_output != from_node.is_active_output: to_node.is_active_output = from_node.is_active_output if to_node.target != from_node.target: to_node.target = from_node.target if to_node.use_alpha != from_node.use_alpha: to_node.use_alpha = from_node.use_alpha if to_node.use_clamp != from_node.use_clamp: to_node.use_clamp = from_node.use_clamp if isinstance(from_node, ShaderNodeOutputLight) and isinstance( to_node, ShaderNodeOutputLight ): if to_node.is_active_output != from_node.is_active_output: to_node.is_active_output = from_node.is_active_output if to_node.target != from_node.target: to_node.target = from_node.target if isinstance(from_node, ShaderNodeOutputAOV) and isinstance( to_node, ShaderNodeOutputAOV ): if to_node.name != from_node.name: to_node.name = from_node.name if isinstance(from_node, ShaderNodeNormalMap) and isinstance( to_node, ShaderNodeNormalMap ): if to_node.space != from_node.space: to_node.space = from_node.space if to_node.uv_map != from_node.uv_map: to_node.uv_map = from_node.uv_map if isinstance(from_node, ShaderNodeMixRGB) and isinstance( to_node, ShaderNodeMixRGB ): if to_node.blend_type != from_node.blend_type: to_node.blend_type = from_node.blend_type if to_node.use_alpha != from_node.use_alpha: to_node.use_alpha = from_node.use_alpha if to_node.use_clamp != from_node.use_clamp: to_node.use_clamp = from_node.use_clamp if isinstance(from_node, ShaderNodeMath) and isinstance(to_node, ShaderNodeMath): if to_node.operation != from_node.operation: to_node.operation = from_node.operation if to_node.use_clamp != from_node.use_clamp: to_node.use_clamp = from_node.use_clamp if isinstance(from_node, ShaderNodeMapping) and isinstance( to_node, ShaderNodeMapping ): if to_node.vector_type != from_node.vector_type: to_node.vector_type = from_node.vector_type if isinstance(from_node, ShaderNodeMapRange) and isinstance( to_node, ShaderNodeMapRange ): if to_node.clamp != from_node.clamp: to_node.clamp = from_node.clamp if to_node.interpolation_type != from_node.interpolation_type: to_node.interpolation_type = from_node.interpolation_type if isinstance(from_node, ShaderNodeGroup) and isinstance(to_node, ShaderNodeGroup): copy_shader_node_group(context, from_node, to_node) if isinstance(from_node, ShaderNodeDisplacement) and isinstance( to_node, ShaderNodeDisplacement ): if to_node.space != from_node.space: to_node.space = from_node.space if isinstance(from_node, ShaderNodeCustomGroup) and isinstance( to_node, ShaderNodeCustomGroup ): # to_node.node_tree = from_node.node_tree logger.error("Importing ShaderNodeCustomGroup doesn't be supported yet") if isinstance(from_node, ShaderNodeClamp) and isinstance(to_node, ShaderNodeClamp): if to_node.clamp_type != from_node.clamp_type: to_node.clamp_type = from_node.clamp_type if isinstance(from_node, ShaderNodeBump) and isinstance(to_node, ShaderNodeBump): if to_node.invert != from_node.invert: to_node.invert = from_node.invert if isinstance(from_node, ShaderNodeBsdfToon) and isinstance( to_node, ShaderNodeBsdfToon ): if to_node.component != from_node.component: to_node.component = from_node.component if isinstance(from_node, ShaderNodeBsdfRefraction) and isinstance( to_node, ShaderNodeBsdfRefraction ): if to_node.distribution != from_node.distribution: to_node.distribution = from_node.distribution if isinstance(from_node, ShaderNodeBsdfPrincipled) and isinstance( to_node, ShaderNodeBsdfPrincipled ): if to_node.distribution != from_node.distribution: to_node.distribution = from_node.distribution if to_node.subsurface_method != from_node.subsurface_method: to_node.subsurface_method = from_node.subsurface_method if isinstance(from_node, ShaderNodeBsdfHairPrincipled) and isinstance( to_node, ShaderNodeBsdfHairPrincipled ): if to_node.parametrization != from_node.parametrization: to_node.parametrization = from_node.parametrization if isinstance(from_node, ShaderNodeBsdfHair) and isinstance( to_node, ShaderNodeBsdfHair ): if to_node.component != from_node.component: to_node.component = from_node.component if isinstance(from_node, ShaderNodeBsdfGlass) and isinstance( to_node, ShaderNodeBsdfGlass ): if to_node.distribution != from_node.distribution: to_node.distribution = from_node.distribution if isinstance(from_node, ShaderNodeBsdfAnisotropic) and isinstance( to_node, ShaderNodeBsdfAnisotropic ): if to_node.distribution != from_node.distribution: to_node.distribution = from_node.distribution if isinstance(from_node, ShaderNodeBevel) and isinstance(to_node, ShaderNodeBevel): if to_node.samples != from_node.samples: to_node.samples = from_node.samples if isinstance(from_node, ShaderNodeAttribute) and isinstance( to_node, ShaderNodeAttribute ): if to_node.attribute_name != from_node.attribute_name: to_node.attribute_name = from_node.attribute_name if isinstance(from_node, ShaderNodeAmbientOcclusion) and isinstance( to_node, ShaderNodeAmbientOcclusion ): if to_node.inside != from_node.inside: to_node.inside = from_node.inside if to_node.only_local != from_node.only_local: to_node.only_local = from_node.only_local if to_node.samples != from_node.samples: to_node.samples = from_node.samples if bpy.app.version >= (3, 3): from bpy.types import ( GeometryNodeDeleteGeometry, GeometryNodeExtrudeMesh, GeometryNodeSeparateGeometry, GeometryNodeSwitch, ShaderNodeCombineColor, ShaderNodeSeparateColor, ) if isinstance(from_node, ShaderNodeCombineColor) and isinstance( to_node, ShaderNodeCombineColor ): if to_node.mode != from_node.mode: to_node.mode = from_node.mode if isinstance(from_node, ShaderNodeSeparateColor) and isinstance( to_node, ShaderNodeSeparateColor ): if to_node.mode != from_node.mode: to_node.mode = from_node.mode if isinstance(from_node, GeometryNodeSwitch) and isinstance( to_node, GeometryNodeSwitch ): if to_node.input_type != from_node.input_type: to_node.input_type = from_node.input_type if isinstance(from_node, GeometryNodeExtrudeMesh) and isinstance( to_node, GeometryNodeExtrudeMesh ): if to_node.mode != from_node.mode: to_node.mode = from_node.mode if isinstance(from_node, GeometryNodeDeleteGeometry) and isinstance( to_node, GeometryNodeDeleteGeometry ): if to_node.domain != from_node.domain: to_node.domain = from_node.domain if to_node.mode != from_node.mode: to_node.mode = from_node.mode if isinstance(from_node, GeometryNodeSeparateGeometry) and isinstance( to_node, GeometryNodeSeparateGeometry ): if to_node.domain != from_node.domain: to_node.domain = from_node.domain if bpy.app.version >= (3, 4): from bpy.types import ShaderNodeMix if isinstance(from_node, ShaderNodeMix) and isinstance(to_node, ShaderNodeMix): if to_node.blend_type != from_node.blend_type: to_node.blend_type = from_node.blend_type if to_node.clamp_factor != from_node.clamp_factor: to_node.clamp_factor = from_node.clamp_factor if to_node.clamp_result != from_node.clamp_result: to_node.clamp_result = from_node.clamp_result if to_node.data_type != from_node.data_type: to_node.data_type = from_node.data_type if to_node.factor_mode != from_node.factor_mode: to_node.factor_mode = from_node.factor_mode if bpy.app.version < (4, 0): from bpy.types import ShaderNodeBsdfGlossy if to_node.width_hidden != from_node.width_hidden: to_node.width_hidden = from_node.width_hidden if isinstance(from_node, ShaderNodeBsdfGlossy) and isinstance( to_node, ShaderNodeBsdfGlossy ): if to_node.distribution != from_node.distribution: to_node.distribution = from_node.distribution if bpy.app.version < (4, 1): from bpy.types import ShaderNodeTexMusgrave if isinstance(from_node, ShaderNodeTexMusgrave) and isinstance( to_node, ShaderNodeTexMusgrave ): if to_node.musgrave_dimensions != from_node.musgrave_dimensions: to_node.musgrave_dimensions = from_node.musgrave_dimensions if to_node.musgrave_type != from_node.musgrave_type: to_node.musgrave_type = from_node.musgrave_type if bpy.app.version < (5, 0): from bpy.types import ShaderNodeTexPointDensity if isinstance(from_node, ShaderNodeTexPointDensity) and isinstance( to_node, ShaderNodeTexPointDensity ): if to_node.interpolation != from_node.interpolation: to_node.interpolation = from_node.interpolation if to_node.object != from_node.object: to_node.object = from_node.object if to_node.particle_color_source != from_node.particle_color_source: to_node.particle_color_source = from_node.particle_color_source if to_node.point_source != from_node.point_source: to_node.point_source = from_node.point_source if to_node.radius != from_node.radius: to_node.radius = from_node.radius if to_node.resolution != from_node.resolution: to_node.resolution = from_node.resolution if to_node.space != from_node.space: to_node.space = from_node.space if to_node.vertex_attribute_name != from_node.vertex_attribute_name: to_node.vertex_attribute_name = from_node.vertex_attribute_name if to_node.vertex_color_source != from_node.vertex_color_source: to_node.vertex_color_source = from_node.vertex_color_source def clear_node_tree( node_tree: Optional[NodeTree], *, clear_inputs_outputs: bool = False ) -> None: if node_tree is None: return # node_tree.links.clear() while node_tree.links: node_tree.links.remove(node_tree.links[0]) # node_tree.nodes.clear() while node_tree.nodes: node_tree.nodes.remove(node_tree.nodes[0]) if not clear_inputs_outputs: return if bpy.app.version < (4, 0): # node_tree.inputs.clear() while node_tree.inputs: node_tree.inputs.remove(node_tree.inputs[0]) # node_tree.outputs.clear() while node_tree.outputs: node_tree.outputs.remove(node_tree.outputs[0]) return node_tree.interface.clear() def copy_node_tree_inputs_outputs( from_node_tree: NodeTree, to_node_tree: NodeTree ) -> None: # bpy.app.version < (4,) from bpy.types import NodeSocketInterfaceStandard while len(to_node_tree.inputs) > len(from_node_tree.inputs): to_node_tree.inputs.remove(to_node_tree.inputs[-1]) for index, from_input in enumerate(from_node_tree.inputs): to_input = None if isinstance(from_input, NodeSocketInterfaceStandard) and 0 <= index < len( to_node_tree.inputs ): to_input = to_node_tree.inputs[index] if ( isinstance(to_input, NodeSocketInterfaceStandard) and to_input.type != from_input.type ): to_input = None while len(to_node_tree.inputs) > index: to_node_tree.inputs.remove(to_node_tree.inputs[-1]) if not to_input: to_input = to_node_tree.inputs.new( from_input.bl_socket_idname, from_input.name ) copy_socket_interface(from_input, to_input) while len(to_node_tree.outputs) > len(from_node_tree.outputs): to_node_tree.outputs.remove(to_node_tree.outputs[-1]) for index, from_output in enumerate(from_node_tree.outputs): to_output = None if isinstance(from_output, NodeSocketInterfaceStandard) and 0 <= index < len( to_node_tree.outputs ): to_output = to_node_tree.outputs[index] if ( isinstance(to_output, NodeSocketInterfaceStandard) and to_output.type != from_output.type ): to_output = None while len(to_node_tree.outputs) > index: to_node_tree.outputs.remove(to_node_tree.outputs[-1]) if not to_output: to_output = to_node_tree.outputs.new( from_output.bl_socket_idname, from_output.name ) copy_socket_interface(from_output, to_output) def copy_node_tree_interface_socket( from_socket: "bpy.types.NodeTreeInterfaceSocket", to_socket: "bpy.types.NodeTreeInterfaceSocket", ) -> None: from bpy.types import ( NodeTreeInterfaceSocketColor, NodeTreeInterfaceSocketFloat, NodeTreeInterfaceSocketFloatAngle, NodeTreeInterfaceSocketFloatDistance, NodeTreeInterfaceSocketFloatFactor, NodeTreeInterfaceSocketFloatPercentage, NodeTreeInterfaceSocketFloatTime, NodeTreeInterfaceSocketFloatTimeAbsolute, NodeTreeInterfaceSocketFloatUnsigned, NodeTreeInterfaceSocketInt, NodeTreeInterfaceSocketIntFactor, NodeTreeInterfaceSocketIntPercentage, NodeTreeInterfaceSocketVector, NodeTreeInterfaceSocketVectorAcceleration, NodeTreeInterfaceSocketVectorDirection, NodeTreeInterfaceSocketVectorEuler, NodeTreeInterfaceSocketVectorTranslation, NodeTreeInterfaceSocketVectorVelocity, NodeTreeInterfaceSocketVectorXYZ, ) float_classes = ( NodeTreeInterfaceSocketFloat, NodeTreeInterfaceSocketFloatAngle, NodeTreeInterfaceSocketFloatDistance, NodeTreeInterfaceSocketFloatFactor, NodeTreeInterfaceSocketFloatPercentage, NodeTreeInterfaceSocketFloatTime, NodeTreeInterfaceSocketFloatTimeAbsolute, NodeTreeInterfaceSocketFloatUnsigned, ) int_classes = ( NodeTreeInterfaceSocketInt, NodeTreeInterfaceSocketIntFactor, NodeTreeInterfaceSocketIntPercentage, ) color_classes = (NodeTreeInterfaceSocketColor,) vector_classes = ( NodeTreeInterfaceSocketVector, NodeTreeInterfaceSocketVectorAcceleration, NodeTreeInterfaceSocketVectorDirection, NodeTreeInterfaceSocketVectorEuler, NodeTreeInterfaceSocketVectorTranslation, NodeTreeInterfaceSocketVectorVelocity, NodeTreeInterfaceSocketVectorXYZ, ) to_socket.attribute_domain = from_socket.attribute_domain to_socket.default_attribute_name = from_socket.default_attribute_name to_socket.hide_value = from_socket.hide_value to_socket.hide_in_modifier = ( True # For internal use only, always hide from modifier ) to_socket.force_non_field = from_socket.force_non_field if isinstance(from_socket, float_classes) and isinstance(to_socket, float_classes): to_socket.default_value = from_socket.default_value to_socket.min_value = from_socket.min_value to_socket.max_value = from_socket.max_value elif isinstance(from_socket, float_classes) and isinstance(to_socket, int_classes): max_int = 1_000_000_000 min_int = -1_000_000_000 to_socket.default_value = int( min(max(min_int, from_socket.default_value), max_int) ) to_socket.min_value = int(min(max(min_int, from_socket.min_value), max_int)) to_socket.max_value = int(min(max(min_int, from_socket.max_value), max_int)) elif isinstance(from_socket, color_classes) and isinstance( to_socket, color_classes ): to_socket.default_value = deepcopy( ( from_socket.default_value[0], from_socket.default_value[1], from_socket.default_value[2], from_socket.default_value[3], ) ) elif isinstance(from_socket, vector_classes) and isinstance( to_socket, vector_classes ): if isinstance(to_socket, NodeTreeInterfaceSocketVector): to_socket.default_value = deepcopy( ( from_socket.default_value[0], from_socket.default_value[1], from_socket.default_value[2], ) ) elif isinstance(to_socket, NodeTreeInterfaceSocketVectorEuler): if isinstance(from_socket, NodeTreeInterfaceSocketVectorEuler): to_socket.default_value = from_socket.default_value.copy() else: to_socket.default_value = Euler( deepcopy( ( from_socket.default_value[0], from_socket.default_value[1], from_socket.default_value[2], ) ), "XYZ", ) elif isinstance(from_socket, NodeTreeInterfaceSocketVectorEuler): to_socket.default_value = Vector( ( from_socket.default_value.x, from_socket.default_value.y, from_socket.default_value.z, ) ) else: to_socket.default_value = Vector( ( from_socket.default_value[0], from_socket.default_value[1], from_socket.default_value[2], ) ) to_socket.min_value = from_socket.min_value to_socket.max_value = from_socket.max_value def copy_node_tree_interface(from_node_tree: NodeTree, to_node_tree: NodeTree) -> None: from bpy.types import ( NodeTreeInterfaceSocket, NodeTreeInterfaceSocketFloatFactor, ) socket_type_to_group_name_to_socket_names: dict[str, dict[str, list[str]]] = { "NodeSocketBool": { OUTPUT_GROUP_NAME: [ OUTPUT_GROUP_DOUBLE_SIDED_LABEL, OUTPUT_GROUP_TRANSPARENT_WITH_Z_WRITE_LABEL, OUTPUT_GROUP_IS_OUTLINE_LABEL, ], NORMAL_GROUP_NAME: [ NORMAL_GROUP_IS_OUTLINE_LABEL, ], }, "NodeSocketInt": { OUTPUT_GROUP_NAME: [ OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_LABEL, OUTPUT_GROUP_OUTLINE_WIDTH_MODE_LABEL, ], UV_GROUP_NAME: [ UV_GROUP_WRAP_S_LABEL, UV_GROUP_WRAP_T_LABEL, UV_GROUP_IMAGE_WIDTH_LABEL, UV_GROUP_IMAGE_HEIGHT_LABEL, ], }, } to_items_index = 0 for from_item in from_node_tree.interface.items_tree: if from_item.item_type != "SOCKET" or not isinstance( from_item, NodeTreeInterfaceSocket ): continue from_socket_type = from_item.socket_type if not from_socket_type: logger.error( 'Socket source "%s" has empty socket_type. type=%s', from_item.name, type(from_item).__name__, ) if isinstance(from_item, NodeTreeInterfaceSocketFloatFactor): from_socket_type = "NodeSocketFloat" else: continue translated_from_socket_type = from_socket_type if bpy.app.version >= (4, 1): for ( socket_type, group_name_to_socket_names, ) in socket_type_to_group_name_to_socket_names.items(): socket_names = group_name_to_socket_names.get(to_node_tree.name) if not socket_names: continue if from_item.name in socket_names: translated_from_socket_type = socket_type break to_socket: Optional[NodeTreeInterfaceSocket] = None while len(to_node_tree.interface.items_tree) > to_items_index: to_item = list(to_node_tree.interface.items_tree.values())[to_items_index] if ( to_item.item_type != "SOCKET" or not isinstance(to_item, NodeTreeInterfaceSocket) or to_item.in_out != from_item.in_out ): to_node_tree.interface.remove(to_item) continue if not to_item.socket_type: logger.error( 'Socket destination "%s" has empty socket_type. type=%s', to_item.name, type(to_item).__name__, ) if isinstance(to_item, NodeTreeInterfaceSocketFloatFactor): to_item.socket_type = "NodeSocketFloat" if to_item.socket_type not in [ from_socket_type, translated_from_socket_type, ]: to_node_tree.interface.remove(to_item) continue if to_item.name != from_item.name: to_item.name = from_item.name if to_item.description != from_item.description: to_item.description = from_item.description to_socket = to_item break if to_socket is None: to_socket = to_node_tree.interface.new_socket( from_item.name, description=from_item.description, in_out=from_item.in_out, socket_type=translated_from_socket_type, ) copy_node_tree_interface_socket(from_item, to_socket) to_items_index += 1 while len(to_node_tree.interface.items_tree) > to_items_index: to_node_tree.interface.remove( list(to_node_tree.interface.items_tree.values())[-1] ) def copy_node_tree( context: Context, from_node_tree: NodeTree, to_node_tree: NodeTree ) -> None: clear_node_tree(to_node_tree, clear_inputs_outputs=False) if bpy.app.version < (4, 0): copy_node_tree_inputs_outputs(from_node_tree, to_node_tree) else: copy_node_tree_interface(from_node_tree, to_node_tree) from_to: dict[Node, Node] = {} # Use sorted because glTF add-on uses index-aware logic. # https://github.com/KhronosGroup/glTF-Blender-IO/blob/86453b20c28d7c78a3f9106911df69fd0138fb2a/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_search_node_tree.py#L806 for from_node in sorted(from_node_tree.nodes, key=lambda n: n.name): to_node = to_node_tree.nodes.new(from_node.bl_idname) from_to[from_node] = to_node for from_node, to_node in from_to.items(): copy_node(context, from_node, to_node, from_to) for from_link in from_node_tree.links: if not from_link.is_valid: continue input_socket_index = next( ( i for i, s in enumerate(from_link.to_node.inputs) if s == from_link.to_socket ), None, ) if input_socket_index is None: continue input_node = from_to.get(from_link.to_node) if input_node is None: continue if not 0 <= input_socket_index < len(input_node.inputs): logger.error( "Input socket out of range: %d < %d", input_socket_index, len(input_node.inputs), ) continue input_socket = input_node.inputs[input_socket_index] if not input_socket: logger.error("No input socket: %s", from_link.to_socket.name) continue output_socket_index = next( ( i for i, s in enumerate(from_link.from_node.outputs) if s == from_link.from_socket ), None, ) if output_socket_index is None: continue output_node = from_to.get(from_link.from_node) if output_node is None: continue if not 0 <= output_socket_index < len(output_node.outputs): logger.error( "Output socket out of range: %d < %d", output_socket_index, len(output_node.outputs), ) continue output_socket = output_node.outputs[output_socket_index] if not output_socket: logger.error("No output socket: %s", from_link.from_socket.name) continue to_node_tree.links.new(input_socket, output_socket) for from_node, to_node in from_to.items(): copy_node_socket_default_value(from_node, to_node) for from_node, to_node in from_to.items(): # The location of the node can be completely restored by setting the location # again with a consistent parent-child relationship. from_node_location = ( from_node.location[0], from_node.location[1], ) if tuple(to_node.location) != from_node_location: to_node.location = from_node_location def get_image_name_and_sampler_type( shader_node: Node, input_socket_name: str ) -> Optional[tuple[str, int, int]]: if ( input_socket_name == "NormalmapTexture" and "NormalmapTexture" not in shader_node.inputs and "NomalmapTexture" in shader_node.inputs ): input_socket_name = "NomalmapTexture" socket = shader_node.inputs.get(input_socket_name) if not socket: return None links = socket.links if not links: return None from_node = links[0].from_node if not from_node: return None if not isinstance(from_node, ShaderNodeTexImage): return None image = from_node.image if not image: return None image_name = image.name if not image_name: return None # blender is ('Linear', 'Closest', 'Cubic', 'Smart') # glTF is Linear, Closest filter_type = GL_NEAREST if from_node.interpolation == "Closest" else GL_LINEAR # blender is ('REPEAT', 'EXTEND', 'CLIP') # glTF is CLAMP_TO_EDGE, MIRRORED_REPEAT, REPEAT wrap_type = GL_REPEAT if from_node.extension == "REPEAT" else GL_CLAMP_TO_EDGE return image_name, wrap_type, filter_type def get_float_value( shader_node: Node, input_socket_name: str, min_value: float = -float_info.max, max_value: float = float_info.max, ) -> Optional[float]: socket = shader_node.inputs.get(input_socket_name) if not socket: return None default_value = convert.float_or_none( getattr(socket, "default_value", None), min_value, max_value ) links = socket.links if not links: return default_value from_node = links[0].from_node if not from_node: return default_value outputs = from_node.outputs if not outputs: return default_value return convert.float_or_none( getattr(outputs[0], "default_value", None), min_value, max_value ) def get_float_value_or( shader_node: Node, input_socket_name: str, min_value: float = -float_info.max, max_value: float = float_info.max, default_value: float = 0.0, ) -> float: float_value = get_float_value(shader_node, input_socket_name, min_value, max_value) if float_value is None: return default_value return float_value def rgba_or_none( vs: object, min_value: float = -float_info.max, max_value: float = float_info.max ) -> Optional[tuple[float, float, float, float]]: default_alpha_value = max(min_value, min(1.0, max_value)) if isinstance(vs, Color): return ( max(min_value, min(vs.r, max_value)), max(min_value, min(vs.g, max_value)), max(min_value, min(vs.b, max_value)), default_alpha_value, ) iterator = convert.iterator_or_none(vs) if iterator is None: return None rgba: list[float] = [] for v in iterator: f = convert.float_or_none(v, min_value, max_value) if f is None: return None rgba.append(f) if len(rgba) > 4: return None if len(rgba) == 3: rgba.append(default_alpha_value) if len(rgba) != 4: return None return (rgba[0], rgba[1], rgba[2], rgba[3]) def get_rgba_value( shader_node: Node, input_socket_name: str, min_value: float = -float_info.max, max_value: float = float_info.max, ) -> Optional[tuple[float, float, float, float]]: socket = shader_node.inputs.get(input_socket_name) if not socket: return None default_value = rgba_or_none( getattr(socket, "default_value", None), min_value, max_value ) links = socket.links if not links: return default_value from_node = links[0].from_node if not from_node: return default_value outputs = from_node.outputs if not outputs: return default_value return rgba_or_none( getattr(outputs[0], "default_value", None), min_value, max_value ) def get_rgba_value_or( shader_node: Node, input_socket_name: str, min_value: float = -float_info.max, max_value: float = float_info.max, default_value: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0), ) -> tuple[float, float, float, float]: rgba_value = get_rgba_value(shader_node, input_socket_name, min_value, max_value) if rgba_value is None: return default_value return rgba_value def rgb_or_none( vs: object, min_value: float = -float_info.max, max_value: float = float_info.max ) -> Optional[tuple[float, float, float]]: if isinstance(vs, Color): return ( max(min_value, min(vs.r, max_value)), max(min_value, min(vs.g, max_value)), max(min_value, min(vs.b, max_value)), ) iterator = convert.iterator_or_none(vs) if iterator is None: return None rgb: list[float] = [] for v in iterator: f = convert.float_or_none(v, min_value, max_value) if f is None: return None rgb.append(f) if len(rgb) > 4: return None if len(rgb) == 4: rgb.pop() if len(rgb) != 3: return None return (rgb[0], rgb[1], rgb[2]) def get_rgb_value( shader_node: Node, input_socket_name: str, min_value: float = -float_info.max, max_value: float = float_info.max, ) -> Optional[tuple[float, float, float]]: socket = shader_node.inputs.get(input_socket_name) if not socket: return None default_value = rgb_or_none( getattr(socket, "default_value", None), min_value, max_value ) links = socket.links if not links: return default_value from_node = links[0].from_node if not from_node: return default_value outputs = from_node.outputs if not outputs: return default_value return rgb_or_none(getattr(outputs[0], "default_value", None), min_value, max_value) def get_rgb_value_or( shader_node: Node, input_socket_name: str, min_value: float = -float_info.max, max_value: float = float_info.max, default: tuple[float, float, float] = (0.0, 0.0, 0.0), ) -> tuple[float, float, float]: rgb_value = get_rgb_value(shader_node, input_socket_name, min_value, max_value) if rgb_value is None: return default return rgb_value def setup_frame_count_driver(context: Context) -> None: if bpy.app.version < (3, 6): return node_group = context.blend_data.node_groups.get(UV_ANIMATION_GROUP_NAME) if not node_group: return animation_data = node_group.animation_data if not animation_data: animation_data = node_group.animation_data_create() if not animation_data: logger.error( 'Failed to create anomation data for node group "%s"', node_group.name ) return for data_path, target_data_path, enabled in [ ( f'nodes["{UV_ANIMATION_GROUP_FPS_BASE_NODE_NAME}"]' + ".outputs[0]" + ".default_value", "render.fps_base", True, ), ( f'nodes["{UV_ANIMATION_GROUP_FPS_NODE_NAME}"]' + ".outputs[0]" + ".default_value", "render.fps", True, ), ( f'nodes["{UV_ANIMATION_GROUP_FRAME_CURRENT_NODE_NAME}"]' + ".outputs[0]" + ".default_value", "frame_current", False, ), ]: fcurve: Optional[Union[FCurve, list[FCurve]]] = next( iter( fcurve for fcurve in animation_data.drivers if fcurve.data_path == data_path ), None, ) if not enabled: if fcurve is not None: node_group.driver_remove(data_path) continue if fcurve is None: try: fcurve = node_group.driver_add(data_path) except TypeError: continue if not isinstance(fcurve, FCurve): logger.error( 'Failed to get fcurve "%s" for node tree "%s"', data_path, node_group.name, ) continue if fcurve.array_index != 0: fcurve.array_index = 0 driver = fcurve.driver if not isinstance(driver, Driver): logger.error('Failed to get driver for fcurve "%s"', data_path) continue if driver.type != "SUM": driver.type = "SUM" if not driver.variables: driver.variables.new() while len(driver.variables) > 1: driver.variables.remove(driver.variables[-1]) variable = driver.variables[0] if not variable.targets: logger.error( 'No targets in variable for fcurve "%s" in node_tree "%s"', data_path, node_group.name, ) continue target = variable.targets[0] if variable.type != "CONTEXT_PROP": variable.type = "CONTEXT_PROP" if target.context_property != "ACTIVE_SCENE": target.context_property = "ACTIVE_SCENE" if target.data_path != target_data_path: target.data_path = target_data_path __NodeType = TypeVar("__NodeType", bound=Node) def search_input_node( node_socket: NodeSocket, node_type: type[__NodeType] ) -> Optional[__NodeType]: searching_node_sockets = [node_socket] while searching_node_sockets: searching_node_socket = searching_node_sockets.pop() for link in searching_node_socket.links: if not link.is_valid or link.is_muted: continue from_node = link.from_node if from_node.mute: continue if isinstance(from_node, node_type): return from_node if not isinstance(from_node, NodeReroute): continue searching_node_sockets.extend(from_node.inputs) return None