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