- 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
1876 lines
75 KiB
Python
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
|