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

1159 lines
43 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import math
from collections.abc import Set as AbstractSet
from dataclasses import dataclass
from pathlib import Path
from sys import float_info
from typing import TYPE_CHECKING, Optional
import bpy
from bpy.app.translations import pgettext
from bpy.props import BoolProperty, EnumProperty, StringProperty
from bpy.types import (
Context,
Event,
Material,
Mesh,
Node,
NodesModifier,
Object,
Operator,
ShaderNodeMapping,
)
from bpy_extras.io_utils import ImportHelper
from bpy_extras.node_shader_utils import PrincipledBSDFWrapper
from ...common import convert, shader
from ...common.logger import get_logger
from ...common.preferences import get_preferences
from .. import search
from ..extension import get_material_extension
from .property_group import (
Mtoon0ReceiveShadowTexturePropertyGroup,
Mtoon0ShadingGradeTexturePropertyGroup,
Mtoon1BaseColorTexturePropertyGroup,
Mtoon1EmissiveTexturePropertyGroup,
Mtoon1MatcapTexturePropertyGroup,
Mtoon1MaterialPropertyGroup,
Mtoon1NormalTexturePropertyGroup,
Mtoon1OutlineWidthMultiplyTexturePropertyGroup,
Mtoon1RimMultiplyTexturePropertyGroup,
Mtoon1SamplerPropertyGroup,
Mtoon1ShadeMultiplyTexturePropertyGroup,
Mtoon1ShadingShiftTexturePropertyGroup,
Mtoon1TextureInfoPropertyGroup,
Mtoon1UvAnimationMaskTexturePropertyGroup,
reset_shader_node_group,
)
logger = get_logger(__name__)
class VRM_OT_convert_material_to_mtoon1(Operator):
bl_idname = "vrm.convert_material_to_mtoon1"
bl_label = "Convert Material to MToon 1.0"
bl_description = "Convert Material to MToon 1.0"
bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}
material_name: StringProperty( # type: ignore[valid-type]
options={"HIDDEN"}
)
def execute(self, context: Context) -> set[str]:
material = context.blend_data.materials.get(self.material_name)
if not isinstance(material, Material):
return {"CANCELLED"}
self.convert_material_to_mtoon1(context, material)
return {"FINISHED"}
@staticmethod
def assign_mtoon_unversioned_image(
context: Context,
texture_info: Mtoon1TextureInfoPropertyGroup,
image_name_and_sampler_type: Optional[tuple[str, int, int]],
uv_offset: tuple[float, float],
uv_scale: tuple[float, float],
) -> None:
texture_info.extensions.khr_texture_transform.offset = uv_offset
texture_info.extensions.khr_texture_transform.scale = uv_scale
if image_name_and_sampler_type is None:
return
image_name, wrap_number, filter_number = image_name_and_sampler_type
image = context.blend_data.images.get(image_name)
if not image:
return
texture_info.index.source = image
texture_info.index.sampler.mag_filter = (
Mtoon1SamplerPropertyGroup.mag_filter_enum.value_to_identifier(
filter_number, Mtoon1SamplerPropertyGroup.MAG_FILTER_DEFAULT.identifier
)
)
texture_info.index.sampler.min_filter = (
Mtoon1SamplerPropertyGroup.min_filter_enum.value_to_identifier(
filter_number, Mtoon1SamplerPropertyGroup.MIN_FILTER_DEFAULT.identifier
)
)
wrap = Mtoon1SamplerPropertyGroup.wrap_enum.value_to_identifier(
wrap_number, Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.identifier
)
texture_info.index.sampler.wrap_s = wrap
texture_info.index.sampler.wrap_t = wrap
def convert_material_to_mtoon1(self, context: Context, material: Material) -> None:
node, legacy_shader_name = search.legacy_shader_node(material)
if isinstance(node, Node) and legacy_shader_name == "MToon_unversioned":
self.convert_mtoon_unversioned_to_mtoon1(context, material, node)
return
principled_bsdf = PrincipledBSDFWrapper(material)
if not principled_bsdf.node_principled_bsdf:
reset_shader_node_group(
context,
material,
reset_material_node_tree=True,
reset_node_groups=False,
)
return
base_color_factor = (
principled_bsdf.base_color[0],
principled_bsdf.base_color[1],
principled_bsdf.base_color[2],
principled_bsdf.alpha,
)
base_color_texture_node = principled_bsdf.base_color_texture
if base_color_texture_node:
base_color_texture_image = base_color_texture_node.image
else:
base_color_texture_image = None
emissive_factor = principled_bsdf.emission_color
emission_color_texture_node = principled_bsdf.emission_color_texture
if emission_color_texture_node:
emissive_texture_image = emission_color_texture_node.image
else:
emissive_texture_image = None
emissive_strength = principled_bsdf.emission_strength
normalmap_texture_node = principled_bsdf.normalmap_texture
if normalmap_texture_node:
normal_texture_image = normalmap_texture_node.image
normal_texture_scale = principled_bsdf.normalmap_strength
else:
normal_texture_image = None
normal_texture_scale = None
reset_shader_node_group(
context,
material,
reset_material_node_tree=True,
reset_node_groups=False,
)
gltf = get_material_extension(material).mtoon1
gltf.pbr_metallic_roughness.base_color_factor = base_color_factor
if base_color_texture_image:
gltf.pbr_metallic_roughness.base_color_texture.index.source = (
base_color_texture_image
)
gltf.emissive_factor = emissive_factor
gltf.extensions.khr_materials_emissive_strength.emissive_strength = (
emissive_strength
)
if emissive_texture_image:
gltf.emissive_texture.index.source = emissive_texture_image
if normal_texture_image:
gltf.normal_texture.index.source = normal_texture_image
if normal_texture_scale is not None:
gltf.normal_texture.scale = normal_texture_scale
def convert_mtoon_unversioned_to_mtoon1(
self,
context: Context,
material: Material,
node: Node,
) -> None:
transparent_with_z_write = False
alpha_cutoff: Optional[float] = 0.5
if material.blend_method == "OPAQUE":
alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_OPAQUE.identifier
elif material.blend_method == "CLIP":
alpha_cutoff = shader.get_float_value(node, "CutoffRate", 0, float_info.max)
if alpha_cutoff is None:
alpha_cutoff = 0.5
alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_MASK.identifier
else:
alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_BLEND.identifier
transparent_with_z_write_float = shader.get_float_value(
node, "TransparentWithZWrite"
)
transparent_with_z_write = (
transparent_with_z_write_float is not None
and math.fabs(transparent_with_z_write_float) >= float_info.epsilon
)
base_color_factor = shader.get_rgba_value(node, "DiffuseColor", 0.0, 1.0) or (
0,
0,
0,
1,
)
base_color_texture = shader.get_image_name_and_sampler_type(
node,
"MainTexture",
)
uv_offset = (0.0, 0.0)
uv_scale = (1.0, 1.0)
main_texture_socket = node.inputs.get("MainTexture")
if (
main_texture_socket
and main_texture_socket.links
and main_texture_socket.links[0]
and main_texture_socket.links[0].from_node
and main_texture_socket.links[0].from_node.inputs
and main_texture_socket.links[0].from_node.inputs[0]
and main_texture_socket.links[0].from_node.inputs[0].links
and main_texture_socket.links[0].from_node.inputs[0].links[0]
and main_texture_socket.links[0].from_node.inputs[0].links[0].from_node
):
mapping_node = (
main_texture_socket.links[0].from_node.inputs[0].links[0].from_node
)
if isinstance(mapping_node, ShaderNodeMapping):
location_socket = mapping_node.inputs.get("Location")
if location_socket and isinstance(
location_socket, shader.VECTOR_SOCKET_CLASSES
):
uv_offset = (
float(location_socket.default_value[0]),
float(location_socket.default_value[1]),
)
scale_socket = mapping_node.inputs.get("Scale")
if scale_socket and isinstance(
scale_socket, shader.VECTOR_SOCKET_CLASSES
):
uv_scale = (
float(scale_socket.default_value[0]),
float(scale_socket.default_value[1]),
)
shade_color_factor = shader.get_rgb_value(node, "ShadeColor", 0.0, 1.0) or (
0,
0,
0,
)
shade_multiply_texture = shader.get_image_name_and_sampler_type(
node,
"ShadeTexture",
)
normal_texture = shader.get_image_name_and_sampler_type(
node,
"NormalmapTexture",
)
if not normal_texture:
normal_texture = shader.get_image_name_and_sampler_type(
node,
"NomalmapTexture",
)
normal_texture_scale = shader.get_float_value(node, "BumpScale")
shading_shift_0x = shader.get_float_value(node, "ShadeShift")
if shading_shift_0x is None:
shading_shift_0x = 0.0
shading_toony_0x = shader.get_float_value(node, "ShadeToony")
if shading_toony_0x is None:
shading_toony_0x = 0.0
shading_shift_factor = convert.mtoon_shading_shift_0_to_1(
shading_toony_0x, shading_shift_0x
)
shading_toony_factor = convert.mtoon_shading_toony_0_to_1(
shading_toony_0x, shading_shift_0x
)
gi_equalization_0x = shader.get_float_value(node, "IndirectLightIntensity")
gi_equalization_factor = None
if gi_equalization_0x is not None:
gi_equalization_factor = convert.mtoon_intensity_to_gi_equalization(
gi_equalization_0x
)
emissive_factor = shader.get_rgb_value(node, "EmissionColor", 0.0, 1.0) or (
0,
0,
0,
)
emissive_texture = shader.get_image_name_and_sampler_type(
node,
"Emission_Texture",
)
matcap_texture = shader.get_image_name_and_sampler_type(
node,
"SphereAddTexture",
)
parametric_rim_color_factor = shader.get_rgb_value(node, "RimColor", 0.0, 1.0)
parametric_rim_fresnel_power_factor = shader.get_float_value(
node, "RimFresnelPower", 0.0, float_info.max
)
parametric_rim_lift_factor = shader.get_float_value(node, "RimLift")
rim_multiply_texture = shader.get_image_name_and_sampler_type(
node,
"RimTexture",
)
centimeter_to_meter = 0.01
one_hundredth = 0.01
outline_width_mode_float = shader.get_float_value(node, "OutlineWidthMode")
outline_width = shader.get_float_value(node, "OutlineWidth")
if outline_width is None:
outline_width = 0.0
outline_width_multiply_texture = shader.get_image_name_and_sampler_type(
node,
"OutlineWidthTexture",
)
outline_color_factor = shader.get_rgb_value(node, "OutlineColor", 0.0, 1.0) or (
0,
0,
0,
)
outline_color_mode = shader.get_float_value(node, "OutlineColorMode")
if outline_color_mode is not None:
outline_color_mode = round(outline_color_mode)
else:
outline_color_mode = 0
outline_lighting_mix_factor = 0.0
if outline_color_mode == 1:
outline_lighting_mix_factor = (
shader.get_float_value(node, "OutlineLightingMix") or 1.0
)
uv_animation_mask_texture = shader.get_image_name_and_sampler_type(
node,
"UV_Animation_Mask_Texture",
)
uv_animation_rotation_speed_factor = shader.get_float_value(
node, "UV_Scroll_Rotation"
)
uv_animation_scroll_x_speed_factor = shader.get_float_value(node, "UV_Scroll_X")
uv_animation_scroll_y_speed_factor = shader.get_float_value(node, "UV_Scroll_Y")
if uv_animation_scroll_y_speed_factor is not None:
uv_animation_scroll_y_speed_factor *= -1
shader.load_mtoon1_shader(context, material, reset_node_groups=True)
gltf = get_material_extension(material).mtoon1
mtoon = gltf.extensions.vrmc_materials_mtoon
gltf.alpha_mode = alpha_mode
gltf.alpha_cutoff = alpha_cutoff
mtoon.transparent_with_z_write = transparent_with_z_write
gltf.pbr_metallic_roughness.base_color_factor = base_color_factor
self.assign_mtoon_unversioned_image(
context,
gltf.pbr_metallic_roughness.base_color_texture,
base_color_texture,
uv_offset,
uv_scale,
)
mtoon.shade_color_factor = shade_color_factor
self.assign_mtoon_unversioned_image(
context,
mtoon.shade_multiply_texture,
shade_multiply_texture,
uv_offset,
uv_scale,
)
self.assign_mtoon_unversioned_image(
context,
gltf.normal_texture,
normal_texture,
uv_offset,
uv_scale,
)
if normal_texture_scale is not None:
gltf.normal_texture.scale = normal_texture_scale
mtoon.shading_shift_factor = shading_shift_factor
mtoon.shading_toony_factor = shading_toony_factor
if gi_equalization_factor is not None:
mtoon.gi_equalization_factor = gi_equalization_factor
gltf.emissive_factor = emissive_factor
self.assign_mtoon_unversioned_image(
context,
gltf.emissive_texture,
emissive_texture,
uv_offset,
uv_scale,
)
self.assign_mtoon_unversioned_image(
context,
mtoon.matcap_texture,
matcap_texture,
(0, 0),
(1, 1),
)
if parametric_rim_color_factor is not None:
mtoon.parametric_rim_color_factor = parametric_rim_color_factor
if parametric_rim_fresnel_power_factor is not None:
mtoon.parametric_rim_fresnel_power_factor = (
parametric_rim_fresnel_power_factor
)
if parametric_rim_lift_factor is not None:
mtoon.parametric_rim_lift_factor = parametric_rim_lift_factor
self.assign_mtoon_unversioned_image(
context,
mtoon.rim_multiply_texture,
rim_multiply_texture,
uv_offset,
uv_scale,
)
# https://github.com/vrm-c/UniVRM/blob/7c9919ef47a25c04100a2dcbe6a75dff49ef4857/Assets/VRM10/Runtime/Migration/Materials/MigrationMToonMaterial.cs#L287-L290
mtoon.rim_lighting_mix_factor = 1.0
centimeter_to_meter = 0.01
one_hundredth = 0.01
mtoon.enable_outline_preview = get_preferences(
context
).enable_mtoon_outline_preview
if outline_width_mode_float is not None:
outline_width_mode = round(outline_width_mode_float)
else:
outline_width_mode = 0
if outline_width_mode == 1:
mtoon.outline_width_mode = (
mtoon.OUTLINE_WIDTH_MODE_WORLD_COORDINATES.identifier
)
mtoon.outline_width_factor = max(0.0, outline_width * centimeter_to_meter)
elif outline_width_mode == 2:
mtoon.outline_width_mode = (
mtoon.OUTLINE_WIDTH_MODE_SCREEN_COORDINATES.identifier
)
mtoon.outline_width_factor = max(0.0, outline_width * one_hundredth * 0.5)
else:
mtoon.outline_width_mode = mtoon.OUTLINE_WIDTH_MODE_NONE.identifier
self.assign_mtoon_unversioned_image(
context,
mtoon.outline_width_multiply_texture,
outline_width_multiply_texture,
uv_offset,
uv_scale,
)
mtoon.outline_color_factor = outline_color_factor
mtoon.outline_lighting_mix_factor = 0.0
if outline_color_mode == 1:
mtoon.outline_lighting_mix_factor = outline_lighting_mix_factor
self.assign_mtoon_unversioned_image(
context,
mtoon.uv_animation_mask_texture,
uv_animation_mask_texture,
uv_offset,
uv_scale,
)
if uv_animation_rotation_speed_factor is not None:
mtoon.uv_animation_rotation_speed_factor = (
uv_animation_rotation_speed_factor
)
if uv_animation_scroll_x_speed_factor is not None:
mtoon.uv_animation_scroll_x_speed_factor = (
uv_animation_scroll_x_speed_factor
)
if uv_animation_scroll_y_speed_factor is not None:
mtoon.uv_animation_scroll_y_speed_factor = (
uv_animation_scroll_y_speed_factor
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
material_name: str # type: ignore[no-redef]
class VRM_OT_convert_mtoon1_to_bsdf_principled(Operator):
bl_idname = "vrm.convert_mtoon1_to_bsdf_principled"
bl_label = "Convert MToon 1.0 to Principled BSDF"
bl_description = "Convert MToon 1.0 to Principled BSDF"
bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}
material_name: StringProperty( # type: ignore[valid-type]
options={"HIDDEN"}
)
def execute(self, context: Context) -> set[str]:
material = context.blend_data.materials.get(self.material_name)
if not isinstance(material, Material):
return {"CANCELLED"}
self.convert_mtoon1_to_bsdf_principled(material)
return {"FINISHED"}
def convert_mtoon1_to_bsdf_principled(self, material: Material) -> None:
if bpy.app.version < (5, 0, 0) and not material.use_nodes:
material.use_nodes = True
shader.clear_node_tree(material.node_tree, clear_inputs_outputs=True)
if not material.node_tree:
logger.error("%s's node tree is None", material.name)
return
principled_bsdf = PrincipledBSDFWrapper(material, is_readonly=False)
gltf = get_material_extension(material).mtoon1
principled_bsdf.base_color = gltf.pbr_metallic_roughness.base_color_factor[:3]
base_color_texture_image = (
gltf.pbr_metallic_roughness.base_color_texture.index.source
)
if base_color_texture_image:
base_color_texture_node = principled_bsdf.base_color_texture
if base_color_texture_node:
base_color_texture_node.image = base_color_texture_image
principled_bsdf.alpha = gltf.pbr_metallic_roughness.base_color_factor[3]
principled_bsdf.emission_color = list(gltf.emissive_factor)
principled_bsdf.emission_strength = (
gltf.extensions.khr_materials_emissive_strength.emissive_strength
)
emissive_texture_image = gltf.emissive_texture.index.source
if emissive_texture_image:
emission_color_texture_node = principled_bsdf.emission_color_texture
if emission_color_texture_node:
emission_color_texture_node.image = emissive_texture_image
normal_texture_image = gltf.normal_texture.index.source
if normal_texture_image:
normalmap_texture_node = principled_bsdf.normalmap_texture
if normalmap_texture_node:
normalmap_texture_node.image = normal_texture_image
normal_scale = gltf.normal_texture.scale
if abs(normal_scale - 1) >= float_info.epsilon:
principled_bsdf.normalmap_strength = gltf.normal_texture.scale
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
material_name: str # type: ignore[no-redef]
class VRM_OT_reset_mtoon1_material_shader_node_tree(Operator):
bl_idname = "vrm.reset_mtoon1_material_shader_node_group"
bl_label = "Reset Shader Nodes"
bl_description = "Reset MToon 1.0 Material Shader Node Tree"
bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}
material_name: StringProperty( # type: ignore[valid-type]
options={"HIDDEN"}
)
def execute(self, context: Context) -> set[str]:
material = context.blend_data.materials.get(self.material_name)
if not isinstance(material, Material):
return {"CANCELLED"}
reset_shader_node_group(
context, material, reset_material_node_tree=True, reset_node_groups=True
)
return {"FINISHED"}
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
material_name: str # type: ignore[no-redef]
class VRM_OT_import_mtoon1_texture_image_file(Operator, ImportHelper):
bl_idname = "vrm.import_mtoon1_texture_image_file"
bl_label = "Open"
bl_description = "Import Texture Image File"
bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}
filepath: StringProperty( # type: ignore[valid-type]
options={"HIDDEN"},
default="",
)
filter_glob: StringProperty( # type: ignore[valid-type]
options={"HIDDEN"},
# https://docs.blender.org/api/2.83/Image.html#Image.file_format
default=(
"*.bmp"
";*.sgi"
";*.bw"
";*.rgb"
";*.rgba"
";*.png"
";*.jpg"
";*.jpeg"
";*.jp2"
";*.tga"
";*.cin"
";*.dpx"
";*.exr"
";*.hdr"
";*.tif"
";*.tiff"
),
)
material_name: StringProperty( # type: ignore[valid-type]
options={"HIDDEN"},
)
target_texture_items = (
(Mtoon1BaseColorTexturePropertyGroup.__name__, "", "", "NONE", 0),
(Mtoon1ShadeMultiplyTexturePropertyGroup.__name__, "", "", "NONE", 1),
(Mtoon1NormalTexturePropertyGroup.__name__, "", "", "NONE", 2),
(Mtoon1ShadingShiftTexturePropertyGroup.__name__, "", "", "NONE", 3),
(Mtoon1EmissiveTexturePropertyGroup.__name__, "", "", "NONE", 4),
(Mtoon1RimMultiplyTexturePropertyGroup.__name__, "", "", "NONE", 5),
(Mtoon1MatcapTexturePropertyGroup.__name__, "", "", "NONE", 6),
(
Mtoon1OutlineWidthMultiplyTexturePropertyGroup.__name__,
"",
"",
"NONE",
7,
),
(Mtoon1UvAnimationMaskTexturePropertyGroup.__name__, "", "", "NONE", 8),
(Mtoon0ReceiveShadowTexturePropertyGroup.__name__, "", "", "NONE", 9),
(Mtoon0ShadingGradeTexturePropertyGroup.__name__, "", "", "NONE", 10),
)
target_texture: EnumProperty( # type: ignore[valid-type]
options={"HIDDEN"},
items=target_texture_items,
name="Target Texture",
)
def execute(self, context: Context) -> set[str]:
filepath = self.filepath
if not filepath or not Path(filepath).exists():
return {"CANCELLED"}
last_images_len = len(context.blend_data.images)
image = context.blend_data.images.load(filepath, check_existing=True)
created = last_images_len < len(context.blend_data.images)
material = context.blend_data.materials.get(self.material_name)
if not isinstance(material, Material):
return {"FINISHED"}
gltf = get_material_extension(material).mtoon1
mtoon = gltf.extensions.vrmc_materials_mtoon
for texture in [
gltf.pbr_metallic_roughness.base_color_texture.index,
mtoon.shade_multiply_texture.index,
gltf.normal_texture.index,
mtoon.shading_shift_texture.index,
gltf.emissive_texture.index,
mtoon.rim_multiply_texture.index,
mtoon.matcap_texture.index,
mtoon.outline_width_multiply_texture.index,
mtoon.uv_animation_mask_texture.index,
gltf.mtoon0_receive_shadow_texture,
gltf.mtoon0_shading_grade_texture,
]:
if self.target_texture != type(texture).__name__:
continue
texture.source = image
if created:
image.colorspace_settings.name = type(texture).colorspace
return {"FINISHED"}
return {"CANCELLED"}
def invoke(self, context: Context, event: Event) -> set[str]:
self.filepath = ""
return ImportHelper.invoke(self, context, event)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
filepath: str # type: ignore[no-redef]
filter_glob: str # type: ignore[no-redef]
material_name: str # type: ignore[no-redef]
target_texture: str # type: ignore[no-redef]
@dataclass(frozen=True)
class NodesModifierInputKey:
geometry_key: str
material_key: str
outline_material_key: str
outline_width_mode_key: str
outline_width_factor_key: str
outline_width_multiply_texture_key: str
outline_width_multiply_texture_exists_key: str
outline_width_multiply_texture_uv_key: str
outline_width_multiply_texture_uv_offset_x_key: str
outline_width_multiply_texture_uv_offset_y_key: str
outline_width_multiply_texture_uv_scale_x_key: str
outline_width_multiply_texture_uv_scale_y_key: str
extrude_mesh_individual_key: str
object_key: str
enabled_key: str
def outline_width_multiply_texture_uv_use_attribute_key(self) -> str:
return self.outline_width_multiply_texture_uv_key + "_use_attribute"
def outline_width_multiply_texture_uv_attribute_name_key(self) -> str:
return self.outline_width_multiply_texture_uv_key + "_attribute_name"
def get_nodes_modifier_input_key(
modifier: NodesModifier,
) -> Optional[NodesModifierInputKey]:
node_group = modifier.node_group
if not node_group:
return None
if bpy.app.version < (4, 0):
keys = [i.identifier for i in node_group.inputs]
else:
from bpy.types import NodeTreeInterfaceSocket
keys = [
item.identifier
for item in node_group.interface.items_tree
if item.item_type == "SOCKET"
and isinstance(item, NodeTreeInterfaceSocket)
and item.in_out == "INPUT"
]
if len(keys) < 15:
return None
return NodesModifierInputKey(
geometry_key=keys[0],
material_key=keys[1],
outline_material_key=keys[2],
outline_width_mode_key=keys[3],
outline_width_factor_key=keys[4],
outline_width_multiply_texture_key=keys[5],
outline_width_multiply_texture_exists_key=keys[6],
outline_width_multiply_texture_uv_key=keys[7],
outline_width_multiply_texture_uv_offset_x_key=keys[8],
outline_width_multiply_texture_uv_offset_y_key=keys[9],
outline_width_multiply_texture_uv_scale_x_key=keys[10],
outline_width_multiply_texture_uv_scale_y_key=keys[11],
extrude_mesh_individual_key=keys[12],
object_key=keys[13],
enabled_key=keys[14],
)
def generate_mtoon1_outline_material_name(material: Material) -> str:
return f"MToon Outline ({material.name})"
class VRM_OT_refresh_mtoon1_outline(Operator):
bl_idname = "vrm.refresh_mtoon1_outline"
bl_label = "Refresh MToon 1.0 Outline Width Mode"
bl_description = "Import Texture Image File"
bl_options: AbstractSet[str] = {"UNDO"}
material_name: StringProperty( # type: ignore[valid-type]
options={"HIDDEN"}
)
create_modifier: BoolProperty( # type: ignore[valid-type]
options={"HIDDEN"}
)
@staticmethod
def assign(
context: Context,
material: Material,
obj: Object,
*,
create_modifier: bool,
) -> None:
shader.load_mtoon1_outline_geometry_node_group(context, reset_node_groups=False)
node_group = context.blend_data.node_groups.get(
shader.OUTLINE_GEOMETRY_GROUP_NAME
)
if not node_group:
return
mtoon = get_material_extension(material).mtoon1.extensions.vrmc_materials_mtoon
outline_width_mode_value = mtoon.outline_width_mode_enum.identifier_to_value(
mtoon.outline_width_mode,
mtoon.OUTLINE_WIDTH_MODE_NONE.value,
)
no_outline = (
not mtoon.enable_outline_preview
or outline_width_mode_value == mtoon.OUTLINE_WIDTH_MODE_NONE.value
or mtoon.outline_width_factor < float_info.epsilon
)
cleanup = no_outline
modifier = None
for search_modifier_name in list(obj.modifiers.keys()):
search_modifier = obj.modifiers.get(search_modifier_name)
if not search_modifier:
continue
if search_modifier.type != "NODES":
continue
if not isinstance(search_modifier, NodesModifier):
continue
if not search_modifier.node_group:
continue
if search_modifier.node_group.name != shader.OUTLINE_GEOMETRY_GROUP_NAME:
continue
input_key = get_nodes_modifier_input_key(search_modifier)
if input_key is None:
continue
search_material = search_modifier.get(input_key.material_key)
if not isinstance(search_material, Material):
continue
if search_material.name != material.name:
continue
if cleanup:
obj.modifiers.remove(search_modifier)
continue
modifier = search_modifier
cleanup = True
if no_outline:
return
if not modifier and not create_modifier:
mtoon.outline_width_mode = mtoon.OUTLINE_WIDTH_MODE_NONE.identifier
return
outline_material_name = generate_mtoon1_outline_material_name(material)
modifier_name = f"MToon Outline ({material.name})"
outline_material = get_material_extension(material).mtoon1.outline_material
reset_outline_material = False
if not outline_material:
reset_outline_material = True
outline_material = context.blend_data.materials.new(
name=outline_material_name
)
if bpy.app.version < (5, 0, 0) and not outline_material.use_nodes:
outline_material.use_nodes = True
if outline_material.diffuse_color[3] != 0.25:
outline_material.diffuse_color[3] = 0.25
if outline_material.roughness != 0:
outline_material.roughness = 0
shader.load_mtoon1_shader(
context, outline_material, reset_node_groups=False
)
get_material_extension(outline_material).mtoon1.is_outline_material = True
get_material_extension(material).mtoon1.outline_material = outline_material
if outline_material.name != outline_material_name:
outline_material.name = outline_material_name
if bpy.app.version < (5, 0, 0) and not outline_material.use_nodes:
outline_material.use_nodes = True
if not get_material_extension(outline_material).mtoon1.is_outline_material:
get_material_extension(outline_material).mtoon1.is_outline_material = True
if (
get_material_extension(outline_material).mtoon1.alpha_cutoff
!= get_material_extension(material).mtoon1.alpha_cutoff
):
get_material_extension(
outline_material
).mtoon1.alpha_cutoff = get_material_extension(material).mtoon1.alpha_cutoff
if (
get_material_extension(outline_material).mtoon1.alpha_mode
!= get_material_extension(material).mtoon1.alpha_mode
):
get_material_extension(
outline_material
).mtoon1.alpha_mode = get_material_extension(material).mtoon1.alpha_mode
if bpy.app.version < (4, 3) and outline_material.shadow_method != "NONE":
outline_material.shadow_method = "NONE"
if not outline_material.use_backface_culling:
outline_material.use_backface_culling = True
if outline_material.show_transparent_back:
outline_material.show_transparent_back = False
if not modifier:
new_modifier = obj.modifiers.new(modifier_name, "NODES")
if not isinstance(new_modifier, NodesModifier):
message = f"{type(new_modifier)} is not a NodesModifier"
raise AssertionError(message)
modifier = new_modifier
modifier.show_expanded = False
modifier.show_in_editmode = False
if modifier.name != modifier_name:
modifier.name = modifier_name
if modifier.node_group != node_group:
modifier.node_group = node_group
input_key = get_nodes_modifier_input_key(modifier)
if input_key is None:
return
modifier_input_changed = False
(
outline_width_multiply_texture_uv_offset_x,
outline_width_multiply_texture_uv_offset_y,
) = mtoon.outline_width_multiply_texture.extensions.khr_texture_transform.offset
(
outline_width_multiply_texture_uv_scale_x,
outline_width_multiply_texture_uv_scale_y,
) = mtoon.outline_width_multiply_texture.extensions.khr_texture_transform.scale
uv_layer_name = None
if isinstance((mesh_data := obj.data), Mesh):
uv_layer_name = next(
(
uv_layer.name
for uv_layer in mesh_data.uv_layers
if uv_layer and uv_layer.active_render
),
None,
)
if uv_layer_name is None:
uv_layer_name = "UVMap"
has_auto_smooth = tuple(bpy.app.version) < (4, 1)
outline_width_multiply_texture_uv_use_attribute = (
True if tuple(bpy.app.version) >= (4, 2) else 1
)
for k, v in [
(input_key.material_key, material),
(input_key.outline_material_key, outline_material),
(input_key.outline_width_mode_key, outline_width_mode_value),
(input_key.outline_width_factor_key, mtoon.outline_width_factor),
(
input_key.outline_width_multiply_texture_key,
mtoon.outline_width_multiply_texture.index.source,
),
(
input_key.outline_width_multiply_texture_exists_key,
bool(mtoon.outline_width_multiply_texture.index.source),
),
(
input_key.outline_width_multiply_texture_uv_use_attribute_key(),
outline_width_multiply_texture_uv_use_attribute,
),
(
input_key.outline_width_multiply_texture_uv_attribute_name_key(),
uv_layer_name,
),
(
input_key.outline_width_multiply_texture_uv_offset_x_key,
outline_width_multiply_texture_uv_offset_x,
),
(
input_key.outline_width_multiply_texture_uv_offset_y_key,
outline_width_multiply_texture_uv_offset_y,
),
(
input_key.outline_width_multiply_texture_uv_scale_x_key,
outline_width_multiply_texture_uv_scale_x,
),
(
input_key.outline_width_multiply_texture_uv_scale_y_key,
outline_width_multiply_texture_uv_scale_y,
),
(
input_key.extrude_mesh_individual_key,
(
obj.type == "MESH"
and isinstance(mesh_data := obj.data, Mesh)
and not (has_auto_smooth and mesh_data.use_auto_smooth)
and not any(polygon.use_smooth for polygon in mesh_data.polygons)
),
),
(input_key.object_key, obj),
(
input_key.enabled_key,
obj.mode
not in ["VERTEX_PAINT", "TEXTURE_PAINT", "WEIGHT_PAINT", "SCULPT"],
),
]:
if modifier.get(k) != v:
modifier[k] = v
modifier_input_changed = True
# Apply input values
if modifier_input_changed:
modifier.show_viewport = not modifier.show_viewport
modifier.show_viewport = not modifier.show_viewport
if reset_outline_material:
reset_shader_node_group(
context,
material,
reset_material_node_tree=False,
reset_node_groups=False,
)
@staticmethod
def refresh_object(context: Context, obj: Object) -> None:
if bpy.app.version < (3, 3):
return
for material_slot in obj.material_slots:
material_ref = material_slot.material
if not material_ref:
continue
material = context.blend_data.materials.get(material_ref.name)
if not material:
continue
mtoon1 = get_material_extension(material).mtoon1
if not mtoon1.enabled or mtoon1.is_outline_material:
continue
VRM_OT_refresh_mtoon1_outline.assign(
context, material, obj, create_modifier=False
)
@staticmethod
def refresh(
context: Context,
material_name: Optional[str] = None,
*,
create_modifier: bool,
) -> None:
if bpy.app.version < (3, 3):
return
for obj in context.blend_data.objects:
if obj.type != "MESH":
continue
outline_material_names: list[str] = []
for material_slot in obj.material_slots:
material_ref = material_slot.material
if not material_ref:
continue
if material_name is not None and material_name != material_ref.name:
continue
material = context.blend_data.materials.get(material_ref.name)
if not material:
continue
mtoon1 = get_material_extension(material).mtoon1
if not mtoon1.enabled or mtoon1.is_outline_material:
continue
VRM_OT_refresh_mtoon1_outline.assign(
context, material, obj, create_modifier=create_modifier
)
outline_material_names.append(material.name)
if material_name is not None:
continue
# Remove unnecessary outline modifiers if no material name is specified.
for search_modifier_name in list(obj.modifiers.keys()):
search_modifier = obj.modifiers.get(search_modifier_name)
if not search_modifier:
continue
if search_modifier.type != "NODES":
continue
if not isinstance(search_modifier, NodesModifier):
continue
node_group = search_modifier.node_group
if not node_group:
continue
if node_group.name != shader.OUTLINE_GEOMETRY_GROUP_NAME:
continue
input_key = get_nodes_modifier_input_key(search_modifier)
if input_key is None:
continue
search_material = search_modifier.get(input_key.material_key)
if (
isinstance(search_material, Material)
and search_material.name in outline_material_names
):
continue
obj.modifiers.remove(search_modifier)
def execute(self, context: Context) -> set[str]:
material_name: Optional[str] = self.material_name
if not material_name:
material_name = None
self.refresh(context, material_name, create_modifier=self.create_modifier)
return {"FINISHED"}
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
material_name: str # type: ignore[no-redef]
create_modifier: bool # type: ignore[no-redef]
class VRM_OT_show_material_blender_4_2_warning(Operator):
bl_idname = "vrm.show_material_blender_4_2_warning"
bl_label = "Blender 4.2 Material Upgrade Warning"
bl_description = "Show Material Blender 4.2 Warning"
bl_options: AbstractSet[str] = {"REGISTER"}
material_name_lines: StringProperty( # type: ignore[valid-type]
options={"HIDDEN"}
)
def execute(self, _context: Context) -> set[str]:
return {"FINISHED"}
def invoke(self, context: Context, _event: Event) -> set[str]:
return context.window_manager.invoke_props_dialog(self, width=750)
def draw(self, _context: Context) -> None:
column = self.layout.row(align=True).column()
text = pgettext(
'Updating to Blender 4.2 may unintentionally change the "{alpha_mode}"'
+ ' of some MToon materials to "{transparent}".\n'
+ 'This was previously implemented using the material\'s "{blend_mode}"'
+ " but since that setting was removed in Blender 4.2.\n"
+ 'In the current VRM add-on, the "{alpha_mode}" function has been'
+ " re-implemented using a different method. However, it\n"
+ "was not possible"
+ " to implement automatic migration of old settings values because those"
+ " values could no longer be read.\n"
+ 'Please check the "{alpha_mode}" settings for materials that have'
+ " MToon enabled.\n"
+ "Materials that may be affected are as follows:"
).format(
blend_mode=pgettext("Blend Mode"),
alpha_mode=pgettext("Alpha Mode"),
transparent=pgettext("Transparent"),
)
description_outer_column = column.column()
description_outer_column.emboss = "NONE"
description_column = description_outer_column.box().column(align=True)
for i, line in enumerate(text.splitlines()):
icon = "ERROR" if i == 0 else "NONE"
description_column.label(text=line, translate=False, icon=icon)
material_column = column.box().column(align=True)
for line in self.material_name_lines.splitlines():
material_column.label(text=line, translate=False, icon="MATERIAL")
column.separator()
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
material_name_lines: str # type: ignore[no-redef]