# SPDX-License-Identifier: MIT OR GPL-3.0-or-later import re import sys from collections.abc import Mapping, Sequence from dataclasses import dataclass from typing import TYPE_CHECKING, Callable, Final, Optional, Protocol, Union import bpy from bpy.props import ( BoolProperty, EnumProperty, FloatProperty, FloatVectorProperty, IntProperty, IntVectorProperty, PointerProperty, ) from bpy.types import ( Context, Driver, FCurve, Image, Material, Node, NodeReroute, NodeSocketColor, NodeSocketFloat, PropertyGroup, ShaderNodeBsdfPrincipled, ShaderNodeEmission, ShaderNodeGroup, ShaderNodeMath, ShaderNodeNormalMap, ShaderNodeOutputMaterial, ShaderNodeTexImage, ) from bpy_extras.node_shader_utils import PrincipledBSDFWrapper from mathutils import Vector from ...common import convert, ops, shader from ...common.gl import ( GL_CLAMP_TO_EDGE, GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST, GL_MIRRORED_REPEAT, GL_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_REPEAT, ) from ...common.logger import get_logger from ...common.preferences import VrmAddonPreferences from ...common.version import get_addon_version from ..property_group import property_group_enum logger = get_logger(__name__) MTOON1_OUTPUT_NODE_GROUP_NAME: Final = "Mtoon1Material.Mtoon1Output" PRINCIPLED_BSDF_BASE_COLOR_INPUT_KEY: Final = "Base Color" PRINCIPLED_BSDF_ALPHA_INPUT_KEY: Final = "Alpha" PRINCIPLED_BSDF_EMISSION_INPUT_KEY: Final = ( "Emission Color" if bpy.app.version >= (4,) else "Emission" ) PRINCIPLED_BSDF_EMISSION_STRENGTH_INPUT_KEY: Final = "Emission Strength" PRINCIPLED_BSDF_NORMAL_INPUT_KEY: Final = "Normal" NORMAL_MAP_COLOR_INPUT_KEY: Final = "Color" EMISSION_COLOR_INPUT_KEY: Final = "Color" EMISSION_STRENGTH_INPUT_KEY: Final = "Strength" TEX_IMAGE_COLOR_OUTPUT_KEY: Final = "Color" TEX_IMAGE_ALPHA_OUTPUT_KEY: Final = "Alpha" IMAGE_INTERPOLATION_CLOSEST: Final = "Closest" IMAGE_INTERPOLATION_LINEAR: Final = "Linear" IMAGE_INTERPOLATION_CUBIC: Final = "Cubic" IMAGE_INTERPOLATION_SMART: Final = "Smart" GL_LINEAR_IMAGE_INTERPOLATIONS: Final = ( IMAGE_INTERPOLATION_LINEAR, IMAGE_INTERPOLATION_CUBIC, IMAGE_INTERPOLATION_SMART, ) ALPHA_CLIP_INPUT_NODE_NAME: Final = "Mtoon1Material.AlphaClip.Input" ALPHA_CLIP_INPUT_NODE_SOCKET_NAME: Final = "Value" ALPHA_CLIP_OUTPUT_NODE_NAME: Final = "Mtoon1Material.AlphaClip.Output" ALPHA_CLIP_OUTPUT_NODE_SOCKET_NAME: Final = "Value" def get_gltf_emissive_node(material: Material) -> Optional[ShaderNodeEmission]: node_tree = material.node_tree if not node_tree: return None return next( ( node for node in node_tree.nodes if isinstance(node, ShaderNodeEmission) and node.name == "Mtoon1Material.GltfEmissive" ), None, ) class NodeSocketTarget(Protocol): def get_in_socket_name(self) -> str: ... def create_node_selector(self, material: Material) -> Callable[[Node], bool]: ... class PrincipledBsdfNodeSocketTarget(NodeSocketTarget): def __init__(self, *, in_socket_name: str) -> None: self.in_socket_name = in_socket_name def get_in_socket_name(self) -> str: return self.in_socket_name @staticmethod def get_node_name(material: Material) -> Optional[str]: # node has a short lifetime on the native side and is dangerous, so avoid # exposing it outside the function node = PrincipledBSDFWrapper(material).node_principled_bsdf if node is None: return None # Use intern to avoid using str directly referenced from node that will be # destroyed in the future # Previously, using str referenced from destroyed Bone caused problems, # so this is a precautionary measure, though it might be overly cautious return sys.intern(node.name) def create_node_selector(self, material: Material) -> Callable[[Node], bool]: name = self.get_node_name(material) if name is None: return lambda _: False return ( lambda node: isinstance(node, ShaderNodeBsdfPrincipled) and node.name == name ) class StaticNodeSocketTarget(NodeSocketTarget): def __init__( self, *, in_node_name: str, in_node_type: type[Node], in_socket_name: str ) -> None: self.in_node_name = in_node_name self.in_node_type = in_node_type self.in_socket_name = in_socket_name def get_in_socket_name(self) -> str: return self.in_socket_name def create_node_selector(self, material: Material) -> Callable[[Node], bool]: _ = material return ( lambda node: isinstance(node, self.in_node_type) and node.name == self.in_node_name ) class PrincipledBsdfNormalMapNodeSocketTarget(NodeSocketTarget): def get_in_socket_name(self) -> str: return NORMAL_MAP_COLOR_INPUT_KEY @staticmethod def get_node_name(material: Material) -> Optional[str]: # node has a short lifetime on the native side and is dangerous, so avoid # exposing it outside the function node = PrincipledBSDFWrapper(material).node_normalmap if node is None: return None # Use intern to avoid using str directly referenced from node that will be # destroyed in the future # Previously, using str referenced from destroyed Bone caused problems, # so this is a precautionary measure, though it might be overly cautious return sys.intern(node.name) def create_node_selector(self, material: Material) -> Callable[[Node], bool]: name = self.get_node_name(material) if name is None: return lambda _: False return lambda node: isinstance(node, ShaderNodeNormalMap) and node.name == name class GltfEmissionNodeSocketTarget(NodeSocketTarget): def get_in_socket_name(self) -> str: return EMISSION_COLOR_INPUT_KEY def create_node_selector(self, material: Material) -> Callable[[Node], bool]: _ = material # https://github.com/KhronosGroup/glTF-Blender-IO/pull/740 return lambda node: isinstance(node, ShaderNodeEmission) class NodeGroupSocketTarget(NodeSocketTarget): def __init__(self, *, node_group_node_tree_name: str, in_socket_name: str) -> None: self.node_group_node_tree_name = node_group_node_tree_name self.in_socket_name = in_socket_name def get_in_socket_name(self) -> str: return self.in_socket_name def select_node(self, node: Node) -> bool: if not isinstance(node, ShaderNodeGroup): return False node_tree = node.node_tree if not node_tree: return False return node_tree.name == self.node_group_node_tree_name def create_node_selector(self, material: Material) -> Callable[[Node], bool]: _ = material return self.select_node class MaterialTraceablePropertyGroup(PropertyGroup): def find_material(self) -> Material: id_data = self.id_data if isinstance(id_data, Material): return id_data message = ( f"{type(self)}/{self}.id_data is not a {Material}" + f" but {type(id_data)}/{id_data}" ) raise AssertionError(message) def find_outline_property_group( self, material: Material ) -> Optional["MaterialTraceablePropertyGroup"]: if get_material_mtoon1_extension(material).is_outline_material: return None outline_material = get_material_mtoon1_extension(material).outline_material if not outline_material: return None if material.name == outline_material.name: logger.error( "Base material and outline material are same. name={material.name}" ) return None path_from_id = self.path_from_id() property_group = outline_material.path_resolve(path_from_id, False) if isinstance(property_group, MaterialTraceablePropertyGroup): return property_group message = f"No matching property group: {type(self)}/{self} {path_from_id}" raise AssertionError(message) def get_bool( self, node_group_name: str, group_label: str, *, default_value: bool, ) -> bool: value = self.get_value( node_group_name, group_label, default_value=int(default_value) ) if isinstance(value, float): return abs(value) > 0.000001 return bool(value) def get_float( self, node_group_name: str, group_label: str, *, default_value: float, ) -> float: value = self.get_value( node_group_name, group_label, default_value=default_value ) return float(value) def get_int( self, node_group_name: str, group_label: str, *, default_value: int, ) -> int: value = self.get_value( node_group_name, group_label, default_value=default_value ) if isinstance(value, bool): return int(value) if isinstance(value, int): return value return round(value) def get_value( self, node_group_name: str, group_label: str, *, default_value: float, ) -> Union[float, int, bool]: material = self.find_material() node_tree = material.node_tree if not node_tree: return default_value node = next( ( node for node in node_tree.nodes if isinstance(node, ShaderNodeGroup) and node.node_tree and node.node_tree.name == node_group_name ), None, ) if not node: return default_value socket = node.inputs.get(group_label) if isinstance( socket, ( *shader.BOOL_SOCKET_CLASSES, *shader.FLOAT_SOCKET_CLASSES, *shader.INT_SOCKET_CLASSES, ), ): return socket.default_value return default_value def get_rgb( self, node_group_name: str, group_label: str, *, default_value: tuple[float, float, float], ) -> tuple[float, float, float]: material = self.find_material() node_tree = material.node_tree if not node_tree: return default_value node = next( ( node for node in node_tree.nodes if isinstance(node, ShaderNodeGroup) and node.node_tree and node.node_tree.name == node_group_name ), None, ) if not node: return default_value socket = node.inputs.get(group_label) if isinstance(socket, shader.COLOR_SOCKET_CLASSES): return ( socket.default_value[0], socket.default_value[1], socket.default_value[2], ) return default_value def set_value( self, node_group_name: str, group_label: str, value_obj: object, ) -> None: if isinstance(value_obj, float): value = convert.float_or_none(value_obj) if value is None: return elif isinstance(value_obj, int): value = value_obj else: return material = self.find_material() outline = self.find_outline_property_group(material) if outline: outline.set_value(node_group_name, group_label, value) node_tree = material.node_tree if not node_tree: return node = next( ( node for node in node_tree.nodes if isinstance(node, ShaderNodeGroup) and node.node_tree and node.node_tree.name == node_group_name ), None, ) if not node: logger.warning('No group node "%s"', node_group_name) return socket = node.inputs.get(group_label) if isinstance(socket, shader.BOOL_SOCKET_CLASSES): socket.default_value = bool(value) elif isinstance(socket, shader.FLOAT_SOCKET_CLASSES): socket.default_value = float(value) elif isinstance(socket, shader.INT_SOCKET_CLASSES): socket.default_value = int(value) else: logger.warning( 'No "%s" in shader node group "%s"', group_label, node_group_name ) def set_bool( self, node_group_name: str, group_label: str, value: object, ) -> None: self.set_value(node_group_name, group_label, 1 if value else 0) def set_int( self, node_group_name: str, group_label: str, value: object, ) -> None: self.set_value(node_group_name, group_label, value) def set_rgba( self, node_group_name: str, group_label: str, value: object, default_value: Optional[tuple[float, float, float, float]] = None, ) -> None: material = self.find_material() outline = self.find_outline_property_group(material) if outline: outline.set_rgba(node_group_name, group_label, value, default_value) if not default_value: default_value = (0.0, 0.0, 0.0, 0.0) node_tree = material.node_tree if not node_tree: return rgba = shader.rgba_or_none(value) or default_value node = next( ( node for node in node_tree.nodes if isinstance(node, ShaderNodeGroup) and node.node_tree and node.node_tree.name == node_group_name ), None, ) if not node: logger.warning('No group node "%s"', node_group_name) return socket = node.inputs.get(group_label) if not isinstance(socket, shader.COLOR_SOCKET_CLASSES): logger.warning( 'No "%s" in shader node group "%s"', group_label, node_group_name ) return socket.default_value = rgba def set_rgb( self, node_group_name: str, group_label: str, value: object, default_value: Optional[tuple[float, float, float]] = None, ) -> None: material = self.find_material() outline = self.find_outline_property_group(material) if outline: outline.set_rgb(node_group_name, group_label, value, default_value) if not default_value: default_value = (0.0, 0.0, 0.0) node_tree = material.node_tree if not node_tree: return rgb = shader.rgb_or_none(value) or default_value node = next( ( node for node in node_tree.nodes if isinstance(node, ShaderNodeGroup) and node.node_tree and node.node_tree.name == node_group_name ), None, ) if not node: logger.warning('No group node "%s"', node_group_name) return socket = node.inputs.get(group_label) if not isinstance(socket, shader.COLOR_SOCKET_CLASSES): logger.warning( 'No "%s" in shader node group "%s"', group_label, node_group_name ) return socket.default_value = (*rgb, 1.0) class TextureTraceablePropertyGroup(MaterialTraceablePropertyGroup): def get_texture_info_property_group(self) -> "Mtoon1TextureInfoPropertyGroup": chain = self.path_from_id().split(".") if chain[-1:] == ["sampler"]: chain = chain[:-1] if chain[-1:] == ["index"]: chain = chain[:-1] if chain[-1:] == ["khr_texture_transform"]: chain = chain[:-1] if chain[-1:] == ["extensions"]: chain = chain[:-1] material = self.find_material() property_group = material.path_resolve(".".join(chain), False) if not isinstance(property_group, Mtoon1TextureInfoPropertyGroup): message = f"{property_group} is not a Mtoon1TextureInfoPropertyGroup" raise TypeError(message) return property_group def get_texture_node_name(self, extra: str) -> str: texture_info = self.get_texture_info_property_group() name = type(texture_info.index).__name__ return re.sub("PropertyGroup$", "", name) + "." + extra def get_image_texture_node_name(self) -> str: return self.get_texture_node_name("Image") def get_image_texture_uv_node_name(self) -> str: return self.get_texture_node_name("Uv") @classmethod def link_nodes( cls, material: Material, out_node_name: str, out_node_type: type[Node], out_node_socket_name: str, node_socket_target: NodeSocketTarget, ) -> None: node_tree = material.node_tree if not node_tree: return select_in_node = node_socket_target.create_node_selector(material) in_socket_name = node_socket_target.get_in_socket_name() # Check if already connected connection_check_node = next( ( link.from_node for link in node_tree.links if select_in_node(link.to_node) and link.to_socket and link.to_socket.name == in_socket_name ), None, ) while connection_check_node: if ( isinstance(connection_check_node, out_node_type) and connection_check_node.name == out_node_name ): return if isinstance(connection_check_node, NodeReroute): connection_check_inputs = connection_check_node.inputs if len(connection_check_inputs) == 0: break connection_check_links = connection_check_inputs[0].links if len(connection_check_links) == 0: break connection_check_link = connection_check_links[0] connection_check_node = connection_check_link.from_node continue break # If connected to unrelated node, disconnect the link cls.unlink_nodes(material, node_socket_target) # Search for output node and socket out_node = node_tree.nodes.get(out_node_name) if not isinstance(out_node, out_node_type): logger.error("No output node: %s", out_node_name) return out_socket = out_node.outputs.get(out_node_socket_name) if not out_socket: logger.error("No output node socket: %s", out_node_socket_name) return traversing_sockets = [out_socket] while traversing_sockets: traversing_socket = traversing_sockets.pop() for connection_check_link in traversing_socket.links: connection_check_node = connection_check_link.to_node if not isinstance(connection_check_node, NodeReroute): continue if connection_check_node.label != in_socket_name: traversing_sockets.extend(connection_check_node.outputs) continue traversing_outputs = connection_check_node.outputs if len(traversing_outputs) == 0: continue out_socket = traversing_outputs[0] out_node = connection_check_node break # Search for input node and socket in_node = next( (n for n in node_tree.nodes if select_in_node(n)), None, ) if not in_node: logger.error("No input node") return in_socket = in_node.inputs.get(in_socket_name) if not in_socket: logger.error("No input socket: %s", in_socket_name) return node_tree.links.new(in_socket, out_socket) @classmethod def unlink_nodes( cls, material: Material, node_socket_target: NodeSocketTarget, ) -> None: while True: # Refresh in_node/out_node. These nodes may be invalidated. node_tree = material.node_tree if not node_tree: return select_in_node = node_socket_target.create_node_selector(material) in_socket_name = node_socket_target.get_in_socket_name() disconnecting_link = next( ( link for link in node_tree.links if select_in_node(link.to_node) and link.to_socket and link.to_socket.name == in_socket_name ), None, ) if not disconnecting_link: return node_tree.links.remove(disconnecting_link) @classmethod def link_or_unlink_nodes( cls, material: Material, out_node_name: str, out_node_type: type[Node], out_node_socket_name: str, node_socket_target: NodeSocketTarget, *, link: bool, ) -> None: if link: cls.link_nodes( material, out_node_name, out_node_type, out_node_socket_name, node_socket_target, ) else: cls.unlink_nodes( material, node_socket_target, ) def update_image(self, image: Optional[Image]) -> None: material = self.find_material() outline = self.find_outline_property_group(material) if outline and isinstance(outline, TextureTraceablePropertyGroup): outline.update_image(image) node_tree = material.node_tree if not node_tree: return node_name = self.get_image_texture_node_name() node = node_tree.nodes.get(node_name) if not isinstance(node, ShaderNodeTexImage): logger.warning('No shader node tex image "%s"', node_name) return node.image = image texture_info = self.get_texture_info_property_group() for ( output_socket_name, node_socket_targets, ) in texture_info.node_socket_targets.items(): for node_socket_target in node_socket_targets: self.link_or_unlink_nodes( material, node_name, ShaderNodeTexImage, output_socket_name, node_socket_target, link=bool(image), ) texture_info.setup_drivers(material) def get_connected_node_image(self) -> Optional[Image]: material = self.find_material() if bpy.app.version < (5, 0, 0) and not material.use_nodes: return None node_tree = material.node_tree if not node_tree: return None texture_info = self.get_texture_info_property_group() node_socket_targets = next( iter(texture_info.node_socket_targets.values()), None ) if not node_socket_targets: return None node_socket_target = next(iter(node_socket_targets), None) if not node_socket_target: return None node_selector = node_socket_target.create_node_selector(material) node = next((node for node in node_tree.nodes if node_selector(node)), None) if not node: return None traversing_input_socket = None for input_socket in node.inputs: if input_socket.name == node_socket_target.get_in_socket_name(): traversing_input_socket = input_socket break while True: if not traversing_input_socket: return None links_to_input_socket = traversing_input_socket.links traversing_input_socket = None for link_to_input_socket in links_to_input_socket: from_node = link_to_input_socket.from_node if isinstance(from_node, ShaderNodeTexImage): return from_node.image if isinstance(from_node, NodeReroute): traversing_input_socket = from_node.inputs[0] break def get_texture_uv_int(self, name: str, default_value: int) -> int: node_name = self.get_image_texture_uv_node_name() material = self.find_material() if not material.node_tree: return default_value node_tree = material.node_tree node = node_tree.nodes.get(node_name) if not isinstance(node, ShaderNodeGroup): return default_value socket = node.inputs.get(name) if not socket: logger.warning('No "%s" in shader node group "%s"', name, node_name) return default_value if isinstance(socket, shader.FLOAT_SOCKET_CLASSES): return round(socket.default_value) if isinstance(socket, shader.INT_SOCKET_CLASSES): return socket.default_value return default_value def get_texture_uv_float(self, name: str, default_value: float) -> float: node_name = self.get_image_texture_uv_node_name() material = self.find_material() if not material.node_tree: return default_value node_tree = material.node_tree node = node_tree.nodes.get(node_name) if not isinstance(node, ShaderNodeGroup): return default_value socket = node.inputs.get(name) if not socket: logger.warning('No "%s" in shader node group "%s"', name, node_name) return default_value if isinstance(socket, shader.FLOAT_SOCKET_CLASSES): return socket.default_value if isinstance(socket, shader.INT_SOCKET_CLASSES): return float(socket.default_value) return default_value def set_texture_uv(self, name: str, value: object) -> None: node_name = self.get_image_texture_uv_node_name() material = self.find_material() if not material.node_tree: return node_tree = material.node_tree node = node_tree.nodes.get(node_name) if not isinstance(node, ShaderNodeGroup): return socket = node.inputs.get(name) if not socket: logger.warning('No "%s" in shader node group "%s"', name, node_name) return if isinstance(value, (float, int)): if isinstance(socket, shader.FLOAT_SOCKET_CLASSES): socket.default_value = float(value) if isinstance(socket, shader.INT_SOCKET_CLASSES): socket.default_value = int(value) outline = self.find_outline_property_group(material) if not outline: return if not isinstance(outline, TextureTraceablePropertyGroup): return outline.set_texture_uv(name, value) class Mtoon1KhrTextureTransformPropertyGroup(TextureTraceablePropertyGroup): def get_texture_offset(self) -> tuple[float, float]: x = self.get_texture_uv_float( shader.UV_GROUP_UV_OFFSET_X_LABEL, default_value=shader.UV_GROUP_UV_OFFSET_X_DEFAULT, ) y = self.get_texture_uv_float( shader.UV_GROUP_UV_OFFSET_Y_LABEL, default_value=shader.UV_GROUP_UV_OFFSET_Y_DEFAULT, ) return x, y def set_texture_offset(self, value: object) -> None: offset = convert.float2_or_none(value) if offset is None: return self.set_texture_uv(shader.UV_GROUP_UV_OFFSET_X_LABEL, offset[0]) self.set_texture_uv(shader.UV_GROUP_UV_OFFSET_Y_LABEL, offset[1]) node_name = self.get_image_texture_node_name() material = self.find_material() if not material.node_tree: return node = material.node_tree.nodes.get(node_name) if not isinstance(node, ShaderNodeTexImage): logger.warning('No shader node tex image "%s"', node_name) return node.texture_mapping.translation = Vector((0, 0, 0)) outline = self.find_outline_property_group(material) if not outline: return if not isinstance(outline, Mtoon1KhrTextureTransformPropertyGroup): return outline.set_texture_offset(offset) def get_texture_scale(self) -> tuple[float, float]: x = self.get_texture_uv_float( shader.UV_GROUP_UV_SCALE_X_LABEL, default_value=shader.UV_GROUP_UV_SCALE_X_DEFAULT, ) y = self.get_texture_uv_float( shader.UV_GROUP_UV_SCALE_Y_LABEL, default_value=shader.UV_GROUP_UV_SCALE_Y_DEFAULT, ) return x, y def set_texture_scale(self, value: object) -> None: scale = convert.float2_or_none(value) if scale is None: return self.set_texture_uv(shader.UV_GROUP_UV_SCALE_X_LABEL, scale[0]) self.set_texture_uv(shader.UV_GROUP_UV_SCALE_Y_LABEL, scale[1]) node_name = self.get_image_texture_node_name() material = self.find_material() if not material.node_tree: return node = material.node_tree.nodes.get(node_name) if not isinstance(node, ShaderNodeTexImage): logger.warning('No shader node tex image "%s"', node_name) return node.texture_mapping.scale = Vector((1, 1, 1)) outline = self.find_outline_property_group(material) if not outline: return if not isinstance(outline, Mtoon1KhrTextureTransformPropertyGroup): return outline.set_texture_scale(scale) offset: FloatVectorProperty( # type: ignore[valid-type] name="Offset", size=2, default=( shader.UV_GROUP_UV_OFFSET_X_DEFAULT, shader.UV_GROUP_UV_OFFSET_Y_DEFAULT, ), get=get_texture_offset, set=set_texture_offset, ) scale: FloatVectorProperty( # type: ignore[valid-type] name="Scale", size=2, default=( shader.UV_GROUP_UV_SCALE_X_DEFAULT, shader.UV_GROUP_UV_SCALE_Y_DEFAULT, ), get=get_texture_scale, set=set_texture_scale, ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. offset: Sequence[float] # type: ignore[no-redef] scale: Sequence[float] # type: ignore[no-redef] class Mtoon1BaseColorKhrTextureTransformPropertyGroup( Mtoon1KhrTextureTransformPropertyGroup ): pass class Mtoon1ShadeMultiplyKhrTextureTransformPropertyGroup( Mtoon1KhrTextureTransformPropertyGroup ): pass class Mtoon1NormalKhrTextureTransformPropertyGroup( Mtoon1KhrTextureTransformPropertyGroup ): pass class Mtoon1ShadingShiftKhrTextureTransformPropertyGroup( Mtoon1KhrTextureTransformPropertyGroup ): pass class Mtoon1EmissiveKhrTextureTransformPropertyGroup( Mtoon1KhrTextureTransformPropertyGroup ): pass class Mtoon1RimMultiplyKhrTextureTransformPropertyGroup( Mtoon1KhrTextureTransformPropertyGroup ): pass class Mtoon1MatcapKhrTextureTransformPropertyGroup( Mtoon1KhrTextureTransformPropertyGroup ): pass class Mtoon1OutlineWidthMultiplyKhrTextureTransformPropertyGroup( Mtoon1KhrTextureTransformPropertyGroup ): def set_texture_offset_and_outline(self, value: object) -> None: offset = convert.float2_or_none(value) if offset is None: return material = self.find_material() if get_material_mtoon1_extension(material).is_outline_material: return self.set_texture_offset(offset) ops.vrm.refresh_mtoon1_outline(material_name=material.name) def set_texture_scale_and_outline(self, value: object) -> None: scale = convert.float2_or_none(value) if scale is None: return material = self.find_material() if get_material_mtoon1_extension(material).is_outline_material: return self.set_texture_scale(scale) ops.vrm.refresh_mtoon1_outline(material_name=material.name) offset: FloatVectorProperty( # type: ignore[valid-type] name="Offset", size=2, default=( shader.UV_GROUP_UV_OFFSET_X_DEFAULT, shader.UV_GROUP_UV_OFFSET_Y_DEFAULT, ), get=Mtoon1KhrTextureTransformPropertyGroup.get_texture_offset, set=set_texture_offset_and_outline, ) scale: FloatVectorProperty( # type: ignore[valid-type] name="Scale", size=2, default=( shader.UV_GROUP_UV_SCALE_X_DEFAULT, shader.UV_GROUP_UV_SCALE_Y_DEFAULT, ), get=Mtoon1KhrTextureTransformPropertyGroup.get_texture_scale, set=set_texture_scale_and_outline, ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. offset: Sequence[float] # type: ignore[no-redef] scale: Sequence[float] # type: ignore[no-redef] class Mtoon1UvAnimationMaskKhrTextureTransformPropertyGroup( Mtoon1KhrTextureTransformPropertyGroup ): pass class Mtoon1TextureInfoExtensionsPropertyGroup(PropertyGroup): khr_texture_transform: PointerProperty( # type: ignore[valid-type] type=Mtoon1KhrTextureTransformPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. khr_texture_transform: ( # type: ignore[no-redef] Mtoon1KhrTextureTransformPropertyGroup ) class Mtoon1BaseColorTextureInfoExtensionsPropertyGroup( Mtoon1TextureInfoExtensionsPropertyGroup ): khr_texture_transform: PointerProperty( # type: ignore[valid-type] type=Mtoon1BaseColorKhrTextureTransformPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. khr_texture_transform: ( # type: ignore[no-redef] Mtoon1BaseColorKhrTextureTransformPropertyGroup ) class Mtoon1ShadeMultiplyTextureInfoExtensionsPropertyGroup( Mtoon1TextureInfoExtensionsPropertyGroup ): khr_texture_transform: PointerProperty( # type: ignore[valid-type] type=Mtoon1ShadeMultiplyKhrTextureTransformPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. khr_texture_transform: ( # type: ignore[no-redef] Mtoon1ShadeMultiplyKhrTextureTransformPropertyGroup ) class Mtoon1NormalTextureInfoExtensionsPropertyGroup( Mtoon1TextureInfoExtensionsPropertyGroup ): khr_texture_transform: PointerProperty( # type: ignore[valid-type] type=Mtoon1NormalKhrTextureTransformPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. khr_texture_transform: ( # type: ignore[no-redef] Mtoon1NormalKhrTextureTransformPropertyGroup ) class Mtoon1ShadingShiftTextureInfoExtensionsPropertyGroup( Mtoon1TextureInfoExtensionsPropertyGroup ): khr_texture_transform: PointerProperty( # type: ignore[valid-type] type=Mtoon1ShadingShiftKhrTextureTransformPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. khr_texture_transform: ( # type: ignore[no-redef] Mtoon1ShadingShiftKhrTextureTransformPropertyGroup ) class Mtoon1EmissiveTextureInfoExtensionsPropertyGroup( Mtoon1TextureInfoExtensionsPropertyGroup ): khr_texture_transform: PointerProperty( # type: ignore[valid-type] type=Mtoon1EmissiveKhrTextureTransformPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. khr_texture_transform: ( # type: ignore[no-redef] Mtoon1EmissiveKhrTextureTransformPropertyGroup ) class Mtoon1RimMultiplyTextureInfoExtensionsPropertyGroup( Mtoon1TextureInfoExtensionsPropertyGroup ): khr_texture_transform: PointerProperty( # type: ignore[valid-type] type=Mtoon1RimMultiplyKhrTextureTransformPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. khr_texture_transform: ( # type: ignore[no-redef] Mtoon1RimMultiplyKhrTextureTransformPropertyGroup ) class Mtoon1MatcapTextureInfoExtensionsPropertyGroup( Mtoon1TextureInfoExtensionsPropertyGroup ): khr_texture_transform: PointerProperty( # type: ignore[valid-type] type=Mtoon1MatcapKhrTextureTransformPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. khr_texture_transform: ( # type: ignore[no-redef] Mtoon1MatcapKhrTextureTransformPropertyGroup ) class Mtoon1OutlineWidthMultiplyTextureInfoExtensionsPropertyGroup( Mtoon1TextureInfoExtensionsPropertyGroup ): khr_texture_transform: PointerProperty( # type: ignore[valid-type] type=Mtoon1OutlineWidthMultiplyKhrTextureTransformPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. khr_texture_transform: ( # type: ignore[no-redef] Mtoon1OutlineWidthMultiplyKhrTextureTransformPropertyGroup ) class Mtoon1UvAnimationMaskTextureInfoExtensionsPropertyGroup( Mtoon1TextureInfoExtensionsPropertyGroup ): khr_texture_transform: PointerProperty( # type: ignore[valid-type] type=Mtoon1UvAnimationMaskKhrTextureTransformPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. khr_texture_transform: ( # type: ignore[no-redef] Mtoon1UvAnimationMaskKhrTextureTransformPropertyGroup ) class Mtoon1SamplerPropertyGroup(TextureTraceablePropertyGroup): ( mag_filter_enum, ( MAG_FILTER_NEAREST, MAG_FILTER_LINEAR, ), ) = property_group_enum( ("NEAREST", "Nearest", "", "NONE", GL_NEAREST), ("LINEAR", "Linear", "", "NONE", GL_LINEAR), ) MAG_FILTER_DEFAULT = MAG_FILTER_LINEAR def get_mag_filter(self) -> int: default_value = self.MAG_FILTER_DEFAULT.value material = self.find_material() node_name = self.get_image_texture_node_name() node_tree = material.node_tree if node_tree is None: return default_value node = node_tree.nodes.get(node_name) if not isinstance(node, ShaderNodeTexImage): return default_value if node.interpolation == IMAGE_INTERPOLATION_CLOSEST: return GL_NEAREST if node.interpolation in GL_LINEAR_IMAGE_INTERPOLATIONS: return GL_LINEAR value = self.get("mag_filter") if isinstance(value, int) and value in self.mag_filter_enum.values(): return value return default_value def set_mag_filter(self, value: int) -> None: # If input value conflicts with TexImage value, change TexImage value # If input value is GL_NEAREST and TexImage is Closest, delete internal value # If input value is GL_LINEAR and TexImage is Linear/Cubic/Smart, # delete internal value if value not in self.mag_filter_enum.values(): self.pop("mag_filter", None) return material = self.find_material() node_name = self.get_image_texture_node_name() node_tree = material.node_tree if node_tree is not None: node = node_tree.nodes.get(node_name) if isinstance(node, ShaderNodeTexImage): if value == GL_NEAREST: if node.interpolation == IMAGE_INTERPOLATION_CLOSEST: self.pop("mag_filter", None) return node.interpolation = IMAGE_INTERPOLATION_CLOSEST if value == GL_LINEAR: if node.interpolation in GL_LINEAR_IMAGE_INTERPOLATIONS: self.pop("mag_filter", None) return node.interpolation = IMAGE_INTERPOLATION_LINEAR self["mag_filter"] = value def get_min_filter(self) -> int: value = self.get("min_filter") if isinstance(value, int) and value in self.min_filter_enum.values(): return value default_value = self.MIN_FILTER_DEFAULT.value material = self.find_material() node_name = self.get_image_texture_node_name() node_tree = material.node_tree if node_tree is None: return default_value node = node_tree.nodes.get(node_name) if not isinstance(node, ShaderNodeTexImage): return default_value if node.interpolation == IMAGE_INTERPOLATION_CLOSEST: return GL_NEAREST if node.interpolation in GL_LINEAR_IMAGE_INTERPOLATIONS: return GL_LINEAR return default_value def set_min_filter(self, value: int) -> None: # If input value is GL_NEAREST and TexImage is Closest, delete internal value # If input value is GL_LINEAR and TexImage is Linear/Cubic/Smart, # delete internal value if value not in self.min_filter_enum.values(): self.pop("min_filter", None) return material = self.find_material() node_name = self.get_image_texture_node_name() node_tree = material.node_tree if node_tree is not None: node = node_tree.nodes.get(node_name) if isinstance(node, ShaderNodeTexImage): if ( value == GL_NEAREST and node.interpolation == IMAGE_INTERPOLATION_CLOSEST ): self.pop("min_filter", None) return if ( value == GL_LINEAR and node.interpolation in GL_LINEAR_IMAGE_INTERPOLATIONS ): self.pop("min_filter", None) return self["min_filter"] = value ( min_filter_enum, ( MIN_FILTER_NEAREST, MIN_FILTER_LINEAR, MIN_FILTER_NEAREST_MIPMAP_NEAREST, MIN_FILTER_LINEAR_MIPMAP_NEAREST, MIN_FILTER_NEAREST_MIPMAP_LINEAR, MIN_FILTER_LINEAR_MIPMAP_LINEAR, ), ) = property_group_enum( ("NEAREST", "Nearest", "", "NONE", GL_NEAREST), ("LINEAR", "Linear", "", "NONE", GL_LINEAR), ( "NEAREST_MIPMAP_NEAREST", "Nearest Mipmap Nearest", "", "NONE", GL_NEAREST_MIPMAP_NEAREST, ), ( "LINEAR_MIPMAP_NEAREST", "Linear Mipmap Nearest", "", "NONE", GL_LINEAR_MIPMAP_NEAREST, ), ( "NEAREST_MIPMAP_LINEAR", "Nearest Mipmap Linear", "", "NONE", GL_NEAREST_MIPMAP_LINEAR, ), ( "LINEAR_MIPMAP_LINEAR", "Linear Mipmap Linear", "", "NONE", GL_LINEAR_MIPMAP_LINEAR, ), ) MIN_FILTER_DEFAULT = MIN_FILTER_LINEAR # https://github.com/KhronosGroup/glTF/blob/2a9996a2ea66ab712590eaf62f39f1115996f5a3/specification/2.0/schema/sampler.schema.json#L67-L117 ( wrap_enum, ( WRAP_CLAMP_TO_EDGE, WRAP_MIRRORED_REPEAT, WRAP_REPEAT, ), ) = property_group_enum( ("CLAMP_TO_EDGE", "Clamp to Edge", "", "NONE", GL_CLAMP_TO_EDGE), ("MIRRORED_REPEAT", "Mirrored Repeat", "", "NONE", GL_MIRRORED_REPEAT), ("REPEAT", "Repeat", "", "NONE", GL_REPEAT), ) WRAP_DEFAULT = WRAP_REPEAT def get_wrap_s(self) -> int: wrap_s = self.get_texture_uv_int( shader.UV_GROUP_WRAP_S_LABEL, shader.UV_GROUP_WRAP_S_DEFAULT ) if wrap_s in self.wrap_enum.values(): return wrap_s return shader.UV_GROUP_WRAP_S_DEFAULT def set_wrap_s(self, value: object) -> None: self.set_texture_uv(shader.UV_GROUP_WRAP_S_LABEL, value) def get_wrap_t(self) -> int: wrap_t = self.get_texture_uv_int( shader.UV_GROUP_WRAP_T_LABEL, shader.UV_GROUP_WRAP_T_DEFAULT ) if wrap_t in self.wrap_enum.values(): return wrap_t return shader.UV_GROUP_WRAP_T_DEFAULT def set_wrap_t(self, value: object) -> None: self.set_texture_uv(shader.UV_GROUP_WRAP_T_LABEL, value) mag_filter: EnumProperty( # type: ignore[valid-type] items=mag_filter_enum.items(), get=get_mag_filter, set=set_mag_filter, name="Mag Filter", ) min_filter: EnumProperty( # type: ignore[valid-type] items=min_filter_enum.items(), get=get_min_filter, set=set_min_filter, name="Min Filter", ) wrap_s: EnumProperty( # type: ignore[valid-type] items=wrap_enum.items(), name="Wrap S", default=WRAP_DEFAULT.value, get=get_wrap_s, set=set_wrap_s, ) wrap_t: EnumProperty( # type: ignore[valid-type] items=wrap_enum.items(), name="Wrap T", default=WRAP_DEFAULT.value, get=get_wrap_t, set=set_wrap_t, ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. mag_filter: str # type: ignore[no-redef] min_filter: str # type: ignore[no-redef] wrap_s: str # type: ignore[no-redef] wrap_t: str # type: ignore[no-redef] class Mtoon1BaseColorSamplerPropertyGroup(Mtoon1SamplerPropertyGroup): pass class Mtoon1ShadeMultiplySamplerPropertyGroup(Mtoon1SamplerPropertyGroup): pass class Mtoon1NormalSamplerPropertyGroup(Mtoon1SamplerPropertyGroup): pass class Mtoon1ShadingShiftSamplerPropertyGroup(Mtoon1SamplerPropertyGroup): pass class Mtoon1EmissiveSamplerPropertyGroup(Mtoon1SamplerPropertyGroup): pass class Mtoon1RimMultiplySamplerPropertyGroup(Mtoon1SamplerPropertyGroup): pass class Mtoon1MatcapSamplerPropertyGroup(Mtoon1SamplerPropertyGroup): pass class Mtoon1OutlineWidthMultiplySamplerPropertyGroup(Mtoon1SamplerPropertyGroup): pass class Mtoon1UvAnimationMaskSamplerPropertyGroup(Mtoon1SamplerPropertyGroup): pass class Mtoon1TexturePropertyGroup(TextureTraceablePropertyGroup): label = "" panel_label = label colorspace = "sRGB" def update_source(self, _context: Context) -> None: self.update_image(self.source) source: PointerProperty( # type: ignore[valid-type] type=Image, update=update_source, ) def update_source_for_desynced_node_tree(self, context: Context) -> None: """Update PointerProperty() passed to prop() when not synced with NodeTree. When not synced with NodeTree, want to display the correct Image name on the Placeholder side of prop(). Therefore, it operates as follows: - Always display Placeholder, so the value always returns None - When a value is input from outside, reset the value to None and transfer to self.source """ original_syncing_source_name: Optional[str] = None if self.source_for_desynced_node_tree: original_syncing_source_name = self.source_for_desynced_node_tree.name if self.source_for_desynced_node_tree is not None: self.source_for_desynced_node_tree = None # trigger recursive assignment if original_syncing_source_name is not None: image = context.blend_data.images.get(original_syncing_source_name) if image: self.source = image else: self.source = None source_for_desynced_node_tree: PointerProperty( # type: ignore[valid-type] type=Image, update=update_source_for_desynced_node_tree, ) sampler: PointerProperty( # type: ignore[valid-type] type=Mtoon1SamplerPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. source: Optional[Image] # type: ignore[no-redef] source_for_desynced_node_tree: Optional[Image] # type: ignore[no-redef] sampler: Mtoon1SamplerPropertyGroup # type: ignore[no-redef] class Mtoon1BaseColorTexturePropertyGroup(Mtoon1TexturePropertyGroup): label = "Lit Color, Alpha" panel_label = label colorspace = "sRGB" sampler: PointerProperty( # type: ignore[valid-type] type=Mtoon1BaseColorSamplerPropertyGroup ) def update_image(self, image: Optional[Image]) -> None: super().update_image(image) material = self.find_material() mtoon1 = get_material_mtoon1_extension(material) mtoon1.update_alpha_nodes( material, mtoon1.get_alpha_mode(), ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. sampler: Mtoon1BaseColorSamplerPropertyGroup # type: ignore[no-redef] class Mtoon1ShadeMultiplyTexturePropertyGroup(Mtoon1TexturePropertyGroup): label = "Shade Color" panel_label = label colorspace = "sRGB" sampler: PointerProperty( # type: ignore[valid-type] type=Mtoon1ShadeMultiplySamplerPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. sampler: Mtoon1ShadeMultiplySamplerPropertyGroup # type: ignore[no-redef] class Mtoon1NormalTexturePropertyGroup(Mtoon1TexturePropertyGroup): label = "Normal Map" panel_label = label colorspace = "Non-Color" sampler: PointerProperty( # type: ignore[valid-type] type=Mtoon1NormalSamplerPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. sampler: Mtoon1NormalSamplerPropertyGroup # type: ignore[no-redef] class Mtoon1ShadingShiftTexturePropertyGroup(Mtoon1TexturePropertyGroup): label = "Additive Shading Shift" panel_label = label colorspace = "Non-Color" sampler: PointerProperty( # type: ignore[valid-type] type=Mtoon1ShadingShiftSamplerPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. sampler: Mtoon1ShadingShiftSamplerPropertyGroup # type: ignore[no-redef] class Mtoon1EmissiveTexturePropertyGroup(Mtoon1TexturePropertyGroup): label = "Emission" panel_label = label colorspace = "sRGB" sampler: PointerProperty( # type: ignore[valid-type] type=Mtoon1EmissiveSamplerPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. sampler: Mtoon1EmissiveSamplerPropertyGroup # type: ignore[no-redef] class Mtoon1RimMultiplyTexturePropertyGroup(Mtoon1TexturePropertyGroup): label = "Rim Color" panel_label = label colorspace = "sRGB" sampler: PointerProperty( # type: ignore[valid-type] type=Mtoon1RimMultiplySamplerPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. sampler: Mtoon1RimMultiplySamplerPropertyGroup # type: ignore[no-redef] class Mtoon1MatcapTexturePropertyGroup(Mtoon1TexturePropertyGroup): label = "Matcap Rim" panel_label = label colorspace = "sRGB" sampler: PointerProperty( # type: ignore[valid-type] type=Mtoon1MatcapSamplerPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. sampler: Mtoon1MatcapSamplerPropertyGroup # type: ignore[no-redef] class Mtoon1OutlineWidthMultiplyTexturePropertyGroup(Mtoon1TexturePropertyGroup): label = "Outline Width" panel_label = label colorspace = "Non-Color" def update_source(self, context: Context) -> None: mtoon1 = get_material_mtoon1_extension(self.find_material()) mtoon1.extensions.vrmc_materials_mtoon.update_outline_geometry() super().update_source(context) sampler: PointerProperty( # type: ignore[valid-type] type=Mtoon1OutlineWidthMultiplySamplerPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. sampler: ( # type: ignore[no-redef] Mtoon1OutlineWidthMultiplySamplerPropertyGroup ) class Mtoon1UvAnimationMaskTexturePropertyGroup(Mtoon1TexturePropertyGroup): label = "UV Animation Mask" panel_label = "Mask" colorspace = "Non-Color" sampler: PointerProperty( # type: ignore[valid-type] type=Mtoon1UvAnimationMaskSamplerPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. sampler: Mtoon1UvAnimationMaskSamplerPropertyGroup # type: ignore[no-redef] class Mtoon1TextureInfoPropertyGroup(MaterialTraceablePropertyGroup): node_socket_targets: Mapping[str, Sequence[NodeSocketTarget]] = {} index: PointerProperty( # type: ignore[valid-type] type=Mtoon1TexturePropertyGroup ) extensions: PointerProperty( # type: ignore[valid-type] type=Mtoon1TextureInfoExtensionsPropertyGroup ) @dataclass(frozen=True) class TextureInfoBackup: source: Optional[Image] mag_filter: Optional[str] min_filter: Optional[str] wrap_s: str wrap_t: str offset: tuple[float, float] scale: tuple[float, float] def backup(self) -> TextureInfoBackup: return Mtoon1TextureInfoPropertyGroup.TextureInfoBackup( source=self.index.get_connected_node_image(), mag_filter=self.index.sampler.mag_filter, min_filter=self.index.sampler.min_filter, wrap_s=self.index.sampler.wrap_s, wrap_t=self.index.sampler.wrap_t, offset=( self.extensions.khr_texture_transform.offset[0], self.extensions.khr_texture_transform.offset[1], ), scale=( self.extensions.khr_texture_transform.scale[0], self.extensions.khr_texture_transform.scale[1], ), ) def restore(self, backup: TextureInfoBackup) -> None: self.index.source = backup.source if ( backup.mag_filter in Mtoon1SamplerPropertyGroup.mag_filter_enum.identifiers() ): self.index.sampler.mag_filter = backup.mag_filter elif backup.mag_filter is not None: logger.warning("invalid mag filter: %s", backup.mag_filter) self.index.sampler.mag_filter = ( Mtoon1SamplerPropertyGroup.MAG_FILTER_DEFAULT.identifier ) if ( backup.min_filter in Mtoon1SamplerPropertyGroup.min_filter_enum.identifiers() ): self.index.sampler.min_filter = backup.min_filter elif backup.min_filter is not None: logger.warning("invalid min filter: %s", backup.min_filter) self.index.sampler.min_filter = ( Mtoon1SamplerPropertyGroup.MIN_FILTER_DEFAULT.identifier ) if backup.wrap_s in Mtoon1SamplerPropertyGroup.wrap_enum.identifiers(): self.index.sampler.wrap_s = backup.wrap_s else: logger.warning("invalid wrap s: %s", backup.wrap_s) self.index.sampler.wrap_s = ( Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.identifier ) if backup.wrap_t in Mtoon1SamplerPropertyGroup.wrap_enum.identifiers(): self.index.sampler.wrap_t = backup.wrap_t else: logger.warning("invalid wrap t: %s", backup.wrap_t) self.index.sampler.wrap_t = ( Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.identifier ) self.extensions.khr_texture_transform.offset = backup.offset self.extensions.khr_texture_transform.scale = backup.scale show_expanded: BoolProperty() # type: ignore[valid-type] def setup_drivers(self, material: Material) -> None: mtoon1 = get_material_mtoon1_extension(material) if not mtoon1.enabled: return node_tree = material.node_tree if not node_tree: return animation_data = node_tree.animation_data if not animation_data: animation_data = node_tree.animation_data_create() if not animation_data: logger.error( 'Failed to create anomation data for node tree "%s"', node_tree.name ) return uv_node_name = self.index.get_image_texture_uv_node_name() uv_node = node_tree.nodes.get(uv_node_name) if not uv_node or not isinstance(uv_node, ShaderNodeGroup): logger.error('Failed to get uv node "%s"', uv_node_name) return image_node_name = self.index.get_image_texture_node_name() image_node = node_tree.nodes.get(image_node_name) image_exists = isinstance(image_node, ShaderNodeTexImage) and isinstance( image_node.image, Image ) for size_index, uv_input_label in ( (0, shader.UV_GROUP_IMAGE_WIDTH_LABEL), (1, shader.UV_GROUP_IMAGE_HEIGHT_LABEL), ): uv_input_index = next( iter( index for index, socket in enumerate(uv_node.inputs) if socket.name == uv_input_label ), None, ) if uv_input_index is None: logger.error('Failed to get uv input index for "%s"', uv_input_label) continue data_path = ( f'nodes["{uv_node_name}"]' + f".inputs[{uv_input_index}]" + ".default_value" ) fcurve: Optional[Union[FCurve, list[FCurve]]] = next( iter( fcurve for fcurve in animation_data.drivers if fcurve.data_path == data_path ), None, ) if fcurve is None: try: fcurve = node_tree.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_tree.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 variable.type != "SINGLE_PROP": variable.type = "SINGLE_PROP" if not variable.targets: logger.error( 'No targets in variable for fcurve "%s" in node_tree "%s"', data_path, node_tree.name, ) continue target = variable.targets[0] if target.id_type != "MATERIAL": target.id_type = "MATERIAL" if target.id != material: target.id = material target_data_path = ( "node_tree" + f'.nodes["{image_node_name}"]' + ".image" + f".size[{size_index}]" ) if target.data_path != target_data_path or ( # Blender 2.93 sets is_valid to False once image is deleted. # This is resolved by resetting data_path. image_exists and (not fcurve.is_valid or not driver.is_valid) ): target.data_path = target_data_path if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. index: Mtoon1TexturePropertyGroup # type: ignore[no-redef] extensions: Mtoon1TextureInfoExtensionsPropertyGroup # type: ignore[no-redef] show_expanded: bool # type: ignore[no-redef] # https://github.com/KhronosGroup/glTF/blob/1ab49ec412e638f2e5af0289e9fbb60c7271e457/specification/2.0/schema/textureInfo.schema.json class Mtoon1BaseColorTextureInfoPropertyGroup(Mtoon1TextureInfoPropertyGroup): node_socket_targets: Mapping[str, Sequence[NodeSocketTarget]] = { TEX_IMAGE_COLOR_OUTPUT_KEY: [ NodeGroupSocketTarget( node_group_node_tree_name=shader.OUTPUT_GROUP_NAME, in_socket_name="Lit Color Texture Color", ), PrincipledBsdfNodeSocketTarget( in_socket_name=PRINCIPLED_BSDF_BASE_COLOR_INPUT_KEY ), ], TEX_IMAGE_ALPHA_OUTPUT_KEY: [ NodeGroupSocketTarget( node_group_node_tree_name=shader.OUTPUT_GROUP_NAME, in_socket_name="Lit Color Texture Alpha", ), ], } index: PointerProperty( # type: ignore[valid-type] type=Mtoon1BaseColorTexturePropertyGroup ) extensions: PointerProperty( # type: ignore[valid-type] type=Mtoon1BaseColorTextureInfoExtensionsPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. index: Mtoon1BaseColorTexturePropertyGroup # type: ignore[no-redef] extensions: ( # type: ignore[no-redef] Mtoon1BaseColorTextureInfoExtensionsPropertyGroup ) class Mtoon1ShadeMultiplyTextureInfoPropertyGroup(Mtoon1TextureInfoPropertyGroup): node_socket_targets: Mapping[str, Sequence[NodeSocketTarget]] = { TEX_IMAGE_COLOR_OUTPUT_KEY: [ NodeGroupSocketTarget( node_group_node_tree_name=shader.OUTPUT_GROUP_NAME, in_socket_name="Shade Color Texture", ), ], } index: PointerProperty( # type: ignore[valid-type] type=Mtoon1ShadeMultiplyTexturePropertyGroup ) extensions: PointerProperty( # type: ignore[valid-type] type=Mtoon1ShadeMultiplyTextureInfoExtensionsPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. index: Mtoon1ShadeMultiplyTexturePropertyGroup # type: ignore[no-redef] extensions: ( # type: ignore[no-redef] Mtoon1ShadeMultiplyTextureInfoExtensionsPropertyGroup ) # https://github.com/KhronosGroup/glTF/blob/1ab49ec412e638f2e5af0289e9fbb60c7271e457/specification/2.0/schema/material.normalTextureInfo.schema.json class Mtoon1NormalTextureInfoPropertyGroup(Mtoon1TextureInfoPropertyGroup): node_socket_targets: Mapping[str, Sequence[NodeSocketTarget]] = { TEX_IMAGE_COLOR_OUTPUT_KEY: [ NodeGroupSocketTarget( node_group_node_tree_name=shader.NORMAL_GROUP_NAME, in_socket_name="Normal Map Texture", ), PrincipledBsdfNormalMapNodeSocketTarget(), ], } index: PointerProperty( # type: ignore[valid-type] type=Mtoon1NormalTexturePropertyGroup ) def get_scale(self) -> float: return self.get_float( shader.NORMAL_GROUP_NAME, shader.NORMAL_GROUP_SCALE_LABEL, default_value=shader.NORMAL_GROUP_SCALE_DEFAULT, ) def set_scale(self, value: object) -> None: self.set_value( shader.NORMAL_GROUP_NAME, shader.NORMAL_GROUP_SCALE_LABEL, value, ) material = self.find_material() principled_bsdf = PrincipledBSDFWrapper(material, is_readonly=False) principled_bsdf.normalmap_strength = self.scale mtoon1 = get_material_mtoon1_extension(material) if mtoon1.is_outline_material: return outline_material = mtoon1.outline_material if not outline_material: return outline_principled_bsdf = PrincipledBSDFWrapper( outline_material, is_readonly=False ) outline_principled_bsdf.normalmap_strength = self.scale scale: FloatProperty( # type: ignore[valid-type] name="Scale", default=shader.NORMAL_GROUP_SCALE_DEFAULT, get=get_scale, set=set_scale, ) extensions: PointerProperty( # type: ignore[valid-type] type=Mtoon1NormalTextureInfoExtensionsPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. index: Mtoon1NormalTexturePropertyGroup # type: ignore[no-redef] scale: float # type: ignore[no-redef] extensions: ( # type: ignore[no-redef] Mtoon1NormalTextureInfoExtensionsPropertyGroup ) # https://github.com/vrm-c/vrm-specification/blob/c5d1afdc4d59c292cb4fd6d54cad1dc0c4d19c60/specification/VRMC_materials_mtoon-1.0/schema/mtoon.shadingShiftTexture.schema.json class Mtoon1ShadingShiftTextureInfoPropertyGroup(Mtoon1TextureInfoPropertyGroup): node_socket_targets: Mapping[str, Sequence[NodeSocketTarget]] = { TEX_IMAGE_COLOR_OUTPUT_KEY: [ NodeGroupSocketTarget( node_group_node_tree_name=shader.OUTPUT_GROUP_NAME, in_socket_name="Shading Shift Texture", ), ], } index: PointerProperty( # type: ignore[valid-type] type=Mtoon1ShadingShiftTexturePropertyGroup ) def get_scale(self) -> float: return self.get_float( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_SHADING_SHIFT_TEXTURE_SCALE_LABEL, default_value=shader.OUTPUT_GROUP_SHADING_SHIFT_TEXTURE_SCALE_DEFAULT, ) def set_scale(self, value: object) -> None: self.set_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_SHADING_SHIFT_TEXTURE_SCALE_LABEL, value, ) scale: FloatProperty( # type: ignore[valid-type] name="Scale", default=shader.OUTPUT_GROUP_SHADING_SHIFT_TEXTURE_SCALE_DEFAULT, set=set_scale, get=get_scale, ) extensions: PointerProperty( # type: ignore[valid-type] type=Mtoon1ShadingShiftTextureInfoExtensionsPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. index: Mtoon1ShadingShiftTexturePropertyGroup # type: ignore[no-redef] scale: float # type: ignore[no-redef] extensions: ( # type: ignore[no-redef] Mtoon1ShadingShiftTextureInfoExtensionsPropertyGroup ) # https://github.com/KhronosGroup/glTF/blob/1ab49ec412e638f2e5af0289e9fbb60c7271e457/specification/2.0/schema/textureInfo.schema.json class Mtoon1EmissiveTextureInfoPropertyGroup(Mtoon1TextureInfoPropertyGroup): node_socket_targets: Mapping[str, Sequence[NodeSocketTarget]] = { TEX_IMAGE_COLOR_OUTPUT_KEY: [ NodeGroupSocketTarget( node_group_node_tree_name=shader.OUTPUT_GROUP_NAME, in_socket_name="Emissive Texture", ), PrincipledBsdfNodeSocketTarget( in_socket_name=PRINCIPLED_BSDF_EMISSION_INPUT_KEY, ), GltfEmissionNodeSocketTarget(), ], } index: PointerProperty( # type: ignore[valid-type] type=Mtoon1EmissiveTexturePropertyGroup ) extensions: PointerProperty( # type: ignore[valid-type] type=Mtoon1EmissiveTextureInfoExtensionsPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. index: Mtoon1EmissiveTexturePropertyGroup # type: ignore[no-redef] extensions: ( # type: ignore[no-redef] Mtoon1EmissiveTextureInfoExtensionsPropertyGroup ) class Mtoon1RimMultiplyTextureInfoPropertyGroup(Mtoon1TextureInfoPropertyGroup): node_socket_targets: Mapping[str, Sequence[NodeSocketTarget]] = { TEX_IMAGE_COLOR_OUTPUT_KEY: [ NodeGroupSocketTarget( node_group_node_tree_name=shader.OUTPUT_GROUP_NAME, in_socket_name="Rim Color Texture", ), ], } index: PointerProperty( # type: ignore[valid-type] type=Mtoon1RimMultiplyTexturePropertyGroup ) extensions: PointerProperty( # type: ignore[valid-type] type=Mtoon1RimMultiplyTextureInfoExtensionsPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. index: Mtoon1RimMultiplyTexturePropertyGroup # type: ignore[no-redef] extensions: ( # type: ignore[no-redef] Mtoon1RimMultiplyTextureInfoExtensionsPropertyGroup ) class Mtoon1MatcapTextureInfoPropertyGroup(Mtoon1TextureInfoPropertyGroup): node_socket_targets: Mapping[str, Sequence[NodeSocketTarget]] = { TEX_IMAGE_COLOR_OUTPUT_KEY: [ NodeGroupSocketTarget( node_group_node_tree_name=shader.OUTPUT_GROUP_NAME, in_socket_name="MatCap Texture", ), ], } index: PointerProperty( # type: ignore[valid-type] type=Mtoon1MatcapTexturePropertyGroup ) extensions: PointerProperty( # type: ignore[valid-type] type=Mtoon1MatcapTextureInfoExtensionsPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. index: Mtoon1MatcapTexturePropertyGroup # type: ignore[no-redef] extensions: ( # type: ignore[no-redef] Mtoon1MatcapTextureInfoExtensionsPropertyGroup ) class Mtoon1OutlineWidthMultiplyTextureInfoPropertyGroup( Mtoon1TextureInfoPropertyGroup ): node_socket_targets: Mapping[str, Sequence[NodeSocketTarget]] = { TEX_IMAGE_COLOR_OUTPUT_KEY: [ NodeGroupSocketTarget( node_group_node_tree_name=shader.OUTPUT_GROUP_NAME, in_socket_name="Outline Width Texture", ), ], } index: PointerProperty( # type: ignore[valid-type] type=Mtoon1OutlineWidthMultiplyTexturePropertyGroup ) extensions: PointerProperty( # type: ignore[valid-type] type=Mtoon1OutlineWidthMultiplyTextureInfoExtensionsPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. index: Mtoon1OutlineWidthMultiplyTexturePropertyGroup # type: ignore[no-redef] extensions: ( # type: ignore[no-redef] Mtoon1OutlineWidthMultiplyTextureInfoExtensionsPropertyGroup ) class Mtoon1UvAnimationMaskTextureInfoPropertyGroup(Mtoon1TextureInfoPropertyGroup): node_socket_targets: Mapping[str, Sequence[NodeSocketTarget]] = { TEX_IMAGE_COLOR_OUTPUT_KEY: [ NodeGroupSocketTarget( node_group_node_tree_name=shader.UV_ANIMATION_GROUP_NAME, in_socket_name="Mask Texture", ), ], } index: PointerProperty( # type: ignore[valid-type] type=Mtoon1UvAnimationMaskTexturePropertyGroup ) extensions: PointerProperty( # type: ignore[valid-type] type=Mtoon1UvAnimationMaskTextureInfoExtensionsPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. index: Mtoon1UvAnimationMaskTexturePropertyGroup # type: ignore[no-redef] extensions: ( # type: ignore[no-redef] Mtoon1UvAnimationMaskTextureInfoExtensionsPropertyGroup ) class Mtoon0SamplerPropertyGroup(PropertyGroup): mag_filter: EnumProperty( # type: ignore[valid-type] items=Mtoon1SamplerPropertyGroup.mag_filter_enum.items(), name="Mag Filter", ) min_filter: EnumProperty( # type: ignore[valid-type] items=Mtoon1SamplerPropertyGroup.min_filter_enum.items(), name="Min Filter", ) wrap_s: EnumProperty( # type: ignore[valid-type] items=Mtoon1SamplerPropertyGroup.wrap_enum.items(), name="Wrap S", default=Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.value, ) wrap_t: EnumProperty( # type: ignore[valid-type] items=Mtoon1SamplerPropertyGroup.wrap_enum.items(), name="Wrap T", default=Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.value, ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. mag_filter: str # type: ignore[no-redef] min_filter: str # type: ignore[no-redef] wrap_s: str # type: ignore[no-redef] wrap_t: str # type: ignore[no-redef] class Mtoon0TexturePropertyGroup(PropertyGroup): label = "" panel_label = "" colorspace = "sRGB" def get_connected_node_image(self) -> Optional[Image]: return self.source if isinstance(self.source, Image) else None source: PointerProperty( # type: ignore[valid-type] type=Image, ) sampler: PointerProperty( # type: ignore[valid-type] type=Mtoon0SamplerPropertyGroup, ) show_expanded: BoolProperty() # type: ignore[valid-type] if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. source: Optional[Image] # type: ignore[no-redef] sampler: Mtoon0SamplerPropertyGroup # type: ignore[no-redef] show_expanded: bool # type: ignore[no-redef] class Mtoon0ReceiveShadowTexturePropertyGroup(Mtoon0TexturePropertyGroup): label = "Shadow Receive Multiplier" panel_label = label colorspace = "Non-Color" class Mtoon0ShadingGradeTexturePropertyGroup(Mtoon0TexturePropertyGroup): label = "Lit & Shade Mixing Multiplier" panel_label = label colorspace = "Non-Color" # https://github.com/KhronosGroup/glTF/blob/1ab49ec412e638f2e5af0289e9fbb60c7271e457/specification/2.0/schema/material.pbrMetallicRoughness.schema.json#L9-L26 class Mtoon1PbrMetallicRoughnessPropertyGroup(MaterialTraceablePropertyGroup): def get_base_color_factor(self) -> tuple[float, float, float, float]: rgb = self.get_rgb( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_BASE_COLOR_FACTOR_COLOR_LABEL, default_value=shader.OUTPUT_GROUP_BASE_COLOR_FACTOR_COLOR_DEFAULT, ) a = self.get_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_BASE_COLOR_FACTOR_ALPHA_LABEL, default_value=shader.OUTPUT_GROUP_BASE_COLOR_FACTOR_ALPHA_DEFAULT, ) return (*rgb, a) def set_base_color_factor(self, value: object) -> None: color = convert.float4_or_none(value) if color is None: return self.set_rgba( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_BASE_COLOR_FACTOR_COLOR_LABEL, color, ) self.set_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_BASE_COLOR_FACTOR_ALPHA_LABEL, color[3], ) material = self.find_material() principled_bsdf = PrincipledBSDFWrapper(material, is_readonly=False) principled_bsdf.base_color = ( color[0], color[1], color[2], ) mtoon1 = get_material_mtoon1_extension(material) alpha_mode_value = mtoon1.get_alpha_mode() mtoon1.update_alpha_nodes(material, alpha_mode_value) if mtoon1.is_outline_material: return outline_material = mtoon1.outline_material if not outline_material: return outline_mtoon1 = get_material_mtoon1_extension(outline_material) outline_mtoon1.update_alpha_nodes(outline_material, alpha_mode_value) base_color_factor: FloatVectorProperty( # type: ignore[valid-type] size=4, subtype="COLOR", default=( *shader.OUTPUT_GROUP_BASE_COLOR_FACTOR_COLOR_DEFAULT, shader.OUTPUT_GROUP_BASE_COLOR_FACTOR_ALPHA_DEFAULT, ), min=0, max=1, get=get_base_color_factor, set=set_base_color_factor, ) base_color_texture: PointerProperty( # type: ignore[valid-type] type=Mtoon1BaseColorTextureInfoPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. base_color_factor: Sequence[float] # type: ignore[no-redef] base_color_texture: ( # type: ignore[no-redef] Mtoon1BaseColorTextureInfoPropertyGroup ) class Mtoon1VrmcMaterialsMtoonPropertyGroup(MaterialTraceablePropertyGroup): def get_transparent_with_z_write(self) -> bool: return self.get_bool( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_TRANSPARENT_WITH_Z_WRITE_LABEL, default_value=shader.OUTPUT_GROUP_TRANSPARENT_WITH_Z_WRITE_DEFAULT, ) def set_transparent_with_z_write(self, value: object) -> None: self.set_bool( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_TRANSPARENT_WITH_Z_WRITE_LABEL, value, ) mtoon1 = get_material_mtoon1_extension(self.find_material()) mtoon1.set_mtoon0_render_queue_and_clamp(mtoon1.mtoon0_render_queue) transparent_with_z_write: BoolProperty( # type: ignore[valid-type] name="Transparent With ZWrite Mode", default=shader.OUTPUT_GROUP_TRANSPARENT_WITH_Z_WRITE_DEFAULT, get=get_transparent_with_z_write, set=set_transparent_with_z_write, ) def get_render_queue_offset_number(self) -> int: return self.get_int( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_LABEL, default_value=shader.OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_DEFAULT, ) def set_render_queue_offset_number(self, value: int) -> None: self.set_int( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_LABEL, value, ) render_queue_offset_number: IntProperty( # type: ignore[valid-type] name="RenderQueue Offset", min=shader.OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_MIN, default=shader.OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_DEFAULT, max=shader.OUTPUT_GROUP_RENDER_QUEUE_OFFSET_NUMBER_MAX, get=get_render_queue_offset_number, set=set_render_queue_offset_number, ) shade_multiply_texture: PointerProperty( # type: ignore[valid-type] type=Mtoon1ShadeMultiplyTextureInfoPropertyGroup ) def get_shade_color_factor(self) -> tuple[float, float, float]: return self.get_rgb( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_SHADE_COLOR_FACTOR_LABEL, default_value=shader.OUTPUT_GROUP_SHADE_COLOR_FACTOR_DEFAULT, ) def set_shade_color_factor(self, value: object) -> None: self.set_rgb( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_SHADE_COLOR_FACTOR_LABEL, value, ) shade_color_factor: FloatVectorProperty( # type: ignore[valid-type] size=3, subtype="COLOR", default=shader.OUTPUT_GROUP_SHADE_COLOR_FACTOR_DEFAULT, min=0.0, max=1.0, get=get_shade_color_factor, set=set_shade_color_factor, ) shading_shift_texture: PointerProperty( # type: ignore[valid-type] type=Mtoon1ShadingShiftTextureInfoPropertyGroup ) def get_shading_shift_factor(self) -> float: return self.get_float( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_SHADING_SHIFT_FACTOR_LABEL, default_value=shader.OUTPUT_GROUP_SHADING_SHIFT_FACTOR_DEFAULT, ) def set_shading_shift_factor(self, value: object) -> None: self.set_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_SHADING_SHIFT_FACTOR_LABEL, value, ) shading_shift_factor: FloatProperty( # type: ignore[valid-type] name=shader.OUTPUT_GROUP_SHADING_SHIFT_FACTOR_LABEL, soft_min=-1.0, default=shader.OUTPUT_GROUP_SHADING_SHIFT_FACTOR_DEFAULT, soft_max=1.0, get=get_shading_shift_factor, set=set_shading_shift_factor, ) def get_shading_toony_factor(self) -> float: return self.get_float( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_SHADING_TOONY_FACTOR_LABEL, default_value=shader.OUTPUT_GROUP_SHADING_TOONY_FACTOR_DEFAULT, ) def set_shading_toony_factor(self, value: object) -> None: self.set_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_SHADING_TOONY_FACTOR_LABEL, value, ) shading_toony_factor: FloatProperty( # type: ignore[valid-type] name=shader.OUTPUT_GROUP_SHADING_TOONY_FACTOR_LABEL, min=0.0, default=shader.OUTPUT_GROUP_SHADING_TOONY_FACTOR_DEFAULT, max=1.0, get=get_shading_toony_factor, set=set_shading_toony_factor, ) def get_gi_equalization_factor(self) -> float: return self.get_float( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_GI_EQUALIZATION_FACTOR_LABEL, default_value=shader.OUTPUT_GROUP_GI_EQUALIZATION_FACTOR_DEFAULT, ) def set_gi_equalization_factor(self, value: object) -> None: self.set_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_GI_EQUALIZATION_FACTOR_LABEL, value, ) gi_equalization_factor: FloatProperty( # type: ignore[valid-type] name="GI Equalization", min=0.0, default=shader.OUTPUT_GROUP_GI_EQUALIZATION_FACTOR_DEFAULT, max=1.0, get=get_gi_equalization_factor, set=set_gi_equalization_factor, ) def get_matcap_factor(self) -> tuple[float, float, float]: return self.get_rgb( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_MATCAP_FACTOR_LABEL, default_value=shader.OUTPUT_GROUP_MATCAP_FACTOR_DEFAULT, ) def set_matcap_factor(self, value: object) -> None: self.set_rgb( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_MATCAP_FACTOR_LABEL, value, ) matcap_factor: FloatVectorProperty( # type: ignore[valid-type] size=3, subtype="COLOR", default=shader.OUTPUT_GROUP_MATCAP_FACTOR_DEFAULT, min=0, max=1, get=get_matcap_factor, set=set_matcap_factor, ) matcap_texture: PointerProperty( # type: ignore[valid-type] type=Mtoon1MatcapTextureInfoPropertyGroup ) def get_parametric_rim_color_factor(self) -> tuple[float, float, float]: return self.get_rgb( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_PARAMETRIC_RIM_COLOR_FACTOR_LABEL, default_value=shader.OUTPUT_GROUP_PARAMETRIC_RIM_COLOR_FACTOR_DEFAULT, ) def set_parametric_rim_color_factor(self, value: object) -> None: self.set_rgb( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_PARAMETRIC_RIM_COLOR_FACTOR_LABEL, value, ) parametric_rim_color_factor: FloatVectorProperty( # type: ignore[valid-type] name=shader.OUTPUT_GROUP_PARAMETRIC_RIM_COLOR_FACTOR_LABEL, size=3, subtype="COLOR", default=shader.OUTPUT_GROUP_PARAMETRIC_RIM_COLOR_FACTOR_DEFAULT, min=0, max=1, get=get_parametric_rim_color_factor, set=set_parametric_rim_color_factor, ) rim_multiply_texture: PointerProperty( # type: ignore[valid-type] type=Mtoon1RimMultiplyTextureInfoPropertyGroup ) def get_rim_lighting_mix_factor(self) -> float: return self.get_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_RIM_LIGHTING_MIX_FACTOR_LABEL, default_value=shader.OUTPUT_GROUP_RIM_LIGHTING_MIX_FACTOR_DEFAULT, ) def set_rim_lighting_mix_factor(self, value: object) -> None: self.set_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_RIM_LIGHTING_MIX_FACTOR_LABEL, value, ) rim_lighting_mix_factor: FloatProperty( # type: ignore[valid-type] name=shader.OUTPUT_GROUP_RIM_LIGHTING_MIX_FACTOR_LABEL, default=shader.OUTPUT_GROUP_RIM_LIGHTING_MIX_FACTOR_DEFAULT, soft_min=0, soft_max=1, get=get_rim_lighting_mix_factor, set=set_rim_lighting_mix_factor, ) def get_parametric_rim_fresnel_power_factor(self) -> float: return self.get_float( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_PARAMETRIC_RIM_FRESNEL_POWER_LABEL, default_value=shader.OUTPUT_GROUP_PARAMETRIC_RIM_FRESNEL_POWER_DEFAULT, ) def set_parametric_rim_fresnel_power_factor(self, value: object) -> None: self.set_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_PARAMETRIC_RIM_FRESNEL_POWER_LABEL, value, ) parametric_rim_fresnel_power_factor: FloatProperty( # type: ignore[valid-type] name=shader.OUTPUT_GROUP_PARAMETRIC_RIM_FRESNEL_POWER_LABEL, min=0.0, default=shader.OUTPUT_GROUP_PARAMETRIC_RIM_FRESNEL_POWER_DEFAULT, soft_max=100.0, get=get_parametric_rim_fresnel_power_factor, set=set_parametric_rim_fresnel_power_factor, ) def get_parametric_rim_lift_factor(self) -> float: return self.get_float( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_PARAMETRIC_RIM_LIFT_FACTOR_LABEL, default_value=shader.OUTPUT_GROUP_PARAMETRIC_RIM_LIFT_FACTOR_DEFAULT, ) def set_parametric_rim_lift_factor(self, value: object) -> None: self.set_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_PARAMETRIC_RIM_LIFT_FACTOR_LABEL, value, ) parametric_rim_lift_factor: FloatProperty( # type: ignore[valid-type] name=shader.OUTPUT_GROUP_PARAMETRIC_RIM_LIFT_FACTOR_LABEL, soft_min=0.0, default=shader.OUTPUT_GROUP_PARAMETRIC_RIM_LIFT_FACTOR_DEFAULT, soft_max=1.0, get=get_parametric_rim_lift_factor, set=set_parametric_rim_lift_factor, ) ( outline_width_mode_enum, ( OUTLINE_WIDTH_MODE_NONE, OUTLINE_WIDTH_MODE_WORLD_COORDINATES, OUTLINE_WIDTH_MODE_SCREEN_COORDINATES, ), ) = property_group_enum( ( "none", "None", "", "NONE", shader.OUTPUT_GROUP_OUTLINE_WIDTH_MODE_MIN, ), ("worldCoordinates", "World Coordinates", "", "NONE", 1), ( "screenCoordinates", "Screen Coordinates", "", "NONE", shader.OUTPUT_GROUP_OUTLINE_WIDTH_MODE_MAX, ), ) def get_outline_width_mode(self) -> int: return self.get_int( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_OUTLINE_WIDTH_MODE_LABEL, default_value=shader.OUTPUT_GROUP_OUTLINE_WIDTH_MODE_DEFAULT, ) def set_outline_width_mode(self, value: object) -> None: self.set_int( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_OUTLINE_WIDTH_MODE_LABEL, value, ) self.update_outline_geometry() def update_outline_geometry(self) -> None: material = self.find_material() if get_material_mtoon1_extension(material).is_outline_material: return ops.vrm.refresh_mtoon1_outline( material_name=material.name, create_modifier=True ) outline_width_mode: EnumProperty( # type: ignore[valid-type] items=outline_width_mode_enum.items(), name=shader.OUTPUT_GROUP_OUTLINE_WIDTH_MODE_LABEL, get=get_outline_width_mode, set=set_outline_width_mode, ) def get_outline_width_factor(self) -> float: return self.get_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_OUTLINE_WIDTH_FACTOR_LABEL, default_value=shader.OUTPUT_GROUP_OUTLINE_WIDTH_FACTOR_DEFAULT, ) def set_outline_width_factor(self, value: object) -> None: self.set_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_OUTLINE_WIDTH_FACTOR_LABEL, value, ) self.update_outline_geometry() outline_width_factor: FloatProperty( # type: ignore[valid-type] name=shader.OUTPUT_GROUP_OUTLINE_WIDTH_FACTOR_LABEL, default=shader.OUTPUT_GROUP_OUTLINE_WIDTH_FACTOR_DEFAULT, min=0.0, soft_max=0.05, get=get_outline_width_factor, set=set_outline_width_factor, ) outline_width_multiply_texture: PointerProperty( # type: ignore[valid-type] type=Mtoon1OutlineWidthMultiplyTextureInfoPropertyGroup ) def get_outline_color_factor(self) -> tuple[float, float, float]: return self.get_rgb( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_OUTLINE_COLOR_FACTOR_LABEL, default_value=shader.OUTPUT_GROUP_OUTLINE_COLOR_FACTOR_DEFAULT, ) def set_outline_color_factor(self, value: object) -> None: self.set_rgb( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_OUTLINE_COLOR_FACTOR_LABEL, value, shader.OUTPUT_GROUP_OUTLINE_COLOR_FACTOR_DEFAULT, ) self.update_outline_geometry() material = self.find_material() mtoon1 = get_material_mtoon1_extension(material) if mtoon1.is_outline_material: return outline_material = mtoon1.outline_material if not outline_material: return outline_principled_bsdf = PrincipledBSDFWrapper( outline_material, is_readonly=False ) outline_diffuse_alpha = outline_material.diffuse_color[3] outline_principled_bsdf.base_color = ( self.outline_color_factor[0], self.outline_color_factor[1], self.outline_color_factor[2], ) outline_material.diffuse_color[3] = outline_diffuse_alpha outline_color_factor: FloatVectorProperty( # type: ignore[valid-type] name=shader.OUTPUT_GROUP_OUTLINE_COLOR_FACTOR_LABEL, size=3, subtype="COLOR", default=shader.OUTPUT_GROUP_OUTLINE_COLOR_FACTOR_DEFAULT, min=0, max=1, get=get_outline_color_factor, set=set_outline_color_factor, ) def get_outline_lighting_mix_factor(self) -> float: return self.get_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_OUTLINE_LIGHTING_MIX_FACTOR_LABEL, default_value=shader.OUTPUT_GROUP_OUTLINE_LIGHTING_MIX_FACTOR_DEFAULT, ) def set_outline_lighting_mix_factor(self, value: object) -> None: self.set_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_OUTLINE_LIGHTING_MIX_FACTOR_LABEL, value, ) self.update_outline_geometry() outline_lighting_mix_factor: FloatProperty( # type: ignore[valid-type] name=shader.OUTPUT_GROUP_OUTLINE_LIGHTING_MIX_FACTOR_LABEL, min=0.0, default=shader.OUTPUT_GROUP_OUTLINE_LIGHTING_MIX_FACTOR_DEFAULT, max=1.0, get=get_outline_lighting_mix_factor, set=set_outline_lighting_mix_factor, ) def update_enable_outline_preview(self, context: Context) -> None: material_name = self.find_material().name ops.vrm.refresh_mtoon1_outline( material_name=material_name, create_modifier=True ) for material in context.blend_data.materials: if material.name == material_name: continue ext = get_material_mtoon1_extension(material) if not ext.enabled: continue mtoon = ext.extensions.vrmc_materials_mtoon if mtoon.enable_outline_preview != self.enable_outline_preview: mtoon.enable_outline_preview = self.enable_outline_preview enable_outline_preview: BoolProperty( # type: ignore[valid-type] default=True, update=update_enable_outline_preview, ) uv_animation_mask_texture: PointerProperty( # type: ignore[valid-type] type=Mtoon1UvAnimationMaskTextureInfoPropertyGroup ) def get_uv_animation_scroll_x_speed_factor(self) -> float: return self.get_value( shader.UV_ANIMATION_GROUP_NAME, shader.UV_ANIMATION_GROUP_TRANSLATE_X_LABEL, default_value=shader.UV_ANIMATION_GROUP_TRANSLATE_X_DEFAULT, ) def set_uv_animation_scroll_x_speed_factor(self, value: object) -> None: self.set_value( shader.UV_ANIMATION_GROUP_NAME, shader.UV_ANIMATION_GROUP_TRANSLATE_X_LABEL, value, ) uv_animation_scroll_x_speed_factor: FloatProperty( # type: ignore[valid-type] name=shader.UV_ANIMATION_GROUP_TRANSLATE_X_LABEL, default=shader.UV_ANIMATION_GROUP_TRANSLATE_X_DEFAULT, get=get_uv_animation_scroll_x_speed_factor, set=set_uv_animation_scroll_x_speed_factor, ) def get_uv_animation_scroll_y_speed_factor(self) -> float: return self.get_value( shader.UV_ANIMATION_GROUP_NAME, shader.UV_ANIMATION_GROUP_TRANSLATE_Y_LABEL, default_value=shader.UV_ANIMATION_GROUP_TRANSLATE_Y_DEFAULT, ) def set_uv_animation_scroll_y_speed_factor(self, value: object) -> None: self.set_value( shader.UV_ANIMATION_GROUP_NAME, shader.UV_ANIMATION_GROUP_TRANSLATE_Y_LABEL, value, ) uv_animation_scroll_y_speed_factor: FloatProperty( # type: ignore[valid-type] name=shader.UV_ANIMATION_GROUP_TRANSLATE_Y_LABEL, default=shader.UV_ANIMATION_GROUP_TRANSLATE_Y_DEFAULT, get=get_uv_animation_scroll_y_speed_factor, set=set_uv_animation_scroll_y_speed_factor, ) def get_uv_animation_rotation_speed_factor(self) -> float: return self.get_value( shader.UV_ANIMATION_GROUP_NAME, shader.UV_ANIMATION_GROUP_ROTATION_LABEL, default_value=shader.UV_ANIMATION_GROUP_ROTATION_DEFAULT, ) def set_uv_animation_rotation_speed_factor(self, value: object) -> None: self.set_value( shader.UV_ANIMATION_GROUP_NAME, shader.UV_ANIMATION_GROUP_ROTATION_LABEL, value, ) uv_animation_rotation_speed_factor: FloatProperty( # type: ignore[valid-type] name=shader.UV_ANIMATION_GROUP_ROTATION_LABEL, get=get_uv_animation_rotation_speed_factor, set=set_uv_animation_rotation_speed_factor, ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. transparent_with_z_write: bool # type: ignore[no-redef] render_queue_offset_number: int # type: ignore[no-redef] shade_multiply_texture: ( # type: ignore[no-redef] Mtoon1ShadeMultiplyTextureInfoPropertyGroup ) shade_color_factor: Sequence[float] # type: ignore[no-redef] shading_shift_texture: ( # type: ignore[no-redef] Mtoon1ShadingShiftTextureInfoPropertyGroup ) shading_shift_factor: float # type: ignore[no-redef] shading_toony_factor: float # type: ignore[no-redef] gi_equalization_factor: float # type: ignore[no-redef] matcap_factor: Sequence[float] # type: ignore[no-redef] matcap_texture: Mtoon1MatcapTextureInfoPropertyGroup # type: ignore[no-redef] parametric_rim_color_factor: Sequence[float] # type: ignore[no-redef] rim_multiply_texture: ( # type: ignore[no-redef] Mtoon1RimMultiplyTextureInfoPropertyGroup ) rim_lighting_mix_factor: float # type: ignore[no-redef] parametric_rim_fresnel_power_factor: float # type: ignore[no-redef] parametric_rim_lift_factor: float # type: ignore[no-redef] outline_width_mode: str # type: ignore[no-redef] outline_width_factor: float # type: ignore[no-redef] outline_width_multiply_texture: ( # type: ignore[no-redef] Mtoon1OutlineWidthMultiplyTextureInfoPropertyGroup ) outline_color_factor: Sequence[float] # type: ignore[no-redef] outline_lighting_mix_factor: float # type: ignore[no-redef] enable_outline_preview: bool # type: ignore[no-redef] uv_animation_mask_texture: ( # type: ignore[no-redef] Mtoon1UvAnimationMaskTextureInfoPropertyGroup ) uv_animation_scroll_x_speed_factor: float # type: ignore[no-redef] uv_animation_scroll_y_speed_factor: float # type: ignore[no-redef] uv_animation_rotation_speed_factor: float # type: ignore[no-redef] # https://github.com/KhronosGroup/glTF/blob/d997b7dc7e426bc791f5613475f5b4490da0b099/extensions/2.0/Khronos/KHR_materials_emissive_strength/schema/glTF.KHR_materials_emissive_strength.schema.json class Mtoon1KhrMaterialsEmissiveStrengthPropertyGroup(MaterialTraceablePropertyGroup): def get_emissive_strength(self) -> float: return self.get_float( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_EMISSIVE_STRENGTH_LABEL, default_value=shader.OUTPUT_GROUP_EMISSIVE_STRENGTH_DEFAULT, ) def set_emissive_strength(self, value: object) -> None: self.set_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_EMISSIVE_STRENGTH_LABEL, value, ) material = self.find_material() principled_bsdf = PrincipledBSDFWrapper(material, is_readonly=False) principled_bsdf.emission_strength = self.emissive_strength emissive_node = get_gltf_emissive_node(material) if emissive_node is not None: socket = emissive_node.inputs.get(EMISSION_STRENGTH_INPUT_KEY) if isinstance(socket, NodeSocketFloat): socket.default_value = self.emissive_strength mtoon1 = get_material_mtoon1_extension(material) if mtoon1.is_outline_material: return outline_material = mtoon1.outline_material if not outline_material: return outline_principled_bsdf = PrincipledBSDFWrapper( outline_material, is_readonly=False ) outline_principled_bsdf.emission_strength = self.emissive_strength outline_emissive_node = get_gltf_emissive_node(outline_material) if outline_emissive_node is not None: outline_socket = outline_emissive_node.inputs.get( EMISSION_STRENGTH_INPUT_KEY ) if isinstance(outline_socket, NodeSocketFloat): outline_socket.default_value = self.emissive_strength emissive_strength: FloatProperty( # type: ignore[valid-type] name="Strength", min=0.0, default=shader.OUTPUT_GROUP_EMISSIVE_STRENGTH_DEFAULT, get=get_emissive_strength, set=set_emissive_strength, ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. emissive_strength: float # type: ignore[no-redef] class Mtoon1MaterialExtensionsPropertyGroup(PropertyGroup): vrmc_materials_mtoon: PointerProperty( # type: ignore[valid-type] type=Mtoon1VrmcMaterialsMtoonPropertyGroup ) khr_materials_emissive_strength: PointerProperty( # type: ignore[valid-type] type=Mtoon1KhrMaterialsEmissiveStrengthPropertyGroup ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. vrmc_materials_mtoon: ( # type: ignore[no-redef] Mtoon1VrmcMaterialsMtoonPropertyGroup ) khr_materials_emissive_strength: ( # type: ignore[no-redef] Mtoon1KhrMaterialsEmissiveStrengthPropertyGroup ) # https://github.com/vrm-c/vrm-specification/blob/8dc51ec7241be27ee95f159cefc0190a0e41967b/specification/VRMC_materials_mtoon-1.0-beta/schema/VRMC_materials_mtoon.schema.json class Mtoon1MaterialPropertyGroup(MaterialTraceablePropertyGroup): INITIAL_ADDON_VERSION = VrmAddonPreferences.INITIAL_ADDON_VERSION addon_version: IntVectorProperty( # type: ignore[valid-type] size=3, default=INITIAL_ADDON_VERSION, ) pbr_metallic_roughness: PointerProperty( # type: ignore[valid-type] type=Mtoon1PbrMetallicRoughnessPropertyGroup ) ( alpha_mode_enum, ( ALPHA_MODE_OPAQUE, ALPHA_MODE_MASK, ALPHA_MODE_BLEND, ), ) = property_group_enum( ("OPAQUE", "Opaque", "", "NONE", 0), ("MASK", "Cutout", "", "NONE", 1), ("BLEND", "Transparent", "", "NONE", 2), ) alpha_mode_blend_method_hashed: BoolProperty() # type: ignore[valid-type] def get_alpha_mode(self) -> int: alpha_mode_value = self.get_int( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_ALPHA_MODE_LABEL, default_value=self.ALPHA_MODE_OPAQUE.value, ) if alpha_mode_value in self.alpha_mode_enum.values(): return alpha_mode_value return self.ALPHA_MODE_OPAQUE.value @staticmethod def update_alpha_nodes(material: Material, alpha_mode_value: int) -> None: node_tree = material.node_tree if not node_tree: return # Align with glTF nodes # https://docs.blender.org/manual/en/4.2/addons/import_export/scene_gltf2.html#alpha-modes mtoon1 = get_material_mtoon1_extension(material) texture = mtoon1.pbr_metallic_roughness.base_color_texture.index tex_image_node_name = texture.get_image_texture_node_name() if alpha_mode_value != mtoon1.ALPHA_MODE_MASK.value: TextureTraceablePropertyGroup.unlink_nodes( material, StaticNodeSocketTarget( in_node_name=ALPHA_CLIP_INPUT_NODE_NAME, in_node_type=ShaderNodeMath, in_socket_name=ALPHA_CLIP_INPUT_NODE_SOCKET_NAME, ), ) TextureTraceablePropertyGroup.unlink_nodes( material, PrincipledBsdfNodeSocketTarget( in_socket_name=PRINCIPLED_BSDF_ALPHA_INPUT_KEY ), ) if alpha_mode_value != mtoon1.ALPHA_MODE_BLEND.value or bpy.app.version < ( 4, 2, ): TextureTraceablePropertyGroup.unlink_nodes( material, PrincipledBsdfNodeSocketTarget( in_socket_name=PRINCIPLED_BSDF_ALPHA_INPUT_KEY ), ) principled_bsdf = PrincipledBSDFWrapper(material, is_readonly=False) if bpy.app.version < (4, 2): principled_bsdf.alpha = mtoon1.pbr_metallic_roughness.base_color_factor[3] tex_image_node = node_tree.nodes.get(tex_image_node_name) if isinstance(tex_image_node, ShaderNodeTexImage): TextureTraceablePropertyGroup.link_or_unlink_nodes( material, tex_image_node_name, ShaderNodeTexImage, TEX_IMAGE_ALPHA_OUTPUT_KEY, PrincipledBsdfNodeSocketTarget( in_socket_name=PRINCIPLED_BSDF_ALPHA_INPUT_KEY ), link=bool(tex_image_node.image), ) elif alpha_mode_value == mtoon1.ALPHA_MODE_OPAQUE.value: principled_bsdf.alpha = 1.0 elif alpha_mode_value == mtoon1.ALPHA_MODE_MASK.value: tex_image_node = node_tree.nodes.get(tex_image_node_name) image_exists = False if isinstance(tex_image_node, ShaderNodeTexImage): image_exists = bool(tex_image_node.image) TextureTraceablePropertyGroup.link_or_unlink_nodes( material, tex_image_node_name, ShaderNodeTexImage, TEX_IMAGE_ALPHA_OUTPUT_KEY, StaticNodeSocketTarget( in_node_name=ALPHA_CLIP_INPUT_NODE_NAME, in_node_type=ShaderNodeMath, in_socket_name=ALPHA_CLIP_INPUT_NODE_SOCKET_NAME, ), link=image_exists, ) TextureTraceablePropertyGroup.link_or_unlink_nodes( material, ALPHA_CLIP_OUTPUT_NODE_NAME, ShaderNodeMath, ALPHA_CLIP_OUTPUT_NODE_SOCKET_NAME, PrincipledBsdfNodeSocketTarget( in_socket_name=PRINCIPLED_BSDF_ALPHA_INPUT_KEY ), link=image_exists, ) if not image_exists: alpha = mtoon1.pbr_metallic_roughness.base_color_factor[3] if alpha >= mtoon1.alpha_cutoff: principled_bsdf.alpha = 1.0 else: principled_bsdf.alpha = 0.0 elif alpha_mode_value == mtoon1.ALPHA_MODE_BLEND.value: principled_bsdf.alpha = mtoon1.pbr_metallic_roughness.base_color_factor[3] tex_image_node = node_tree.nodes.get(tex_image_node_name) if isinstance(tex_image_node, ShaderNodeTexImage): TextureTraceablePropertyGroup.link_or_unlink_nodes( material, tex_image_node_name, ShaderNodeTexImage, TEX_IMAGE_ALPHA_OUTPUT_KEY, PrincipledBsdfNodeSocketTarget( in_socket_name=PRINCIPLED_BSDF_ALPHA_INPUT_KEY ), link=bool(tex_image_node.image), ) alpha_clip_input_node = node_tree.nodes.get(ALPHA_CLIP_INPUT_NODE_NAME) if ( isinstance(alpha_clip_input_node, ShaderNodeMath) and alpha_clip_input_node.operation == "MINIMUM" ): inputs = alpha_clip_input_node.inputs if len(inputs) >= 2: alpha_input = inputs[0] if isinstance(alpha_input, shader.FLOAT_SOCKET_CLASSES): alpha_input.default_value = ( mtoon1.pbr_metallic_roughness.base_color_factor[3] ) else: logger.error( "Unexpected alpha input node input type: %s", type(alpha_input) ) alpha_cutoff_input = inputs[1] if isinstance(alpha_cutoff_input, shader.FLOAT_SOCKET_CLASSES): alpha_cutoff_input.default_value = mtoon1.alpha_cutoff else: logger.error( "Unexpected alpha clip input node input type: %s", type(alpha_cutoff_input), ) else: logger.error( "Unexpected alpha clip input node input length: %s", len(inputs) ) else: logger.error( "Unexpected alpha clip input node type: %s", type(alpha_clip_input_node), ) def set_alpha_mode(self, value: int) -> None: changed = self.get_alpha_mode() != value material = self.find_material() shadow_method = None if bpy.app.version < (4, 2): if material.blend_method == "HASHED": self.alpha_mode_blend_method_hashed = True if material.blend_method == "BLEND": self.alpha_mode_blend_method_hashed = False if value == self.ALPHA_MODE_OPAQUE.value: material.blend_method = "OPAQUE" shadow_method = "OPAQUE" elif value == self.ALPHA_MODE_MASK.value: material.blend_method = "CLIP" shadow_method = "CLIP" elif value == self.ALPHA_MODE_BLEND.value: material.blend_method = "HASHED" shadow_method = "HASHED" else: logger.error("Unexpected alpha mode: {value}") material.blend_method = "OPAQUE" shadow_method = "OPAQUE" self.set_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_ALPHA_MODE_LABEL, value, ) if changed: self.set_mtoon0_render_queue_and_clamp(self.mtoon0_render_queue) self.update_alpha_nodes(material, value) if self.is_outline_material: if bpy.app.version < (4, 2): material.shadow_method = "NONE" return if bpy.app.version < (4, 3) and shadow_method is not None: material.shadow_method = shadow_method outline_material = self.outline_material if not outline_material: return get_material_mtoon1_extension(outline_material).set_alpha_mode(value) alpha_mode: EnumProperty( # type: ignore[valid-type] items=alpha_mode_enum.items(), name="Alpha Mode", get=get_alpha_mode, set=set_alpha_mode, ) def get_double_sided(self) -> bool: return not self.find_material().use_backface_culling def set_double_sided(self, value: object) -> None: material = self.find_material() material.use_backface_culling = not value self.set_bool( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_DOUBLE_SIDED_LABEL, value ) if get_material_mtoon1_extension(material).is_outline_material: return outline_material = get_material_mtoon1_extension(material).outline_material if not outline_material: return get_material_mtoon1_extension(outline_material).double_sided = False double_sided: BoolProperty( # type: ignore[valid-type] name=shader.OUTPUT_GROUP_DOUBLE_SIDED_LABEL, get=get_double_sided, set=set_double_sided, ) def get_alpha_cutoff(self) -> float: return self.get_float( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_ALPHA_CUTOFF_LABEL, default_value=shader.OUTPUT_GROUP_ALPHA_CUTOFF_DEFAULT, ) def set_alpha_cutoff(self, value: float) -> None: material = self.find_material() if bpy.app.version < (4, 2): material.alpha_threshold = max(0, min(1, value - 0.00001)) # TODO: ... self.set_value( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_ALPHA_CUTOFF_LABEL, max(0, value), ) self.update_alpha_nodes(material, self.get_alpha_mode()) if self.is_outline_material: return outline_material = self.outline_material if not outline_material: return get_material_mtoon1_extension(outline_material).set_alpha_cutoff(value) alpha_cutoff: FloatProperty( # type: ignore[valid-type] name="Cutoff", min=0, soft_max=1, get=get_alpha_cutoff, set=set_alpha_cutoff, ) normal_texture: PointerProperty( # type: ignore[valid-type] type=Mtoon1NormalTextureInfoPropertyGroup ) emissive_texture: PointerProperty( # type: ignore[valid-type] type=Mtoon1EmissiveTextureInfoPropertyGroup ) def get_emissive_factor(self) -> tuple[float, float, float]: return self.get_rgb( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_EMISSIVE_FACTOR_LABEL, default_value=shader.OUTPUT_GROUP_EMISSIVE_FACTOR_DEFAULT, ) def set_emissive_factor(self, value: object) -> None: self.set_rgb( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_EMISSIVE_FACTOR_LABEL, value, shader.OUTPUT_GROUP_EMISSIVE_FACTOR_DEFAULT, ) material = self.find_material() principled_bsdf = PrincipledBSDFWrapper(material, is_readonly=False) principled_bsdf.emission_color = ( self.emissive_factor[0], self.emissive_factor[1], self.emissive_factor[2], ) emissive_node = get_gltf_emissive_node(material) if emissive_node is not None: socket = emissive_node.inputs.get(EMISSION_COLOR_INPUT_KEY) if isinstance(socket, NodeSocketColor): socket.default_value = ( self.emissive_factor[0], self.emissive_factor[1], self.emissive_factor[2], 1, ) mtoon1 = get_material_mtoon1_extension(material) if mtoon1.is_outline_material: return outline_material = mtoon1.outline_material if not outline_material: return outline_principled_bsdf = PrincipledBSDFWrapper( outline_material, is_readonly=False ) outline_principled_bsdf.emission_color = ( self.emissive_factor[0], self.emissive_factor[1], self.emissive_factor[2], ) outline_emissive_node = get_gltf_emissive_node(material) if outline_emissive_node is not None: outline_socket = outline_emissive_node.inputs.get(EMISSION_COLOR_INPUT_KEY) if isinstance(outline_socket, NodeSocketColor): outline_socket.default_value = ( self.emissive_factor[0], self.emissive_factor[1], self.emissive_factor[2], 1, ) emissive_factor: FloatVectorProperty( # type: ignore[valid-type] size=3, subtype="COLOR", default=shader.OUTPUT_GROUP_EMISSIVE_FACTOR_DEFAULT, min=0, max=1, get=get_emissive_factor, set=set_emissive_factor, ) extensions: PointerProperty( # type: ignore[valid-type] type=Mtoon1MaterialExtensionsPropertyGroup ) def get_enabled(self) -> bool: if self.is_outline_material: return False material = self.find_material() if bpy.app.version < (5, 0, 0) and not material.use_nodes: return False node_tree = material.node_tree if not node_tree: return False group_node = node_tree.nodes.get("Mtoon1Material.Mtoon1Output") if not isinstance(group_node, ShaderNodeGroup): return False group_node_tree = group_node.node_tree if not group_node_tree: return False if group_node_tree.name != shader.OUTPUT_GROUP_NAME: return False return bool(self.get("enabled")) def set_enabled(self, value: object) -> None: material = self.find_material() if not value: if self.get("enabled") and ( bpy.app.version >= (5, 0, 0) or material.use_nodes ): ops.vrm.convert_mtoon1_to_bsdf_principled(material_name=material.name) self["enabled"] = False return if bpy.app.version < (5, 0, 0) and not material.use_nodes: material.use_nodes = True if self.enabled: return ops.vrm.convert_material_to_mtoon1(material_name=material.name) self["enabled"] = True self.setup_drivers() enabled: BoolProperty( # type: ignore[valid-type] name="Enable VRM MToon Material", get=get_enabled, set=set_enabled, ) export_shape_key_normals: BoolProperty( # type: ignore[valid-type] name="Export Shape Key Normals", ) def update_is_outline_material(self, _context: Context) -> None: self.set_bool( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_IS_OUTLINE_LABEL, self.is_outline_material, ) self.set_bool( shader.NORMAL_GROUP_NAME, shader.NORMAL_GROUP_IS_OUTLINE_LABEL, self.is_outline_material, ) self.set_bool( shader.OUTPUT_GROUP_NAME, shader.OUTPUT_GROUP_DOUBLE_SIDED_LABEL, False if self.is_outline_material else self.double_sided, ) is_outline_material: BoolProperty( # type: ignore[valid-type] update=update_is_outline_material, ) outline_material: PointerProperty( # type: ignore[valid-type] type=Material, ) def all_texture_info(self) -> list[Mtoon1TextureInfoPropertyGroup]: return [ self.pbr_metallic_roughness.base_color_texture, self.normal_texture, self.emissive_texture, self.extensions.vrmc_materials_mtoon.shade_multiply_texture, self.extensions.vrmc_materials_mtoon.shading_shift_texture, self.extensions.vrmc_materials_mtoon.matcap_texture, self.extensions.vrmc_materials_mtoon.rim_multiply_texture, self.extensions.vrmc_materials_mtoon.outline_width_multiply_texture, self.extensions.vrmc_materials_mtoon.uv_animation_mask_texture, ] def all_textures( self, *, downgrade_to_mtoon0: bool ) -> list[Union[Mtoon0TexturePropertyGroup, Mtoon1TexturePropertyGroup]]: # TODO: remove code duplication result: list[Union[Mtoon0TexturePropertyGroup, Mtoon1TexturePropertyGroup]] = [] result.extend( [ self.pbr_metallic_roughness.base_color_texture.index, self.extensions.vrmc_materials_mtoon.shade_multiply_texture.index, self.normal_texture.index, ] ) if downgrade_to_mtoon0: result.extend( [ self.mtoon0_receive_shadow_texture, self.mtoon0_shading_grade_texture, ] ) result.append(self.emissive_texture.index) if not downgrade_to_mtoon0: result.append( self.extensions.vrmc_materials_mtoon.shading_shift_texture.index ) result.extend( [ self.extensions.vrmc_materials_mtoon.matcap_texture.index, self.extensions.vrmc_materials_mtoon.rim_multiply_texture.index, self.extensions.vrmc_materials_mtoon.outline_width_multiply_texture.index, self.extensions.vrmc_materials_mtoon.uv_animation_mask_texture.index, ] ) return result show_expanded_mtoon0: BoolProperty( # type: ignore[valid-type] name="Show MToon 0.0 Options", ) mtoon0_front_cull_mode: BoolProperty( # type: ignore[valid-type] name="Front Face Culling", ) mtoon0_outline_scaled_max_distance: FloatProperty( # type: ignore[valid-type] name="Outline Width Scaled Max Distance", min=1, default=1, max=10, ) mtoon0_light_color_attenuation: FloatProperty( # type: ignore[valid-type] name="LightColor Attenuation", min=0, default=0, max=1, ) mtoon0_receive_shadow_texture: PointerProperty( # type: ignore[valid-type] type=Mtoon0ReceiveShadowTexturePropertyGroup ) mtoon0_receive_shadow_rate: FloatProperty( # type: ignore[valid-type] min=0, default=1, max=1, ) mtoon0_shading_grade_texture: PointerProperty( # type: ignore[valid-type] type=Mtoon0ShadingGradeTexturePropertyGroup ) mtoon0_shading_grade_rate: FloatProperty( # type: ignore[valid-type] min=0, default=1, max=1, ) mtoon0_rim_lighting_mix: FloatProperty( # type: ignore[valid-type] name="Rim LightingMix (MToon 0.0)", min=0, default=0, max=1, ) def get_mtoon0_render_queue_and_clamp(self) -> int: return int(self.mtoon0_render_queue) def set_mtoon0_render_queue_and_clamp(self, value: int) -> None: # https://github.com/Santarh/MToon/blob/42b03163459ac8e6b7aee08070d0f4f912035069/MToon/Scripts/Utils.cs#L74-L113 if self.alpha_mode == self.ALPHA_MODE_OPAQUE.identifier: mtoon0_render_queue = 2000 elif self.alpha_mode == self.ALPHA_MODE_MASK.identifier: mtoon0_render_queue = 2450 elif not self.extensions.vrmc_materials_mtoon.transparent_with_z_write: mtoon0_render_queue = max(2951, min(3000, value)) else: mtoon0_render_queue = max(2501, min(2550, value)) if self.mtoon0_render_queue != mtoon0_render_queue: self.mtoon0_render_queue = mtoon0_render_queue # Set the Render Queue value for MToon0. Performs clamping when assigning values. # UniVRM performs clamping when setting values from UI or when changing # Alpha Mode etc., so use this to match that behavior. mtoon0_render_queue_and_clamp: IntProperty( # type: ignore[valid-type] name="Render Queue", get=get_mtoon0_render_queue_and_clamp, set=set_mtoon0_render_queue_and_clamp, ) # Set the Render Queue value for MToon0. Does not perform clamping when # assigning values. # UniVRM does not perform clamping during VRM0 import or export, # so use this during import or export to match that behavior. mtoon0_render_queue: IntProperty( # type: ignore[valid-type] name="Render Queue", default=2000, ) def setup_drivers(self) -> None: material = self.find_material() for texture_info in self.all_texture_info(): texture_info.setup_drivers(material) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. addon_version: Sequence[int] # type: ignore[no-redef] pbr_metallic_roughness: ( # type: ignore[no-redef] Mtoon1PbrMetallicRoughnessPropertyGroup ) alpha_mode_blend_method_hashed: bool # type: ignore[no-redef] alpha_mode: str # type: ignore[no-redef] double_sided: bool # type: ignore[no-redef] alpha_cutoff: float # type: ignore[no-redef] normal_texture: Mtoon1NormalTextureInfoPropertyGroup # type: ignore[no-redef] emissive_texture: ( # type: ignore[no-redef] Mtoon1EmissiveTextureInfoPropertyGroup ) emissive_factor: Sequence[float] # type: ignore[no-redef] extensions: Mtoon1MaterialExtensionsPropertyGroup # type: ignore[no-redef] enabled: bool # type: ignore[no-redef] export_shape_key_normals: bool # type: ignore[no-redef] is_outline_material: bool # type: ignore[no-redef] outline_material: Optional[Material] # type: ignore[no-redef] show_expanded_mtoon0: bool # type: ignore[no-redef] mtoon0_front_cull_mode: bool # type: ignore[no-redef] mtoon0_outline_scaled_max_distance: float # type: ignore[no-redef] mtoon0_light_color_attenuation: float # type: ignore[no-redef] mtoon0_receive_shadow_texture: ( # type: ignore[no-redef] Mtoon0ReceiveShadowTexturePropertyGroup ) mtoon0_receive_shadow_rate: float # type: ignore[no-redef] mtoon0_shading_grade_texture: ( # type: ignore[no-redef] Mtoon0ShadingGradeTexturePropertyGroup ) mtoon0_shading_grade_rate: float # type: ignore[no-redef] mtoon0_rim_lighting_mix: float # type: ignore[no-redef] mtoon0_render_queue_and_clamp: int # type: ignore[no-redef] mtoon0_render_queue: int # type: ignore[no-redef] def reset_shader_node_group( context: Context, material: Material, *, reset_material_node_tree: bool, reset_node_groups: bool, ) -> None: gltf = get_material_mtoon1_extension(material) mtoon = gltf.extensions.vrmc_materials_mtoon base_color_factor = list(gltf.pbr_metallic_roughness.base_color_factor) base_color_texture = gltf.pbr_metallic_roughness.base_color_texture.backup() alpha_mode_blend_method_hashed = gltf.alpha_mode_blend_method_hashed alpha_mode = gltf.alpha_mode double_sided = gltf.double_sided alpha_cutoff = gltf.alpha_cutoff normal_texture = gltf.normal_texture.backup() normal_texture_scale = gltf.normal_texture.scale emissive_texture = gltf.emissive_texture.backup() emissive_factor = list(gltf.emissive_factor) export_shape_key_normals = gltf.export_shape_key_normals emissive_strength = ( gltf.extensions.khr_materials_emissive_strength.emissive_strength ) transparent_with_z_write = mtoon.transparent_with_z_write render_queue_offset_number = mtoon.render_queue_offset_number shade_multiply_texture = mtoon.shade_multiply_texture.backup() shade_color_factor = list(mtoon.shade_color_factor) shading_shift_texture = mtoon.shading_shift_texture.backup() shading_shift_texture_scale = mtoon.shading_shift_texture.scale shading_shift_factor = mtoon.shading_shift_factor shading_toony_factor = mtoon.shading_toony_factor gi_equalization_factor = mtoon.gi_equalization_factor matcap_factor = list(mtoon.matcap_factor) matcap_texture = mtoon.matcap_texture.backup() parametric_rim_color_factor = list(mtoon.parametric_rim_color_factor) rim_multiply_texture = mtoon.rim_multiply_texture.backup() rim_lighting_mix_factor = mtoon.rim_lighting_mix_factor parametric_rim_fresnel_power_factor = mtoon.parametric_rim_fresnel_power_factor parametric_rim_lift_factor = mtoon.parametric_rim_lift_factor enable_outline_preview = mtoon.enable_outline_preview outline_width_mode = mtoon.outline_width_mode outline_width_factor = mtoon.outline_width_factor outline_width_multiply_texture = mtoon.outline_width_multiply_texture.backup() outline_color_factor = list(mtoon.outline_color_factor) outline_lighting_mix_factor = mtoon.outline_lighting_mix_factor uv_animation_mask_texture = mtoon.uv_animation_mask_texture.backup() uv_animation_scroll_x_speed_factor = mtoon.uv_animation_scroll_x_speed_factor uv_animation_scroll_y_speed_factor = mtoon.uv_animation_scroll_y_speed_factor uv_animation_rotation_speed_factor = mtoon.uv_animation_rotation_speed_factor if reset_material_node_tree: shader.load_mtoon1_shader( context, material, reset_node_groups=reset_node_groups, ) outline_material = gltf.outline_material if outline_material: shader.load_mtoon1_shader( context, outline_material, reset_node_groups=reset_node_groups, ) gltf.is_outline_material = False outline_material = gltf.outline_material if outline_material: get_material_mtoon1_extension(outline_material).is_outline_material = True gltf.pbr_metallic_roughness.base_color_factor = base_color_factor gltf.pbr_metallic_roughness.base_color_texture.restore(base_color_texture) gltf.alpha_mode_blend_method_hashed = alpha_mode_blend_method_hashed gltf.alpha_mode = alpha_mode gltf.double_sided = double_sided gltf.alpha_cutoff = alpha_cutoff gltf.normal_texture.restore(normal_texture) gltf.normal_texture.scale = normal_texture_scale gltf.emissive_texture.restore(emissive_texture) gltf.emissive_factor = emissive_factor gltf.export_shape_key_normals = export_shape_key_normals gltf.extensions.khr_materials_emissive_strength.emissive_strength = ( emissive_strength ) mtoon.transparent_with_z_write = transparent_with_z_write mtoon.render_queue_offset_number = render_queue_offset_number mtoon.shade_multiply_texture.restore(shade_multiply_texture) mtoon.shade_color_factor = shade_color_factor mtoon.shading_shift_texture.restore(shading_shift_texture) mtoon.shading_shift_texture.scale = shading_shift_texture_scale mtoon.shading_shift_factor = shading_shift_factor mtoon.shading_toony_factor = shading_toony_factor mtoon.gi_equalization_factor = gi_equalization_factor mtoon.matcap_factor = matcap_factor mtoon.matcap_texture.restore(matcap_texture) mtoon.parametric_rim_color_factor = parametric_rim_color_factor mtoon.rim_multiply_texture.restore(rim_multiply_texture) mtoon.rim_lighting_mix_factor = rim_lighting_mix_factor mtoon.parametric_rim_fresnel_power_factor = parametric_rim_fresnel_power_factor mtoon.parametric_rim_lift_factor = parametric_rim_lift_factor mtoon.enable_outline_preview = enable_outline_preview mtoon.outline_width_mode = outline_width_mode mtoon.outline_width_factor = outline_width_factor mtoon.outline_width_multiply_texture.restore(outline_width_multiply_texture) mtoon.outline_color_factor = outline_color_factor mtoon.outline_lighting_mix_factor = outline_lighting_mix_factor mtoon.uv_animation_mask_texture.restore(uv_animation_mask_texture) mtoon.uv_animation_scroll_x_speed_factor = uv_animation_scroll_x_speed_factor mtoon.uv_animation_scroll_y_speed_factor = uv_animation_scroll_y_speed_factor mtoon.uv_animation_rotation_speed_factor = uv_animation_rotation_speed_factor gltf.setup_drivers() gltf.addon_version = get_addon_version() def get_material_mtoon1_extension(material: Material) -> Mtoon1MaterialPropertyGroup: from ..extension import get_material_extension mtoon1: Mtoon1MaterialPropertyGroup = get_material_extension(material).mtoon1 return mtoon1 def setup_drivers(context: Context) -> None: for material in context.blend_data.materials: get_material_mtoon1_extension(material).setup_drivers() shader.setup_frame_count_driver(context) def link_or_unlink_gltf_material_nodes( context: Context, material: Material, *, link: bool ) -> None: TextureTraceablePropertyGroup.link_or_unlink_nodes( material, "Mtoon1Material.CompatibleShader", ShaderNodeBsdfPrincipled, "BSDF", StaticNodeSocketTarget( in_node_name="Mtoon1Material.CompatibleMaterialOutput", in_node_type=ShaderNodeOutputMaterial, in_socket_name="Surface", ), link=link, ) output_group = context.blend_data.node_groups.get(shader.OUTPUT_GROUP_NAME) if output_group is None: return for from_node_name, to_node_name in [ ( "EeveeMaterialOutputSwitch", "EeveeMaterialOutput", ), ( "CyclesMaterialOutputSwitch", "CyclesMaterialOutput", ), ]: from_node = output_group.nodes.get(from_node_name) if not from_node or not isinstance(from_node, NodeReroute): continue from_socket = from_node.outputs[0] to_node = output_group.nodes.get(to_node_name) if not to_node or not isinstance(to_node, ShaderNodeOutputMaterial): continue to_socket = to_node.inputs["Surface"] existing_link = next( ( link for link in output_group.links if link.from_node == from_node and link.from_socket == from_socket and link.to_node == to_node and link.to_socket == to_socket ), None, ) if existing_link: if not link: output_group.links.remove(existing_link) elif link: output_group.links.new( from_socket, to_socket, )