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
This commit is contained in:
2026-01-01 14:21:56 +08:00
commit 091ad6a49a
243 changed files with 60636 additions and 0 deletions

529
editor/mtoon1/migration.py Normal file
View File

@@ -0,0 +1,529 @@
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import functools
from dataclasses import dataclass
from typing import Final, Optional
import bpy
from bpy.types import (
Context,
Image,
Material,
NodeReroute,
PropertyGroup,
ShaderNodeGroup,
ShaderNodeTexImage,
)
from ...common import convert, ops, shader, version
from ...common.gl import GL_LINEAR, GL_NEAREST
from ...common.logger import get_logger
from ...common.progress import create_progress
from .. import search
from ..extension import get_material_extension
from .property_group import (
GL_LINEAR_IMAGE_INTERPOLATIONS,
IMAGE_INTERPOLATION_CLOSEST,
IMAGE_INTERPOLATION_LINEAR,
Mtoon1MaterialPropertyGroup,
Mtoon1SamplerPropertyGroup,
Mtoon1TextureInfoPropertyGroup,
Mtoon1VrmcMaterialsMtoonPropertyGroup,
reset_shader_node_group,
)
TextureInfoBackup = Mtoon1TextureInfoPropertyGroup.TextureInfoBackup
logger = get_logger(__name__)
@dataclass
class State:
material_blender_4_2_warning_shown: bool = False
state: Final = State()
def show_material_blender_4_2_warning_delay(material_name_lines: str) -> None:
ops.vrm.show_material_blender_4_2_warning(
"INVOKE_DEFAULT",
material_name_lines=material_name_lines,
)
def migrate(context: Context, *, show_progress: bool = False) -> None:
blender_4_2_migrated_material_names: list[str] = []
with create_progress(context, show_progress=show_progress) as progress:
for material_index, material in enumerate(context.blend_data.materials):
if not material:
continue
migrate_material(context, material, blender_4_2_migrated_material_names)
progress.update(float(material_index) / len(context.blend_data.materials))
progress.update(1)
if (
blender_4_2_migrated_material_names
and tuple(context.blend_data.version) < (4, 2)
and bpy.app.version >= (4, 2)
):
logger.warning(
"Migrating Materials from blender version data=%s app=%s",
context.blend_data.version,
bpy.app.version,
)
if not state.material_blender_4_2_warning_shown:
state.material_blender_4_2_warning_shown = True
# In Blender 4.2.0, if you don't run it with a timer, the dialog will
# disappear automatically.
bpy.app.timers.register(
functools.partial(
show_material_blender_4_2_warning_delay,
"\n".join(blender_4_2_migrated_material_names),
),
first_interval=0.1,
)
def migrate_material(
context: Context,
material: Material,
blender_4_2_migrated_material_names: list[str],
) -> None:
_, legacy_legacy_shader_name = search.legacy_shader_node(material)
if legacy_legacy_shader_name in search.LEGACY_SHADER_NAMES:
# Since old shader node groups are not compatible with Blender 4.2 as they are,
# always warn when upgrading to Blender 4.2 or later.
blender_4_2_migrated_material_names.append(material.name)
return
if bpy.app.version < (5, 0, 0) and not material.use_nodes:
return
node_tree = material.node_tree
if not node_tree:
return
mtoon1 = get_material_extension(material).mtoon1
if not mtoon1.get("enabled"):
return
vrmc_materials_mtoon = mtoon1.extensions.vrmc_materials_mtoon
if vrmc_materials_mtoon.get("is_outline_material"):
return
addon_version = convert.float3_or(mtoon1.get("addon_version"), (0, 0, 0))
if addon_version < (2, 16, 4):
# https://github.com/saturday06/VRM-Addon-for-Blender/blob/2_10_0/io_scene_vrm/editor/mtoon1/property_group.py#L1658-L1683
surface_node_name = "Mtoon1Material.MaterialOutputSurfaceIn"
surface_node = node_tree.nodes.get(surface_node_name)
if not isinstance(surface_node, NodeReroute):
return
connected = False
surface_socket = surface_node.outputs[0]
for link in node_tree.links:
if (
link.from_socket == surface_socket
and link.to_socket
and link.to_socket.node
and link.to_socket.node.type == "OUTPUT_MATERIAL"
):
connected = True
break
if not connected:
return
else:
# https://github.com/saturday06/VRM-Addon-for-Blender/blob/2_16_4/io_scene_vrm/editor/mtoon1/property_group.py#L1913-L1929
group_node = node_tree.nodes.get("Mtoon1Material.Mtoon1Output")
if not isinstance(group_node, ShaderNodeGroup):
return
if not group_node.node_tree:
return
if group_node.node_tree.name != "VRM Add-on MToon 1.0 Output Revision 1":
return
if addon_version < (2, 20, 50):
migrate_sampler_filter_node(material)
alpha_mode: Optional[str] = None
alpha_cutoff: Optional[float] = None
if addon_version < (2, 20, 55):
blender_4_2_migrated_material_names.append(material.name)
alpha_cutoff = material.alpha_threshold
blend_method = material.blend_method
if blend_method in ["BLEND", "HASHED"]:
alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_BLEND.identifier
elif blend_method == "CLIP":
alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_MASK.identifier
else:
alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_OPAQUE.identifier
base_color_factor: Optional[tuple[float, float, float, float]] = None
base_color_texture_backup: Optional[TextureInfoBackup] = None
normal_texture_backup: Optional[TextureInfoBackup] = None
normal_texture_scale: Optional[float] = None
emissive_texture_backup: Optional[TextureInfoBackup] = None
emissive_factor: Optional[tuple[float, float, float]] = None
emissive_strength: Optional[float] = None
transparent_with_z_write: Optional[bool] = None
render_queue_offset_number: Optional[int] = None
shade_multiply_texture_backup: Optional[TextureInfoBackup] = None
shade_color_factor: Optional[tuple[float, float, float]] = None
shading_shift_texture_backup: Optional[TextureInfoBackup] = None
shading_shift_texture_scale: Optional[float] = None
shading_shift_factor: Optional[float] = None
shading_toony_factor: Optional[float] = None
gi_equalization_factor: Optional[float] = None
matcap_factor: Optional[tuple[float, float, float]] = None
matcap_texture_backup: Optional[TextureInfoBackup] = None
parametric_rim_color_factor: Optional[tuple[float, float, float]] = None
rim_multiply_texture_backup: Optional[TextureInfoBackup] = None
rim_lighting_mix_factor: Optional[float] = None
parametric_rim_fresnel_power_factor: Optional[float] = None
parametric_rim_lift_factor: Optional[float] = None
outline_width_mode: Optional[str] = None
outline_width_factor: Optional[float] = None
outline_width_multiply_texture_backup: Optional[TextureInfoBackup] = None
outline_color_factor: Optional[tuple[float, float, float]] = None
outline_lighting_mix_factor: Optional[float] = None
uv_animation_mask_texture_backup: Optional[TextureInfoBackup] = None
uv_animation_scroll_x_speed_factor: Optional[float] = None
uv_animation_scroll_y_speed_factor: Optional[float] = None
uv_animation_rotation_speed_factor: Optional[float] = None
if addon_version < (2, 20, 62):
pbr_metallic_roughness = mtoon1.pbr_metallic_roughness
base_color_factor = convert.float4_or_none(
pbr_metallic_roughness.get("base_color_factor")
)
base_color_texture_backup = backup_texture_info(
pbr_metallic_roughness.base_color_texture
)
normal_texture = mtoon1.normal_texture
normal_texture_backup = backup_texture_info(normal_texture)
normal_texture_scale = convert.float_or_none(normal_texture.get("scale"))
emissive_factor = convert.float3_or_none(mtoon1.get("emissive_factor"))
emissive_texture_backup = backup_texture_info(mtoon1.emissive_texture)
emissive_strength = convert.float_or_none(
mtoon1.extensions.khr_materials_emissive_strength.get("emissive_strength")
)
transparent_with_z_write_object = vrmc_materials_mtoon.get(
"transparent_with_z_write"
)
if isinstance(transparent_with_z_write_object, int):
transparent_with_z_write = bool(transparent_with_z_write_object)
render_queue_offset_number_object = vrmc_materials_mtoon.get(
"render_queue_offset_number"
)
if isinstance(render_queue_offset_number_object, int):
render_queue_offset_number = render_queue_offset_number_object
shade_multiply_texture_backup = backup_texture_info(
vrmc_materials_mtoon.shade_multiply_texture
)
shade_color_factor = convert.float3_or_none(
vrmc_materials_mtoon.get("shade_color_factor")
)
shading_shift_texture = vrmc_materials_mtoon.shading_shift_texture
shading_shift_texture_backup = backup_texture_info(shading_shift_texture)
shading_shift_texture_scale = convert.float_or_none(
shading_shift_texture.get("scale")
)
shading_shift_factor = convert.float_or_none(
vrmc_materials_mtoon.get("shading_shift_factor")
)
shading_toony_factor = convert.float_or_none(
vrmc_materials_mtoon.get("shading_toony_factor")
)
matcap_factor = convert.float3_or_none(
vrmc_materials_mtoon.get("matcap_factor")
)
matcap_texture_backup = backup_texture_info(vrmc_materials_mtoon.matcap_texture)
gi_equalization_factor = convert.float_or_none(
vrmc_materials_mtoon.get("gi_equalization_factor")
)
parametric_rim_color_factor = convert.float3_or_none(
vrmc_materials_mtoon.get("parametric_rim_color_factor")
)
rim_multiply_texture_backup = backup_texture_info(
vrmc_materials_mtoon.rim_multiply_texture
)
rim_lighting_mix_factor = convert.float_or_none(
vrmc_materials_mtoon.get("rim_lighting_mix_factor")
)
parametric_rim_fresnel_power_factor = convert.float_or_none(
vrmc_materials_mtoon.get("parametric_rim_fresnel_power_factor")
)
parametric_rim_lift_factor = convert.float_or_none(
vrmc_materials_mtoon.get("parametric_rim_lift_factor")
)
outline_width_mode_number = vrmc_materials_mtoon.get("outline_width_mode")
if isinstance(outline_width_mode_number, int):
outline_width_mode_item = (
Mtoon1VrmcMaterialsMtoonPropertyGroup.outline_width_mode_enum
)
outline_width_mode = outline_width_mode_item.value_to_identifier(
outline_width_mode_number,
Mtoon1VrmcMaterialsMtoonPropertyGroup.OUTLINE_WIDTH_MODE_NONE.identifier,
)
outline_width_factor = convert.float_or_none(
vrmc_materials_mtoon.get("outline_width_factor")
)
outline_width_multiply_texture_backup = backup_texture_info(
vrmc_materials_mtoon.outline_width_multiply_texture
)
outline_color_factor = convert.float3_or_none(
vrmc_materials_mtoon.get("outline_color_factor")
)
outline_lighting_mix_factor = convert.float_or_none(
vrmc_materials_mtoon.get("outline_lighting_mix_factor")
)
uv_animation_mask_texture_backup = backup_texture_info(
vrmc_materials_mtoon.uv_animation_mask_texture
)
uv_animation_scroll_x_speed_factor = convert.float_or_none(
vrmc_materials_mtoon.get("uv_animation_scroll_x_speed_factor")
)
uv_animation_scroll_y_speed_factor = convert.float_or_none(
vrmc_materials_mtoon.get("uv_animation_scroll_y_speed_factor")
)
uv_animation_rotation_speed_factor = convert.float_or_none(
vrmc_materials_mtoon.get("uv_animation_rotation_speed_factor")
)
if (
addon_version < shader.LAST_MODIFIED_VERSION
# Blender 4.2 has a node specification change, so it will be forcibly reset.
or (bpy.app.version >= (4, 2) and tuple(context.blend_data.version) < (4, 2))
):
reset_shader_node_group(
context, material, reset_material_node_tree=True, reset_node_groups=False
)
# From this point on, you can write code that assumes the shader node is up to date.
vrmc_materials_mtoon = mtoon1.extensions.vrmc_materials_mtoon
if alpha_mode is not None:
mtoon1.alpha_mode = alpha_mode
if alpha_cutoff is not None:
mtoon1.alpha_cutoff = alpha_cutoff
if base_color_factor is not None:
mtoon1.pbr_metallic_roughness.base_color_factor = base_color_factor
if base_color_texture_backup is not None:
mtoon1.pbr_metallic_roughness.base_color_texture.restore(
base_color_texture_backup
)
if normal_texture_backup is not None:
mtoon1.normal_texture.restore(normal_texture_backup)
if normal_texture_scale is not None:
mtoon1.normal_texture.scale = normal_texture_scale
if emissive_texture_backup is not None:
mtoon1.emissive_texture.restore(emissive_texture_backup)
if emissive_factor is not None:
mtoon1.emissive_factor = emissive_factor
if emissive_strength is not None:
mtoon1.extensions.khr_materials_emissive_strength.emissive_strength = (
emissive_strength
)
if transparent_with_z_write is not None:
vrmc_materials_mtoon.transparent_with_z_write = transparent_with_z_write
if render_queue_offset_number is not None:
vrmc_materials_mtoon.render_queue_offset_number = render_queue_offset_number
if shade_multiply_texture_backup is not None:
vrmc_materials_mtoon.shade_multiply_texture.restore(
shade_multiply_texture_backup
)
if shade_color_factor is not None:
vrmc_materials_mtoon.shade_color_factor = shade_color_factor
if shading_shift_texture_backup is not None:
vrmc_materials_mtoon.shading_shift_texture.restore(shading_shift_texture_backup)
if shading_shift_factor is not None:
vrmc_materials_mtoon.shading_shift_factor = shading_shift_factor
if shading_shift_texture_scale is not None:
vrmc_materials_mtoon.shading_shift_texture.scale = shading_shift_texture_scale
if shading_toony_factor is not None:
vrmc_materials_mtoon.shading_toony_factor = shading_toony_factor
if gi_equalization_factor is not None:
vrmc_materials_mtoon.gi_equalization_factor = gi_equalization_factor
if matcap_factor is not None:
vrmc_materials_mtoon.matcap_factor = matcap_factor
if matcap_texture_backup is not None:
vrmc_materials_mtoon.matcap_texture.restore(matcap_texture_backup)
if parametric_rim_color_factor is not None:
vrmc_materials_mtoon.parametric_rim_color_factor = parametric_rim_color_factor
if rim_multiply_texture_backup is not None:
vrmc_materials_mtoon.rim_multiply_texture.restore(rim_multiply_texture_backup)
if rim_lighting_mix_factor is not None:
vrmc_materials_mtoon.rim_lighting_mix_factor = rim_lighting_mix_factor
if parametric_rim_fresnel_power_factor is not None:
vrmc_materials_mtoon.parametric_rim_fresnel_power_factor = (
parametric_rim_fresnel_power_factor
)
if parametric_rim_lift_factor is not None:
vrmc_materials_mtoon.parametric_rim_lift_factor = parametric_rim_lift_factor
if outline_width_mode is not None:
vrmc_materials_mtoon.outline_width_mode = outline_width_mode
if outline_width_factor is not None:
vrmc_materials_mtoon.outline_width_factor = outline_width_factor
if outline_width_multiply_texture_backup is not None:
vrmc_materials_mtoon.outline_width_multiply_texture.restore(
outline_width_multiply_texture_backup
)
if outline_color_factor is not None:
vrmc_materials_mtoon.outline_color_factor = outline_color_factor
if outline_lighting_mix_factor is not None:
vrmc_materials_mtoon.outline_lighting_mix_factor = outline_lighting_mix_factor
if uv_animation_mask_texture_backup is not None:
vrmc_materials_mtoon.uv_animation_mask_texture.restore(
uv_animation_mask_texture_backup
)
if uv_animation_scroll_x_speed_factor is not None:
vrmc_materials_mtoon.uv_animation_scroll_x_speed_factor = (
uv_animation_scroll_x_speed_factor
)
if uv_animation_scroll_y_speed_factor is not None:
vrmc_materials_mtoon.uv_animation_scroll_y_speed_factor = (
uv_animation_scroll_y_speed_factor
)
if uv_animation_rotation_speed_factor is not None:
vrmc_materials_mtoon.uv_animation_rotation_speed_factor = (
uv_animation_rotation_speed_factor
)
mtoon1.setup_drivers()
updated_addon_version = version.get_addon_version()
if tuple(mtoon1.addon_version) != updated_addon_version:
mtoon1.addon_version = updated_addon_version
def backup_texture_info(texture_info: object) -> Optional[TextureInfoBackup]:
if not isinstance(texture_info, PropertyGroup):
return None
source: Optional[Image] = None
wrap_s = Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.identifier
wrap_t = Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.identifier
offset_x = 0.0
offset_y = 0.0
scale_x = 1.0
scale_y = 1.0
index = getattr(texture_info, "index", None)
if isinstance(index, PropertyGroup):
source_object = index.get("source")
if isinstance(source_object, Image):
source = source_object
sampler = getattr(index, "sampler", None)
if isinstance(sampler, PropertyGroup):
wrap_s_number = sampler.get("wrap_s")
if isinstance(wrap_s_number, int):
wrap_s = Mtoon1SamplerPropertyGroup.wrap_enum.value_to_identifier(
wrap_s_number,
wrap_s,
)
wrap_t_number = sampler.get("wrap_t")
if isinstance(wrap_t_number, int):
wrap_t = Mtoon1SamplerPropertyGroup.wrap_enum.value_to_identifier(
wrap_t_number,
wrap_t,
)
extensions = getattr(texture_info, "extensions", None)
if isinstance(extensions, PropertyGroup):
khr_texture_transform = getattr(extensions, "khr_texture_transform", None)
if isinstance(khr_texture_transform, PropertyGroup):
offset = convert.float2_or_none(khr_texture_transform.get("offset"))
if offset is not None:
offset_x, offset_y = offset
scale = convert.float2_or_none(khr_texture_transform.get("scale"))
if scale is not None:
scale_x, scale_y = scale
return TextureInfoBackup(
source=source,
mag_filter=None,
min_filter=None,
wrap_s=wrap_s,
wrap_t=wrap_t,
offset=(offset_x, offset_y),
scale=(scale_x, scale_y),
)
def migrate_sampler_filter_node(material: Material) -> None:
node_tree = material.node_tree
if not node_tree:
return
mtoon1 = get_material_extension(material).mtoon1
for node_name, attrs in [
(
"Mtoon1BaseColorTexture.Image",
("pbr_metallic_roughness", "base_color_texture"),
),
("Mtoon1EmissiveTexture.Image", ("emissive_texture",)),
("Mtoon1NormalTexture.Image", ("normal_texture",)),
(
"Mtoon1ShadeMultiplyTexture.Image",
("extensions", "vrmc_materials_mtoon", "shade_multiply_texture"),
),
(
"Mtoon1ShadingShiftTexture.Image",
("extensions", "vrmc_materials_mtoon", "shading_shift_texture"),
),
(
"Mtoon1OutlineWidthMultiplyTexture.Image",
(
"extensions",
"vrmc_materials_mtoon",
"outline_width_multiply_texture",
),
),
(
"Mtoon1UvAnimationMaskTexture.Image",
("extensions", "vrmc_materials_mtoon", "uv_animation_mask_texture"),
),
(
"Mtoon1MatcapTexture.Image",
("extensions", "vrmc_materials_mtoon", "matcap_texture"),
),
(
"Mtoon1RimMultiplyTexture.Image",
("extensions", "vrmc_materials_mtoon", "rim_multiply_texture"),
),
]:
sampler = functools.reduce(
lambda prop, attr: getattr(prop, attr, None),
(*attrs, "index", "sampler"),
mtoon1,
)
if not isinstance(sampler, PropertyGroup):
continue
mag_filter = sampler.get("mag_filter")
if (
not isinstance(mag_filter, int)
or mag_filter not in Mtoon1SamplerPropertyGroup.mag_filter_enum.values()
):
continue
node = node_tree.nodes.get(node_name)
if not isinstance(node, ShaderNodeTexImage):
continue
if (
mag_filter == GL_NEAREST
and node.interpolation != IMAGE_INTERPOLATION_CLOSEST
):
node.interpolation = IMAGE_INTERPOLATION_CLOSEST
if (
mag_filter == GL_LINEAR
and node.interpolation not in GL_LINEAR_IMAGE_INTERPOLATIONS
):
node.interpolation = IMAGE_INTERPOLATION_LINEAR