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

1876 lines
75 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import json
from typing import Optional, Union
import bpy
from bpy.types import (
Bone,
CopyRotationConstraint,
DampedTrackConstraint,
EditBone,
Image,
Mesh,
Object,
PoseBone,
)
from mathutils import Matrix, Vector
from ..common import convert, ops, 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.vrm1 import human_bone as vrm1_human_bone
from ..common.vrm1.human_bone import HumanBoneName, HumanBoneSpecifications
from ..editor import make_armature, migration
from ..editor.extension import (
VrmAddonBoneExtensionPropertyGroup as BoneExtension,
)
from ..editor.extension import (
get_armature_extension,
get_bone_extension,
get_material_extension,
get_object_extension,
)
from ..editor.make_armature import (
connect_parent_tail_and_child_head_if_very_close_position,
)
from ..editor.mtoon1.property_group import (
Mtoon1KhrTextureTransformPropertyGroup,
Mtoon1SamplerPropertyGroup,
Mtoon1TextureInfoPropertyGroup,
Mtoon1TexturePropertyGroup,
)
from ..editor.spring_bone1.property_group import (
SpringBone1ColliderPropertyGroup,
SpringBone1SpringBonePropertyGroup,
)
from ..editor.vrm1.property_group import (
Vrm1ExpressionPropertyGroup,
Vrm1ExpressionsPropertyGroup,
Vrm1FirstPersonPropertyGroup,
Vrm1HumanBonesPropertyGroup,
Vrm1HumanoidPropertyGroup,
Vrm1LookAtPropertyGroup,
Vrm1MaterialColorBindPropertyGroup,
Vrm1MetaPropertyGroup,
)
from .abstract_base_vrm_importer import AbstractBaseVrmImporter
logger = get_logger(__name__)
class Vrm1Importer(AbstractBaseVrmImporter):
@staticmethod
def assign_texture_colorspace(image: Image, preferred_colorspace: str) -> None:
colorspaces = [preferred_colorspace]
if preferred_colorspace == "Non-Color":
# https://github.com/saturday06/VRM-Addon-for-Blender/issues/336#issuecomment-1760729404
colorspaces.extend(["Linear", "Generic Data"])
colorspace_settings = image.colorspace_settings
exceptions: list[Exception] = []
for colorspace in colorspaces:
# The values that can be set in colorspace_settings.name vary
# depending on the Blender setup. For example, bpy 3.6.0 from pypi
# can only choose Linear or sRGB.
# To detect this, we would ideally want to reference
# colorspace_settings.bl_rna.properties.get("name").enum_items
# etc., but it cannot be used because it crashes in Blender 2.93 etc.
# So we catch exceptions and try until assignment succeeds.
try:
colorspace_settings.name = colorspace
except TypeError as e:
exceptions.append(e)
else:
return
logger.error(
"image.colorspace_settings.name doesn't support %s:\n%s",
colorspaces,
"\n".join(map(str, exceptions)),
)
def assign_texture(
self,
texture: Mtoon1TexturePropertyGroup,
texture_dict: dict[str, Json],
) -> None:
source = texture_dict.get("source")
if isinstance(source, int):
image = self.images.get(source)
if image:
texture.source = image
self.assign_texture_colorspace(image, texture.colorspace)
sampler = texture_dict.get("sampler")
samplers = self.parse_result.json_dict.get("samplers")
if not isinstance(sampler, int) or not isinstance(samplers, list):
return
if not 0 <= sampler < len(samplers):
return
sampler_dict = samplers[sampler]
if not isinstance(sampler_dict, dict):
return
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
)
)
def assign_khr_texture_transform(
self,
khr_texture_transform: Mtoon1KhrTextureTransformPropertyGroup,
khr_texture_transform_dict: dict[str, Json],
) -> None:
offset = convert.float2_or_none(khr_texture_transform_dict.get("offset"))
if offset:
khr_texture_transform.offset = offset
scale = convert.float2_or_none(khr_texture_transform_dict.get("scale"))
if scale:
khr_texture_transform.scale = scale
def assign_texture_info(
self,
texture_info: Mtoon1TextureInfoPropertyGroup,
texture_info_dict: dict[str, Json],
) -> None:
index = texture_info_dict.get("index")
if isinstance(index, int):
texture_dicts = self.parse_result.json_dict.get("textures")
if isinstance(texture_dicts, list) and 0 <= index < len(texture_dicts):
texture_dict = texture_dicts[index]
if isinstance(texture_dict, dict):
self.assign_texture(texture_info.index, texture_dict)
extensions_dict = texture_info_dict.get("extensions")
if not isinstance(extensions_dict, dict):
return
khr_texture_transform_dict = extensions_dict.get("KHR_texture_transform")
if not isinstance(khr_texture_transform_dict, dict):
return
self.assign_khr_texture_transform(
texture_info.extensions.khr_texture_transform,
khr_texture_transform_dict,
)
def make_mtoon1_material(
self, material_index: int, gltf_dict: dict[str, Json]
) -> None:
extensions_dict = gltf_dict.get("extensions")
if not isinstance(extensions_dict, dict):
return
mtoon_dict = extensions_dict.get("VRMC_materials_mtoon")
if not isinstance(mtoon_dict, dict):
return
material = self.materials.get(material_index)
if not material:
name = gltf_dict.get("name")
if not isinstance(name, str):
name = "Material"
material = self.context.blend_data.materials.new(name)
self.reset_material(material)
material.use_backface_culling = True
gltf = get_material_extension(material).mtoon1
gltf.addon_version = get_addon_version()
gltf.enabled = True
mtoon = gltf.extensions.vrmc_materials_mtoon
pbr_metallic_roughness_dict = gltf_dict.get("pbrMetallicRoughness")
if isinstance(pbr_metallic_roughness_dict, dict):
base_color_factor = shader.rgba_or_none(
pbr_metallic_roughness_dict.get("baseColorFactor")
)
if base_color_factor:
gltf.pbr_metallic_roughness.base_color_factor = base_color_factor
base_color_texture_dict = pbr_metallic_roughness_dict.get(
"baseColorTexture"
)
if isinstance(base_color_texture_dict, dict):
self.assign_texture_info(
gltf.pbr_metallic_roughness.base_color_texture,
base_color_texture_dict,
)
alpha_mode = gltf_dict.get("alphaMode")
if (
isinstance(alpha_mode, str)
and alpha_mode in gltf.alpha_mode_enum.identifiers()
):
gltf.alpha_mode = alpha_mode
alpha_cutoff = gltf_dict.get("alphaCutoff")
if isinstance(alpha_cutoff, (float, int)):
gltf.alpha_cutoff = float(alpha_cutoff)
double_sided = gltf_dict.get("doubleSided")
if isinstance(double_sided, bool):
gltf.double_sided = double_sided
normal_texture_dict = gltf_dict.get("normalTexture")
if isinstance(normal_texture_dict, dict):
self.assign_texture_info(
gltf.normal_texture,
normal_texture_dict,
)
normal_texture_scale = normal_texture_dict.get("scale")
if isinstance(normal_texture_scale, (float, int)):
gltf.normal_texture.scale = float(normal_texture_scale)
emissive_factor = shader.rgb_or_none(gltf_dict.get("emissiveFactor"))
if emissive_factor:
gltf.emissive_factor = emissive_factor
emissive_texture_dict = gltf_dict.get("emissiveTexture")
if isinstance(emissive_texture_dict, dict):
self.assign_texture_info(
gltf.emissive_texture,
emissive_texture_dict,
)
emissive_strength_dict = extensions_dict.get("KHR_materials_emissive_strength")
if isinstance(emissive_strength_dict, dict):
emissive_strength = convert.float_or_none(
emissive_strength_dict.get("emissiveStrength")
)
if emissive_strength is not None:
gltf.extensions.khr_materials_emissive_strength.emissive_strength = (
emissive_strength
)
shade_color_factor = shader.rgb_or_none(mtoon_dict.get("shadeColorFactor"))
if shade_color_factor:
mtoon.shade_color_factor = shade_color_factor
shade_multiply_texture_dict = mtoon_dict.get("shadeMultiplyTexture")
if isinstance(shade_multiply_texture_dict, dict):
self.assign_texture_info(
mtoon.shade_multiply_texture,
shade_multiply_texture_dict,
)
transparent_with_z_write = mtoon_dict.get("transparentWithZWrite")
if isinstance(transparent_with_z_write, bool):
mtoon.transparent_with_z_write = transparent_with_z_write
render_queue_offset_number = mtoon_dict.get("renderQueueOffsetNumber")
if isinstance(render_queue_offset_number, (float, int)):
mtoon.render_queue_offset_number = max(
-9, min(9, int(render_queue_offset_number))
)
shading_shift_factor = mtoon_dict.get("shadingShiftFactor")
if isinstance(shading_shift_factor, (float, int)):
mtoon.shading_shift_factor = float(shading_shift_factor)
shading_shift_texture_dict = mtoon_dict.get("shadingShiftTexture")
if isinstance(shading_shift_texture_dict, dict):
self.assign_texture_info(
mtoon.shading_shift_texture,
shading_shift_texture_dict,
)
shading_shift_texture_scale = shading_shift_texture_dict.get("scale")
if isinstance(shading_shift_texture_scale, (float, int)):
mtoon.shading_shift_texture.scale = float(shading_shift_texture_scale)
shading_toony_factor = mtoon_dict.get("shadingToonyFactor")
if isinstance(shading_toony_factor, (float, int)):
mtoon.shading_toony_factor = float(shading_toony_factor)
gi_equalization_factor = mtoon_dict.get("giEqualizationFactor")
if isinstance(gi_equalization_factor, (float, int)):
mtoon.gi_equalization_factor = float(gi_equalization_factor)
matcap_factor = shader.rgb_or_none(mtoon_dict.get("matcapFactor"))
if matcap_factor:
mtoon.matcap_factor = matcap_factor
matcap_texture_dict = mtoon_dict.get("matcapTexture")
if isinstance(matcap_texture_dict, dict):
self.assign_texture_info(
mtoon.matcap_texture,
matcap_texture_dict,
)
parametric_rim_color_factor = shader.rgb_or_none(
mtoon_dict.get("parametricRimColorFactor")
)
if parametric_rim_color_factor:
mtoon.parametric_rim_color_factor = parametric_rim_color_factor
parametric_rim_fresnel_power_factor = mtoon_dict.get(
"parametricRimFresnelPowerFactor"
)
if isinstance(parametric_rim_fresnel_power_factor, (float, int)):
mtoon.parametric_rim_fresnel_power_factor = float(
parametric_rim_fresnel_power_factor
)
parametric_rim_lift_factor = mtoon_dict.get("parametricRimLiftFactor")
if isinstance(parametric_rim_lift_factor, (float, int)):
mtoon.parametric_rim_lift_factor = float(parametric_rim_lift_factor)
rim_multiply_texture_dict = mtoon_dict.get("rimMultiplyTexture")
if isinstance(rim_multiply_texture_dict, dict):
self.assign_texture_info(
mtoon.rim_multiply_texture,
rim_multiply_texture_dict,
)
rim_lighting_mix_factor = mtoon_dict.get("rimLightingMixFactor")
if isinstance(rim_lighting_mix_factor, (float, int)):
mtoon.rim_lighting_mix_factor = float(rim_lighting_mix_factor)
mtoon.enable_outline_preview = get_preferences(
self.context
).enable_mtoon_outline_preview
outline_width_mode = mtoon_dict.get("outlineWidthMode")
if outline_width_mode in mtoon.outline_width_mode_enum.identifiers():
mtoon.outline_width_mode = outline_width_mode
outline_width_factor = mtoon_dict.get("outlineWidthFactor")
if isinstance(outline_width_factor, (float, int)):
mtoon.outline_width_factor = float(outline_width_factor)
outline_width_multiply_texture_dict = mtoon_dict.get(
"outlineWidthMultiplyTexture"
)
if isinstance(outline_width_multiply_texture_dict, dict):
self.assign_texture_info(
mtoon.outline_width_multiply_texture,
outline_width_multiply_texture_dict,
)
outline_color_factor = shader.rgb_or_none(mtoon_dict.get("outlineColorFactor"))
if outline_color_factor:
mtoon.outline_color_factor = outline_color_factor
outline_lighting_mix_factor = mtoon_dict.get("outlineLightingMixFactor")
if isinstance(outline_lighting_mix_factor, (float, int)):
mtoon.outline_lighting_mix_factor = float(outline_lighting_mix_factor)
uv_animation_mask_texture_dict = mtoon_dict.get("uvAnimationMaskTexture")
if isinstance(uv_animation_mask_texture_dict, dict):
self.assign_texture_info(
mtoon.uv_animation_mask_texture,
uv_animation_mask_texture_dict,
)
uv_animation_rotation_speed_factor = mtoon_dict.get(
"uvAnimationRotationSpeedFactor"
)
if isinstance(uv_animation_rotation_speed_factor, (float, int)):
mtoon.uv_animation_rotation_speed_factor = float(
uv_animation_rotation_speed_factor
)
uv_animation_scroll_x_speed_factor = mtoon_dict.get(
"uvAnimationScrollXSpeedFactor"
)
if isinstance(uv_animation_scroll_x_speed_factor, (float, int)):
mtoon.uv_animation_scroll_x_speed_factor = float(
uv_animation_scroll_x_speed_factor
)
uv_animation_scroll_y_speed_factor = mtoon_dict.get(
"uvAnimationScrollYSpeedFactor"
)
if isinstance(uv_animation_scroll_y_speed_factor, (float, int)):
mtoon.uv_animation_scroll_y_speed_factor = float(
uv_animation_scroll_y_speed_factor
)
def load_materials(self, progress: PartialProgress) -> None:
material_dicts = self.parse_result.json_dict.get("materials")
if not isinstance(material_dicts, list):
progress.update(1)
return
for index, material_dict in enumerate(material_dicts):
if isinstance(material_dict, dict):
self.make_mtoon1_material(index, material_dict)
progress.update(float(index) / len(material_dicts))
progress.update(1)
def find_vrm1_bone_node_indices(self) -> list[int]:
result: list[int] = []
vrm1_dict = self.parse_result.vrm1_extension_dict
if not isinstance(vrm1_dict, dict):
return result
humanoid_dict = vrm1_dict.get("humanoid")
if not isinstance(humanoid_dict, dict):
return result
human_bones_dict = humanoid_dict.get("humanBones")
if not isinstance(human_bones_dict, dict):
return result
for human_bone_dict in human_bones_dict.values():
if not isinstance(human_bone_dict, dict):
continue
node_index = human_bone_dict.get("node")
if isinstance(node_index, int):
result.append(node_index)
return list(dict.fromkeys(result)) # Remove duplicates
def find_spring_bone1_bone_node_indices(self) -> list[int]:
result: list[int] = []
extensions_dict = self.parse_result.json_dict.get("extensions")
if not isinstance(extensions_dict, dict):
return result
spring_bone1_dict = extensions_dict.get("VRMC_springBone")
if not isinstance(spring_bone1_dict, dict):
return result
collider_dicts = spring_bone1_dict.get("colliders")
if isinstance(collider_dicts, list):
for collider_dict in collider_dicts:
if not isinstance(collider_dict, dict):
continue
node_index = collider_dict.get("node")
if isinstance(node_index, int):
result.append(node_index)
spring_dicts = spring_bone1_dict.get("springs")
if isinstance(spring_dicts, list):
for spring_dict in spring_dicts:
if not isinstance(spring_dict, dict):
continue
center_index = spring_dict.get("center")
if isinstance(center_index, int):
result.append(center_index)
joint_dicts = spring_dict.get("joints")
if not isinstance(joint_dicts, list):
continue
for joint_dict in joint_dicts:
if not isinstance(joint_dict, dict):
continue
node_index = joint_dict.get("node")
if isinstance(node_index, int):
result.append(node_index)
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_vrm1_bone_node_indices()
+ self.find_spring_bone1_bone_node_indices()
)
)
def setup_vrm1_humanoid_bones(self) -> None:
armature = self.armature
if not armature:
return
addon_extension = get_armature_extension(self.armature_data)
Vrm1HumanBonesPropertyGroup.update_all_node_candidates(
self.context, self.armature_data.name, force=True
)
human_bones = addon_extension.vrm1.humanoid.human_bones
for name, human_bone in human_bones.human_bone_name_to_human_bone().items():
if (
human_bone.node.bone_name
and human_bone.node.bone_name not in human_bone.node_candidates
):
# Invalid bone structure
return
spec = HumanBoneSpecifications.get(name)
if spec.requirement and not human_bone.node.bone_name:
# Missing required bones
return
with self.save_bone_child_object_transforms(
self.context, armature
) as armature_data:
bone_name_to_human_bone_name: dict[str, HumanBoneName] = {}
for (
human_bone_name,
human_bone,
) in human_bones.human_bone_name_to_human_bone().items():
if not human_bone.node.bone_name:
continue
bone_name_to_human_bone_name[human_bone.node.bone_name] = (
human_bone_name
)
# When a bone has multiple children
# Create a dict to pick the child bone name to point the tail to
# from that bone name
bone_name_to_main_child_bone_name: dict[str, str] = {}
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
bone_name_to_main_child_bone_name[parent.name] = bone.name
bone = parent
# Get human bones and their ancestor bones
human_bone_tree_bone_names: set[str] = set()
for bone_name in bone_name_to_human_bone_name:
bone = armature_data.edit_bones.get(bone_name)
while bone:
human_bone_tree_bone_names.add(bone.name)
bone = bone.parent
bone_name_to_axis_translation: dict[str, str] = {}
human_bone_tree_bones: list[EditBone] = []
non_human_bone_tree_bones: list[EditBone] = []
constraint_node_index_to_source_index: dict[int, int] = {}
constraint_node_index_groups: list[set[int]] = []
nodes = self.parse_result.json_dict.get("nodes")
if not isinstance(nodes, list):
nodes = []
for node_index, node_dict in enumerate(nodes):
if not isinstance(node_dict, dict):
continue
node_extensions_dict = node_dict.get("extensions")
if not isinstance(node_extensions_dict, dict):
continue
vrmc_node_constraint_dict = node_extensions_dict.get(
"VRMC_node_constraint"
)
if not isinstance(vrmc_node_constraint_dict, dict):
continue
constraint_dict = vrmc_node_constraint_dict.get("constraint")
if not isinstance(constraint_dict, dict):
continue
object_or_bone = self.get_object_or_bone_by_node_index(node_index)
if not object_or_bone:
continue
roll_dict = constraint_dict.get("roll")
if isinstance(roll_dict, dict):
source = roll_dict.get("source")
if isinstance(source, int):
constraint_node_index_to_source_index[node_index] = source
continue
aim_dict = constraint_dict.get("aim")
if isinstance(aim_dict, dict):
source = aim_dict.get("source")
if isinstance(source, int):
constraint_node_index_to_source_index[node_index] = source
continue
rotation_dict = constraint_dict.get("rotation")
if isinstance(rotation_dict, dict):
source = rotation_dict.get("source")
if isinstance(source, int):
constraint_node_index_to_source_index[node_index] = source
continue
node_indices = list(constraint_node_index_to_source_index.keys())
while node_indices:
node_index = node_indices.pop()
source_index = constraint_node_index_to_source_index.get(node_index)
if not isinstance(source_index, int):
continue
node_indices.append(source_index)
found = False
for constraint_node_index_group in constraint_node_index_groups:
if node_index in constraint_node_index_group:
constraint_node_index_group.add(source_index)
found = True
break
if source_index in constraint_node_index_group:
constraint_node_index_group.add(node_index)
found = True
break
if not found:
constraint_node_index_groups.append({node_index, source_index})
# When constraints are attached during axis conversion, we want to
# prioritize human bones and their ancestor bones, so we process
# them first in depth-first order, then process other bones in
# depth-first order
unsorted_bones = [
bone for bone in armature_data.edit_bones if not bone.parent
]
while unsorted_bones:
bone = unsorted_bones.pop()
unsorted_bones.extend(bone.children)
if bone.name in human_bone_tree_bone_names:
human_bone_tree_bones.append(bone)
else:
non_human_bone_tree_bones.append(bone)
for bone in human_bone_tree_bones + non_human_bone_tree_bones:
bone_index = next(
(i for i, n in self.bone_names.items() if n == bone.name), None
)
if isinstance(bone_index, int):
group_axis_translation: Optional[str] = None
for node_index in next(
(g for g in constraint_node_index_groups if bone_index in g),
set[int](),
):
object_or_bone = self.get_object_or_bone_by_node_index(
node_index
)
if isinstance(object_or_bone, PoseBone):
group_axis_translation = bone_name_to_axis_translation.get(
object_or_bone.name
)
if group_axis_translation is not None:
break
if group_axis_translation is not None:
bone.matrix = BoneExtension.translate_axis(
bone.matrix,
group_axis_translation,
)
if bone.parent and not bone.children:
bone.length = max(
bone.parent.length / 2, make_armature.MIN_BONE_LENGTH
)
bone_name_to_axis_translation[bone.name] = (
group_axis_translation
)
continue
main_child_bone_name = bone_name_to_main_child_bone_name.get(bone.name)
target_vector: Optional[Vector] = None
if main_child_bone_name:
main_child = next(
bone
for bone in bone.children
if bone.name == main_child_bone_name
)
target_translation = (
armature.matrix_world @ Matrix.Translation(main_child.head)
).to_translation()
base_translation = (
armature.matrix_world @ Matrix.Translation(bone.head)
).to_translation()
target_vector = target_translation - base_translation
elif bone.children:
target_translation = Vector((0.0, 0.0, 0.0))
for child in bone.children:
child_translation = (
armature.matrix_world @ Matrix.Translation(child.head)
).to_translation()
target_translation += child_translation
target_translation /= len(bone.children)
base_translation = (
armature.matrix_world @ Matrix.Translation(bone.head)
).to_translation()
target_vector = target_translation - base_translation
elif not bone.parent:
bone_name_to_axis_translation[bone.name] = (
BoneExtension.AXIS_TRANSLATION_NONE.identifier
)
continue
elif bone_name_to_human_bone_name.get(bone.name) in [
HumanBoneName.RIGHT_EYE,
HumanBoneName.LEFT_EYE,
HumanBoneName.RIGHT_FOOT,
HumanBoneName.LEFT_FOOT,
]:
target_vector = Vector((0, -1, 0))
else:
target_translation = (
armature.matrix_world @ Matrix.Translation(bone.head)
).to_translation()
base_translation = (
armature.matrix_world @ Matrix.Translation(bone.parent.head)
).to_translation()
target_vector = target_translation - base_translation
base_rotation = (armature.matrix_world @ bone.matrix).to_quaternion()
x_vector = Vector((1, 0, 0))
x_vector.rotate(base_rotation)
x_negated_vector = x_vector.copy()
x_negated_vector.negate()
y_vector = Vector((0, 1, 0))
y_vector.rotate(base_rotation)
y_negated_vector = y_vector.copy()
y_negated_vector.negate()
z_vector = Vector((0, 0, 1))
z_vector.rotate(base_rotation)
z_negated_vector = z_vector.copy()
z_negated_vector.negate()
bone_length_and_axis_translations: list[tuple[float, str]] = [
(
target_vector.dot(x_vector),
BoneExtension.AXIS_TRANSLATION_X_TO_Y.identifier,
),
(
target_vector.dot(y_vector),
BoneExtension.AXIS_TRANSLATION_NONE.identifier,
),
(
target_vector.dot(z_vector),
BoneExtension.AXIS_TRANSLATION_Z_TO_Y.identifier,
),
(
target_vector.dot(x_negated_vector),
BoneExtension.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier,
),
(
target_vector.dot(y_negated_vector),
BoneExtension.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier,
),
(
target_vector.dot(z_negated_vector),
BoneExtension.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier,
),
]
bone_length, axis_translation = sorted(
bone_length_and_axis_translations, reverse=True, key=lambda x: x[0]
)[0]
bone.matrix = BoneExtension.translate_axis(
bone.matrix,
axis_translation,
)
if bone.children:
bone.length = max(bone_length, make_armature.MIN_BONE_LENGTH)
elif bone.parent:
bone.length = max(
bone.parent.length / 2, make_armature.MIN_BONE_LENGTH
)
bone_name_to_axis_translation[bone.name] = axis_translation
connect_parent_tail_and_child_head_if_very_close_position(armature_data)
bpy.ops.object.mode_set(mode="OBJECT")
for bone_name, axis_translation in bone_name_to_axis_translation.items():
data_bone = armature_data.bones.get(bone_name)
if not data_bone:
continue
get_bone_extension(
data_bone
).axis_translation = BoneExtension.reverse_axis_translation(
axis_translation
)
for node_index in set(constraint_node_index_to_source_index.keys()) | set(
constraint_node_index_to_source_index.values()
):
object_or_bone = self.get_object_or_bone_by_node_index(node_index)
if not isinstance(object_or_bone, Object):
continue
for group_node_index in next(
(g for g in constraint_node_index_groups if node_index in g),
set[int](),
):
group_object_or_bone = self.get_object_or_bone_by_node_index(
group_node_index
)
if isinstance(group_object_or_bone, PoseBone):
self.translate_object_axis(
object_or_bone,
BoneExtension.reverse_axis_translation(
get_bone_extension(
group_object_or_bone.bone
).axis_translation
),
)
break
def translate_object_axis(self, obj: Object, axis_translation: str) -> None:
if axis_translation != BoneExtension.AXIS_TRANSLATION_AUTO.identifier:
return
matrix = BoneExtension.translate_axis(obj.matrix_world, axis_translation)
location, rotation, _ = matrix.decompose()
matrix = Matrix.Translation(location) @ rotation.to_matrix().to_4x4()
bpy.ops.object.select_all(action="DESELECT")
self.context.scene.cursor.matrix = matrix
obj.select_set(True)
bpy.ops.object.origin_set(type="ORIGIN_CURSOR", center="MEDIAN")
get_object_extension(
obj
).axis_translation = BoneExtension.reverse_axis_translation(axis_translation)
def load_gltf_extensions(self) -> None:
armature = self.armature
if not armature:
return
addon_extension = get_armature_extension(self.armature_data)
vrm1 = addon_extension.vrm1
addon_extension.spec_version = addon_extension.SPEC_VERSION_VRM1
vrm1_extension_dict = self.parse_result.vrm1_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_vrm1_meta(vrm1.meta, vrm1_extension_dict.get("meta"))
self.load_vrm1_humanoid(vrm1.humanoid, vrm1_extension_dict.get("humanoid"))
self.setup_vrm1_humanoid_bones()
self.load_vrm1_first_person(
vrm1.first_person, vrm1_extension_dict.get("firstPerson")
)
self.load_vrm1_look_at(vrm1.look_at, vrm1_extension_dict.get("lookAt"))
self.load_vrm1_expressions(
vrm1.expressions, vrm1_extension_dict.get("expressions")
)
extensions_dict = self.parse_result.json_dict.get("extensions")
if isinstance(extensions_dict, dict):
self.load_spring_bone1(
addon_extension.spring_bone1, extensions_dict.get("VRMC_springBone")
)
self.load_node_constraint1()
migration.migrate(self.context, armature.name)
def load_vrm1_meta(self, meta: Vrm1MetaPropertyGroup, meta_dict: Json) -> None:
if not isinstance(meta_dict, dict):
return
name = meta_dict.get("name")
if name is not None:
meta.vrm_name = str(name)
version = meta_dict.get("version")
if version is not None:
meta.version = str(version)
authors = meta_dict.get("authors")
if isinstance(authors, list):
for author in authors:
if author is not None:
meta.authors.add().value = str(author)
copyright_information = meta_dict.get("copyrightInformation")
if copyright_information is not None:
meta.copyright_information = str(copyright_information)
contact_information = meta_dict.get("contactInformation")
if contact_information is not None:
meta.contact_information = str(contact_information)
references = meta_dict.get("references")
if isinstance(references, list):
for reference in references:
if reference is not None:
meta.references.add().value = str(reference)
third_party_licenses = meta_dict.get("thirdPartyLicenses")
if third_party_licenses is not None:
meta.third_party_licenses = str(third_party_licenses)
thumbnail_image_index = meta_dict.get("thumbnailImage")
if isinstance(thumbnail_image_index, int):
thumbnail_image = self.images.get(thumbnail_image_index)
if thumbnail_image:
meta.thumbnail_image = thumbnail_image
avatar_permission = meta_dict.get("avatarPermission")
if (
isinstance(avatar_permission, str)
and avatar_permission
in Vrm1MetaPropertyGroup.avatar_permission_enum.identifiers()
):
meta.avatar_permission = avatar_permission
meta.allow_excessively_violent_usage = bool(
meta_dict.get("allowExcessivelyViolentUsage")
)
meta.allow_excessively_sexual_usage = bool(
meta_dict.get("allowExcessivelySexualUsage")
)
commercial_usage = meta_dict.get("commercialUsage")
if (
isinstance(commercial_usage, str)
and commercial_usage
in Vrm1MetaPropertyGroup.commercial_usage_enum.identifiers()
):
meta.commercial_usage = commercial_usage
meta.allow_political_or_religious_usage = bool(
meta_dict.get("allowPoliticalOrReligiousUsage")
)
meta.allow_antisocial_or_hate_usage = bool(
meta_dict.get("allowAntisocialOrHateUsage")
)
credit_notation = meta_dict.get("creditNotation")
if (
isinstance(credit_notation, str)
and credit_notation
in Vrm1MetaPropertyGroup.credit_notation_enum.identifiers()
):
meta.credit_notation = credit_notation
meta.allow_redistribution = bool(meta_dict.get("allowRedistribution"))
modification = meta_dict.get("modification")
if (
isinstance(modification, str)
and modification in Vrm1MetaPropertyGroup.modification_enum.identifiers()
):
meta.modification = modification
other_license_url = meta_dict.get("otherLicenseUrl")
if other_license_url is not None:
meta.other_license_url = str(other_license_url)
def load_vrm1_humanoid(
self, humanoid: Vrm1HumanoidPropertyGroup, humanoid_dict: Json
) -> None:
if not isinstance(humanoid_dict, dict):
return
human_bones_dict = humanoid_dict.get("humanBones")
if not isinstance(human_bones_dict, dict):
return
human_bone_name_to_human_bone = (
humanoid.human_bones.human_bone_name_to_human_bone()
)
assigned_bone_names: list[str] = []
for human_bone_name in [
human_bone.name
for human_bone in vrm1_human_bone.HumanBoneSpecifications.all_human_bones
]:
human_bone_dict = human_bones_dict.get(human_bone_name.value)
if not isinstance(human_bone_dict, dict):
continue
node_index = human_bone_dict.get("node")
if not isinstance(node_index, int):
continue
bone_name = self.bone_names.get(node_index)
if not isinstance(bone_name, str) or bone_name in assigned_bone_names:
continue
human_bone_name_to_human_bone[human_bone_name].node.bone_name = bone_name
assigned_bone_names.append(bone_name)
def load_vrm1_first_person(
self,
first_person: Vrm1FirstPersonPropertyGroup,
first_person_dict: Json,
) -> None:
if not isinstance(first_person_dict, dict):
return
mesh_annotation_dicts = first_person_dict.get("meshAnnotations")
if not isinstance(mesh_annotation_dicts, list):
mesh_annotation_dicts = []
for mesh_annotation_dict in mesh_annotation_dicts:
mesh_annotation = first_person.mesh_annotations.add()
if not isinstance(mesh_annotation_dict, dict):
continue
node = mesh_annotation_dict.get("node")
if isinstance(node, int):
mesh_object_name = self.mesh_object_names.get(node)
if isinstance(mesh_object_name, str):
mesh_annotation.node.mesh_object_name = mesh_object_name
type_ = mesh_annotation_dict.get("type")
if type_ in mesh_annotation.type_enum.identifiers():
mesh_annotation.type = type_
def load_vrm1_look_at(
self,
look_at: Vrm1LookAtPropertyGroup,
look_at_dict: Json,
) -> None:
if not isinstance(look_at_dict, dict):
return
armature = self.armature
if armature is None:
return
offset_from_head_bone = Vector(
convert.vrm_json_array_to_float_vector(
look_at_dict.get("offsetFromHeadBone"), [0, 0, 0]
)
)
humanoid = get_armature_extension(self.armature_data).vrm1.humanoid
head_bone_name = humanoid.human_bones.head.node.bone_name
head_bone = self.armature_data.bones.get(head_bone_name)
if head_bone:
offset_from_head_bone = (
offset_from_head_bone
@ BoneExtension.translate_axis(
Matrix(),
BoneExtension.reverse_axis_translation(
get_bone_extension(head_bone).axis_translation
),
)
)
look_at.offset_from_head_bone = offset_from_head_bone
type_ = look_at_dict.get("type")
if type_ in look_at.type_enum.identifiers():
look_at.type = type_
for range_map, range_map_dict in [
(
look_at.range_map_horizontal_inner,
look_at_dict.get("rangeMapHorizontalInner"),
),
(
look_at.range_map_horizontal_outer,
look_at_dict.get("rangeMapHorizontalOuter"),
),
(
look_at.range_map_vertical_down,
look_at_dict.get("rangeMapVerticalDown"),
),
(
look_at.range_map_vertical_up,
look_at_dict.get("rangeMapVerticalUp"),
),
]:
if not isinstance(range_map_dict, dict):
continue
input_max_value = range_map_dict.get("inputMaxValue")
if isinstance(input_max_value, (float, int)):
range_map.input_max_value = float(input_max_value)
output_scale = range_map_dict.get("outputScale")
if isinstance(output_scale, (float, int)):
range_map.output_scale = float(output_scale)
def load_vrm1_expression(
self,
expression: Vrm1ExpressionPropertyGroup,
expression_dict: Json,
) -> None:
if not isinstance(expression_dict, dict):
return
morph_target_bind_dicts = expression_dict.get("morphTargetBinds")
if not isinstance(morph_target_bind_dicts, list):
morph_target_bind_dicts = []
for morph_target_bind_dict in morph_target_bind_dicts:
if not isinstance(morph_target_bind_dict, dict):
continue
morph_target_bind = expression.morph_target_binds.add()
weight = convert.float_or(morph_target_bind_dict.get("weight"), 0.0)
morph_target_bind.weight = weight
node_index = morph_target_bind_dict.get("node")
if not isinstance(node_index, int):
continue
node_dicts = self.parse_result.json_dict.get("nodes")
if not isinstance(node_dicts, list) or not (
0 <= node_index < len(node_dicts)
):
continue
node_dict = node_dicts[node_index]
if not isinstance(node_dict, dict):
continue
mesh_index = node_dict.get("mesh")
if not isinstance(mesh_index, int):
continue
mesh_obj = self.meshes.get(mesh_index)
if not mesh_obj:
continue
morph_target_bind.node.mesh_object_name = mesh_obj.name
index = morph_target_bind_dict.get("index")
if not isinstance(index, int):
continue
mesh_data = mesh_obj.data
if not isinstance(mesh_data, Mesh):
continue
shape_keys = mesh_data.shape_keys
if not shape_keys:
continue
key_blocks = shape_keys.key_blocks
if 1 <= (index + 1) < len(key_blocks):
morph_target_bind.index = key_blocks[index + 1].name
material_color_bind_dicts = expression_dict.get("materialColorBinds")
if not isinstance(material_color_bind_dicts, list):
material_color_bind_dicts = []
for material_color_bind_dict in material_color_bind_dicts:
material_color_bind = expression.material_color_binds.add()
if not isinstance(material_color_bind_dict, dict):
continue
material_index = material_color_bind_dict.get("material")
if isinstance(material_index, int):
material = self.materials.get(material_index)
if material:
material_color_bind.material = material
type_ = material_color_bind_dict.get("type")
if type_ in Vrm1MaterialColorBindPropertyGroup.type_enum.identifiers():
material_color_bind.type = type_
target_value = material_color_bind_dict.get("targetValue")
if not isinstance(target_value, list):
target_value = []
target_value = [
float(v) if isinstance(v, (float, int)) else 0.0 for v in target_value
]
while len(target_value) < 4:
target_value.append(0.0)
material_color_bind.target_value = target_value[:4]
texture_transform_bind_dicts = expression_dict.get("textureTransformBinds")
if not isinstance(texture_transform_bind_dicts, list):
texture_transform_bind_dicts = []
for texture_transform_bind_dict in texture_transform_bind_dicts:
texture_transform_bind = expression.texture_transform_binds.add()
if not isinstance(texture_transform_bind_dict, dict):
continue
material_index = texture_transform_bind_dict.get("material")
if isinstance(material_index, int):
material = self.materials.get(material_index)
if material:
texture_transform_bind.material = material
texture_transform_bind.scale = convert.vrm_json_array_to_float_vector(
texture_transform_bind_dict.get("scale"), [1, 1]
)
texture_transform_bind.offset = convert.vrm_json_array_to_float_vector(
texture_transform_bind_dict.get("offset"), [0, 0]
)
is_binary = expression_dict.get("isBinary")
if isinstance(is_binary, bool):
expression.is_binary = is_binary
override_blink = expression_dict.get("overrideBlink")
if (
isinstance(override_blink, str)
and override_blink
in Vrm1ExpressionPropertyGroup.expression_override_type_enum.identifiers()
):
expression.override_blink = override_blink
override_look_at = expression_dict.get("overrideLookAt")
if (
isinstance(override_look_at, str)
and override_look_at
in Vrm1ExpressionPropertyGroup.expression_override_type_enum.identifiers()
):
expression.override_look_at = override_look_at
override_mouth = expression_dict.get("overrideMouth")
if (
isinstance(override_mouth, str)
and override_mouth
in Vrm1ExpressionPropertyGroup.expression_override_type_enum.identifiers()
):
expression.override_mouth = override_mouth
def load_vrm1_expressions(
self,
expressions: Vrm1ExpressionsPropertyGroup,
expressions_dict: Json,
) -> None:
if not isinstance(expressions_dict, dict):
return
preset_dict = expressions_dict.get("preset")
if isinstance(preset_dict, dict):
for (
preset_name,
expression,
) in expressions.preset.name_to_expression_dict().items():
self.load_vrm1_expression(expression, preset_dict.get(preset_name))
custom_dict = expressions_dict.get("custom")
if isinstance(custom_dict, dict):
for custom_name, expression_dict in custom_dict.items():
expression = expressions.custom.add()
expression.custom_name = custom_name
self.load_vrm1_expression(expression, expression_dict)
def load_spring_bone1_collider(
self,
collider: SpringBone1ColliderPropertyGroup,
collider_dict: dict[str, Json],
) -> None:
bone: Optional[Bone] = None
node_index = collider_dict.get("node")
if isinstance(node_index, int):
bone_name = self.bone_names.get(node_index)
if isinstance(bone_name, str):
collider.node.bone_name = bone_name
collider_bpy_object = collider.bpy_object
if collider_bpy_object:
collider_bpy_object.name = f"{bone_name} Collider"
bone = self.armature_data.bones.get(collider.node.bone_name)
shape_dict = collider_dict.get("shape")
if not isinstance(shape_dict, dict):
collider.shape.sphere.radius = 0.0001
collider.shape.sphere.offset = (0, 0, -10000)
return
shape = collider.shape
sphere_dict = shape_dict.get("sphere")
capsule_dict = shape_dict.get("capsule")
if isinstance(sphere_dict, dict):
collider.shape_type = collider.SHAPE_TYPE_SPHERE.identifier
offset = Vector(
convert.vrm_json_array_to_float_vector(
sphere_dict.get("offset"), [0, 0, 0]
)
)
if bone:
axis_translation = get_bone_extension(bone).axis_translation
offset = Vector(offset) @ BoneExtension.translate_axis(
Matrix(),
BoneExtension.reverse_axis_translation(axis_translation),
)
shape.sphere.offset = offset
radius = sphere_dict.get("radius")
if isinstance(radius, (float, int)):
shape.sphere.radius = float(radius)
elif isinstance(capsule_dict, dict):
collider.shape_type = collider.SHAPE_TYPE_CAPSULE.identifier
offset = Vector(
convert.vrm_json_array_to_float_vector(
capsule_dict.get("offset"), [0, 0, 0]
)
)
if bone:
axis_translation = get_bone_extension(bone).axis_translation
offset = Vector(offset) @ BoneExtension.translate_axis(
Matrix(),
BoneExtension.reverse_axis_translation(axis_translation),
)
shape.capsule.offset = offset
radius = capsule_dict.get("radius")
if isinstance(radius, (float, int)):
shape.capsule.radius = float(radius)
tail = Vector(
convert.vrm_json_array_to_float_vector(
capsule_dict.get("tail"), [0, 0, 0]
)
)
if bone:
axis_translation = get_bone_extension(bone).axis_translation
tail = Vector(tail) @ BoneExtension.translate_axis(
Matrix(),
BoneExtension.reverse_axis_translation(axis_translation),
)
shape.capsule.tail = tail
extensions_dict = collider_dict.get("extensions")
if not isinstance(extensions_dict, dict):
return
extended_collider_dict = extensions_dict.get(
"VRMC_springBone_extended_collider"
)
if not isinstance(extended_collider_dict, dict):
return
extended_shape_dict = extended_collider_dict.get("shape")
if not isinstance(extended_shape_dict, dict):
return
extended_collider = collider.extensions.vrmc_spring_bone_extended_collider
extended_shape = extended_collider.shape
extended_sphere_dict = extended_shape_dict.get("sphere")
extended_capsule_dict = extended_shape_dict.get("capsule")
extended_plane_dict = extended_shape_dict.get("plane")
if isinstance(extended_sphere_dict, dict):
extended_collider.shape_type = (
extended_collider.SHAPE_TYPE_EXTENDED_SPHERE.identifier
)
offset = Vector(
convert.vrm_json_array_to_float_vector(
extended_sphere_dict.get("offset"), [0, 0, 0]
)
)
if bone:
axis_translation = get_bone_extension(bone).axis_translation
offset = Vector(offset) @ BoneExtension.translate_axis(
Matrix(),
BoneExtension.reverse_axis_translation(axis_translation),
)
extended_shape.sphere.offset = offset
radius = extended_sphere_dict.get("radius")
if isinstance(radius, (float, int)):
extended_shape.sphere.radius = float(radius)
inside = extended_sphere_dict.get("inside")
if isinstance(inside, bool):
extended_shape.sphere.inside = inside
elif isinstance(extended_capsule_dict, dict):
extended_collider.shape_type = (
extended_collider.SHAPE_TYPE_EXTENDED_CAPSULE.identifier
)
offset = Vector(
convert.vrm_json_array_to_float_vector(
extended_capsule_dict.get("offset"), [0, 0, 0]
)
)
if bone:
axis_translation = get_bone_extension(bone).axis_translation
offset = Vector(offset) @ BoneExtension.translate_axis(
Matrix(),
BoneExtension.reverse_axis_translation(axis_translation),
)
extended_shape.capsule.offset = offset
radius = extended_capsule_dict.get("radius")
if isinstance(radius, (float, int)):
extended_shape.capsule.radius = float(radius)
tail = Vector(
convert.vrm_json_array_to_float_vector(
extended_capsule_dict.get("tail"), [0, 0, 0]
)
)
if bone:
axis_translation = get_bone_extension(bone).axis_translation
tail = Vector(tail) @ BoneExtension.translate_axis(
Matrix(),
BoneExtension.reverse_axis_translation(axis_translation),
)
extended_shape.capsule.tail = tail
inside = extended_capsule_dict.get("inside")
if isinstance(inside, bool):
extended_shape.capsule.inside = inside
elif isinstance(extended_plane_dict, dict):
extended_collider.shape_type = (
extended_collider.SHAPE_TYPE_EXTENDED_PLANE.identifier
)
offset = Vector(
convert.vrm_json_array_to_float_vector(
extended_plane_dict.get("offset"), [0, 0, 0]
)
)
if bone:
axis_translation = get_bone_extension(bone).axis_translation
offset = Vector(offset) @ BoneExtension.translate_axis(
Matrix(),
BoneExtension.reverse_axis_translation(axis_translation),
)
extended_shape.plane.offset = offset
normal = Vector(
convert.vrm_json_array_to_float_vector(
extended_plane_dict.get("normal"), [0, 0, 0]
)
)
if bone:
axis_translation = get_bone_extension(bone).axis_translation
normal = Vector(normal) @ BoneExtension.translate_axis(
Matrix(),
BoneExtension.reverse_axis_translation(axis_translation),
)
extended_shape.plane.normal = normal
collider_bpy_object = collider.bpy_object
if collider_bpy_object:
# Override the fallback collider values with fixed values when loading
collider_bpy_object.empty_display_size = 0.125
else:
return
extended_collider.enabled = True
extended_collider.automatic_fallback_generation = False
def load_spring_bone1_colliders(
self,
spring_bone: SpringBone1SpringBonePropertyGroup,
spring_bone_dict: dict[str, Json],
armature: Object,
) -> None:
collider_dicts = spring_bone_dict.get("colliders")
if not isinstance(collider_dicts, list):
collider_dicts = []
for collider_dict in collider_dicts:
# Since the reference from ColliderGroup to Collider is by index,
# create empty data even if the contents of collider_dict are invalid
if ops.vrm.add_spring_bone1_collider(
armature_object_name=armature.name
) != {"FINISHED"}:
message = f'Failed to add spring bone 1.0 collider to "{armature.name}"'
raise ValueError(message)
collider = spring_bone.colliders[-1]
if not isinstance(collider_dict, dict):
collider.shape.sphere.radius = 0.0001
collider.shape.sphere.offset = (0, 0, -10000)
continue
self.load_spring_bone1_collider(collider, collider_dict)
for collider in spring_bone.colliders:
collider.reset_bpy_object(self.context, armature)
collider_object_names: list[str] = []
if spring_bone.colliders:
colliders_collection = self.context.blend_data.collections.new("Colliders")
self.context.scene.collection.children.link(colliders_collection)
for collider in spring_bone.colliders:
collider_bpy_object = collider.bpy_object
if not collider_bpy_object:
continue
colliders_collection.objects.link(collider_bpy_object)
collider_object_names.append(collider_bpy_object.name)
if collider_bpy_object.name in self.context.scene.collection.objects:
self.context.scene.collection.objects.unlink(collider_bpy_object)
for child in collider_bpy_object.children:
collider_object_names.append(child.name)
colliders_collection.objects.link(child)
if child.name in self.context.scene.collection.objects:
self.context.scene.collection.objects.unlink(child)
spring_bone.active_collider_index = 0
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 load_spring_bone1_collider_groups(
self,
spring_bone: SpringBone1SpringBonePropertyGroup,
spring_bone_dict: dict[str, Json],
armature_object_name: str,
) -> None:
collider_group_dicts = spring_bone_dict.get("colliderGroups")
if not isinstance(collider_group_dicts, list):
collider_group_dicts = []
for collider_group_index, collider_group_dict in enumerate(
collider_group_dicts
):
# Since the reference from Spring to ColliderGroup is by index,
# create empty data even if the contents of collider_group_dict are invalid
if ops.vrm.add_spring_bone1_collider_group(
armature_object_name=armature_object_name
) != {"FINISHED"}:
message = (
"Failed to add spring bone 1.0 collider group"
+ f" to {armature_object_name}"
)
raise ValueError(message)
if not isinstance(collider_group_dict, dict):
continue
collider_group = spring_bone.collider_groups[-1]
name = collider_group_dict.get("name")
if isinstance(name, str):
collider_group.vrm_name = name
collider_indices = collider_group_dict.get("colliders")
if not isinstance(collider_indices, list):
continue
for collider_index in collider_indices:
if ops.vrm.add_spring_bone1_collider_group_collider(
armature_object_name=armature_object_name,
collider_group_index=collider_group_index,
) != {"FINISHED"}:
raise ValueError(
"Failed to assign spring bone 1.0 collider to collider group "
+ f"{collider_group_index} in {armature_object_name}"
)
if not isinstance(collider_index, int):
continue
if not 0 <= collider_index < len(spring_bone.colliders):
continue
collider = spring_bone.colliders[collider_index]
if not collider:
continue
collider_reference = collider_group.colliders[-1]
collider_reference.collider_name = collider.name
collider_group.active_collider_index = 0
spring_bone.active_collider_group_index = 0
def load_spring_bone1_springs(
self,
spring_bone: SpringBone1SpringBonePropertyGroup,
spring_bone_dict: dict[str, Json],
armature_object_name: str,
) -> None:
spring_dicts = spring_bone_dict.get("springs")
if not isinstance(spring_dicts, list):
spring_dicts = []
for spring_dict in spring_dicts:
if ops.vrm.add_spring_bone1_spring(
armature_object_name=armature_object_name
) != {"FINISHED"} or not isinstance(spring_dict, dict):
continue
spring = spring_bone.springs[-1]
name = spring_dict.get("name")
if isinstance(name, str):
spring.vrm_name = name
center_index = spring_dict.get("center")
if isinstance(center_index, int):
bone_name = self.bone_names.get(center_index)
if bone_name:
spring.center.bone_name = bone_name
joint_dicts = spring_dict.get("joints")
if not isinstance(joint_dicts, list):
joint_dicts = []
for joint_dict in joint_dicts:
if ops.vrm.add_spring_bone1_spring_joint(
armature_object_name=armature_object_name,
spring_index=len(spring_bone.springs) - 1,
) != {"FINISHED"}:
continue
if not isinstance(joint_dict, dict):
continue
joint = spring.joints[-1]
node_index = joint_dict.get("node")
if isinstance(node_index, int):
bone_name = self.bone_names.get(node_index)
if bone_name:
joint.node.bone_name = bone_name
hit_radius = convert.float_or_none(joint_dict.get("hitRadius"))
if hit_radius is not None:
joint.hit_radius = hit_radius
stiffness = convert.float_or_none(joint_dict.get("stiffness"))
if stiffness is not None:
joint.stiffness = stiffness
gravity_power = convert.float_or_none(joint_dict.get("gravityPower"))
if gravity_power is not None:
joint.gravity_power = gravity_power
gltf_axis_gravity_dir = convert.vrm_json_array_to_float_vector(
joint_dict.get("gravityDir"),
[0.0, -1.0, 0.0],
)
joint.gravity_dir = [
gltf_axis_gravity_dir[0],
-gltf_axis_gravity_dir[2],
gltf_axis_gravity_dir[1],
]
drag_force = convert.float_or_none(joint_dict.get("dragForce"))
if drag_force is not None:
joint.drag_force = drag_force
spring.active_joint_index = 0
collider_group_indices = spring_dict.get("colliderGroups")
if not isinstance(collider_group_indices, list):
collider_group_indices = []
for collider_group_index in collider_group_indices:
if ops.vrm.add_spring_bone1_spring_collider_group(
armature_object_name=armature_object_name,
spring_index=len(spring_bone.springs) - 1,
) != {"FINISHED"}:
continue
if not isinstance(collider_group_index, int):
continue
if not 0 <= collider_group_index < len(spring_bone.collider_groups):
continue
collider_group = spring_bone.collider_groups[collider_group_index]
if not collider_group:
continue
collider_group_reference = spring.collider_groups[-1]
collider_group_reference.collider_group_name = collider_group.name
spring.active_collider_group_index = 0
spring_bone.active_spring_index = 0
def load_spring_bone1(
self,
spring_bone: SpringBone1SpringBonePropertyGroup,
spring_bone_dict: Json,
) -> None:
if not isinstance(spring_bone_dict, dict):
return
armature = self.armature
if armature is None:
message = "armature is None"
raise ValueError(message)
self.load_spring_bone1_colliders(spring_bone, spring_bone_dict, armature)
self.load_spring_bone1_collider_groups(
spring_bone,
spring_bone_dict,
armature.name,
)
self.load_spring_bone1_springs(
spring_bone,
spring_bone_dict,
armature.name,
)
def get_object_or_bone_by_node_index(
self, node_index: int
) -> Union[Object, PoseBone, None]:
object_name = self.object_names.get(node_index)
bone_name = self.bone_names.get(node_index)
if object_name is not None:
return self.context.blend_data.objects.get(object_name)
if self.armature and bone_name is not None:
return self.armature.pose.bones.get(bone_name)
return None
def load_node_constraint1(
self,
) -> None:
armature = self.armature
if not armature:
return
nodes = self.parse_result.json_dict.get("nodes")
if not isinstance(nodes, list):
nodes = []
for node_index, node_dict in enumerate(nodes):
if not isinstance(node_dict, dict):
continue
node_extensions_dict = node_dict.get("extensions")
if not isinstance(node_extensions_dict, dict):
continue
vrmc_node_constraint = node_extensions_dict.get("VRMC_node_constraint")
if not isinstance(vrmc_node_constraint, dict):
continue
constraint_dict = vrmc_node_constraint.get("constraint")
if not isinstance(constraint_dict, dict):
continue
roll_dict = constraint_dict.get("roll")
aim_dict = constraint_dict.get("aim")
rotation_dict = constraint_dict.get("rotation")
object_or_bone = self.get_object_or_bone_by_node_index(node_index)
if not object_or_bone:
continue
axis_translation = BoneExtension.reverse_axis_translation(
get_bone_extension(object_or_bone.bone).axis_translation
if isinstance(object_or_bone, PoseBone)
else get_object_extension(object_or_bone).axis_translation
)
if isinstance(roll_dict, dict):
constraint = object_or_bone.constraints.new(type="COPY_ROTATION")
if not isinstance(constraint, CopyRotationConstraint):
logger.error("%s is not a CopyRotationConstraint", type(constraint))
continue
constraint.mix_mode = "ADD"
constraint.owner_space = "LOCAL"
constraint.target_space = "LOCAL"
roll_axis = roll_dict.get("rollAxis")
if isinstance(object_or_bone, PoseBone) and isinstance(roll_axis, str):
roll_axis = BoneExtension.node_constraint_roll_axis_translation(
axis_translation,
roll_axis,
)
constraint.use_x = False
constraint.use_y = False
constraint.use_z = False
if roll_axis == "X":
constraint.use_x = True
elif roll_axis == "Y":
constraint.use_y = True
elif roll_axis == "Z":
constraint.use_z = True
weight = convert.float_or_none(roll_dict.get("weight"))
if weight is not None:
constraint.influence = weight
source_index = roll_dict.get("source")
elif isinstance(aim_dict, dict):
constraint = object_or_bone.constraints.new(type="DAMPED_TRACK")
if not isinstance(constraint, DampedTrackConstraint):
logger.error("%s is not a CopyRotationConstraint", type(constraint))
continue
aim_axis = aim_dict.get("aimAxis")
if isinstance(aim_axis, str) and isinstance(object_or_bone, PoseBone):
aim_axis = BoneExtension.node_constraint_aim_axis_translation(
axis_translation,
aim_axis,
)
if isinstance(aim_axis, str):
track_axis = convert.VRM_AIM_AXIS_TO_BPY_TRACK_AXIS.get(aim_axis)
if track_axis:
constraint.track_axis = track_axis
weight = convert.float_or_none(aim_dict.get("weight"))
if weight is not None:
constraint.influence = weight
source_index = aim_dict.get("source")
elif isinstance(rotation_dict, dict):
constraint = object_or_bone.constraints.new(type="COPY_ROTATION")
if not isinstance(constraint, CopyRotationConstraint):
logger.error("%s is not a CopyRotationConstraint", type(constraint))
continue
constraint.mix_mode = "ADD"
constraint.owner_space = "LOCAL"
constraint.target_space = "LOCAL"
constraint.use_x = True
constraint.use_y = True
constraint.use_z = True
weight = convert.float_or_none(rotation_dict.get("weight"))
if weight is not None:
constraint.influence = weight
source_index = rotation_dict.get("source")
else:
continue
if isinstance(source_index, int):
source = self.get_object_or_bone_by_node_index(source_index)
if isinstance(source, Object):
constraint.target = source
elif isinstance(source, PoseBone):
constraint.target = armature
constraint.subtarget = source.name