diff --git a/core/lamp.py b/core/lamp.py deleted file mode 100644 index 10593d3..0000000 --- a/core/lamp.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014 MMD Tools authors -# This file is part of MMD Tools. - -import bpy - -from ..bpyutils import FnContext, Props - - -class MMDLamp: - def __init__(self, obj): - if MMDLamp.isLamp(obj): - obj = obj.parent - if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT": - self.__emptyObj = obj - else: - raise ValueError("%s is not MMDLamp" % str(obj)) - - @staticmethod - def isLamp(obj): - return obj and obj.type in {"LIGHT", "LAMP"} - - @staticmethod - def isMMDLamp(obj): - if MMDLamp.isLamp(obj): - obj = obj.parent - return obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT" - - @staticmethod - def convertToMMDLamp(lampObj, scale=1.0): - if MMDLamp.isMMDLamp(lampObj): - return MMDLamp(lampObj) - - empty = bpy.data.objects.new(name="MMD_Light", object_data=None) - FnContext.link_object(FnContext.ensure_context(), empty) - - empty.rotation_mode = "XYZ" - empty.lock_rotation = (True, True, True) - setattr(empty, Props.empty_display_size, 0.4) - empty.scale = [10 * scale] * 3 - empty.mmd_type = "LIGHT" - empty.location = (0, 0, 11 * scale) - - lampObj.parent = empty - lampObj.data.color = (0.602, 0.602, 0.602) - lampObj.location = (0.5, -0.5, 1.0) - lampObj.rotation_mode = "XYZ" - lampObj.rotation_euler = (0, 0, 0) - lampObj.lock_rotation = (True, True, True) - - constraint = lampObj.constraints.new(type="TRACK_TO") - constraint.name = "mmd_lamp_track" - constraint.target = empty - constraint.track_axis = "TRACK_NEGATIVE_Z" - constraint.up_axis = "UP_Y" - - return MMDLamp(empty) - - def object(self): - return self.__emptyObj - - def lamp(self): - for i in self.__emptyObj.children: - if MMDLamp.isLamp(i): - return i - raise KeyError diff --git a/core/mmd/bpyutils.py b/core/mmd/bpyutils.py index c5c9d76..3bc6d28 100644 --- a/core/mmd/bpyutils.py +++ b/core/mmd/bpyutils.py @@ -6,9 +6,13 @@ # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import contextlib -from typing import Generator, List, Optional, TypeVar +from typing import Generator, List, Optional, TypeVar, Any, Set, Tuple, Dict, Union import bpy +from bpy.types import Object, Context, ID, Key, ShapeKey, FCurve, LayerCollection, Collection +from bpy.types import AddonPreferences, Addon, WindowManager, Area, Region, Window + +from ..logging_setup import logger class Props: # For API changes of only name changed properties @@ -20,7 +24,7 @@ class Props: # For API changes of only name changed properties class __EditMode: - def __init__(self, obj): + def __init__(self, obj: Object): if not isinstance(obj, bpy.types.Object): raise ValueError self.__prevMode = obj.mode @@ -30,10 +34,10 @@ class __EditMode: if obj.mode != "EDIT": bpy.ops.object.mode_set(mode="EDIT") - def __enter__(self): + def __enter__(self) -> Any: return self.__obj.data - def __exit__(self, type, value, traceback): + def __exit__(self, type: Any, value: Any, traceback: Any) -> None: if self.__prevMode == "EDIT": bpy.ops.object.mode_set(mode="OBJECT") # update edited data bpy.ops.object.mode_set(mode=self.__prevMode) @@ -41,17 +45,18 @@ class __EditMode: class __SelectObjects: - def __init__(self, active_object: bpy.types.Object, selected_objects: Optional[List[bpy.types.Object]] = None): + def __init__(self, active_object: Object, selected_objects: Optional[List[Object]] = None): if not isinstance(active_object, bpy.types.Object): raise ValueError try: bpy.ops.object.mode_set(mode="OBJECT") except Exception: + logger.debug("Failed to set object mode") pass - contenxt = FnContext.ensure_context() + context = FnContext.ensure_context() - for i in contenxt.selected_objects: + for i in context.selected_objects: i.select_set(False) self.__active_object = active_object @@ -60,23 +65,23 @@ class __SelectObjects: self.__hides: List[bool] = [] for i in self.__selected_objects: self.__hides.append(i.hide_get()) - FnContext.select_object(contenxt, i) - FnContext.set_active_object(contenxt, active_object) + FnContext.select_object(context, i) + FnContext.set_active_object(context, active_object) - def __enter__(self) -> bpy.types.Object: + def __enter__(self) -> Object: return self.__active_object - def __exit__(self, type, value, traceback): + def __exit__(self, type: Any, value: Any, traceback: Any) -> None: for i, j in zip(self.__selected_objects, self.__hides): i.hide_set(j) -def setParent(obj, parent): +def setParent(obj: Object, parent: Object) -> None: with select_object(parent, objects=[parent, obj]): bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False) -def setParentToBone(obj, parent, bone_name): +def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None: with select_object(parent, objects=[parent, obj]): bpy.ops.object.mode_set(mode="POSE") parent.data.bones.active = parent.data.bones[bone_name] @@ -84,7 +89,7 @@ def setParentToBone(obj, parent, bone_name): bpy.ops.object.mode_set(mode="OBJECT") -def edit_object(obj): +def edit_object(obj: Object) -> __EditMode: """Set the object interaction mode to 'EDIT' It is recommended to use 'edit_object' with 'with' statement like the following code. @@ -95,7 +100,7 @@ def edit_object(obj): return __EditMode(obj) -def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object]] = None): +def select_object(obj: Object, objects: Optional[List[Object]] = None) -> __SelectObjects: """Select objects. It is recommended to use 'select_object' with 'with' statement like the following code. @@ -108,20 +113,23 @@ def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object return __SelectObjects(obj, objects) -def duplicateObject(obj, total_len): +def duplicateObject(obj: Object, total_len: int) -> List[Object]: return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len) -def createObject(name="Object", object_data=None, target_scene=None): +def createObject(name: str = "Object", object_data: Optional[ID] = None, target_scene: Optional[bpy.types.Scene] = None) -> Object: context = FnContext.ensure_context(target_scene) return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data)) -def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None): +def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, target_object: Optional[Object] = None) -> Object: import bmesh if target_object is None: target_object = createObject(name="Sphere") + logger.debug(f"Created new sphere object: {target_object.name}") + else: + logger.debug(f"Using existing object for sphere: {target_object.name}") mesh = target_object.data bm = bmesh.new() @@ -138,12 +146,15 @@ def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None): return target_object -def makeBox(size=(1, 1, 1), target_object=None): +def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optional[Object] = None) -> Object: import bmesh from mathutils import Matrix if target_object is None: target_object = createObject(name="Box") + logger.debug(f"Created new box object: {target_object.name}") + else: + logger.debug(f"Using existing object for box: {target_object.name}") mesh = target_object.data bm = bmesh.new() @@ -159,13 +170,16 @@ def makeBox(size=(1, 1, 1), target_object=None): return target_object -def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=None): +def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, height: float = 1.0, target_object: Optional[Object] = None) -> Object: import math - import bmesh if target_object is None: target_object = createObject(name="Capsule") + logger.debug(f"Created new capsule object: {target_object.name}") + else: + logger.debug(f"Using existing object for capsule: {target_object.name}") + height = max(height, 1e-3) mesh = target_object.data @@ -224,10 +238,10 @@ def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=N class TransformConstraintOp: - __MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"} + __MIN_MAX_MAP: Dict[Union[str, Tuple[str, str]], Union[str, Tuple[str, ...]]] = {"ROTATION": "_rot", "SCALE": "_scale"} @staticmethod - def create(constraints, name, map_type): + def create(constraints: bpy.types.ObjectConstraints, name: str, map_type: str) -> bpy.types.TransformConstraint: c = constraints.get(name, None) if c and c.type != "TRANSFORM": constraints.remove(c) @@ -245,7 +259,7 @@ class TransformConstraintOp: return c @classmethod - def min_max_attributes(cls, map_type, name_id=""): + def min_max_attributes(cls, map_type: str, name_id: str = "") -> Tuple[str, ...]: key = (map_type, name_id) ret = cls.__MIN_MAX_MAP.get(key, None) if ret is None: @@ -255,7 +269,7 @@ class TransformConstraintOp: return ret @classmethod - def update_min_max(cls, constraint, value, influence=1): + def update_min_max(cls, constraint: bpy.types.TransformConstraint, value: float, influence: Optional[float] = 1) -> None: c = constraint if not c or c.type != "TRANSFORM": return @@ -279,14 +293,14 @@ class FnObject: raise NotImplementedError("This class is not expected to be instantiated.") @staticmethod - def mesh_remove_shape_key(mesh_object: bpy.types.Object, shape_key: bpy.types.ShapeKey): + def mesh_remove_shape_key(mesh_object: Object, shape_key: ShapeKey) -> None: assert isinstance(mesh_object.data, bpy.types.Mesh) - key: bpy.types.Key = shape_key.id_data + key: Key = shape_key.id_data assert key == mesh_object.data.shape_keys if mesh_object.animation_data is not None: - fc_curve: bpy.types.FCurve + fc_curve: FCurve for fc_curve in mesh_object.animation_data.drivers: if not fc_curve.data_path.startswith(shape_key.path_from_id()): continue @@ -310,35 +324,35 @@ class FnContext: raise NotImplementedError("This class is not expected to be instantiated.") @staticmethod - def ensure_context(context: Optional[bpy.types.Context] = None) -> bpy.types.Context: + def ensure_context(context: Optional[Context] = None) -> Context: return context or bpy.context @staticmethod - def get_active_object(context: bpy.types.Context) -> Optional[bpy.types.Object]: + def get_active_object(context: Context) -> Optional[Object]: return context.active_object @staticmethod - def set_active_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: + def set_active_object(context: Context, obj: Object) -> Object: context.view_layer.objects.active = obj return obj @staticmethod - def set_active_and_select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: + def set_active_and_select_single_object(context: Context, obj: Object) -> Object: return FnContext.set_active_object(context, FnContext.select_single_object(context, obj)) @staticmethod - def get_scene_objects(context: bpy.types.Context) -> bpy.types.SceneObjects: + def get_scene_objects(context: Context) -> bpy.types.SceneObjects: return context.scene.objects @staticmethod - def ensure_selectable(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: + def ensure_selectable(context: Context, obj: Object) -> Object: obj.hide_viewport = False obj.hide_select = False obj.hide_set(False) if obj not in context.selectable_objects: - def __layer_check(layer_collection: bpy.types.LayerCollection) -> bool: + def __layer_check(layer_collection: LayerCollection) -> bool: for lc in layer_collection.children: if __layer_check(lc): lc.hide_viewport = False @@ -360,44 +374,44 @@ class FnContext: return obj @staticmethod - def select_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: + def select_object(context: Context, obj: Object) -> Object: FnContext.ensure_selectable(context, obj).select_set(True) return obj @staticmethod - def select_objects(context: bpy.types.Context, *objects: bpy.types.Object) -> List[bpy.types.Object]: + def select_objects(context: Context, *objects: Object) -> List[Object]: return [FnContext.select_object(context, obj) for obj in objects] @staticmethod - def select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: + def select_single_object(context: Context, obj: Object) -> Object: for i in context.selected_objects: if i != obj: i.select_set(False) return FnContext.select_object(context, obj) @staticmethod - def link_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: + def link_object(context: Context, obj: Object) -> Object: context.collection.objects.link(obj) return obj @staticmethod - def new_and_link_object(context: bpy.types.Context, name: str, object_data: Optional[bpy.types.ID]) -> bpy.types.Object: + def new_and_link_object(context: Context, name: str, object_data: Optional[ID]) -> Object: return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data)) @staticmethod - def duplicate_object(context: bpy.types.Context, object_to_duplicate: bpy.types.Object, target_count: int) -> List[bpy.types.Object]: + def duplicate_object(context: Context, object_to_duplicate: Object, target_count: int) -> List[Object]: """ Duplicate object. This function duplicates the given object and returns a list of duplicated objects. Args: - context (bpy.types.Context): The context in which the duplication is performed. - object_to_duplicate (bpy.types.Object): The object to be duplicated. + context (Context): The context in which the duplication is performed. + object_to_duplicate (Object): The object to be duplicated. target_count (int): The desired count of duplicated objects. Returns: - List[bpy.types.Object]: A list of duplicated objects. + List[Object]: A list of duplicated objects. Raises: AssertionError: If the number of selected objects in the context is not equal to 1 or if the selected object is not the same as the object to be duplicated. @@ -421,27 +435,28 @@ class FnContext: last_selected_objects[i].select_set(True) last_selected_objects = context.selected_objects assert len(result_objects) == target_count + logger.debug(f"Duplicated object {object_to_duplicate.name} to create {target_count} objects") return result_objects @staticmethod - def find_user_layer_collection_by_object(context: bpy.types.Context, target_object: bpy.types.Object) -> Optional[bpy.types.LayerCollection]: + def find_user_layer_collection_by_object(context: Context, target_object: Object) -> Optional[LayerCollection]: """ Finds the layer collection that contains the given target_object in the user's collections. Args: - context (bpy.types.Context): The Blender context. - target_object (bpy.types.Object): The target object to find the layer collection for. + context (Context): The Blender context. + target_object (Object): The target object to find the layer collection for. Returns: - Optional[bpy.types.LayerCollection]: The layer collection that contains the target_object, or None if not found. + Optional[LayerCollection]: The layer collection that contains the target_object, or None if not found. """ - scene_layer_collection: bpy.types.LayerCollection = context.view_layer.layer_collection + scene_layer_collection: LayerCollection = context.view_layer.layer_collection - def find_layer_collection_by_name(layer_collection: bpy.types.LayerCollection, name: str) -> Optional[bpy.types.LayerCollection]: + def find_layer_collection_by_name(layer_collection: LayerCollection, name: str) -> Optional[LayerCollection]: if layer_collection.name == name: return layer_collection - child_layer_collection: bpy.types.LayerCollection + child_layer_collection: LayerCollection for child_layer_collection in layer_collection.children: found = find_layer_collection_by_name(child_layer_collection, name) if found is not None: @@ -449,7 +464,7 @@ class FnContext: return None - user_collection: bpy.types.Collection + user_collection: Collection for user_collection in target_object.users_collection: found = find_layer_collection_by_name(scene_layer_collection, user_collection.name) if found is not None: @@ -459,7 +474,7 @@ class FnContext: @staticmethod @contextlib.contextmanager - def temp_override_active_layer_collection(context: bpy.types.Context, target_object: bpy.types.Object) -> Generator[bpy.types.Context, None, None]: + def temp_override_active_layer_collection(context: Context, target_object: Object) -> Generator[Context, None, None]: """ Context manager to temporarily override the active_layer_collection that contains the target object. @@ -467,11 +482,11 @@ class FnContext: It ensures that the original active_layer_collection is restored after the context is exited. Args: - context (bpy.types.Context): The context in which the active_layer_collection will be overridden. - target_object (bpy.types.Object): The target object whose layer collection will be set as the active_layer_collection. + context (Context): The context in which the active_layer_collection will be overridden. + target_object (Object): The target object whose layer collection will be set as the active_layer_collection. Yields: - bpy.types.Context: The modified context with the active_layer_collection overridden. + Context: The modified context with the active_layer_collection overridden. Example: with FnContext.temp_override_active_layer_collection(context, target_object): @@ -492,24 +507,24 @@ class FnContext: context.view_layer.active_layer_collection = original_layer_collection @staticmethod - def __get_addon_preferences(context: bpy.types.Context) -> Optional[bpy.types.AddonPreferences]: - addon: bpy.types.Addon = context.preferences.addons.get(__package__, None) + def __get_addon_preferences(context: Context) -> Optional[AddonPreferences]: + addon: Addon = context.preferences.addons.get(__package__, None) return addon.preferences if addon else None @staticmethod - def get_addon_preferences_attribute(context: bpy.types.Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE: + def get_addon_preferences_attribute(context: Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE: return getattr(FnContext.__get_addon_preferences(context), attribute_name, default_value) @staticmethod def temp_override_objects( - context: bpy.types.Context, - window: Optional[bpy.types.Window] = None, - area: Optional[bpy.types.Area] = None, - region: Optional[bpy.types.Region] = None, - active_object: Optional[bpy.types.Object] = None, - selected_objects: Optional[List[bpy.types.Object]] = None, - **keywords, - ) -> Generator[bpy.types.Context, None, None]: + context: Context, + window: Optional[Window] = None, + area: Optional[Area] = None, + region: Optional[Region] = None, + active_object: Optional[Object] = None, + selected_objects: Optional[List[Object]] = None, + **keywords: Any, + ) -> Generator[Context, None, None]: if active_object is not None: keywords["active_object"] = active_object keywords["object"] = active_object diff --git a/core/mmd/cycles_converter.py b/core/mmd/cycles_converter.py index 2a8e531..5f10140 100644 --- a/core/mmd/cycles_converter.py +++ b/core/mmd/cycles_converter.py @@ -5,39 +5,44 @@ # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. -from typing import Iterable, Optional +from typing import Iterable, Optional, Any, List, Tuple, Union import bpy +from bpy.types import Material, NodeTree, Node, NodeSocket, ShaderNodeGroup, ShaderNodeOutputMaterial, NodeLink +from ..logging_setup import logger from .core.shader import _NodeGroupUtils from .core.material import FnMaterial -def __switchToCyclesRenderEngine(): +def __switchToCyclesRenderEngine() -> None: if bpy.context.scene.render.engine != "CYCLES": + logger.debug("Switching render engine to Cycles") bpy.context.scene.render.engine = "CYCLES" -def __exposeNodeTreeInput(in_socket, name, default_value, node_input, shader): +def __exposeNodeTreeInput(in_socket: NodeSocket, name: str, default_value: Any, node_input: Node, shader: NodeTree) -> None: _NodeGroupUtils(shader).new_input_socket(name, in_socket, default_value) -def __exposeNodeTreeOutput(out_socket, name, node_output, shader): +def __exposeNodeTreeOutput(out_socket: NodeSocket, name: str, node_output: Node, shader: NodeTree) -> None: _NodeGroupUtils(shader).new_output_socket(name, out_socket) -def __getMaterialOutput(nodes, bl_idname): +def __getMaterialOutput(nodes: bpy.types.Nodes, bl_idname: str) -> Node: o = next((n for n in nodes if n.bl_idname == bl_idname and n.is_active_output), None) or nodes.new(bl_idname) o.is_active_output = True return o -def create_MMDAlphaShader(): +def create_MMDAlphaShader() -> NodeTree: __switchToCyclesRenderEngine() if "MMDAlphaShader" in bpy.data.node_groups: + logger.debug("Using existing MMDAlphaShader node group") return bpy.data.node_groups["MMDAlphaShader"] + logger.info("Creating new MMDAlphaShader node group") shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree") node_input = shader.nodes.new("NodeGroupInput") @@ -59,26 +64,28 @@ def create_MMDAlphaShader(): return shader -def create_MMDBasicShader(): +def create_MMDBasicShader() -> NodeTree: __switchToCyclesRenderEngine() if "MMDBasicShader" in bpy.data.node_groups: + logger.debug("Using existing MMDBasicShader node group") return bpy.data.node_groups["MMDBasicShader"] - shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree") + logger.info("Creating new MMDBasicShader node group") + shader: NodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree") - node_input: bpy.types.NodeGroupInput = shader.nodes.new("NodeGroupInput") - node_output: bpy.types.NodeGroupOutput = shader.nodes.new("NodeGroupOutput") + node_input: Node = shader.nodes.new("NodeGroupInput") + node_output: Node = shader.nodes.new("NodeGroupOutput") node_output.location.x += 250 node_input.location.x -= 500 - dif: bpy.types.ShaderNodeBsdfDiffuse = shader.nodes.new("ShaderNodeBsdfDiffuse") + dif: Node = shader.nodes.new("ShaderNodeBsdfDiffuse") dif.location.x -= 250 dif.location.y += 150 - glo: bpy.types.ShaderNodeBsdfAnisotropic = shader.nodes.new("ShaderNodeBsdfAnisotropic") + glo: Node = shader.nodes.new("ShaderNodeBsdfAnisotropic") glo.location.x -= 250 glo.location.y -= 150 - mix: bpy.types.ShaderNodeMixShader = shader.nodes.new("ShaderNodeMixShader") + mix: Node = shader.nodes.new("ShaderNodeMixShader") shader.links.new(mix.inputs[1], dif.outputs["BSDF"]) shader.links.new(mix.inputs[2], glo.outputs["BSDF"]) @@ -91,7 +98,7 @@ def create_MMDBasicShader(): return shader -def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]: +def __enum_linked_nodes(node: Node) -> Iterable[Node]: yield node if node.parent: yield node.parent @@ -99,7 +106,8 @@ def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]: yield from __enum_linked_nodes(n) -def __cleanNodeTree(material: bpy.types.Material): +def __cleanNodeTree(material: Material) -> None: + logger.debug(f"Cleaning node tree for material: {material.name}") nodes = material.node_tree.nodes node_names = set(n.name for n in nodes) for o in (n for n in nodes if n.bl_idname in {"ShaderNodeOutput", "ShaderNodeOutputMaterial"}): @@ -109,40 +117,46 @@ def __cleanNodeTree(material: bpy.types.Material): nodes.remove(nodes[name]) -def convertToCyclesShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001): +def convertToCyclesShader(obj: bpy.types.Object, use_principled: bool = False, clean_nodes: bool = False, subsurface: float = 0.001) -> None: + logger.info(f"Converting {obj.name} to Cycles shader (use_principled={use_principled}, clean_nodes={clean_nodes})") __switchToCyclesRenderEngine() convertToBlenderShader(obj, use_principled, clean_nodes, subsurface) -def convertToBlenderShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001): +def convertToBlenderShader(obj: bpy.types.Object, use_principled: bool = False, clean_nodes: bool = False, subsurface: float = 0.001) -> None: for i in obj.material_slots: if not i.material: continue if not i.material.use_nodes: + logger.debug(f"Enabling nodes for material: {i.material.name}") i.material.use_nodes = True __convertToMMDBasicShader(i.material) if use_principled: + logger.debug(f"Converting material to Principled BSDF: {i.material.name}") __convertToPrincipledBsdf(i.material, subsurface) if clean_nodes: __cleanNodeTree(i.material) -def convertToMMDShader(obj): +def convertToMMDShader(obj: bpy.types.Object) -> None: """BSDF -> MMDShaderDev conversion.""" + logger.info(f"Converting {obj.name} to MMD shader") for i in obj.material_slots: if not i.material: continue if not i.material.use_nodes: + logger.debug(f"Enabling nodes for material: {i.material.name}") i.material.use_nodes = True FnMaterial.convert_to_mmd_material(i.material) -def __convertToMMDBasicShader(material: bpy.types.Material): +def __convertToMMDBasicShader(material: Material) -> None: + logger.debug(f"Converting material to MMD Basic Shader: {material.name}") # TODO: test me mmd_basic_shader_grp = create_MMDBasicShader() mmd_alpha_shader_grp = create_MMDAlphaShader() - if not any(filter(lambda x: isinstance(x, bpy.types.ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)): + if not any(filter(lambda x: isinstance(x, ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)): # Add nodes for Cycles Render - shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup") + shader: ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup") shader.node_tree = mmd_basic_shader_grp shader.inputs[0].default_value[:3] = material.diffuse_color[:3] shader.inputs[1].default_value[:3] = material.specular_color[:3] @@ -157,7 +171,8 @@ def __convertToMMDBasicShader(material: bpy.types.Material): alpha_value = material.diffuse_color[3] if alpha_value < 1.0: - alpha_shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup") + logger.debug(f"Material has alpha: {material.name}, alpha={alpha_value}") + alpha_shader: ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup") alpha_shader.location.x = shader.location.x + 250 alpha_shader.location.y = shader.location.y - 150 alpha_shader.node_tree = mmd_alpha_shader_grp @@ -165,21 +180,22 @@ def __convertToMMDBasicShader(material: bpy.types.Material): material.node_tree.links.new(alpha_shader.inputs[0], outplug) outplug = alpha_shader.outputs[0] - material_output: bpy.types.ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial") + material_output: ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial") material.node_tree.links.new(material_output.inputs["Surface"], outplug) material_output.location.x = shader.location.x + 500 material_output.location.y = shader.location.y - 150 -def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float): +def __convertToPrincipledBsdf(material: Material, subsurface: float) -> None: + logger.debug(f"Converting material to Principled BSDF: {material.name}") node_names = set() - for s in (n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)): + for s in (n for n in material.node_tree.nodes if isinstance(n, ShaderNodeGroup)): if s.node_tree.name == "MMDBasicShader": - l: bpy.types.NodeLink + l: NodeLink for l in s.outputs[0].links: to_node = l.to_node # assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader - if isinstance(to_node, bpy.types.ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader": + if isinstance(to_node, ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader": __switchToPrincipledBsdf(material.node_tree, s, subsurface, node_alpha=to_node) node_names.add(to_node.name) else: @@ -194,8 +210,9 @@ def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float): nodes.remove(nodes[name]) -def __switchToPrincipledBsdf(node_tree: bpy.types.NodeTree, node_basic: bpy.types.ShaderNodeGroup, subsurface: float, node_alpha: Optional[bpy.types.ShaderNodeGroup] = None): - shader: bpy.types.ShaderNodeBsdfPrincipled = node_tree.nodes.new("ShaderNodeBsdfPrincipled") +def __switchToPrincipledBsdf(node_tree: NodeTree, node_basic: ShaderNodeGroup, subsurface: float, node_alpha: Optional[ShaderNodeGroup] = None) -> None: + logger.debug(f"Switching to Principled BSDF: {node_basic.name}") + shader: Node = node_tree.nodes.new("ShaderNodeBsdfPrincipled") shader.parent = node_basic.parent shader.location.x = node_basic.location.x shader.location.y = node_basic.location.y diff --git a/core/mmd/utils.py b/core/mmd/utils.py index c4006ac..6d6f731 100644 --- a/core/mmd/utils.py +++ b/core/mmd/utils.py @@ -5,18 +5,19 @@ # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. -import logging import os import re -from typing import Callable, Optional, Set +from typing import Callable, Dict, List, Optional, Set, Tuple, Union, Any import bpy +from bpy.types import Object, Bone, PoseBone, Mesh, VertexGroup +from ..logging_setup import logger from .bpyutils import FnContext ## 指定したオブジェクトのみを選択状態かつアクティブにする -def selectAObject(obj): +def selectAObject(obj: Object) -> None: try: bpy.ops.object.mode_set(mode="OBJECT") except Exception: @@ -27,13 +28,13 @@ def selectAObject(obj): ## 現在のモードを指定したオブジェクトのEdit Modeに変更する -def enterEditMode(obj): +def enterEditMode(obj: Object) -> None: selectAObject(obj) if obj.mode != "EDIT": bpy.ops.object.mode_set(mode="EDIT") -def setParentToBone(obj, parent, bone_name): +def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None: selectAObject(obj) FnContext.set_active_object(FnContext.ensure_context(), parent) bpy.ops.object.mode_set(mode="POSE") @@ -42,7 +43,7 @@ def setParentToBone(obj, parent, bone_name): bpy.ops.object.mode_set(mode="OBJECT") -def selectSingleBone(context, armature, bone_name, reset_pose=False): +def selectSingleBone(context: bpy.types.Context, armature: Object, bone_name: str, reset_pose: bool = False) -> None: try: bpy.ops.object.mode_set(mode="OBJECT") except: @@ -55,7 +56,7 @@ def selectSingleBone(context, armature, bone_name, reset_pose=False): for p_bone in armature.pose.bones: p_bone.matrix_basis.identity() armature_bones: bpy.types.ArmatureBones = armature.data.bones - i: bpy.types.Bone + i: Bone for i in armature_bones: i.select = i.name == bone_name i.select_head = i.select_tail = i.select @@ -69,7 +70,7 @@ __CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$") ## 日本語で左右を命名されている名前をblender方式のL(R)に変更する -def convertNameToLR(name, use_underscore=False): +def convertNameToLR(name: str, use_underscore: bool = False) -> str: m = __CONVERT_NAME_TO_L_REGEXP.match(name) delimiter = "_" if use_underscore else "." if m: @@ -84,7 +85,7 @@ __CONVERT_L_TO_NAME_REGEXP = re.compile(r"(?P(?P[._])[lL])(?P(?P[._])[rR])(?P($|(?P=separator)))") -def convertLRToName(name): +def convertLRToName(name: str) -> str: match = __CONVERT_L_TO_NAME_REGEXP.search(name) if match: return f"左{name[0:match.start()]}{match['after']}{name[match.end():]}" @@ -97,7 +98,7 @@ def convertLRToName(name): ## src_vertex_groupのWeightをdest_vertex_groupにaddする -def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name): +def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_group_name: str) -> None: mesh = meshObj.data src_vertex_group = meshObj.vertex_groups[src_vertex_group_name] dest_vertex_group = meshObj.vertex_groups[dest_vertex_group_name] @@ -111,7 +112,7 @@ def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name): pass -def separateByMaterials(meshObj: bpy.types.Object): +def separateByMaterials(meshObj: Object) -> None: if len(meshObj.data.materials) < 2: selectAObject(meshObj) return @@ -134,7 +135,7 @@ def separateByMaterials(meshObj: bpy.types.Object): bpy.data.objects.remove(dummy_parent) -def clearUnusedMeshes(): +def clearUnusedMeshes() -> None: meshes_to_delete = [] for mesh in bpy.data.meshes: if mesh.users == 0: @@ -146,7 +147,7 @@ def clearUnusedMeshes(): ## Boneのカスタムプロパティにname_jが存在する場合、name_jの値を # それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成 -def makePmxBoneMap(armObj): +def makePmxBoneMap(armObj: Object) -> Dict[str, PoseBone]: # Maintain backward compatibility with mmd_tools v0.4.x or older. return {(i.mmd_bone.name_j or i.get("mmd_bone_name_j", i.get("name_j", i.name))): i for i in armObj.pose.bones} @@ -175,7 +176,7 @@ def unique_name(name: str, used_names: Set[str]) -> str: return new_name -def int2base(x, base, width=0): +def int2base(x: int, base: int, width: int = 0) -> str: """ Method to convert an int to a base Source: http://stackoverflow.com/questions/2267362 @@ -198,7 +199,7 @@ def int2base(x, base, width=0): return digits -def saferelpath(path, start, strategy="inside"): +def saferelpath(path: str, start: str, strategy: str = "inside") -> str: """ On Windows relpath will raise a ValueError when trying to calculate the relative path to a @@ -227,13 +228,13 @@ def saferelpath(path, start, strategy="inside"): class ItemOp: @staticmethod - def get_by_index(items, index): + def get_by_index(items: bpy.types.bpy_prop_collection, index: int) -> Optional[Any]: if 0 <= index < len(items): return items[index] return None @staticmethod - def resize(items: bpy.types.bpy_prop_collection, length: int): + def resize(items: bpy.types.bpy_prop_collection, length: int) -> None: count = length - len(items) if count > 0: for i in range(count): @@ -243,7 +244,7 @@ class ItemOp: items.remove(length) @staticmethod - def add_after(items, index): + def add_after(items: bpy.types.bpy_prop_collection, index: int) -> Tuple[Any, int]: index_end = len(items) index = max(0, min(index_end, index + 1)) items.add() @@ -265,7 +266,8 @@ class ItemMoveOp: ) @staticmethod - def move(items, index, move_type, index_min=0, index_max=None): + def move(items: bpy.types.bpy_prop_collection, index: int, move_type: str, + index_min: int = 0, index_max: Optional[int] = None) -> int: if index_max is None: index_max = len(items) - 1 else: @@ -294,7 +296,7 @@ class ItemMoveOp: return index_new -def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None): +def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None) -> Callable: """Decorator to mark a function as deprecated. Args: deprecated_in (Optional[str]): Version in which the function was deprecated. @@ -303,8 +305,8 @@ def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = Non Callable: The decorated function. """ - def _function_wrapper(function: Callable): - def _inner_wrapper(*args, **kwargs): + def _function_wrapper(function: Callable) -> Callable: + def _inner_wrapper(*args: Any, **kwargs: Any) -> Any: warn_deprecation(function.__name__, deprecated_in, details) return function(*args, **kwargs) @@ -320,7 +322,7 @@ def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, de deprecated_in (Optional[str]): Version in which the function was deprecated. details (Optional[str]): Additional details about the deprecation. """ - logging.warning( + logger.warning( "%s is deprecated%s%s", function_name, f" since {deprecated_in}" if deprecated_in else "", @@ -328,7 +330,3 @@ def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, de stack_info=True, stacklevel=4, ) - - # import warnings # pylint: disable=import-outside-toplevel - - # warnings.warn(f"""{function_name}is deprecated{f" since {deprecated_in}" if deprecated_in else ""}{f": {details}" if details else ""}""", category=DeprecationWarning, stacklevel=2) diff --git a/cycles_converter.py b/cycles_converter.py deleted file mode 100644 index f0d391a..0000000 --- a/cycles_converter.py +++ /dev/null @@ -1,240 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2012 MMD Tools authors -# This file is part of MMD Tools. - -from typing import Iterable, Optional - -import bpy - -from .core.shader import _NodeGroupUtils -from .core.material import FnMaterial - - -def __switchToCyclesRenderEngine(): - if bpy.context.scene.render.engine != "CYCLES": - bpy.context.scene.render.engine = "CYCLES" - - -def __exposeNodeTreeInput(in_socket, name, default_value, node_input, shader): - _NodeGroupUtils(shader).new_input_socket(name, in_socket, default_value) - - -def __exposeNodeTreeOutput(out_socket, name, node_output, shader): - _NodeGroupUtils(shader).new_output_socket(name, out_socket) - - -def __getMaterialOutput(nodes, bl_idname): - o = next((n for n in nodes if n.bl_idname == bl_idname and n.is_active_output), None) or nodes.new(bl_idname) - o.is_active_output = True - return o - - -def create_MMDAlphaShader(): - __switchToCyclesRenderEngine() - - if "MMDAlphaShader" in bpy.data.node_groups: - return bpy.data.node_groups["MMDAlphaShader"] - - shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree") - - node_input = shader.nodes.new("NodeGroupInput") - node_output = shader.nodes.new("NodeGroupOutput") - node_output.location.x += 250 - node_input.location.x -= 500 - - trans = shader.nodes.new("ShaderNodeBsdfTransparent") - trans.location.x -= 250 - trans.location.y += 150 - mix = shader.nodes.new("ShaderNodeMixShader") - - shader.links.new(mix.inputs[1], trans.outputs["BSDF"]) - - __exposeNodeTreeInput(mix.inputs[2], "Shader", None, node_input, shader) - __exposeNodeTreeInput(mix.inputs["Fac"], "Alpha", 1.0, node_input, shader) - __exposeNodeTreeOutput(mix.outputs["Shader"], "Shader", node_output, shader) - - return shader - - -def create_MMDBasicShader(): - __switchToCyclesRenderEngine() - - if "MMDBasicShader" in bpy.data.node_groups: - return bpy.data.node_groups["MMDBasicShader"] - - shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree") - - node_input: bpy.types.NodeGroupInput = shader.nodes.new("NodeGroupInput") - node_output: bpy.types.NodeGroupOutput = shader.nodes.new("NodeGroupOutput") - node_output.location.x += 250 - node_input.location.x -= 500 - - dif: bpy.types.ShaderNodeBsdfDiffuse = shader.nodes.new("ShaderNodeBsdfDiffuse") - dif.location.x -= 250 - dif.location.y += 150 - glo: bpy.types.ShaderNodeBsdfAnisotropic = shader.nodes.new("ShaderNodeBsdfAnisotropic") - glo.location.x -= 250 - glo.location.y -= 150 - mix: bpy.types.ShaderNodeMixShader = shader.nodes.new("ShaderNodeMixShader") - shader.links.new(mix.inputs[1], dif.outputs["BSDF"]) - shader.links.new(mix.inputs[2], glo.outputs["BSDF"]) - - __exposeNodeTreeInput(dif.inputs["Color"], "diffuse", [1.0, 1.0, 1.0, 1.0], node_input, shader) - __exposeNodeTreeInput(glo.inputs["Color"], "glossy", [1.0, 1.0, 1.0, 1.0], node_input, shader) - __exposeNodeTreeInput(glo.inputs["Roughness"], "glossy_rough", 0.0, node_input, shader) - __exposeNodeTreeInput(mix.inputs["Fac"], "reflection", 0.02, node_input, shader) - __exposeNodeTreeOutput(mix.outputs["Shader"], "shader", node_output, shader) - - return shader - - -def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]: - yield node - if node.parent: - yield node.parent - for n in set(l.from_node for i in node.inputs for l in i.links): - yield from __enum_linked_nodes(n) - - -def __cleanNodeTree(material: bpy.types.Material): - nodes = material.node_tree.nodes - node_names = set(n.name for n in nodes) - for o in (n for n in nodes if n.bl_idname in {"ShaderNodeOutput", "ShaderNodeOutputMaterial"}): - if any(i.is_linked for i in o.inputs): - node_names -= set(linked.name for linked in __enum_linked_nodes(o)) - for name in node_names: - nodes.remove(nodes[name]) - - -def convertToCyclesShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001): - __switchToCyclesRenderEngine() - convertToBlenderShader(obj, use_principled, clean_nodes, subsurface) - - -def convertToBlenderShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001): - for i in obj.material_slots: - if not i.material: - continue - if not i.material.use_nodes: - i.material.use_nodes = True - __convertToMMDBasicShader(i.material) - if use_principled: - __convertToPrincipledBsdf(i.material, subsurface) - if clean_nodes: - __cleanNodeTree(i.material) - -def convertToMMDShader(obj): - """BSDF -> MMDShaderDev conversion.""" - for i in obj.material_slots: - if not i.material: - continue - if not i.material.use_nodes: - i.material.use_nodes = True - FnMaterial.convert_to_mmd_material(i.material) - -def __convertToMMDBasicShader(material: bpy.types.Material): - # TODO: test me - mmd_basic_shader_grp = create_MMDBasicShader() - mmd_alpha_shader_grp = create_MMDAlphaShader() - - if not any(filter(lambda x: isinstance(x, bpy.types.ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)): - # Add nodes for Cycles Render - shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup") - shader.node_tree = mmd_basic_shader_grp - shader.inputs[0].default_value[:3] = material.diffuse_color[:3] - shader.inputs[1].default_value[:3] = material.specular_color[:3] - shader.inputs["glossy_rough"].default_value = 1.0 / getattr(material, "specular_hardness", 50) - outplug = shader.outputs[0] - - location = shader.location.copy() - location.x -= 1000 - - alpha_value = 1.0 - if len(material.diffuse_color) > 3: - alpha_value = material.diffuse_color[3] - - if alpha_value < 1.0: - alpha_shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup") - alpha_shader.location.x = shader.location.x + 250 - alpha_shader.location.y = shader.location.y - 150 - alpha_shader.node_tree = mmd_alpha_shader_grp - alpha_shader.inputs[1].default_value = alpha_value - material.node_tree.links.new(alpha_shader.inputs[0], outplug) - outplug = alpha_shader.outputs[0] - - material_output: bpy.types.ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial") - material.node_tree.links.new(material_output.inputs["Surface"], outplug) - material_output.location.x = shader.location.x + 500 - material_output.location.y = shader.location.y - 150 - - -def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float): - node_names = set() - for s in (n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)): - if s.node_tree.name == "MMDBasicShader": - l: bpy.types.NodeLink - for l in s.outputs[0].links: - to_node = l.to_node - # assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader - if isinstance(to_node, bpy.types.ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader": - __switchToPrincipledBsdf(material.node_tree, s, subsurface, node_alpha=to_node) - node_names.add(to_node.name) - else: - __switchToPrincipledBsdf(material.node_tree, s, subsurface) - node_names.add(s.name) - elif s.node_tree.name == "MMDShaderDev": - __switchToPrincipledBsdf(material.node_tree, s, subsurface) - node_names.add(s.name) - # remove MMD shader nodes - nodes = material.node_tree.nodes - for name in node_names: - nodes.remove(nodes[name]) - - -def __switchToPrincipledBsdf(node_tree: bpy.types.NodeTree, node_basic: bpy.types.ShaderNodeGroup, subsurface: float, node_alpha: Optional[bpy.types.ShaderNodeGroup] = None): - shader: bpy.types.ShaderNodeBsdfPrincipled = node_tree.nodes.new("ShaderNodeBsdfPrincipled") - shader.parent = node_basic.parent - shader.location.x = node_basic.location.x - shader.location.y = node_basic.location.y - - alpha_socket_name = "Alpha" - if node_basic.node_tree.name == "MMDShaderDev": - node_alpha, alpha_socket_name = node_basic, "Base Alpha" - if "Base Tex" in node_basic.inputs and node_basic.inputs["Base Tex"].is_linked: - node_tree.links.new(node_basic.inputs["Base Tex"].links[0].from_socket, shader.inputs["Base Color"]) - elif "Diffuse Color" in node_basic.inputs: - shader.inputs["Base Color"].default_value[:3] = node_basic.inputs["Diffuse Color"].default_value[:3] - elif "diffuse" in node_basic.inputs: - shader.inputs["Base Color"].default_value[:3] = node_basic.inputs["diffuse"].default_value[:3] - if node_basic.inputs["diffuse"].is_linked: - node_tree.links.new(node_basic.inputs["diffuse"].links[0].from_socket, shader.inputs["Base Color"]) - - shader.inputs["IOR"].default_value = 1.0 - shader.inputs["Subsurface Weight"].default_value = subsurface - - output_links = node_basic.outputs[0].links - if node_alpha: - output_links = node_alpha.outputs[0].links - shader.parent = node_alpha.parent or shader.parent - shader.location.x = node_alpha.location.x - - if alpha_socket_name in node_alpha.inputs: - if "Alpha" in shader.inputs: - shader.inputs["Alpha"].default_value = node_alpha.inputs[alpha_socket_name].default_value - if node_alpha.inputs[alpha_socket_name].is_linked: - node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, shader.inputs["Alpha"]) - else: - shader.inputs["Transmission"].default_value = 1 - node_alpha.inputs[alpha_socket_name].default_value - if node_alpha.inputs[alpha_socket_name].is_linked: - node_invert = node_tree.nodes.new("ShaderNodeMath") - node_invert.parent = shader.parent - node_invert.location.x = node_alpha.location.x - 250 - node_invert.location.y = node_alpha.location.y - 300 - node_invert.operation = "SUBTRACT" - node_invert.use_clamp = True - node_invert.inputs[0].default_value = 1 - node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, node_invert.inputs[1]) - node_tree.links.new(node_invert.outputs[0], shader.inputs["Transmission"]) - - for l in output_links: - node_tree.links.new(shader.outputs[0], l.to_socket)