Files
blender-vrm/common/shader.py
小煜 091ad6a49a feat: Add VRM Blender addon with complete import/export functionality
- Add core VRM addon infrastructure with manifest and registration
- Add common utilities module with file system, logging, and conversion helpers
- Add human bone mapper with support for multiple rigging standards (Mixamo, MMD, Unreal, Rigify, etc.)
- Add VRM 0.x and 1.x format support with property groups and handlers
- Add editor UI panels for VRM metadata, spring bones, and MToon materials
- Add exporter with glTF2 extension support for VRM format serialization
- Add importer with scene reconstruction and armature generation
- Add MToon shader support with auto-setup and material migration
- Add spring bone physics simulation with constraint handling
- Add node constraint editor for advanced rigging control
- Add comprehensive validation and error handling with user dialogs
- Add scene watcher for real-time property synchronization
- Add workspace management and preference system
- Include Python cache files and Blender manifest configuration
- This is the initial commit establishing the complete VRM addon ecosystem for Blender
2026-01-01 14:21:56 +08:00

2046 lines
76 KiB
Python

# 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