Files
blender-vrm/importer/vrm0_importer.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

1563 lines
60 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
# SPDX-FileCopyrightText: 2018 iCyP
import json
import math
import uuid
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Optional, Union
from bpy.types import (
Armature,
Context,
Material,
Mesh,
Object,
)
from mathutils import Matrix, Vector
from ..common import convert, shader
from ..common.convert import Json
from ..common.logger import get_logger
from ..common.preferences import get_preferences
from ..common.progress import PartialProgress
from ..common.version import get_addon_version
from ..common.vrm0.human_bone import HumanBoneName, HumanBoneSpecifications
from ..editor import make_armature, migration
from ..editor.extension import get_armature_extension, get_material_extension
from ..editor.make_armature import (
connect_parent_tail_and_child_head_if_very_close_position,
)
from ..editor.mtoon1.property_group import (
Mtoon0TexturePropertyGroup,
Mtoon1MaterialPropertyGroup,
Mtoon1SamplerPropertyGroup,
Mtoon1TextureInfoPropertyGroup,
Mtoon1TexturePropertyGroup,
)
from ..editor.vrm0.property_group import (
Vrm0BlendShapeMasterPropertyGroup,
Vrm0FirstPersonPropertyGroup,
Vrm0HumanoidBonePropertyGroup,
Vrm0HumanoidPropertyGroup,
Vrm0MetaPropertyGroup,
Vrm0SecondaryAnimationPropertyGroup,
)
from .abstract_base_vrm_importer import AbstractBaseVrmImporter
logger = get_logger(__name__)
@dataclass(frozen=True)
class MaterialProperty:
name: str
shader: str
render_queue: Optional[int]
keyword_map: Mapping[str, bool]
tag_map: Mapping[str, str]
float_properties: Mapping[str, float]
vector_properties: Mapping[str, Sequence[float]]
texture_properties: Mapping[str, int]
@staticmethod
def create(json_dict: Mapping[str, Json], i: int) -> "MaterialProperty":
fallback = MaterialProperty(
name="Undefined",
shader="VRM_USE_GLTFSHADER",
render_queue=None,
keyword_map={},
tag_map={},
float_properties={},
vector_properties={},
texture_properties={},
)
extensions_dict = json_dict.get("extensions")
if not isinstance(extensions_dict, dict):
return fallback
vrm_dict = extensions_dict.get("VRM")
if not isinstance(vrm_dict, dict):
return fallback
material_property_dicts = vrm_dict.get("materialProperties")
if not isinstance(material_property_dicts, list):
return fallback
if not 0 <= i < len(material_property_dicts):
return fallback
material_property_dict = material_property_dicts[i]
if not isinstance(material_property_dict, dict):
return fallback
name = material_property_dict.get("name")
if not isinstance(name, str):
name = fallback.name
shader = material_property_dict.get("shader")
if not isinstance(shader, str):
shader = fallback.shader
render_queue = material_property_dict.get("renderQueue")
if not isinstance(render_queue, int):
render_queue = fallback.render_queue
raw_keyword_map = material_property_dict.get("keywordMap")
if isinstance(raw_keyword_map, dict):
keyword_map: Mapping[str, bool] = {
k: v for k, v in raw_keyword_map.items() if isinstance(v, bool)
}
else:
keyword_map = fallback.keyword_map
raw_tag_map = material_property_dict.get("tagMap")
if isinstance(raw_tag_map, dict):
tag_map: Mapping[str, str] = {
k: v for k, v in raw_tag_map.items() if isinstance(v, str)
}
else:
tag_map = fallback.tag_map
raw_float_properties = material_property_dict.get("floatProperties")
if isinstance(raw_float_properties, dict):
float_properties: Mapping[str, float] = {
k: float(v)
for k, v in raw_float_properties.items()
if isinstance(v, (float, int))
}
else:
float_properties = fallback.float_properties
raw_vector_properties = material_property_dict.get("vectorProperties")
if isinstance(raw_vector_properties, dict):
vector_properties: Mapping[str, Sequence[float]] = {
k: [
vector_element
for vector_element in vector
if isinstance(vector_element, (float, int))
]
for k, vector in raw_vector_properties.items()
if isinstance(vector, list)
}
else:
vector_properties = fallback.vector_properties
raw_texture_properties = material_property_dict.get("textureProperties")
if isinstance(raw_texture_properties, dict):
texture_properties: Mapping[str, int] = {
k: v for k, v in raw_texture_properties.items() if isinstance(v, int)
}
else:
texture_properties = fallback.texture_properties
return MaterialProperty(
name=name,
shader=shader,
render_queue=render_queue,
keyword_map=keyword_map,
tag_map=tag_map,
float_properties=float_properties,
vector_properties=vector_properties,
texture_properties=texture_properties,
)
class Vrm0Importer(AbstractBaseVrmImporter):
def load_materials(self, progress: PartialProgress) -> None:
shader_to_assignment_method = {
"VRM/MToon": self.assign_mtoon0_property,
"VRM/UnlitTexture": self.assign_unlit_texture_property,
"VRM/UnlitCutout": self.assign_unlit_cutout_property,
"VRM/UnlitTransparent": self.assign_unlit_transparent_property,
"VRM/UnlitTransparentZWrite": (
self.assign_unlit_transparent_z_write_property
),
}
material_dicts = self.parse_result.json_dict.get("materials")
if not isinstance(material_dicts, list):
return
material_properties = [
MaterialProperty.create(self.parse_result.json_dict, index)
for index in range(len(material_dicts))
]
for index, material_property in enumerate(material_properties):
assignment_method = shader_to_assignment_method.get(
material_property.shader
)
if not assignment_method:
continue
material = self.materials.get(index)
if not material:
material = self.context.blend_data.materials.new(material_property.name)
self.materials[index] = material
self.reset_material(material)
assignment_method(material, material_property)
progress.update(float(index) / len(material_properties))
progress.update(1)
def assign_mtoon0_texture(
self,
texture: Union[Mtoon0TexturePropertyGroup, Mtoon1TexturePropertyGroup],
texture_index: Optional[int],
) -> bool:
if texture_index is None:
return False
texture_dicts = self.parse_result.json_dict.get("textures")
if not isinstance(texture_dicts, list):
return False
if not 0 <= texture_index < len(texture_dicts):
return False
texture_dict = texture_dicts[texture_index]
if not isinstance(texture_dict, dict):
return False
source = texture_dict.get("source")
if isinstance(source, int):
image = self.images.get(source)
if image:
image.colorspace_settings.name = texture.colorspace
texture.source = image
sampler = texture_dict.get("sampler")
samplers = self.parse_result.json_dict.get("samplers")
if not isinstance(sampler, int) or not isinstance(samplers, list):
return True
if not 0 <= sampler < len(samplers):
return True
sampler_dict = samplers[sampler]
if not isinstance(sampler_dict, dict):
return True
mag_filter = sampler_dict.get("magFilter")
if isinstance(mag_filter, int):
texture.sampler.mag_filter = (
Mtoon1SamplerPropertyGroup.mag_filter_enum.value_to_identifier(
mag_filter, Mtoon1SamplerPropertyGroup.MAG_FILTER_DEFAULT.identifier
)
)
min_filter = sampler_dict.get("minFilter")
if isinstance(min_filter, int):
texture.sampler.min_filter = (
Mtoon1SamplerPropertyGroup.min_filter_enum.value_to_identifier(
min_filter, Mtoon1SamplerPropertyGroup.MIN_FILTER_DEFAULT.identifier
)
)
wrap_s = sampler_dict.get("wrapS")
if isinstance(wrap_s, int):
texture.sampler.wrap_s = (
Mtoon1SamplerPropertyGroup.wrap_enum.value_to_identifier(
wrap_s, Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.identifier
)
)
wrap_t = sampler_dict.get("wrapT")
if isinstance(wrap_t, int):
texture.sampler.wrap_t = (
Mtoon1SamplerPropertyGroup.wrap_enum.value_to_identifier(
wrap_t, Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.identifier
)
)
return True
def assign_mtoon0_texture_info(
self,
texture_info: Mtoon1TextureInfoPropertyGroup,
texture_index: Optional[int],
uv_transform: tuple[float, float, float, float],
) -> None:
if not self.assign_mtoon0_texture(texture_info.index, texture_index):
return
texture_info.extensions.khr_texture_transform.offset = (
uv_transform[0],
uv_transform[1],
)
texture_info.extensions.khr_texture_transform.scale = (
uv_transform[2],
uv_transform[3],
)
def assign_mtoon0_property(
self, material: Material, material_property: MaterialProperty
) -> None:
# https://github.com/saturday06/VRM-Addon-for-Blender/blob/2_15_26/io_scene_vrm/editor/mtoon1/ops.py#L98
material.use_backface_culling = True
gltf = get_material_extension(material).mtoon1
gltf.addon_version = get_addon_version()
gltf.enabled = True
gltf.show_expanded_mtoon0 = True
mtoon = gltf.extensions.vrmc_materials_mtoon
gltf.alpha_mode = gltf.ALPHA_MODE_OPAQUE.identifier
blend_mode = material_property.float_properties.get("_BlendMode")
if blend_mode is not None:
if math.fabs(blend_mode - 1) < 0.001:
gltf.alpha_mode = gltf.ALPHA_MODE_MASK.identifier
elif math.fabs(blend_mode - 2) < 0.001:
gltf.alpha_mode = gltf.ALPHA_MODE_BLEND.identifier
elif math.fabs(blend_mode - 3) < 0.001:
gltf.alpha_mode = gltf.ALPHA_MODE_BLEND.identifier
mtoon.transparent_with_z_write = True
cutoff = material_property.float_properties.get("_Cutoff")
if cutoff is not None:
gltf.alpha_cutoff = cutoff
gltf.double_sided = True
cull_mode = material_property.float_properties.get("_CullMode")
if cull_mode is not None:
if math.fabs(cull_mode - 1) < 0.001:
gltf.mtoon0_front_cull_mode = True
elif math.fabs(cull_mode - 2) < 0.001:
gltf.double_sided = False
base_color_factor = shader.rgba_or_none(
material_property.vector_properties.get("_Color")
)
if base_color_factor:
gltf.pbr_metallic_roughness.base_color_factor = convert.srgb_to_linear(
base_color_factor
)
raw_uv_transform = material_property.vector_properties.get("_MainTex")
if raw_uv_transform is None or len(raw_uv_transform) != 4:
uv_transform = (0.0, 0.0, 1.0, 1.0)
else:
uv_transform = (
raw_uv_transform[0],
raw_uv_transform[1],
raw_uv_transform[2],
raw_uv_transform[3],
)
self.assign_mtoon0_texture_info(
gltf.pbr_metallic_roughness.base_color_texture,
material_property.texture_properties.get("_MainTex"),
uv_transform,
)
shade_color_factor = shader.rgb_or_none(
material_property.vector_properties.get("_ShadeColor")
)
if shade_color_factor:
mtoon.shade_color_factor = convert.srgb_to_linear(shade_color_factor)
self.assign_mtoon0_texture_info(
mtoon.shade_multiply_texture,
material_property.texture_properties.get("_ShadeTexture"),
uv_transform,
)
self.assign_mtoon0_texture_info(
gltf.normal_texture,
material_property.texture_properties.get("_BumpMap"),
uv_transform,
)
normal_texture_scale = material_property.float_properties.get("_BumpScale")
if isinstance(normal_texture_scale, (float, int)):
gltf.normal_texture.scale = float(normal_texture_scale)
shading_shift_0x = material_property.float_properties.get("_ShadeShift")
if shading_shift_0x is None:
shading_shift_0x = 0
shading_toony_0x = material_property.float_properties.get("_ShadeToony")
if shading_toony_0x is None:
shading_toony_0x = 0
mtoon.shading_shift_factor = convert.mtoon_shading_shift_0_to_1(
shading_toony_0x, shading_shift_0x
)
mtoon.shading_toony_factor = convert.mtoon_shading_toony_0_to_1(
shading_toony_0x, shading_shift_0x
)
indirect_light_intensity = material_property.float_properties.get(
"_IndirectLightIntensity"
)
if indirect_light_intensity is not None:
mtoon.gi_equalization_factor = convert.mtoon_intensity_to_gi_equalization(
indirect_light_intensity
)
else:
mtoon.gi_equalization_factor = 0.5
raw_emissive_factor = shader.rgb_or_none(
material_property.vector_properties.get("_EmissionColor")
) or (0, 0, 0)
max_color_component_value = max(raw_emissive_factor)
if max_color_component_value > 1:
gltf.emissive_factor = convert.srgb_to_linear(
Vector(raw_emissive_factor) / max_color_component_value
)
gltf.extensions.khr_materials_emissive_strength.emissive_strength = (
max_color_component_value
)
else:
gltf.emissive_factor = convert.srgb_to_linear(raw_emissive_factor)
self.assign_mtoon0_texture_info(
gltf.emissive_texture,
material_property.texture_properties.get("_EmissionMap"),
uv_transform,
)
self.assign_mtoon0_texture_info(
mtoon.matcap_texture,
material_property.texture_properties.get("_SphereAdd"),
(0, 0, 1, 1),
)
parametric_rim_color_factor = shader.rgb_or_none(
material_property.vector_properties.get("_RimColor")
)
if parametric_rim_color_factor:
mtoon.parametric_rim_color_factor = convert.srgb_to_linear(
parametric_rim_color_factor
)
parametric_rim_fresnel_power_factor = material_property.float_properties.get(
"_RimFresnelPower"
)
if isinstance(parametric_rim_fresnel_power_factor, (float, int)):
mtoon.parametric_rim_fresnel_power_factor = (
parametric_rim_fresnel_power_factor
)
mtoon.parametric_rim_lift_factor = material_property.float_properties.get(
"_RimLift", 0
)
self.assign_mtoon0_texture_info(
mtoon.rim_multiply_texture,
material_property.texture_properties.get("_RimTexture"),
uv_transform,
)
# https://github.com/vrm-c/UniVRM/blob/7c9919ef47a25c04100a2dcbe6a75dff49ef4857/Assets/VRM10/Runtime/Migration/Materials/MigrationMToonMaterial.cs#L287-L290
mtoon.rim_lighting_mix_factor = 1.0
rim_lighting_mix = material_property.float_properties.get("_RimLightingMix")
if rim_lighting_mix is not None:
gltf.mtoon0_rim_lighting_mix = rim_lighting_mix
mtoon.enable_outline_preview = get_preferences(
self.context
).enable_mtoon_outline_preview
centimeter_to_meter = 0.01
one_hundredth = 0.01
outline_width_mode = round(
material_property.float_properties.get("_OutlineWidthMode", 0)
)
outline_width = material_property.float_properties.get("_OutlineWidth")
if outline_width is None:
outline_width = 0.0
if outline_width_mode == 0:
mtoon.outline_width_mode = mtoon.OUTLINE_WIDTH_MODE_NONE.identifier
elif 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)
else:
mtoon.outline_width_mode = (
mtoon.OUTLINE_WIDTH_MODE_SCREEN_COORDINATES.identifier
)
mtoon.outline_width_factor = max(0.0, outline_width * one_hundredth * 0.5)
outline_scaled_max_distance = material_property.float_properties.get(
"_OutlineScaledMaxDistance"
)
if outline_scaled_max_distance is not None:
gltf.mtoon0_outline_scaled_max_distance = outline_scaled_max_distance
self.assign_mtoon0_texture_info(
mtoon.outline_width_multiply_texture,
material_property.texture_properties.get("_OutlineWidthTexture"),
uv_transform,
)
outline_color_factor = shader.rgb_or_none(
material_property.vector_properties.get("_OutlineColor")
)
if outline_color_factor:
mtoon.outline_color_factor = convert.srgb_to_linear(outline_color_factor)
outline_color_mode = material_property.float_properties.get("_OutlineColorMode")
if (
outline_color_mode is not None
and math.fabs(outline_color_mode - 1) < 0.00001
):
mtoon.outline_lighting_mix_factor = (
material_property.float_properties.get("_OutlineLightingMix") or 1
)
else:
mtoon.outline_lighting_mix_factor = 0 # Fixed Color
self.assign_mtoon0_texture_info(
mtoon.uv_animation_mask_texture,
material_property.texture_properties.get("_UvAnimMaskTexture"),
uv_transform,
)
uv_animation_rotation_speed_factor = material_property.float_properties.get(
"_UvAnimRotation"
)
if isinstance(uv_animation_rotation_speed_factor, (float, int)):
mtoon.uv_animation_rotation_speed_factor = (
uv_animation_rotation_speed_factor
)
uv_animation_scroll_x_speed_factor = material_property.float_properties.get(
"_UvAnimScrollX"
)
if isinstance(uv_animation_scroll_x_speed_factor, (float, int)):
mtoon.uv_animation_scroll_x_speed_factor = (
uv_animation_scroll_x_speed_factor
)
uv_animation_scroll_y_speed_factor = material_property.float_properties.get(
"_UvAnimScrollY"
)
if isinstance(uv_animation_scroll_y_speed_factor, (float, int)):
mtoon.uv_animation_scroll_y_speed_factor = (
-uv_animation_scroll_y_speed_factor
)
light_color_attenuation = material_property.float_properties.get(
"_LightColorAttenuation"
)
if light_color_attenuation is not None:
gltf.mtoon0_light_color_attenuation = light_color_attenuation
self.assign_mtoon0_texture(
gltf.mtoon0_receive_shadow_texture,
material_property.texture_properties.get("_ReceiveShadowTexture"),
)
gltf.mtoon0_receive_shadow_rate = material_property.float_properties.get(
"_ReceiveShadowRate", 0.5
)
self.assign_mtoon0_texture(
gltf.mtoon0_shading_grade_texture,
material_property.texture_properties.get("_ShadingGradeTexture"),
)
gltf.mtoon0_shading_grade_rate = material_property.float_properties.get(
"_ShadingGradeRate", 0.5
)
render_queue = material_property.render_queue
if render_queue is not None:
gltf.mtoon0_render_queue = render_queue
def assign_unlit_common_property(
self,
material: Material,
material_property: MaterialProperty,
) -> None:
gltf = get_material_extension(material).mtoon1
mtoon = gltf.extensions.vrmc_materials_mtoon
main_texture_index = material_property.texture_properties.get("_MainTex")
main_texture_image = None
if isinstance(main_texture_index, int):
texture_dicts = self.parse_result.json_dict.get("textures")
if isinstance(texture_dicts, list) and 0 <= main_texture_index < len(
texture_dicts
):
texture_dict = texture_dicts[main_texture_index]
if isinstance(texture_dict, dict):
image_index = texture_dict.get("source")
if isinstance(image_index, int):
main_texture_image = self.images.get(image_index)
(texture_offset_u, texture_offset_v, texture_scale_u, texture_scale_v) = (
convert.float4_or(
material_property.vector_properties.get("_MainTex"),
(0.0, 0.0, 1.0, 1.0),
)
)
if main_texture_image:
for texture in [
gltf.pbr_metallic_roughness.base_color_texture,
gltf.emissive_texture,
mtoon.shade_multiply_texture,
]:
texture.index.source = main_texture_image
khr_texture_transform = texture.extensions.khr_texture_transform
khr_texture_transform.offset = (texture_offset_u, texture_offset_v)
khr_texture_transform.scale = (texture_scale_u, texture_scale_v)
gltf.pbr_metallic_roughness.base_color_factor = (0, 0, 0, 1)
gltf.emissive_factor = (1, 1, 1)
gltf.double_sided = False
mtoon.shade_color_factor = (0, 0, 0)
mtoon.shading_toony_factor = 0.95
mtoon.shading_shift_factor = -0.05
mtoon.rim_lighting_mix_factor = 1
mtoon.parametric_rim_fresnel_power_factor = 5
mtoon.parametric_rim_lift_factor = 0
render_queue = material_property.render_queue
if render_queue is not None:
gltf.mtoon0_render_queue = render_queue
def assign_unlit_texture_property(
self,
material: Material,
material_property: MaterialProperty,
) -> None:
gltf = get_material_extension(material).mtoon1
gltf.enabled = True
gltf.alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_OPAQUE.identifier
self.assign_unlit_common_property(material, material_property)
def assign_unlit_cutout_property(
self,
material: Material,
material_property: MaterialProperty,
) -> None:
gltf = get_material_extension(material).mtoon1
gltf.enabled = True
gltf.alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_MASK.identifier
cutoff = material_property.float_properties.get("_Cutoff")
if isinstance(cutoff, (int, float)):
gltf.alpha_cutoff = cutoff
self.assign_unlit_common_property(material, material_property)
def assign_unlit_transparent_property(
self,
material: Material,
material_property: MaterialProperty,
) -> None:
gltf = get_material_extension(material).mtoon1
gltf.enabled = True
gltf.alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_BLEND.identifier
self.assign_unlit_common_property(material, material_property)
def assign_unlit_transparent_z_write_property(
self,
material: Material,
material_property: MaterialProperty,
) -> None:
gltf = get_material_extension(material).mtoon1
gltf.enabled = True
gltf.alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_BLEND.identifier
mtoon = gltf.extensions.vrmc_materials_mtoon
mtoon.transparent_with_z_write = True
self.assign_unlit_common_property(material, material_property)
def load_gltf_extensions(self) -> None:
armature = self.armature
if not armature:
return
addon_extension = get_armature_extension(self.armature_data)
addon_extension.spec_version = addon_extension.SPEC_VERSION_VRM0
vrm0 = addon_extension.vrm0
if self.parse_result.spec_version_number >= (1, 0):
return
vrm0_extension = self.parse_result.vrm0_extension_dict
addon_extension.addon_version = get_addon_version()
textblock = self.context.blend_data.texts.new(name="vrm.json")
textblock.write(json.dumps(self.parse_result.json_dict, indent=4))
self.load_vrm0_meta(vrm0.meta, vrm0_extension.get("meta"))
self.load_vrm0_humanoid(vrm0.humanoid, vrm0_extension.get("humanoid"))
setup_bones(self.context, armature)
self.load_vrm0_first_person(
vrm0.first_person, vrm0_extension.get("firstPerson")
)
self.load_vrm0_blend_shape_master(
vrm0.blend_shape_master, vrm0_extension.get("blendShapeMaster")
)
self.load_vrm0_secondary_animation(
vrm0.secondary_animation, vrm0_extension.get("secondaryAnimation")
)
migration.migrate(self.context, armature.name)
def load_vrm0_meta(self, meta: Vrm0MetaPropertyGroup, meta_dict: Json) -> None:
if not isinstance(meta_dict, dict):
return
title = meta_dict.get("title")
if isinstance(title, str):
meta.title = title
version = meta_dict.get("version")
if isinstance(version, str):
meta.version = version
author = meta_dict.get("author")
if isinstance(author, str):
meta.author = author
contact_information = meta_dict.get("contactInformation")
if isinstance(contact_information, str):
meta.contact_information = contact_information
reference = meta_dict.get("reference")
if isinstance(reference, str):
meta.reference = reference
allowed_user_name = meta_dict.get("allowedUserName")
if (
isinstance(allowed_user_name, str)
and allowed_user_name in meta.allowed_user_name_enum.identifiers()
):
meta.allowed_user_name = allowed_user_name
violent_ussage_name = meta_dict.get("violentUssageName")
if (
isinstance(violent_ussage_name, str)
and violent_ussage_name in meta.violent_ussage_name_enum.identifiers()
):
meta.violent_ussage_name = violent_ussage_name
sexual_ussage_name = meta_dict.get("sexualUssageName")
if (
isinstance(sexual_ussage_name, str)
and sexual_ussage_name in meta.sexual_ussage_name_enum.identifiers()
):
meta.sexual_ussage_name = sexual_ussage_name
commercial_ussage_name = meta_dict.get("commercialUssageName")
if (
isinstance(commercial_ussage_name, str)
and commercial_ussage_name in meta.commercial_ussage_name_enum.identifiers()
):
meta.commercial_ussage_name = commercial_ussage_name
other_permission_url = meta_dict.get("otherPermissionUrl")
if isinstance(other_permission_url, str):
meta.other_permission_url = other_permission_url
license_name = meta_dict.get("licenseName")
if (
isinstance(license_name, str)
and license_name in meta.license_name_enum.identifiers()
):
meta.license_name = license_name
other_license_url = meta_dict.get("otherLicenseUrl")
if isinstance(other_license_url, str):
meta.other_license_url = other_license_url
texture = meta_dict.get("texture")
texture_dicts = self.parse_result.json_dict.get("textures")
if (
isinstance(texture, int)
and isinstance(texture_dicts, list)
# extensions.VRM.meta.texture could be -1
# https://github.com/vrm-c/UniVRM/issues/91#issuecomment-454284964
and 0 <= texture < len(texture_dicts)
):
texture_dict = texture_dicts[texture]
if isinstance(texture_dict, dict):
image_index = texture_dict.get("source")
if isinstance(image_index, int) and image_index in self.images:
meta.texture = self.images[image_index]
def load_vrm0_humanoid(
self, humanoid: Vrm0HumanoidPropertyGroup, humanoid_dict: Json
) -> None:
if not isinstance(humanoid_dict, dict):
return
human_bone_dicts = humanoid_dict.get("humanBones")
if isinstance(human_bone_dicts, list):
for human_bone_dict in human_bone_dicts:
if not isinstance(human_bone_dict, dict):
continue
bone = human_bone_dict.get("bone")
if bone not in HumanBoneSpecifications.all_names:
continue
node = human_bone_dict.get("node")
if not isinstance(node, int):
continue
node_bone_name = self.bone_names.get(node)
if node_bone_name is None:
continue
human_bone = next(
(
human_bone
for human_bone in humanoid.human_bones
if human_bone.bone == bone
),
None,
)
if human_bone:
logger.warning('Duplicated bone: "%s"', bone)
else:
human_bone = humanoid.human_bones.add()
human_bone.bone = bone
human_bone.node.bone_name = node_bone_name
use_default_values = human_bone_dict.get("useDefaultValues")
if isinstance(use_default_values, bool):
human_bone.use_default_values = use_default_values
min_ = convert.vrm_json_vector3_to_tuple(human_bone_dict.get("min"))
if min_ is not None:
human_bone.min = min_
max_ = convert.vrm_json_vector3_to_tuple(human_bone_dict.get("max"))
if max_ is not None:
human_bone.max = max_
center = convert.vrm_json_vector3_to_tuple(
human_bone_dict.get("center")
)
if center is not None:
human_bone.center = center
axis_length = convert.float_or_none(human_bone_dict.get("axisLength"))
if axis_length is not None:
human_bone.axis_length = axis_length
arm_stretch = convert.float_or_none(humanoid_dict.get("armStretch"))
if arm_stretch is not None:
humanoid.arm_stretch = arm_stretch
leg_stretch = convert.float_or_none(humanoid_dict.get("legStretch"))
if leg_stretch is not None:
humanoid.leg_stretch = leg_stretch
upper_arm_twist = convert.float_or_none(humanoid_dict.get("upperArmTwist"))
if upper_arm_twist is not None:
humanoid.upper_arm_twist = upper_arm_twist
lower_arm_twist = convert.float_or_none(humanoid_dict.get("lowerArmTwist"))
if lower_arm_twist is not None:
humanoid.lower_arm_twist = lower_arm_twist
upper_leg_twist = convert.float_or_none(humanoid_dict.get("upperLegTwist"))
if upper_leg_twist is not None:
humanoid.upper_leg_twist = upper_leg_twist
lower_leg_twist = convert.float_or_none(humanoid_dict.get("lowerLegTwist"))
if lower_leg_twist is not None:
humanoid.lower_leg_twist = lower_leg_twist
feet_spacing = convert.float_or_none(humanoid_dict.get("feetSpacing"))
if feet_spacing is not None:
humanoid.feet_spacing = feet_spacing
has_translation_dof = humanoid_dict.get("hasTranslationDoF")
if isinstance(has_translation_dof, bool):
humanoid.has_translation_dof = has_translation_dof
def load_vrm0_first_person(
self,
first_person: Vrm0FirstPersonPropertyGroup,
first_person_dict: Json,
) -> None:
if not isinstance(first_person_dict, dict):
return
first_person_bone = first_person_dict.get("firstPersonBone")
if isinstance(first_person_bone, int) and (
first_person_bone_name := self.bone_names.get(first_person_bone)
):
first_person.first_person_bone.bone_name = first_person_bone_name
first_person_bone_offset = convert.vrm_json_vector3_to_tuple(
first_person_dict.get("firstPersonBoneOffset")
)
if first_person_bone_offset is not None:
# Axis confusing
(x, y, z) = first_person_bone_offset
first_person.first_person_bone_offset = (x, z, y)
mesh_annotation_dicts = first_person_dict.get("meshAnnotations")
if isinstance(mesh_annotation_dicts, list):
for mesh_annotation_dict in mesh_annotation_dicts:
mesh_annotation = first_person.mesh_annotations.add()
if not isinstance(mesh_annotation_dict, dict):
continue
mesh = mesh_annotation_dict.get("mesh")
if isinstance(mesh, int) and mesh in self.meshes:
mesh_annotation.mesh.mesh_object_name = self.meshes[mesh].name
first_person_flag = mesh_annotation_dict.get("firstPersonFlag")
if (
isinstance(first_person_flag, str)
and first_person_flag
in mesh_annotation.first_person_flag_enum.identifiers()
):
mesh_annotation.first_person_flag = first_person_flag
look_at_type_name = first_person_dict.get("lookAtTypeName")
if (
isinstance(look_at_type_name, str)
and look_at_type_name in first_person.look_at_type_name_enum.identifiers()
):
first_person.look_at_type_name = look_at_type_name
for look_at, look_at_dict in [
(
first_person.look_at_horizontal_inner,
first_person_dict.get("lookAtHorizontalInner"),
),
(
first_person.look_at_horizontal_outer,
first_person_dict.get("lookAtHorizontalOuter"),
),
(
first_person.look_at_vertical_down,
first_person_dict.get("lookAtVerticalDown"),
),
(
first_person.look_at_vertical_up,
first_person_dict.get("lookAtVerticalUp"),
),
]:
if not isinstance(look_at_dict, dict):
continue
curve = convert.vrm_json_curve_to_list(look_at_dict.get("curve"))
if curve is not None:
look_at.curve = curve
x_range = look_at_dict.get("xRange")
if isinstance(x_range, (float, int)):
look_at.x_range = x_range
y_range = look_at_dict.get("yRange")
if isinstance(y_range, (float, int)):
look_at.y_range = y_range
def load_vrm0_blend_shape_master(
self,
blend_shape_master: Vrm0BlendShapeMasterPropertyGroup,
blend_shape_master_dict: Json,
) -> None:
if not isinstance(blend_shape_master_dict, dict):
return
blend_shape_group_dicts = blend_shape_master_dict.get("blendShapeGroups")
if not isinstance(blend_shape_group_dicts, list):
return
for blend_shape_group_dict in blend_shape_group_dicts:
blend_shape_group = blend_shape_master.blend_shape_groups.add()
if not isinstance(blend_shape_group_dict, dict):
continue
name = blend_shape_group_dict.get("name")
if isinstance(name, str):
blend_shape_group.name = name
preset_name = blend_shape_group_dict.get("presetName")
if (
isinstance(preset_name, str)
and preset_name in blend_shape_group.preset_name_enum.identifiers()
):
blend_shape_group.preset_name = preset_name
bind_dicts = blend_shape_group_dict.get("binds")
if isinstance(bind_dicts, list):
for bind_dict in bind_dicts:
if not isinstance(bind_dict, dict):
continue
mesh_index = bind_dict.get("mesh")
if not isinstance(mesh_index, int):
continue
mesh_object = self.meshes.get(mesh_index)
if not mesh_object:
continue
bind = blend_shape_group.binds.add()
bind.mesh.mesh_object_name = mesh_object.name
mesh_data = mesh_object.data
if isinstance(mesh_data, Mesh):
shape_keys = mesh_data.shape_keys
if shape_keys:
index = bind_dict.get("index")
if isinstance(index, int) and (
1 <= (index + 1) < len(shape_keys.key_blocks)
):
bind.index = list(shape_keys.key_blocks.keys())[
index + 1
]
weight = convert.float_or(bind_dict.get("weight"), 0.0)
bind.weight = min(max(weight / 100.0, 0), 1)
material_value_dicts = blend_shape_group_dict.get("materialValues")
if isinstance(material_value_dicts, list):
for material_value_dict in material_value_dicts:
material_value = blend_shape_group.material_values.add()
if not isinstance(material_value_dict, dict):
continue
material_name = material_value_dict.get("materialName")
if (
isinstance(material_name, str)
and material_name in self.context.blend_data.materials
):
material_value.material = self.context.blend_data.materials[
material_name
]
property_name = material_value_dict.get("propertyName")
if isinstance(property_name, str):
material_value.property_name = property_name
target_value_vector = material_value_dict.get("targetValue")
if isinstance(target_value_vector, list):
for v in target_value_vector:
material_value.target_value.add().value = convert.float_or(
v, 0.0
)
is_binary = blend_shape_group_dict.get("isBinary")
if isinstance(is_binary, bool):
blend_shape_group.is_binary = is_binary
def load_vrm0_secondary_animation(
self,
secondary_animation: Vrm0SecondaryAnimationPropertyGroup,
secondary_animation_dict: Json,
) -> None:
if not isinstance(secondary_animation_dict, dict):
return
armature = self.armature
if armature is None:
message = "armature is None"
raise ValueError(message)
collider_group_dicts = secondary_animation_dict.get("colliderGroups")
if not isinstance(collider_group_dicts, list):
collider_group_dicts = []
self.context.view_layer.depsgraph.update()
self.context.scene.view_layers.update()
collider_objs: list[Object] = []
for collider_group_dict in collider_group_dicts:
collider_group = secondary_animation.collider_groups.add()
collider_group.uuid = uuid.uuid4().hex
collider_group.refresh(armature)
if not isinstance(collider_group_dict, dict):
continue
node = collider_group_dict.get("node")
if not isinstance(node, int):
continue
bone_name = self.bone_names.get(node)
if bone_name is None:
continue
collider_group.node.bone_name = bone_name
collider_dicts = collider_group_dict.get("colliders")
if not isinstance(collider_dicts, list):
continue
for collider_index, collider_dict in enumerate(collider_dicts):
collider = collider_group.colliders.add()
if not isinstance(collider_dict, dict):
continue
offset = convert.vrm_json_vector3_to_tuple(collider_dict.get("offset"))
if offset is None:
offset = (0, 0, 0)
radius = convert.float_or(collider_dict.get("radius"), 0.0)
collider_name = f"{bone_name}_collider_{collider_index}"
obj = self.context.blend_data.objects.new(
name=collider_name, object_data=None
)
collider.bpy_object = obj
obj.parent = self.armature
obj.parent_type = "BONE"
obj.parent_bone = bone_name
fixed_offset = [
offset[axis] * inv for axis, inv in zip([0, 2, 1], [-1, -1, 1])
] # TODO: Y-axis inversion is matched to UniVRM serialization
# Since it's parented to the tail side of the bone, move it to the
# position from the root
obj.matrix_world = Matrix.Translation(
[
armature.matrix_world.to_translation()[i]
+ self.armature_data.bones[
bone_name
].matrix_local.to_translation()[i]
+ fixed_offset[i]
for i in range(3)
]
)
obj.empty_display_size = radius
obj.empty_display_type = "SPHERE"
collider_objs.append(obj)
if collider_objs:
colliders_collection = self.context.blend_data.collections.new("Colliders")
self.context.scene.collection.children.link(colliders_collection)
for collider_obj in collider_objs:
colliders_collection.objects.link(collider_obj)
for collider_group in secondary_animation.collider_groups:
collider_group.refresh(armature)
bone_group_dicts = secondary_animation_dict.get("boneGroups")
if not isinstance(bone_group_dicts, list):
bone_group_dicts = []
for bone_group_dict in bone_group_dicts:
bone_group = secondary_animation.bone_groups.add()
if not isinstance(bone_group_dict, dict):
bone_group.refresh(armature)
continue
comment = bone_group_dict.get("comment")
if isinstance(comment, str):
bone_group.comment = comment
stiffiness = convert.float_or_none(bone_group_dict.get("stiffiness"))
if stiffiness is not None:
bone_group.stiffiness = stiffiness
gravity_power = convert.float_or_none(bone_group_dict.get("gravityPower"))
if gravity_power is not None:
bone_group.gravity_power = gravity_power
gravity_dir = convert.vrm_json_vector3_to_tuple(
bone_group_dict.get("gravityDir")
)
if gravity_dir is not None:
# Axis confusing
(x, y, z) = gravity_dir
bone_group.gravity_dir = (x, z, y)
drag_force = convert.float_or_none(bone_group_dict.get("dragForce"))
if drag_force is not None:
bone_group.drag_force = drag_force
center = bone_group_dict.get("center")
if isinstance(center, int) and (
center_bone_name := self.bone_names.get(center)
):
bone_group.center.bone_name = center_bone_name
hit_radius = convert.float_or_none(bone_group_dict.get("hitRadius"))
if hit_radius is not None:
bone_group.hit_radius = hit_radius
bones = bone_group_dict.get("bones")
if isinstance(bones, list):
for bone in bones:
bone_prop = bone_group.bones.add()
if not isinstance(bone, int):
continue
bone_name = self.bone_names.get(bone)
if bone_name is None:
continue
bone_prop.bone_name = bone_name
collider_group_indices = bone_group_dict.get("colliderGroups")
if isinstance(collider_group_indices, list):
for collider_group_index in collider_group_indices:
if not isinstance(collider_group_index, int) or not (
0
<= collider_group_index
< len(secondary_animation.collider_groups)
):
continue
collider_group_uuid = bone_group.collider_groups.add()
collider_group = secondary_animation.collider_groups[
collider_group_index
]
collider_group_uuid.value = collider_group.uuid
for bone_group in secondary_animation.bone_groups:
bone_group.refresh(armature)
collider_object_names = [
collider_bpy_object.name
for collider_group in secondary_animation.collider_groups
for collider in collider_group.colliders
if (collider_bpy_object := collider.bpy_object)
]
if collider_object_names:
imported_object_names = self.imported_object_names
if imported_object_names is None:
imported_object_names = []
self.imported_object_names = imported_object_names
imported_object_names.extend(collider_object_names)
def find_vrm0_bone_node_indices(self) -> list[int]:
result: list[int] = []
vrm0_dict = self.parse_result.vrm0_extension_dict
first_person_dict = vrm0_dict.get("firstPerson")
if isinstance(first_person_dict, dict):
first_person_bone_index = first_person_dict.get("firstPersonBone")
if isinstance(first_person_bone_index, int):
result.append(first_person_bone_index)
humanoid_dict = vrm0_dict.get("humanoid")
if isinstance(humanoid_dict, dict):
human_bone_dicts = humanoid_dict.get("humanBones")
if isinstance(human_bone_dicts, list):
for human_bone_dict in human_bone_dicts:
if not isinstance(human_bone_dict, dict):
continue
node_index = human_bone_dict.get("node")
if isinstance(node_index, int):
result.append(node_index)
secondary_animation_dict = vrm0_dict.get("secondaryAnimation")
if isinstance(secondary_animation_dict, dict):
collider_group_dicts = secondary_animation_dict.get("colliderGroups")
if isinstance(collider_group_dicts, list):
for collider_group_dict in collider_group_dicts:
if not isinstance(collider_group_dict, dict):
continue
node_index = collider_group_dict.get("node")
if isinstance(node_index, int):
result.append(node_index)
bone_group_dicts = secondary_animation_dict.get("boneGroups")
if isinstance(bone_group_dicts, list):
for bone_group_dict in bone_group_dicts:
if not isinstance(bone_group_dict, dict):
continue
center_index = bone_group_dict.get("center")
if isinstance(center_index, int):
result.append(center_index)
bone_indices = bone_group_dict.get("bones")
if isinstance(bone_indices, list):
result.extend(
bone_index
for bone_index in bone_indices
if isinstance(bone_index, int)
)
return list(dict.fromkeys(result)) # Remove duplicates
def find_vrm_bone_node_indices(self) -> list[int]:
# TODO: SkinnedMeshRenderer root <=> skin.skeleton???
return list(
dict.fromkeys( # Remove duplicates
self.find_vrm0_bone_node_indices()
)
)
def setup_bones(context: Context, armature: Object) -> None:
"""Set the direction and length of Human Bones.
VRM0 does not have bone direction and length, so currently FORTUNE's
direction and length determination is performed at import time. However,
since FORTUNE does not consider human body structure, it can result in
unnatural bones. Therefore, we determine direction and length using a
heuristic method that considers human body structure.
Things to do someday:
Limit the Head angle to within 30 degrees from the parent angle.
Add elevation angle when Toes or Foot are not touching the ground.
"""
armature_data = armature.data
if not isinstance(armature_data, Armature):
return
addon_extension = get_armature_extension(armature_data)
Vrm0HumanoidPropertyGroup.fixup_human_bones(armature)
Vrm0HumanoidPropertyGroup.update_all_node_candidates(
context,
armature_data.name,
force=True,
)
human_bones = addon_extension.vrm0.humanoid.human_bones
for human_bone in human_bones:
if (
human_bone.node.bone_name
and human_bone.node.bone_name not in human_bone.node_candidates
):
# has error
return
for humanoid_name in HumanBoneSpecifications.required_names:
if not any(
human_bone.bone == humanoid_name and human_bone.node.bone_name
for human_bone in human_bones
):
# has error
return
with AbstractBaseVrmImporter.save_bone_child_object_transforms(
context, armature
) as armature_data:
bone_name_to_human_bone_name: dict[str, HumanBoneName] = {}
for human_bone in human_bones:
if not human_bone.node.bone_name:
continue
name = HumanBoneName.from_str(human_bone.bone)
if not name:
continue
bone_name_to_human_bone_name[human_bone.node.bone_name] = name
for bone_name in bone_name_to_human_bone_name:
bone = armature_data.edit_bones.get(bone_name)
while bone:
bone.roll = 0.0
bone = bone.parent
for (
bone_name,
human_bone_name,
) in bone_name_to_human_bone_name.items():
# The current algorithm cannot handle
#
# head ---- node ---- leftEye
# \
# -- rightEye
#
# well, so we don't process leftEye and rightEye
if human_bone_name in [HumanBoneName.RIGHT_EYE, HumanBoneName.LEFT_EYE]:
continue
bone = armature_data.edit_bones.get(bone_name)
if not bone:
continue
last_human_bone_name = human_bone_name
while True:
parent = bone.parent
if not parent:
break
parent_human_bone_name = bone_name_to_human_bone_name.get(parent.name)
if parent_human_bone_name in [
HumanBoneName.RIGHT_HAND,
HumanBoneName.LEFT_HAND,
]:
break
if (
parent_human_bone_name == HumanBoneName.UPPER_CHEST
and last_human_bone_name
not in [HumanBoneName.HEAD, HumanBoneName.NECK]
):
break
if (
parent_human_bone_name == HumanBoneName.CHEST
and last_human_bone_name
not in [
HumanBoneName.HEAD,
HumanBoneName.NECK,
HumanBoneName.UPPER_CHEST,
]
):
break
if (
parent_human_bone_name == HumanBoneName.SPINE
and last_human_bone_name
not in [
HumanBoneName.HEAD,
HumanBoneName.NECK,
HumanBoneName.UPPER_CHEST,
HumanBoneName.CHEST,
]
):
break
if (
parent_human_bone_name == HumanBoneName.HIPS
and last_human_bone_name != HumanBoneName.SPINE
):
break
if parent_human_bone_name:
last_human_bone_name = parent_human_bone_name
if (parent.head - bone.head).length >= make_armature.MIN_BONE_LENGTH:
parent.tail = bone.head
bone = parent
human_bone_name_to_human_bone: dict[
HumanBoneName, Vrm0HumanoidBonePropertyGroup
] = {}
for human_bone in human_bones:
n = HumanBoneName.from_str(human_bone.bone)
if not n:
continue
human_bone_name_to_human_bone[n] = human_bone
# When VRM Humanoid terminal bones other than eye and head bones have
# multiple children, point them in the same direction as the parent.
# This results in a more natural direction than the default.
# Since it might be a direction specification by the VRM creator,
# do nothing when there is only one child.
for tip_bone_name in [
HumanBoneName.JAW,
HumanBoneName.LEFT_THUMB_DISTAL,
HumanBoneName.LEFT_INDEX_DISTAL,
HumanBoneName.LEFT_MIDDLE_DISTAL,
HumanBoneName.LEFT_RING_DISTAL,
HumanBoneName.LEFT_LITTLE_DISTAL,
HumanBoneName.RIGHT_THUMB_DISTAL,
HumanBoneName.RIGHT_INDEX_DISTAL,
HumanBoneName.RIGHT_MIDDLE_DISTAL,
HumanBoneName.RIGHT_RING_DISTAL,
HumanBoneName.RIGHT_LITTLE_DISTAL,
HumanBoneName.LEFT_TOES,
HumanBoneName.RIGHT_TOES,
]:
bone = None
searching_tip_bone_name: Optional[HumanBoneName] = tip_bone_name
while searching_tip_bone_name:
# Break if there is a corresponding bone
human_bone = human_bone_name_to_human_bone.get(searching_tip_bone_name)
if human_bone:
bone = armature_data.edit_bones.get(human_bone.node.bone_name)
if bone:
break
# If there is no corresponding bone and it was a required bone,
# abort due to data error
specification = HumanBoneSpecifications.get(searching_tip_bone_name)
if specification.requirement:
break
parent_specification = specification.parent()
if not parent_specification:
logger.error(
'logic error: "%s" has no parent', searching_tip_bone_name
)
break
# Do nothing if there are already assigned bones in the parent's
# descendants
assigned_parent_descendant_found = False
for parent_descendant in parent_specification.descendants():
parent_descendant_human_bone = human_bone_name_to_human_bone.get(
parent_descendant.name
)
if not parent_descendant_human_bone:
continue
if (
parent_descendant_human_bone.node.bone_name
in armature_data.edit_bones
):
assigned_parent_descendant_found = True
break
if assigned_parent_descendant_found:
break
searching_tip_bone_name = specification.parent_name
if not bone:
continue
if len(bone.children) <= 1:
continue
parent_bone = bone.parent
if not parent_bone:
continue
bone.roll = parent_bone.roll
bone.tail = bone.head + parent_bone.vector / 2
# When eye bones have no children or multiple children, point them forward.
# This results in a more natural direction than the default.
# Since it might be a direction specification by the VRM creator,
# do nothing when there is only one child.
for eye_human_bone in [
human_bone_name_to_human_bone.get(HumanBoneName.LEFT_EYE),
human_bone_name_to_human_bone.get(HumanBoneName.RIGHT_EYE),
]:
if not eye_human_bone or not eye_human_bone.node.bone_name:
continue
bone = armature_data.edit_bones.get(eye_human_bone.node.bone_name)
if not bone:
continue
if len(bone.children) == 1:
continue
world_head = (
armature.matrix_world @ Matrix.Translation(bone.head)
).to_translation()
world_tail = list(world_head)
world_tail[1] -= 0.03125
world_inv = armature.matrix_world.inverted()
if not world_inv:
continue
bone.tail = (Matrix.Translation(world_tail) @ world_inv).to_translation()
# When head bones have no children or multiple children, point them upward.
# This results in a more natural direction than the default.
# Since it might be a direction specification by the VRM creator,
# do nothing when there is only one child.
for head_human_bone in [human_bone_name_to_human_bone.get(HumanBoneName.HEAD)]:
if not head_human_bone or not head_human_bone.node.bone_name:
continue
bone = armature_data.edit_bones.get(head_human_bone.node.bone_name)
if not bone:
continue
if len(bone.children) == 1:
continue
world_head = (
armature.matrix_world @ Matrix.Translation(bone.head)
).to_translation()
world_tail = list(world_head)
parent_bone = bone.parent
world_tail[2] += parent_bone.length if parent_bone else 0.2
world_inv = armature.matrix_world.inverted()
if not world_inv:
continue
parent_bone = bone.parent
if parent_bone:
bone.roll = parent_bone.roll
bone.tail = (Matrix.Translation(world_tail) @ world_inv).to_translation()
connect_parent_tail_and_child_head_if_very_close_position(armature_data)