feat: Add VRM Blender addon with complete import/export functionality
- Add core VRM addon infrastructure with manifest and registration - Add common utilities module with file system, logging, and conversion helpers - Add human bone mapper with support for multiple rigging standards (Mixamo, MMD, Unreal, Rigify, etc.) - Add VRM 0.x and 1.x format support with property groups and handlers - Add editor UI panels for VRM metadata, spring bones, and MToon materials - Add exporter with glTF2 extension support for VRM format serialization - Add importer with scene reconstruction and armature generation - Add MToon shader support with auto-setup and material migration - Add spring bone physics simulation with constraint handling - Add node constraint editor for advanced rigging control - Add comprehensive validation and error handling with user dialogs - Add scene watcher for real-time property synchronization - Add workspace management and preference system - Include Python cache files and Blender manifest configuration - This is the initial commit establishing the complete VRM addon ecosystem for Blender
This commit is contained in:
1
editor/__init__.py
Normal file
1
editor/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
BIN
editor/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
editor/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/__pycache__/extension.cpython-311.pyc
Normal file
BIN
editor/__pycache__/extension.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/__pycache__/handler.cpython-311.pyc
Normal file
BIN
editor/__pycache__/handler.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/__pycache__/make_armature.cpython-311.pyc
Normal file
BIN
editor/__pycache__/make_armature.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/__pycache__/migration.cpython-311.pyc
Normal file
BIN
editor/__pycache__/migration.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/__pycache__/ops.cpython-311.pyc
Normal file
BIN
editor/__pycache__/ops.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/__pycache__/panel.cpython-311.pyc
Normal file
BIN
editor/__pycache__/panel.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/__pycache__/property_group.cpython-311.pyc
Normal file
BIN
editor/__pycache__/property_group.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/__pycache__/search.cpython-311.pyc
Normal file
BIN
editor/__pycache__/search.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/__pycache__/subscription.cpython-311.pyc
Normal file
BIN
editor/__pycache__/subscription.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/__pycache__/t_pose.cpython-311.pyc
Normal file
BIN
editor/__pycache__/t_pose.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/__pycache__/validation.cpython-311.pyc
Normal file
BIN
editor/__pycache__/validation.cpython-311.pyc
Normal file
Binary file not shown.
539
editor/extension.py
Normal file
539
editor/extension.py
Normal file
@@ -0,0 +1,539 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import math
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Optional, TypeVar
|
||||
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
IntVectorProperty,
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.types import (
|
||||
Armature,
|
||||
Bone,
|
||||
Context,
|
||||
Material,
|
||||
NodeTree,
|
||||
Object,
|
||||
PropertyGroup,
|
||||
Scene,
|
||||
)
|
||||
from mathutils import Matrix, Quaternion
|
||||
|
||||
from ..common.logger import get_logger
|
||||
from ..common.preferences import VrmAddonPreferences
|
||||
from .mtoon1.property_group import Mtoon1MaterialPropertyGroup
|
||||
from .node_constraint1.property_group import NodeConstraint1NodeConstraintPropertyGroup
|
||||
from .property_group import StringPropertyGroup, property_group_enum
|
||||
from .spring_bone1.property_group import SpringBone1SpringBonePropertyGroup
|
||||
from .vrm0.property_group import Vrm0HumanoidPropertyGroup, Vrm0PropertyGroup
|
||||
from .vrm1.property_group import Vrm1HumanBonesPropertyGroup, Vrm1PropertyGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .property_group import CollectionPropertyProtocol
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class VrmAddonSceneExtensionPropertyGroup(PropertyGroup):
|
||||
vrm0_material_gltf_property_names: CollectionProperty( # type: ignore[valid-type]
|
||||
type=StringPropertyGroup
|
||||
)
|
||||
|
||||
vrm0_material_mtoon0_property_names: CollectionProperty( # type: ignore[valid-type]
|
||||
type=StringPropertyGroup
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update_vrm0_material_property_names_timer_callback() -> Optional[float]:
|
||||
context = bpy.context
|
||||
|
||||
VrmAddonSceneExtensionPropertyGroup.update_vrm0_material_property_names(context)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def defer_update_vrm0_material_property_names() -> None:
|
||||
s = VrmAddonSceneExtensionPropertyGroup
|
||||
bpy.app.timers.register(s.update_vrm0_material_property_names_timer_callback)
|
||||
|
||||
@staticmethod
|
||||
def update_vrm0_material_property_names(
|
||||
context: Context, scene_name: Optional[str] = None
|
||||
) -> None:
|
||||
# Unity 2022.3.4 + UniVRM 0.112.0
|
||||
gltf_property_names = [
|
||||
"_Color",
|
||||
"_MainTex_ST",
|
||||
"_MainTex_ST_S",
|
||||
"_MainTex_ST_T",
|
||||
"_MetallicGlossMap_ST",
|
||||
"_MetallicGlossMap_ST_S",
|
||||
"_MetallicGlossMap_ST_T",
|
||||
"_BumpMap_ST",
|
||||
"_BumpMap_ST_S",
|
||||
"_BumpMap_ST_T",
|
||||
"_ParallaxMap_ST",
|
||||
"_ParallaxMap_ST_S",
|
||||
"_ParallaxMap_ST_T",
|
||||
"_OcclusionMap_ST",
|
||||
"_OcclusionMap_ST_S",
|
||||
"_OcclusionMap_ST_T",
|
||||
"_EmissionColor",
|
||||
"_EmissionMap_ST",
|
||||
"_EmissionMap_ST_S",
|
||||
"_EmissionMap_ST_T",
|
||||
"_DetailMask_ST",
|
||||
"_DetailMask_ST_S",
|
||||
"_DetailMask_ST_T",
|
||||
"_DetailAlbedoMap_ST",
|
||||
"_DetailAlbedoMap_ST_S",
|
||||
"_DetailAlbedoMap_ST_T",
|
||||
"_DetailNormalMap_ST",
|
||||
"_DetailNormalMap_ST_S",
|
||||
"_DetailNormalMap_ST_T",
|
||||
]
|
||||
|
||||
# UniVRM 0.112.0
|
||||
mtoon0_property_names = [
|
||||
"_Color",
|
||||
"_ShadeColor",
|
||||
"_MainTex_ST",
|
||||
"_MainTex_ST_S",
|
||||
"_MainTex_ST_T",
|
||||
"_ShadeTexture_ST",
|
||||
"_ShadeTexture_ST_S",
|
||||
"_ShadeTexture_ST_T",
|
||||
"_BumpMap_ST",
|
||||
"_BumpMap_ST_S",
|
||||
"_BumpMap_ST_T",
|
||||
"_ReceiveShadowTexture_ST",
|
||||
"_ReceiveShadowTexture_ST_S",
|
||||
"_ReceiveShadowTexture_ST_T",
|
||||
"_ShadingGradeTexture_ST",
|
||||
"_ShadingGradeTexture_ST_S",
|
||||
"_ShadingGradeTexture_ST_T",
|
||||
"_RimColor",
|
||||
"_RimTexture_ST",
|
||||
"_RimTexture_ST_S",
|
||||
"_RimTexture_ST_T",
|
||||
"_SphereAdd_ST",
|
||||
"_SphereAdd_ST_S",
|
||||
"_SphereAdd_ST_T",
|
||||
"_EmissionColor",
|
||||
"_EmissionMap_ST",
|
||||
"_EmissionMap_ST_S",
|
||||
"_EmissionMap_ST_T",
|
||||
"_OutlineWidthTexture_ST",
|
||||
"_OutlineWidthTexture_ST_S",
|
||||
"_OutlineWidthTexture_ST_T",
|
||||
"_OutlineColor",
|
||||
"_UvAnimMaskTexture_ST",
|
||||
"_UvAnimMaskTexture_ST_S",
|
||||
"_UvAnimMaskTexture_ST_T",
|
||||
]
|
||||
|
||||
if scene_name is None:
|
||||
scenes: Sequence[Scene] = list(context.blend_data.scenes)
|
||||
else:
|
||||
scene = context.blend_data.scenes.get(scene_name)
|
||||
if not scene:
|
||||
logger.error('No scene "%s"', scene_name)
|
||||
return
|
||||
scenes = [scene]
|
||||
|
||||
for scene in scenes:
|
||||
ext = get_scene_extension(scene)
|
||||
|
||||
if gltf_property_names != [
|
||||
n.value for n in ext.vrm0_material_gltf_property_names
|
||||
]:
|
||||
ext.vrm0_material_gltf_property_names.clear()
|
||||
for gltf_property_name in gltf_property_names:
|
||||
n = ext.vrm0_material_gltf_property_names.add()
|
||||
n.value = gltf_property_name
|
||||
|
||||
if mtoon0_property_names != [
|
||||
n.value for n in ext.vrm0_material_mtoon0_property_names
|
||||
]:
|
||||
ext.vrm0_material_mtoon0_property_names.clear()
|
||||
for mtoon0_property_name in mtoon0_property_names:
|
||||
n = ext.vrm0_material_mtoon0_property_names.add()
|
||||
n.value = mtoon0_property_name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
vrm0_material_gltf_property_names: CollectionPropertyProtocol[ # type: ignore[no-redef]
|
||||
StringPropertyGroup
|
||||
]
|
||||
vrm0_material_mtoon0_property_names: CollectionPropertyProtocol[ # type: ignore[no-redef]
|
||||
StringPropertyGroup
|
||||
]
|
||||
|
||||
|
||||
class VrmAddonBoneExtensionPropertyGroup(PropertyGroup):
|
||||
uuid: StringProperty() # type: ignore[valid-type]
|
||||
|
||||
(
|
||||
axis_translation_enum,
|
||||
(
|
||||
AXIS_TRANSLATION_AUTO,
|
||||
AXIS_TRANSLATION_NONE,
|
||||
AXIS_TRANSLATION_X_TO_Y,
|
||||
AXIS_TRANSLATION_MINUS_X_TO_Y,
|
||||
AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z,
|
||||
AXIS_TRANSLATION_Z_TO_Y,
|
||||
AXIS_TRANSLATION_MINUS_Z_TO_Y,
|
||||
),
|
||||
) = property_group_enum(
|
||||
("AUTO", "Auto", "", "NONE", 0),
|
||||
("NONE", "None", "", "NONE", 1),
|
||||
("X_TO_Y", "X,Y to Y,-X", "", "NONE", 2),
|
||||
("MINUS_X_TO_Y", "X,Y to -Y,X", "", "NONE", 3),
|
||||
("MINUS_Y_TO_Y_AROUND_Z", "X,Y to -X,-Y", "", "NONE", 4),
|
||||
("Z_TO_Y", "Y,Z to -Z,Y", "", "NONE", 5),
|
||||
("MINUS_Z_TO_Y", "Y,Z to Z,-Y", "", "NONE", 6),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def reverse_axis_translation(cls, axis_translation: str) -> str:
|
||||
return {
|
||||
cls.AXIS_TRANSLATION_AUTO.identifier: cls.AXIS_TRANSLATION_AUTO,
|
||||
cls.AXIS_TRANSLATION_NONE.identifier: cls.AXIS_TRANSLATION_NONE,
|
||||
cls.AXIS_TRANSLATION_X_TO_Y.identifier: (cls.AXIS_TRANSLATION_MINUS_X_TO_Y),
|
||||
cls.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier: (cls.AXIS_TRANSLATION_X_TO_Y),
|
||||
cls.AXIS_TRANSLATION_Z_TO_Y.identifier: (cls.AXIS_TRANSLATION_MINUS_Z_TO_Y),
|
||||
cls.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier: (cls.AXIS_TRANSLATION_Z_TO_Y),
|
||||
cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier: (
|
||||
cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z
|
||||
),
|
||||
}[axis_translation].identifier
|
||||
|
||||
@classmethod
|
||||
def node_constraint_roll_axis_translation(
|
||||
cls, axis_translation: str, roll_axis: Optional[str]
|
||||
) -> Optional[str]:
|
||||
if roll_axis is None:
|
||||
return None
|
||||
return {
|
||||
cls.AXIS_TRANSLATION_AUTO.identifier: {"X": "X", "Y": "Y", "Z": "Z"},
|
||||
cls.AXIS_TRANSLATION_NONE.identifier: {"X": "X", "Y": "Y", "Z": "Z"},
|
||||
cls.AXIS_TRANSLATION_X_TO_Y.identifier: {"X": "Y", "Y": "X", "Z": "Z"},
|
||||
cls.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier: {
|
||||
"X": "Y",
|
||||
"Y": "X",
|
||||
"Z": "Z",
|
||||
},
|
||||
cls.AXIS_TRANSLATION_Z_TO_Y.identifier: {"X": "X", "Y": "Z", "Z": "Y"},
|
||||
cls.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier: {
|
||||
"X": "X",
|
||||
"Y": "Z",
|
||||
"Z": "Y",
|
||||
},
|
||||
cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier: {
|
||||
"X": "X",
|
||||
"Y": "Y",
|
||||
"Z": "Z",
|
||||
},
|
||||
}[axis_translation][roll_axis]
|
||||
|
||||
@classmethod
|
||||
def node_constraint_aim_axis_translation(
|
||||
cls, axis_translation: str, aim_axis: Optional[str]
|
||||
) -> Optional[str]:
|
||||
if aim_axis is None:
|
||||
return None
|
||||
return {
|
||||
cls.AXIS_TRANSLATION_AUTO.identifier: {
|
||||
"PositiveX": "PositiveX",
|
||||
"PositiveY": "PositiveY",
|
||||
"PositiveZ": "PositiveZ",
|
||||
"NegativeX": "NegativeX",
|
||||
"NegativeY": "NegativeY",
|
||||
"NegativeZ": "NegativeZ",
|
||||
},
|
||||
cls.AXIS_TRANSLATION_NONE.identifier: {
|
||||
"PositiveX": "PositiveX",
|
||||
"PositiveY": "PositiveY",
|
||||
"PositiveZ": "PositiveZ",
|
||||
"NegativeX": "NegativeX",
|
||||
"NegativeY": "NegativeY",
|
||||
"NegativeZ": "NegativeZ",
|
||||
},
|
||||
cls.AXIS_TRANSLATION_X_TO_Y.identifier: {
|
||||
"PositiveX": "PositiveY",
|
||||
"PositiveY": "NegativeX",
|
||||
"PositiveZ": "PositiveZ",
|
||||
"NegativeX": "NegativeY",
|
||||
"NegativeY": "PositiveX",
|
||||
"NegativeZ": "NegativeZ",
|
||||
},
|
||||
cls.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier: {
|
||||
"PositiveY": "PositiveX",
|
||||
"NegativeX": "PositiveY",
|
||||
"PositiveZ": "PositiveZ",
|
||||
"NegativeY": "NegativeX",
|
||||
"PositiveX": "NegativeY",
|
||||
"NegativeZ": "NegativeZ",
|
||||
},
|
||||
cls.AXIS_TRANSLATION_Z_TO_Y.identifier: {
|
||||
"PositiveX": "PositiveX",
|
||||
"PositiveY": "NegativeZ",
|
||||
"PositiveZ": "PositiveY",
|
||||
"NegativeX": "NegativeX",
|
||||
"NegativeY": "PositiveZ",
|
||||
"NegativeZ": "NegativeY",
|
||||
},
|
||||
cls.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier: {
|
||||
"PositiveX": "PositiveX",
|
||||
"NegativeZ": "PositiveY",
|
||||
"PositiveY": "PositiveZ",
|
||||
"NegativeX": "NegativeX",
|
||||
"PositiveZ": "NegativeY",
|
||||
"NegativeY": "NegativeZ",
|
||||
},
|
||||
cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier: {
|
||||
"PositiveX": "NegativeX",
|
||||
"PositiveY": "NegativeY",
|
||||
"PositiveZ": "PositiveZ",
|
||||
"NegativeX": "PositiveX",
|
||||
"NegativeY": "PositiveY",
|
||||
"NegativeZ": "NegativeZ",
|
||||
},
|
||||
}[axis_translation][aim_axis]
|
||||
|
||||
@classmethod
|
||||
def translate_axis(cls, matrix: Matrix, axis_translation: str) -> Matrix:
|
||||
location, rotation, scale = matrix.decompose()
|
||||
|
||||
if axis_translation == cls.AXIS_TRANSLATION_X_TO_Y.identifier:
|
||||
rotation @= Quaternion((0, 0, 1), -math.pi / 2)
|
||||
elif axis_translation == cls.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier:
|
||||
rotation @= Quaternion((0, 0, 1), math.pi / 2)
|
||||
elif axis_translation == cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier:
|
||||
rotation @= Quaternion((0, 0, 1), math.pi)
|
||||
elif axis_translation == cls.AXIS_TRANSLATION_Z_TO_Y.identifier:
|
||||
rotation @= Quaternion((1, 0, 0), math.pi / 2)
|
||||
elif axis_translation == cls.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier:
|
||||
rotation @= Quaternion((1, 0, 0), -math.pi / 2)
|
||||
|
||||
# return Matrix.LocRotScale(location, rotation, scale)
|
||||
return (
|
||||
Matrix.Translation(location)
|
||||
@ rotation.to_matrix().to_4x4()
|
||||
@ Matrix.Diagonal(scale).to_4x4()
|
||||
)
|
||||
|
||||
axis_translation: EnumProperty( # type: ignore[valid-type]
|
||||
items=axis_translation_enum.items(),
|
||||
name="Axis Translation on Export",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
uuid: str # type: ignore[no-redef]
|
||||
axis_translation: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
class VrmAddonObjectExtensionPropertyGroup(PropertyGroup):
|
||||
axis_translation: EnumProperty( # type: ignore[valid-type]
|
||||
items=VrmAddonBoneExtensionPropertyGroup.axis_translation_enum.items(),
|
||||
name="Axis Translation on Export",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
axis_translation: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
class VrmAddonArmatureExtensionPropertyGroup(PropertyGroup):
|
||||
INITIAL_ADDON_VERSION = VrmAddonPreferences.INITIAL_ADDON_VERSION
|
||||
|
||||
addon_version: IntVectorProperty( # type: ignore[valid-type]
|
||||
size=3,
|
||||
default=INITIAL_ADDON_VERSION,
|
||||
)
|
||||
|
||||
vrm0: PointerProperty( # type: ignore[valid-type]
|
||||
type=Vrm0PropertyGroup
|
||||
)
|
||||
|
||||
vrm1: PointerProperty( # type: ignore[valid-type]
|
||||
type=Vrm1PropertyGroup
|
||||
)
|
||||
|
||||
spring_bone1: PointerProperty( # type: ignore[valid-type]
|
||||
type=SpringBone1SpringBonePropertyGroup
|
||||
)
|
||||
|
||||
node_constraint1: PointerProperty( # type: ignore[valid-type]
|
||||
type=NodeConstraint1NodeConstraintPropertyGroup
|
||||
)
|
||||
|
||||
SPEC_VERSION_VRM0 = "0.0"
|
||||
SPEC_VERSION_VRM1 = "1.0"
|
||||
spec_version_items = (
|
||||
(SPEC_VERSION_VRM0, "VRM 0.0", "", "NONE", 0),
|
||||
(SPEC_VERSION_VRM1, "VRM 1.0", "", "NONE", 1),
|
||||
)
|
||||
|
||||
def update_spec_version(self, _context: Context) -> None:
|
||||
for blend_shape_group in self.vrm0.blend_shape_master.blend_shape_groups:
|
||||
blend_shape_group.preview = 0
|
||||
|
||||
if self.spec_version == self.SPEC_VERSION_VRM0:
|
||||
vrm0_hidden = False
|
||||
vrm1_hidden = True
|
||||
elif self.spec_version == self.SPEC_VERSION_VRM1:
|
||||
vrm0_hidden = True
|
||||
vrm1_hidden = False
|
||||
else:
|
||||
return
|
||||
|
||||
for vrm0_collider in [
|
||||
collider.bpy_object
|
||||
for collider_group in self.vrm0.secondary_animation.collider_groups
|
||||
for collider in collider_group.colliders
|
||||
if collider.bpy_object
|
||||
]:
|
||||
vrm0_collider.hide_set(vrm0_hidden)
|
||||
|
||||
for vrm1_collider in [
|
||||
collider.bpy_object
|
||||
for collider in self.spring_bone1.colliders
|
||||
if collider.bpy_object
|
||||
]:
|
||||
vrm1_collider.hide_set(vrm1_hidden)
|
||||
for child in vrm1_collider.children:
|
||||
child.hide_set(vrm1_hidden)
|
||||
|
||||
spec_version: EnumProperty( # type: ignore[valid-type]
|
||||
items=spec_version_items,
|
||||
name="Spec Version",
|
||||
update=update_spec_version,
|
||||
default=SPEC_VERSION_VRM1,
|
||||
)
|
||||
|
||||
def is_vrm0(self) -> bool:
|
||||
return str(self.spec_version) == self.SPEC_VERSION_VRM0
|
||||
|
||||
def is_vrm1(self) -> bool:
|
||||
return str(self.spec_version) == self.SPEC_VERSION_VRM1
|
||||
|
||||
@staticmethod
|
||||
def has_vrm_model_metadata(obj: Object) -> bool:
|
||||
if obj.type != "ARMATURE":
|
||||
return False
|
||||
armature = obj.data
|
||||
if not isinstance(armature, Armature):
|
||||
return False
|
||||
|
||||
# https://github.com/saturday06/VRM-Addon-for-Blender/blob/2_0_3/io_scene_vrm/editor/migration.py#L372-L373
|
||||
ext = get_armature_extension(armature)
|
||||
if tuple(ext.addon_version) > (2, 0, 1):
|
||||
return True
|
||||
|
||||
# https://github.com/saturday06/VRM-Addon-for-Blender/blob/0_79/importer/model_build.py#L731
|
||||
humanoid_params_key = obj.get("humanoid_params")
|
||||
if not isinstance(humanoid_params_key, str):
|
||||
return False
|
||||
|
||||
# https://github.com/saturday06/VRM-Addon-for-Blender/blob/0_79/importer/model_build.py#L706
|
||||
return "hips" in armature
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
addon_version: Sequence[int] # type: ignore[no-redef]
|
||||
vrm0: Vrm0PropertyGroup # type: ignore[no-redef]
|
||||
vrm1: Vrm1PropertyGroup # type: ignore[no-redef]
|
||||
spring_bone1: SpringBone1SpringBonePropertyGroup # type: ignore[no-redef]
|
||||
node_constraint1: ( # type: ignore[no-redef]
|
||||
NodeConstraint1NodeConstraintPropertyGroup
|
||||
)
|
||||
spec_version: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
def update_internal_cache(context: Context) -> None:
|
||||
for armature in context.blend_data.armatures:
|
||||
Vrm0HumanoidPropertyGroup.update_all_node_candidates(context, armature.name)
|
||||
Vrm1HumanBonesPropertyGroup.update_all_node_candidates(context, armature.name)
|
||||
VrmAddonSceneExtensionPropertyGroup.update_vrm0_material_property_names(context)
|
||||
|
||||
|
||||
class VrmAddonMaterialExtensionPropertyGroup(PropertyGroup):
|
||||
mtoon1: PointerProperty( # type: ignore[valid-type]
|
||||
type=Mtoon1MaterialPropertyGroup
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
mtoon1: Mtoon1MaterialPropertyGroup # type: ignore[no-redef]
|
||||
|
||||
|
||||
class VrmAddonNodeTreeExtensionPropertyGroup(PropertyGroup):
|
||||
INITIAL_ADDON_VERSION = VrmAddonPreferences.INITIAL_ADDON_VERSION
|
||||
|
||||
addon_version: IntVectorProperty( # type: ignore[valid-type]
|
||||
size=3,
|
||||
default=INITIAL_ADDON_VERSION,
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
addon_version: Sequence[int] # type: ignore[no-redef]
|
||||
|
||||
|
||||
__Extension = TypeVar("__Extension")
|
||||
|
||||
|
||||
def get_vrm_addon_extension_or_raise(
|
||||
obj: object, expected_type: type[__Extension]
|
||||
) -> __Extension:
|
||||
extension = getattr(obj, "vrm_addon_extension", None)
|
||||
if isinstance(extension, expected_type):
|
||||
return extension
|
||||
|
||||
message = f"{extension} is not a {expected_type} but {type(extension)}"
|
||||
raise TypeError(message)
|
||||
|
||||
|
||||
def get_material_extension(
|
||||
material: Material,
|
||||
) -> VrmAddonMaterialExtensionPropertyGroup:
|
||||
return get_vrm_addon_extension_or_raise(
|
||||
material, VrmAddonMaterialExtensionPropertyGroup
|
||||
)
|
||||
|
||||
|
||||
def get_armature_extension(
|
||||
armature: Armature,
|
||||
) -> VrmAddonArmatureExtensionPropertyGroup:
|
||||
return get_vrm_addon_extension_or_raise(
|
||||
armature, VrmAddonArmatureExtensionPropertyGroup
|
||||
)
|
||||
|
||||
|
||||
def get_node_tree_extension(
|
||||
node_tree: NodeTree,
|
||||
) -> VrmAddonNodeTreeExtensionPropertyGroup:
|
||||
return get_vrm_addon_extension_or_raise(
|
||||
node_tree, VrmAddonNodeTreeExtensionPropertyGroup
|
||||
)
|
||||
|
||||
|
||||
def get_scene_extension(scene: Scene) -> VrmAddonSceneExtensionPropertyGroup:
|
||||
return get_vrm_addon_extension_or_raise(scene, VrmAddonSceneExtensionPropertyGroup)
|
||||
|
||||
|
||||
def get_bone_extension(bone: Bone) -> VrmAddonBoneExtensionPropertyGroup:
|
||||
return get_vrm_addon_extension_or_raise(bone, VrmAddonBoneExtensionPropertyGroup)
|
||||
|
||||
|
||||
def get_object_extension(obj: Object) -> VrmAddonObjectExtensionPropertyGroup:
|
||||
return get_vrm_addon_extension_or_raise(obj, VrmAddonObjectExtensionPropertyGroup)
|
||||
10
editor/handler.py
Normal file
10
editor/handler.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from . import migration
|
||||
|
||||
|
||||
@persistent
|
||||
def load_post(_unsed: object) -> None:
|
||||
migration.state.blend_file_compatibility_warning_shown = False
|
||||
migration.state.blend_file_addon_compatibility_warning_shown = False
|
||||
712
editor/make_armature.py
Normal file
712
editor/make_armature.py
Normal file
@@ -0,0 +1,712 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Set as AbstractSet
|
||||
from math import radians
|
||||
from sys import float_info
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, FloatProperty, StringProperty
|
||||
from bpy.types import Armature, Context, EditBone, Object, Operator
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
from ..common.version import get_addon_version
|
||||
from ..common.workspace import save_workspace
|
||||
from . import migration
|
||||
from .extension import get_armature_extension
|
||||
from .vrm0.property_group import (
|
||||
Vrm0BlendShapeGroupPropertyGroup,
|
||||
Vrm0HumanoidPropertyGroup,
|
||||
)
|
||||
|
||||
MIN_BONE_LENGTH = 0.00001 # 10μm
|
||||
AUTO_BONE_CONNECTION_DISTANCE = 0.000001 # 1μm
|
||||
|
||||
|
||||
class ICYP_OT_make_armature(Operator):
|
||||
bl_idname = "icyp.make_basic_armature"
|
||||
bl_label = "Add VRM Humanoid"
|
||||
bl_description = "make armature and simple setup for VRM export"
|
||||
bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}
|
||||
|
||||
skip_heavy_armature_setup: BoolProperty( # type: ignore[valid-type]
|
||||
default=False,
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
wip_with_template_mesh: BoolProperty( # type: ignore[valid-type]
|
||||
default=False
|
||||
)
|
||||
|
||||
# Height in meters
|
||||
tall: FloatProperty( # type: ignore[valid-type]
|
||||
default=1.70,
|
||||
min=0.3,
|
||||
step=1,
|
||||
name="Bone tall",
|
||||
)
|
||||
|
||||
# Head-to-body ratio
|
||||
head_ratio: FloatProperty( # type: ignore[valid-type]
|
||||
default=8.0,
|
||||
min=4,
|
||||
step=5,
|
||||
description="height per heads",
|
||||
)
|
||||
|
||||
head_width_ratio: FloatProperty( # type: ignore[valid-type]
|
||||
default=2 / 3,
|
||||
min=0.3,
|
||||
max=1.2,
|
||||
step=5,
|
||||
description="height per heads",
|
||||
)
|
||||
|
||||
# Leg-to-torso ratio: 0: child-like, 1: adult-like (effective for low head count)
|
||||
aging_ratio: FloatProperty( # type: ignore[valid-type]
|
||||
default=0.5, min=0, max=1, step=10
|
||||
)
|
||||
|
||||
# Eye depth
|
||||
eye_depth: FloatProperty( # type: ignore[valid-type]
|
||||
default=-0.03, min=-0.1, max=0, step=1
|
||||
)
|
||||
|
||||
# Shoulder width
|
||||
shoulder_in_width: FloatProperty( # type: ignore[valid-type]
|
||||
default=0.05,
|
||||
min=0.01,
|
||||
step=1,
|
||||
description="Inner shoulder position",
|
||||
)
|
||||
|
||||
shoulder_width: FloatProperty( # type: ignore[valid-type]
|
||||
default=0.08,
|
||||
min=0.01,
|
||||
step=1,
|
||||
description="shoulder roll position",
|
||||
)
|
||||
|
||||
# Arm length ratio
|
||||
arm_length_ratio: FloatProperty( # type: ignore[valid-type]
|
||||
default=1, min=0.5, step=1
|
||||
)
|
||||
|
||||
# Hand
|
||||
hand_ratio: FloatProperty( # type: ignore[valid-type]
|
||||
default=1, min=0.5, max=2.0, step=5
|
||||
)
|
||||
|
||||
finger_1_2_ratio: FloatProperty( # type: ignore[valid-type]
|
||||
default=0.75,
|
||||
min=0.5,
|
||||
max=1,
|
||||
step=1,
|
||||
description="proximal / intermediate",
|
||||
)
|
||||
|
||||
finger_2_3_ratio: FloatProperty( # type: ignore[valid-type]
|
||||
default=0.75,
|
||||
min=0.5,
|
||||
max=1,
|
||||
step=1,
|
||||
description="intermediate / distal",
|
||||
)
|
||||
|
||||
nail_bone: BoolProperty( # type: ignore[valid-type]
|
||||
default=False,
|
||||
description="may need for finger collider",
|
||||
) # Needed for fingertip collision detection
|
||||
|
||||
# Foot
|
||||
leg_length_ratio: FloatProperty( # type: ignore[valid-type]
|
||||
default=0.5,
|
||||
min=0.3,
|
||||
max=0.6,
|
||||
step=1,
|
||||
description="upper body/lower body",
|
||||
)
|
||||
|
||||
leg_width_ratio: FloatProperty( # type: ignore[valid-type]
|
||||
default=1, min=0.01, step=1
|
||||
)
|
||||
|
||||
leg_size: FloatProperty( # type: ignore[valid-type]
|
||||
default=0.26, min=0.05, step=1
|
||||
)
|
||||
|
||||
custom_property_name: StringProperty( # type: ignore[valid-type]
|
||||
options={"HIDDEN"}
|
||||
)
|
||||
|
||||
armature_obj: Optional[Object] = None
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
with save_workspace(context):
|
||||
self.armature_obj, compare_dict = self.make_armature(context)
|
||||
self.setup_as_vrm(context, self.armature_obj, compare_dict)
|
||||
if self.custom_property_name:
|
||||
self.armature_obj[self.custom_property_name] = True
|
||||
context.view_layer.objects.active = self.armature_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
def float_prop(self, name: str) -> float:
|
||||
prop = getattr(self, name)
|
||||
if not isinstance(prop, float):
|
||||
message = f"prop {name} is not float"
|
||||
raise TypeError(message)
|
||||
return prop
|
||||
|
||||
def head_size(self) -> float:
|
||||
return self.float_prop("tall") / self.float_prop("head_ratio")
|
||||
|
||||
def hand_size(self) -> float:
|
||||
return self.head_size() * 0.75 * self.float_prop("hand_ratio")
|
||||
|
||||
def make_armature(self, context: Context) -> tuple[Object, dict[str, str]]:
|
||||
def bone_add(
|
||||
armature_data: Armature,
|
||||
name: str,
|
||||
head_pos: Vector,
|
||||
tail_pos: Vector,
|
||||
parent_bone: Optional[EditBone] = None,
|
||||
radius: float = 0.1,
|
||||
roll: float = 0,
|
||||
) -> EditBone:
|
||||
added_bone = armature_data.edit_bones.new(name)
|
||||
added_bone.head = head_pos
|
||||
added_bone.tail = tail_pos
|
||||
added_bone.head_radius = radius
|
||||
added_bone.tail_radius = radius
|
||||
added_bone.envelope_distance = 0.01
|
||||
added_bone.roll = radians(roll)
|
||||
if parent_bone is not None:
|
||||
added_bone.parent = parent_bone
|
||||
bone_dict.update({name: added_bone})
|
||||
return added_bone
|
||||
|
||||
def x_mirror_bones_add(
|
||||
armature_data: Armature,
|
||||
base_name: str,
|
||||
right_head_pos: Vector,
|
||||
right_tail_pos: Vector,
|
||||
parent_bones: tuple[EditBone, EditBone],
|
||||
radius: float = 0.1,
|
||||
bone_type: str = "other",
|
||||
) -> tuple[EditBone, EditBone]:
|
||||
right_roll = 0
|
||||
left_roll = 0
|
||||
if bone_type == "arm":
|
||||
right_roll = 0
|
||||
elif bone_type == "leg":
|
||||
right_roll = 0
|
||||
left_roll = 0
|
||||
left_bone = bone_add(
|
||||
armature_data,
|
||||
base_name + ".L",
|
||||
right_head_pos,
|
||||
right_tail_pos,
|
||||
parent_bones[0],
|
||||
radius=radius,
|
||||
roll=left_roll,
|
||||
)
|
||||
|
||||
head_pos = [pos * axis for pos, axis in zip(right_head_pos, (-1, 1, 1))]
|
||||
tail_pos = [pos * axis for pos, axis in zip(right_tail_pos, (-1, 1, 1))]
|
||||
right_bone = bone_add(
|
||||
armature_data,
|
||||
base_name + ".R",
|
||||
Vector((head_pos[0], head_pos[1], head_pos[2])),
|
||||
Vector((tail_pos[0], tail_pos[1], tail_pos[2])),
|
||||
parent_bones[1],
|
||||
radius=radius,
|
||||
roll=right_roll,
|
||||
)
|
||||
|
||||
return left_bone, right_bone
|
||||
|
||||
def x_add(pos_a: Vector, add_x: float) -> Vector:
|
||||
pos = [p_a + _add for p_a, _add in zip(pos_a, [add_x, 0, 0])]
|
||||
return Vector((pos[0], pos[1], pos[2]))
|
||||
|
||||
def y_add(pos_a: Vector, add_y: float) -> Vector:
|
||||
pos = [p_a + _add for p_a, _add in zip(pos_a, [0, add_y, 0])]
|
||||
return Vector((pos[0], pos[1], pos[2]))
|
||||
|
||||
def z_add(pos_a: Vector, add_z: float) -> Vector:
|
||||
pos = [p_a + _add for p_a, _add in zip(pos_a, [0, 0, add_z])]
|
||||
return Vector((pos[0], pos[1], pos[2]))
|
||||
|
||||
def fingers(
|
||||
armature_data: Armature,
|
||||
finger_name: str,
|
||||
proximal_pos: Vector,
|
||||
finger_len_sum: float,
|
||||
) -> tuple[
|
||||
tuple[EditBone, EditBone],
|
||||
tuple[EditBone, EditBone],
|
||||
tuple[EditBone, EditBone],
|
||||
]:
|
||||
finger_normalize = 1 / (
|
||||
self.finger_1_2_ratio * self.finger_2_3_ratio
|
||||
+ self.finger_1_2_ratio
|
||||
+ 1
|
||||
)
|
||||
proximal_finger_len = finger_len_sum * finger_normalize
|
||||
intermediate_finger_len = (
|
||||
finger_len_sum * finger_normalize * self.finger_1_2_ratio
|
||||
)
|
||||
distal_finger_len = (
|
||||
finger_len_sum
|
||||
* finger_normalize
|
||||
* self.finger_1_2_ratio
|
||||
* self.finger_2_3_ratio
|
||||
)
|
||||
proximal_bones = x_mirror_bones_add(
|
||||
armature_data,
|
||||
f"{finger_name}_proximal",
|
||||
proximal_pos,
|
||||
x_add(proximal_pos, proximal_finger_len),
|
||||
hands,
|
||||
self.hand_size() / 18,
|
||||
bone_type="arm",
|
||||
)
|
||||
intermediate_bones = x_mirror_bones_add(
|
||||
armature_data,
|
||||
f"{finger_name}_intermediate",
|
||||
proximal_bones[0].tail,
|
||||
x_add(proximal_bones[0].tail, intermediate_finger_len),
|
||||
proximal_bones,
|
||||
self.hand_size() / 18,
|
||||
bone_type="arm",
|
||||
)
|
||||
distal_bones = x_mirror_bones_add(
|
||||
armature_data,
|
||||
f"{finger_name}_distal",
|
||||
intermediate_bones[0].tail,
|
||||
x_add(intermediate_bones[0].tail, distal_finger_len),
|
||||
intermediate_bones,
|
||||
self.hand_size() / 18,
|
||||
bone_type="arm",
|
||||
)
|
||||
if self.nail_bone:
|
||||
x_mirror_bones_add(
|
||||
armature_data,
|
||||
f"{finger_name}_nail",
|
||||
distal_bones[0].tail,
|
||||
x_add(distal_bones[0].tail, distal_finger_len),
|
||||
distal_bones,
|
||||
self.hand_size() / 20,
|
||||
bone_type="arm",
|
||||
)
|
||||
return proximal_bones, intermediate_bones, distal_bones
|
||||
|
||||
bpy.ops.object.add(type="ARMATURE", enter_editmode=True, location=(0, 0, 0))
|
||||
armature = context.object
|
||||
if not armature:
|
||||
message = "armature is not created"
|
||||
raise ValueError(message)
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
message = "armature data is not an Armature"
|
||||
raise TypeError(message)
|
||||
get_armature_extension(armature_data).addon_version = get_addon_version()
|
||||
|
||||
bone_dict: dict[str, EditBone] = {}
|
||||
|
||||
# bone_type = "leg" or "arm" for roll setting
|
||||
|
||||
head_size = self.head_size()
|
||||
# down side (previously the lower leg ratio of upper leg/lower leg for
|
||||
# 8-head proportions, later linearly interpolated with age factor for
|
||||
# 4-head proportions)(breaks if upper leg is too high)
|
||||
eight_upside_ratio, four_upside_ratio = (
|
||||
1 - self.leg_length_ratio,
|
||||
(2.5 / 4) * (1 - self.aging_ratio)
|
||||
+ (1 - self.leg_length_ratio) * self.aging_ratio,
|
||||
)
|
||||
hip_up_down_ratio = (
|
||||
eight_upside_ratio * (1 - (8 - self.head_ratio) / 4)
|
||||
+ four_upside_ratio * (8 - self.head_ratio) / 4
|
||||
)
|
||||
# Torso
|
||||
# Groin
|
||||
body_separate = self.tall * (1 - hip_up_down_ratio)
|
||||
# Neck length
|
||||
neck_len = head_size * 2 / 3
|
||||
# Sacrum (pelvic spine base)
|
||||
hips_tall = body_separate + head_size * 3 / 4
|
||||
# Thoracic spine total length # 1/3 of neck is hidden behind the jaw
|
||||
backbone_len = self.tall - hips_tall - head_size - neck_len / 2
|
||||
# TODO: Verify the ratio of thoracic spine to vertebrae
|
||||
# Main flexion point located at the base of the spine, and another flexion
|
||||
# point located at the base of the thoracic cage
|
||||
# by Humanoid Doc
|
||||
spine_len = backbone_len * 5 / 17
|
||||
|
||||
root = bone_add(armature_data, "root", Vector((0, 0, 0)), Vector((0, 0, 0.3)))
|
||||
# Sacrum base
|
||||
hips = bone_add(
|
||||
armature_data,
|
||||
"hips",
|
||||
Vector((0, 0, body_separate)),
|
||||
Vector((0, 0, hips_tall)),
|
||||
root,
|
||||
roll=0,
|
||||
)
|
||||
# Pelvic base -> Thoracic cage base
|
||||
spine = bone_add(
|
||||
armature_data, "spine", hips.tail, z_add(hips.tail, spine_len), hips, roll=0
|
||||
)
|
||||
# Thoracic cage base -> Neck base
|
||||
chest = bone_add(
|
||||
armature_data,
|
||||
"chest",
|
||||
spine.tail,
|
||||
z_add(hips.tail, backbone_len),
|
||||
spine,
|
||||
roll=0,
|
||||
)
|
||||
neck = bone_add(
|
||||
armature_data,
|
||||
"neck",
|
||||
Vector((0, 0, self.tall - head_size - neck_len / 2)),
|
||||
Vector((0, 0, self.tall - head_size + neck_len / 2)),
|
||||
chest,
|
||||
roll=0,
|
||||
)
|
||||
# Half of the neck is hidden behind the jaw
|
||||
head = bone_add(
|
||||
armature_data,
|
||||
"head",
|
||||
Vector((0, 0, self.tall - head_size + neck_len / 2)),
|
||||
Vector((0, 0, self.tall)),
|
||||
neck,
|
||||
roll=0,
|
||||
)
|
||||
|
||||
# Eyes
|
||||
eye_depth = self.eye_depth
|
||||
eyes = x_mirror_bones_add(
|
||||
armature_data,
|
||||
"eye",
|
||||
Vector(
|
||||
(head_size * self.head_width_ratio / 5, 0, self.tall - head_size / 2)
|
||||
),
|
||||
Vector(
|
||||
(
|
||||
head_size * self.head_width_ratio / 5,
|
||||
eye_depth,
|
||||
self.tall - head_size / 2,
|
||||
)
|
||||
),
|
||||
(head, head),
|
||||
)
|
||||
# Legs
|
||||
leg_width = head_size / 4 * self.leg_width_ratio
|
||||
leg_size = self.leg_size
|
||||
|
||||
leg_bone_length = (body_separate + head_size * 3 / 8 - self.tall * 0.05) / 2
|
||||
upside_legs = x_mirror_bones_add(
|
||||
armature_data,
|
||||
"upper_leg",
|
||||
x_add(Vector((0, 0, body_separate + head_size * 3 / 8)), leg_width),
|
||||
x_add(
|
||||
Vector(
|
||||
z_add(
|
||||
Vector((0, 0, body_separate + head_size * 3 / 8)),
|
||||
-leg_bone_length,
|
||||
)
|
||||
),
|
||||
leg_width,
|
||||
),
|
||||
(hips, hips),
|
||||
radius=leg_width * 0.9,
|
||||
bone_type="leg",
|
||||
)
|
||||
lower_legs = x_mirror_bones_add(
|
||||
armature_data,
|
||||
"lower_leg",
|
||||
upside_legs[0].tail,
|
||||
Vector((leg_width, 0, self.tall * 0.05)),
|
||||
upside_legs,
|
||||
radius=leg_width * 0.9,
|
||||
bone_type="leg",
|
||||
)
|
||||
foots = x_mirror_bones_add(
|
||||
armature_data,
|
||||
"foot",
|
||||
lower_legs[0].tail,
|
||||
Vector((leg_width, -leg_size * (2 / 3), 0)),
|
||||
lower_legs,
|
||||
radius=leg_width * 0.9,
|
||||
bone_type="leg",
|
||||
)
|
||||
toes = x_mirror_bones_add(
|
||||
armature_data,
|
||||
"toes",
|
||||
foots[0].tail,
|
||||
Vector((leg_width, -leg_size, 0)),
|
||||
foots,
|
||||
radius=leg_width * 0.5,
|
||||
bone_type="leg",
|
||||
)
|
||||
|
||||
# Shoulder to fingers
|
||||
shoulder_in_pos = self.shoulder_in_width / 2
|
||||
|
||||
shoulder_parent = chest
|
||||
shoulders = x_mirror_bones_add(
|
||||
armature_data,
|
||||
"shoulder",
|
||||
x_add(shoulder_parent.tail, shoulder_in_pos),
|
||||
x_add(shoulder_parent.tail, shoulder_in_pos + self.shoulder_width),
|
||||
(shoulder_parent, shoulder_parent),
|
||||
radius=self.hand_size() * 0.4,
|
||||
bone_type="arm",
|
||||
)
|
||||
|
||||
arm_length = (
|
||||
head_size
|
||||
* (1 * (1 - (self.head_ratio - 6) / 2) + 1.5 * ((self.head_ratio - 6) / 2))
|
||||
* self.arm_length_ratio
|
||||
)
|
||||
arms = x_mirror_bones_add(
|
||||
armature_data,
|
||||
"upper_arm",
|
||||
shoulders[0].tail,
|
||||
x_add(shoulders[0].tail, arm_length),
|
||||
shoulders,
|
||||
radius=self.hand_size() * 0.4,
|
||||
bone_type="arm",
|
||||
)
|
||||
|
||||
# When making a fist, it becomes about half the size of an open palm.
|
||||
# When making a fist, the length of the forearm including the hand
|
||||
# is roughly the same as the length of the upper arm,
|
||||
# but it breaks down if the hand is too big.
|
||||
forearm_length = max(arm_length - self.hand_size() / 2, arm_length * 0.8)
|
||||
forearms = x_mirror_bones_add(
|
||||
armature_data,
|
||||
"lower_arm",
|
||||
arms[0].tail,
|
||||
x_add(arms[0].tail, forearm_length),
|
||||
arms,
|
||||
radius=self.hand_size() * 0.4,
|
||||
bone_type="arm",
|
||||
)
|
||||
hands = x_mirror_bones_add(
|
||||
armature_data,
|
||||
"hand",
|
||||
forearms[0].tail,
|
||||
x_add(forearms[0].tail, self.hand_size() / 2),
|
||||
forearms,
|
||||
radius=self.hand_size() / 4,
|
||||
bone_type="arm",
|
||||
)
|
||||
|
||||
finger_y_offset = -self.hand_size() / 16
|
||||
thumbs = fingers(
|
||||
armature_data,
|
||||
"thumb",
|
||||
y_add(hands[0].head, finger_y_offset * 3),
|
||||
self.hand_size() / 2,
|
||||
)
|
||||
|
||||
mats = [
|
||||
Matrix.Translation(vec)
|
||||
for vec in [thumbs[0][i].matrix.translation for i in [0, 1]]
|
||||
]
|
||||
for j in range(3):
|
||||
for n, angle in enumerate([-45, 45]):
|
||||
thumbs[j][n].transform(mats[n].inverted(), scale=False, roll=False)
|
||||
thumbs[j][n].transform(Matrix.Rotation(radians(angle), 4, "Z"))
|
||||
thumbs[j][n].transform(mats[n], scale=False, roll=False)
|
||||
thumbs[j][n].roll = 0
|
||||
|
||||
index_fingers = fingers(
|
||||
armature_data,
|
||||
"index",
|
||||
y_add(hands[0].tail, finger_y_offset * 3),
|
||||
(self.hand_size() / 2) - (1 / 2.3125) * (self.hand_size() / 2) / 3,
|
||||
)
|
||||
middle_fingers = fingers(
|
||||
armature_data,
|
||||
"middle",
|
||||
y_add(hands[0].tail, finger_y_offset),
|
||||
self.hand_size() / 2,
|
||||
)
|
||||
ring_fingers = fingers(
|
||||
armature_data,
|
||||
"ring",
|
||||
y_add(hands[0].tail, -finger_y_offset),
|
||||
(self.hand_size() / 2) - (1 / 2.3125) * (self.hand_size() / 2) / 3,
|
||||
)
|
||||
little_fingers = fingers(
|
||||
armature_data,
|
||||
"little",
|
||||
y_add(hands[0].tail, -finger_y_offset * 3),
|
||||
((self.hand_size() / 2) - (1 / 2.3125) * (self.hand_size() / 2) / 3)
|
||||
* ((1 / 2.3125) + (1 / 2.3125) * 0.75),
|
||||
)
|
||||
|
||||
body_dict = {
|
||||
"hips": hips.name,
|
||||
"spine": spine.name,
|
||||
"chest": chest.name,
|
||||
"neck": neck.name,
|
||||
"head": head.name,
|
||||
}
|
||||
|
||||
left_right_body_dict = {
|
||||
f"{left_right}{bone_name}": bones[lr].name
|
||||
for bone_name, bones in {
|
||||
"Eye": eyes,
|
||||
"UpperLeg": upside_legs,
|
||||
"LowerLeg": lower_legs,
|
||||
"Foot": foots,
|
||||
"Toes": toes,
|
||||
"Shoulder": shoulders,
|
||||
"UpperArm": arms,
|
||||
"LowerArm": forearms,
|
||||
"Hand": hands,
|
||||
}.items()
|
||||
for lr, left_right in enumerate(["left", "right"])
|
||||
}
|
||||
|
||||
# VRM finger like name key
|
||||
fingers_dict = {
|
||||
f"{left_right}{finger_name}{position}": finger[i][lr].name
|
||||
for finger_name, finger in zip(
|
||||
["Thumb", "Index", "Middle", "Ring", "Little"],
|
||||
[thumbs, index_fingers, middle_fingers, ring_fingers, little_fingers],
|
||||
)
|
||||
for i, position in enumerate(["Proximal", "Intermediate", "Distal"])
|
||||
for lr, left_right in enumerate(["left", "right"])
|
||||
}
|
||||
|
||||
# VRM bone name : blender bone name
|
||||
bone_name_all_dict: dict[str, str] = {}
|
||||
bone_name_all_dict.update(body_dict)
|
||||
bone_name_all_dict.update(left_right_body_dict)
|
||||
bone_name_all_dict.update(fingers_dict)
|
||||
|
||||
armature_data = armature.data
|
||||
if isinstance(armature_data, Armature):
|
||||
connect_parent_tail_and_child_head_if_very_close_position(armature_data)
|
||||
|
||||
context.scene.view_layers.update()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
context.scene.view_layers.update()
|
||||
return armature, bone_name_all_dict
|
||||
|
||||
def setup_as_vrm(
|
||||
self, context: Context, armature: Object, compare_dict: dict[str, str]
|
||||
) -> None:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
message = "armature data is not an Armature"
|
||||
raise TypeError(message)
|
||||
Vrm0HumanoidPropertyGroup.fixup_human_bones(armature)
|
||||
ext = get_armature_extension(armature_data)
|
||||
vrm0_humanoid = ext.vrm0.humanoid
|
||||
vrm1_humanoid = ext.vrm1.humanoid
|
||||
if not self.skip_heavy_armature_setup:
|
||||
for vrm_bone_name, bpy_bone_name in compare_dict.items():
|
||||
for human_bone in vrm0_humanoid.human_bones:
|
||||
if human_bone.bone == vrm_bone_name:
|
||||
human_bone.node.bone_name = bpy_bone_name
|
||||
break
|
||||
vrm0_humanoid.pose = vrm0_humanoid.POSE_REST_POSITION_POSE.identifier
|
||||
vrm1_humanoid.pose = vrm1_humanoid.POSE_REST_POSITION_POSE.identifier
|
||||
self.make_extension_setting_and_metas(
|
||||
armature,
|
||||
offset_from_head_bone=(-self.eye_depth, self.head_size() / 6, 0),
|
||||
)
|
||||
if not self.skip_heavy_armature_setup:
|
||||
migration.migrate(context, armature.name)
|
||||
|
||||
@classmethod
|
||||
def make_extension_setting_and_metas(
|
||||
cls,
|
||||
armature: Object,
|
||||
offset_from_head_bone: tuple[float, float, float] = (0, 0, 0),
|
||||
) -> None:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
vrm0 = get_armature_extension(armature_data).vrm0
|
||||
vrm1 = get_armature_extension(armature_data).vrm1
|
||||
vrm0.first_person.first_person_bone.bone_name = "head"
|
||||
vrm0.first_person.first_person_bone_offset = (0, 0, 0.06)
|
||||
vrm1.look_at.offset_from_head_bone = offset_from_head_bone
|
||||
vrm0.first_person.look_at_horizontal_inner.y_range = 8
|
||||
vrm0.first_person.look_at_horizontal_outer.y_range = 12
|
||||
vrm0.meta.author = "undefined"
|
||||
vrm0.meta.contact_information = "undefined"
|
||||
vrm0.meta.other_license_url = "undefined"
|
||||
vrm0.meta.other_permission_url = "undefined"
|
||||
vrm0.meta.reference = "undefined"
|
||||
vrm0.meta.title = "undefined"
|
||||
vrm0.meta.version = "undefined"
|
||||
for preset in Vrm0BlendShapeGroupPropertyGroup.preset_name_enum:
|
||||
if (
|
||||
preset.identifier
|
||||
== Vrm0BlendShapeGroupPropertyGroup.PRESET_NAME_UNKNOWN.identifier
|
||||
):
|
||||
continue
|
||||
blend_shape_group = vrm0.blend_shape_master.blend_shape_groups.add()
|
||||
blend_shape_group.name = preset.name.replace(" ", "")
|
||||
blend_shape_group.preset_name = preset.identifier
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
skip_heavy_armature_setup: bool # type: ignore[no-redef]
|
||||
wip_with_template_mesh: bool # type: ignore[no-redef]
|
||||
tall: float # type: ignore[no-redef]
|
||||
head_ratio: float # type: ignore[no-redef]
|
||||
head_width_ratio: float # type: ignore[no-redef]
|
||||
aging_ratio: float # type: ignore[no-redef]
|
||||
eye_depth: float # type: ignore[no-redef]
|
||||
shoulder_in_width: float # type: ignore[no-redef]
|
||||
shoulder_width: float # type: ignore[no-redef]
|
||||
arm_length_ratio: float # type: ignore[no-redef]
|
||||
hand_ratio: float # type: ignore[no-redef]
|
||||
finger_1_2_ratio: float # type: ignore[no-redef]
|
||||
finger_2_3_ratio: float # type: ignore[no-redef]
|
||||
nail_bone: bool # type: ignore[no-redef]
|
||||
leg_length_ratio: float # type: ignore[no-redef]
|
||||
leg_width_ratio: float # type: ignore[no-redef]
|
||||
leg_size: float # type: ignore[no-redef]
|
||||
custom_property_name: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
def connect_parent_tail_and_child_head_if_very_close_position(
|
||||
armature: Armature,
|
||||
) -> None:
|
||||
bones = [bone for bone in armature.edit_bones if not bone.parent]
|
||||
while bones:
|
||||
bone = bones.pop()
|
||||
|
||||
children_by_distance = sorted(
|
||||
bone.children,
|
||||
key=lambda child: (child.parent.tail - child.head).length_squared
|
||||
if child.parent
|
||||
else 0.0,
|
||||
)
|
||||
for child in children_by_distance:
|
||||
if (bone.tail - child.head).length < AUTO_BONE_CONNECTION_DISTANCE and (
|
||||
bone.head - child.head
|
||||
).length >= MIN_BONE_LENGTH:
|
||||
bone.tail = child.head
|
||||
break
|
||||
|
||||
bones.extend(bone.children)
|
||||
|
||||
bones = [bone for bone in armature.edit_bones if not bone.parent]
|
||||
while bones:
|
||||
bone = bones.pop()
|
||||
for child in bone.children:
|
||||
if (bone.tail - child.head).length < float_info.epsilon:
|
||||
child.use_connect = True
|
||||
bones.append(child)
|
||||
258
editor/migration.py
Normal file
258
editor/migration.py
Normal file
@@ -0,0 +1,258 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import functools
|
||||
from dataclasses import dataclass
|
||||
from typing import Final, Optional
|
||||
|
||||
import bpy
|
||||
from bpy.types import Armature, Context
|
||||
|
||||
from ..common import ops
|
||||
from ..common.logger import get_logger
|
||||
from ..common.preferences import get_preferences
|
||||
from ..common.version import get_addon_version
|
||||
from .extension import (
|
||||
VrmAddonArmatureExtensionPropertyGroup,
|
||||
VrmAddonSceneExtensionPropertyGroup,
|
||||
get_armature_extension,
|
||||
)
|
||||
from .mtoon1 import migration as mtoon1_migration
|
||||
from .spring_bone1 import migration as spring_bone1_migration
|
||||
from .vrm0 import migration as vrm0_migration
|
||||
from .vrm1 import migration as vrm1_migration
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class State:
|
||||
blend_file_compatibility_warning_shown: bool = False
|
||||
blend_file_addon_compatibility_warning_shown: bool = False
|
||||
|
||||
|
||||
state: Final = State()
|
||||
|
||||
|
||||
def is_unnecessary(armature_data: Armature) -> bool:
|
||||
ext = get_armature_extension(armature_data)
|
||||
return tuple(
|
||||
ext.addon_version
|
||||
) >= get_addon_version() and vrm0_migration.is_unnecessary(ext.vrm0)
|
||||
|
||||
|
||||
def defer_migrate(armature_object_name: str) -> bool:
|
||||
context = bpy.context
|
||||
|
||||
armature = context.blend_data.objects.get(armature_object_name)
|
||||
if not armature:
|
||||
return False
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return False
|
||||
if is_unnecessary(armature_data):
|
||||
return True
|
||||
bpy.app.timers.register(
|
||||
functools.partial(
|
||||
migrate_timer_callback,
|
||||
armature_object_name,
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def migrate_timer_callback(armature_object_name: str) -> None:
|
||||
"""Match the type of migrate() to bpy.app.timers.register."""
|
||||
context = bpy.context # Context cannot span frames, so get it anew
|
||||
migrate(context, armature_object_name)
|
||||
|
||||
|
||||
def migrate(context: Optional[Context], armature_object_name: str) -> bool:
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
|
||||
armature = context.blend_data.objects.get(armature_object_name)
|
||||
if not armature:
|
||||
return False
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return False
|
||||
|
||||
if is_unnecessary(armature_data):
|
||||
return True
|
||||
|
||||
ext = get_armature_extension(armature_data)
|
||||
|
||||
vrm0_migration.migrate(context, ext.vrm0, armature)
|
||||
vrm1_migration.migrate(context, ext.vrm1, armature)
|
||||
spring_bone1_migration.migrate(context, armature)
|
||||
|
||||
if (
|
||||
ext.has_vrm_model_metadata(armature)
|
||||
and tuple(ext.addon_version) < (3, 14, 0)
|
||||
and not ext.get("spec_version")
|
||||
):
|
||||
ext.spec_version = ext.SPEC_VERSION_VRM0
|
||||
|
||||
updated_addon_version = get_addon_version()
|
||||
logger.info(
|
||||
"Upgrade armature %s %s to %s",
|
||||
armature_object_name,
|
||||
tuple(ext.addon_version),
|
||||
updated_addon_version,
|
||||
)
|
||||
ext.addon_version = updated_addon_version
|
||||
return True
|
||||
|
||||
|
||||
def migrate_all_objects(
|
||||
context: Context,
|
||||
*,
|
||||
skip_non_migrated_armatures: bool = False,
|
||||
show_progress: bool = False,
|
||||
) -> None:
|
||||
for obj in context.blend_data.objects:
|
||||
if obj.type == "ARMATURE" and isinstance(armature_data := obj.data, Armature):
|
||||
if skip_non_migrated_armatures:
|
||||
ext = get_armature_extension(armature_data)
|
||||
if (
|
||||
tuple(ext.addon_version)
|
||||
== VrmAddonArmatureExtensionPropertyGroup.INITIAL_ADDON_VERSION
|
||||
):
|
||||
continue
|
||||
migrate(context, obj.name)
|
||||
|
||||
VrmAddonSceneExtensionPropertyGroup.update_vrm0_material_property_names(
|
||||
context, context.scene.name
|
||||
)
|
||||
mtoon1_migration.migrate(context, show_progress=show_progress)
|
||||
validate_blend_file_compatibility(context)
|
||||
validate_blend_file_addon_compatibility(context)
|
||||
|
||||
preferences = get_preferences(context)
|
||||
|
||||
updated_addon_version = get_addon_version()
|
||||
logger.debug(
|
||||
"Upgrade preferences %s to %s",
|
||||
tuple(preferences.addon_version),
|
||||
updated_addon_version,
|
||||
)
|
||||
|
||||
if tuple(preferences.addon_version) != preferences.INITIAL_ADDON_VERSION and tuple(
|
||||
preferences.addon_version
|
||||
) < (2, 34, 0):
|
||||
preferences.enable_advanced_preferences = True
|
||||
preferences.export_gltf_animations = True
|
||||
|
||||
preferences.addon_version = updated_addon_version
|
||||
|
||||
|
||||
def validate_blend_file_compatibility(context: Context) -> None:
|
||||
"""Warn when attempting to edit a file created in newer Blender with older Blender.
|
||||
|
||||
Due to add-on version support issues, there are often reports of users trying to
|
||||
edit files created in newer Blender with older Blender, which can cause shape keys
|
||||
to break. This warning alerts users to be careful.
|
||||
"""
|
||||
if not context.blend_data.filepath:
|
||||
return
|
||||
if not have_vrm_model(context):
|
||||
return
|
||||
|
||||
blend_file_major_minor_version = (
|
||||
context.blend_data.version[0],
|
||||
context.blend_data.version[1],
|
||||
)
|
||||
current_major_minor_version = (bpy.app.version[0], bpy.app.version[1])
|
||||
if blend_file_major_minor_version <= current_major_minor_version:
|
||||
return
|
||||
|
||||
file_version_str = ".".join(map(str, blend_file_major_minor_version))
|
||||
app_version_str = ".".join(map(str, current_major_minor_version))
|
||||
|
||||
logger.error(
|
||||
"Opening incompatible file: file_blender_version=%s running_blender_version=%s",
|
||||
file_version_str,
|
||||
app_version_str,
|
||||
)
|
||||
|
||||
if not state.blend_file_compatibility_warning_shown:
|
||||
state.blend_file_compatibility_warning_shown = True
|
||||
# Use timer because dialog disappears automatically if not executed with
|
||||
# timer in Blender 4.2.0
|
||||
bpy.app.timers.register(
|
||||
functools.partial(
|
||||
show_blend_file_compatibility_warning,
|
||||
file_version_str,
|
||||
app_version_str,
|
||||
),
|
||||
first_interval=0.1,
|
||||
)
|
||||
|
||||
|
||||
def show_blend_file_compatibility_warning(file_version: str, app_version: str) -> None:
|
||||
ops.vrm.show_blend_file_compatibility_warning(
|
||||
"INVOKE_DEFAULT",
|
||||
file_version=file_version,
|
||||
app_version=app_version,
|
||||
)
|
||||
|
||||
|
||||
def validate_blend_file_addon_compatibility(context: Context) -> None:
|
||||
"""Warn when attempting to edit a file created with a newer VRM add-on.
|
||||
|
||||
This warning is for files using an older VRM add-on.
|
||||
"""
|
||||
if not context.blend_data.filepath:
|
||||
return
|
||||
installed_addon_version = get_addon_version()
|
||||
|
||||
# TODO: It might be better to store the version in Scene or similar
|
||||
up_to_date = True
|
||||
file_addon_version: tuple[int, ...] = (0, 0, 0)
|
||||
for armature in context.blend_data.armatures:
|
||||
file_addon_version = tuple(get_armature_extension(armature).addon_version)
|
||||
if file_addon_version > installed_addon_version:
|
||||
up_to_date = False
|
||||
break
|
||||
if up_to_date:
|
||||
return
|
||||
|
||||
file_addon_version_str = ".".join(map(str, file_addon_version))
|
||||
installed_addon_version_str = ".".join(map(str, installed_addon_version))
|
||||
|
||||
logger.error(
|
||||
"Opening incompatible VRM add-on version: file=%s installed=%s",
|
||||
file_addon_version_str,
|
||||
installed_addon_version_str,
|
||||
)
|
||||
|
||||
if not state.blend_file_compatibility_warning_shown:
|
||||
state.blend_file_compatibility_warning_shown = True
|
||||
# Use timer because dialog disappears automatically if not executed with
|
||||
# timer in Blender 4.2.0
|
||||
bpy.app.timers.register(
|
||||
functools.partial(
|
||||
show_blend_file_addon_compatibility_warning,
|
||||
file_addon_version_str,
|
||||
installed_addon_version_str,
|
||||
),
|
||||
first_interval=0.1,
|
||||
)
|
||||
|
||||
|
||||
def show_blend_file_addon_compatibility_warning(
|
||||
file_addon_version: str, installed_addon_version: str
|
||||
) -> None:
|
||||
ops.vrm.show_blend_file_addon_compatibility_warning(
|
||||
"INVOKE_DEFAULT",
|
||||
file_addon_version=file_addon_version,
|
||||
installed_addon_version=installed_addon_version,
|
||||
)
|
||||
|
||||
|
||||
def have_vrm_model(context: Context) -> bool:
|
||||
return any(
|
||||
map(
|
||||
VrmAddonArmatureExtensionPropertyGroup.has_vrm_model_metadata,
|
||||
context.blend_data.objects,
|
||||
)
|
||||
)
|
||||
1
editor/mtoon1/__init__.py
Normal file
1
editor/mtoon1/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
BIN
editor/mtoon1/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/mtoon1/__pycache__/handler.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/handler.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/mtoon1/__pycache__/migration.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/migration.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/mtoon1/__pycache__/ops.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/ops.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/mtoon1/__pycache__/panel.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/panel.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/mtoon1/__pycache__/property_group.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/property_group.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/mtoon1/__pycache__/scene_watcher.cpython-311.pyc
Normal file
BIN
editor/mtoon1/__pycache__/scene_watcher.cpython-311.pyc
Normal file
Binary file not shown.
24
editor/mtoon1/handler.py
Normal file
24
editor/mtoon1/handler.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from ...common.logger import get_logger
|
||||
from ...common.scene_watcher import trigger_scene_watcher
|
||||
from . import migration
|
||||
from .scene_watcher import MToon1AutoSetup, OutlineUpdater
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@persistent
|
||||
def depsgraph_update_pre(_unused: object) -> None:
|
||||
trigger_scene_watcher(MToon1AutoSetup)
|
||||
if bpy.app.version < (3, 3):
|
||||
return
|
||||
trigger_scene_watcher(OutlineUpdater)
|
||||
|
||||
|
||||
@persistent
|
||||
def load_post(_unsed: object) -> None:
|
||||
migration.state.material_blender_4_2_warning_shown = False
|
||||
529
editor/mtoon1/migration.py
Normal file
529
editor/mtoon1/migration.py
Normal file
@@ -0,0 +1,529 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import functools
|
||||
from dataclasses import dataclass
|
||||
from typing import Final, Optional
|
||||
|
||||
import bpy
|
||||
from bpy.types import (
|
||||
Context,
|
||||
Image,
|
||||
Material,
|
||||
NodeReroute,
|
||||
PropertyGroup,
|
||||
ShaderNodeGroup,
|
||||
ShaderNodeTexImage,
|
||||
)
|
||||
|
||||
from ...common import convert, ops, shader, version
|
||||
from ...common.gl import GL_LINEAR, GL_NEAREST
|
||||
from ...common.logger import get_logger
|
||||
from ...common.progress import create_progress
|
||||
from .. import search
|
||||
from ..extension import get_material_extension
|
||||
from .property_group import (
|
||||
GL_LINEAR_IMAGE_INTERPOLATIONS,
|
||||
IMAGE_INTERPOLATION_CLOSEST,
|
||||
IMAGE_INTERPOLATION_LINEAR,
|
||||
Mtoon1MaterialPropertyGroup,
|
||||
Mtoon1SamplerPropertyGroup,
|
||||
Mtoon1TextureInfoPropertyGroup,
|
||||
Mtoon1VrmcMaterialsMtoonPropertyGroup,
|
||||
reset_shader_node_group,
|
||||
)
|
||||
|
||||
TextureInfoBackup = Mtoon1TextureInfoPropertyGroup.TextureInfoBackup
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class State:
|
||||
material_blender_4_2_warning_shown: bool = False
|
||||
|
||||
|
||||
state: Final = State()
|
||||
|
||||
|
||||
def show_material_blender_4_2_warning_delay(material_name_lines: str) -> None:
|
||||
ops.vrm.show_material_blender_4_2_warning(
|
||||
"INVOKE_DEFAULT",
|
||||
material_name_lines=material_name_lines,
|
||||
)
|
||||
|
||||
|
||||
def migrate(context: Context, *, show_progress: bool = False) -> None:
|
||||
blender_4_2_migrated_material_names: list[str] = []
|
||||
|
||||
with create_progress(context, show_progress=show_progress) as progress:
|
||||
for material_index, material in enumerate(context.blend_data.materials):
|
||||
if not material:
|
||||
continue
|
||||
migrate_material(context, material, blender_4_2_migrated_material_names)
|
||||
progress.update(float(material_index) / len(context.blend_data.materials))
|
||||
progress.update(1)
|
||||
|
||||
if (
|
||||
blender_4_2_migrated_material_names
|
||||
and tuple(context.blend_data.version) < (4, 2)
|
||||
and bpy.app.version >= (4, 2)
|
||||
):
|
||||
logger.warning(
|
||||
"Migrating Materials from blender version data=%s app=%s",
|
||||
context.blend_data.version,
|
||||
bpy.app.version,
|
||||
)
|
||||
|
||||
if not state.material_blender_4_2_warning_shown:
|
||||
state.material_blender_4_2_warning_shown = True
|
||||
# In Blender 4.2.0, if you don't run it with a timer, the dialog will
|
||||
# disappear automatically.
|
||||
bpy.app.timers.register(
|
||||
functools.partial(
|
||||
show_material_blender_4_2_warning_delay,
|
||||
"\n".join(blender_4_2_migrated_material_names),
|
||||
),
|
||||
first_interval=0.1,
|
||||
)
|
||||
|
||||
|
||||
def migrate_material(
|
||||
context: Context,
|
||||
material: Material,
|
||||
blender_4_2_migrated_material_names: list[str],
|
||||
) -> None:
|
||||
_, legacy_legacy_shader_name = search.legacy_shader_node(material)
|
||||
if legacy_legacy_shader_name in search.LEGACY_SHADER_NAMES:
|
||||
# Since old shader node groups are not compatible with Blender 4.2 as they are,
|
||||
# always warn when upgrading to Blender 4.2 or later.
|
||||
blender_4_2_migrated_material_names.append(material.name)
|
||||
return
|
||||
|
||||
if bpy.app.version < (5, 0, 0) and not material.use_nodes:
|
||||
return
|
||||
node_tree = material.node_tree
|
||||
if not node_tree:
|
||||
return
|
||||
|
||||
mtoon1 = get_material_extension(material).mtoon1
|
||||
if not mtoon1.get("enabled"):
|
||||
return
|
||||
|
||||
vrmc_materials_mtoon = mtoon1.extensions.vrmc_materials_mtoon
|
||||
if vrmc_materials_mtoon.get("is_outline_material"):
|
||||
return
|
||||
|
||||
addon_version = convert.float3_or(mtoon1.get("addon_version"), (0, 0, 0))
|
||||
if addon_version < (2, 16, 4):
|
||||
# https://github.com/saturday06/VRM-Addon-for-Blender/blob/2_10_0/io_scene_vrm/editor/mtoon1/property_group.py#L1658-L1683
|
||||
surface_node_name = "Mtoon1Material.MaterialOutputSurfaceIn"
|
||||
surface_node = node_tree.nodes.get(surface_node_name)
|
||||
if not isinstance(surface_node, NodeReroute):
|
||||
return
|
||||
|
||||
connected = False
|
||||
surface_socket = surface_node.outputs[0]
|
||||
for link in node_tree.links:
|
||||
if (
|
||||
link.from_socket == surface_socket
|
||||
and link.to_socket
|
||||
and link.to_socket.node
|
||||
and link.to_socket.node.type == "OUTPUT_MATERIAL"
|
||||
):
|
||||
connected = True
|
||||
break
|
||||
if not connected:
|
||||
return
|
||||
else:
|
||||
# https://github.com/saturday06/VRM-Addon-for-Blender/blob/2_16_4/io_scene_vrm/editor/mtoon1/property_group.py#L1913-L1929
|
||||
group_node = node_tree.nodes.get("Mtoon1Material.Mtoon1Output")
|
||||
if not isinstance(group_node, ShaderNodeGroup):
|
||||
return
|
||||
if not group_node.node_tree:
|
||||
return
|
||||
if group_node.node_tree.name != "VRM Add-on MToon 1.0 Output Revision 1":
|
||||
return
|
||||
|
||||
if addon_version < (2, 20, 50):
|
||||
migrate_sampler_filter_node(material)
|
||||
|
||||
alpha_mode: Optional[str] = None
|
||||
alpha_cutoff: Optional[float] = None
|
||||
if addon_version < (2, 20, 55):
|
||||
blender_4_2_migrated_material_names.append(material.name)
|
||||
alpha_cutoff = material.alpha_threshold
|
||||
blend_method = material.blend_method
|
||||
if blend_method in ["BLEND", "HASHED"]:
|
||||
alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_BLEND.identifier
|
||||
elif blend_method == "CLIP":
|
||||
alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_MASK.identifier
|
||||
else:
|
||||
alpha_mode = Mtoon1MaterialPropertyGroup.ALPHA_MODE_OPAQUE.identifier
|
||||
|
||||
base_color_factor: Optional[tuple[float, float, float, float]] = None
|
||||
base_color_texture_backup: Optional[TextureInfoBackup] = None
|
||||
normal_texture_backup: Optional[TextureInfoBackup] = None
|
||||
normal_texture_scale: Optional[float] = None
|
||||
emissive_texture_backup: Optional[TextureInfoBackup] = None
|
||||
emissive_factor: Optional[tuple[float, float, float]] = None
|
||||
emissive_strength: Optional[float] = None
|
||||
|
||||
transparent_with_z_write: Optional[bool] = None
|
||||
render_queue_offset_number: Optional[int] = None
|
||||
shade_multiply_texture_backup: Optional[TextureInfoBackup] = None
|
||||
shade_color_factor: Optional[tuple[float, float, float]] = None
|
||||
shading_shift_texture_backup: Optional[TextureInfoBackup] = None
|
||||
shading_shift_texture_scale: Optional[float] = None
|
||||
shading_shift_factor: Optional[float] = None
|
||||
shading_toony_factor: Optional[float] = None
|
||||
gi_equalization_factor: Optional[float] = None
|
||||
matcap_factor: Optional[tuple[float, float, float]] = None
|
||||
matcap_texture_backup: Optional[TextureInfoBackup] = None
|
||||
parametric_rim_color_factor: Optional[tuple[float, float, float]] = None
|
||||
rim_multiply_texture_backup: Optional[TextureInfoBackup] = None
|
||||
rim_lighting_mix_factor: Optional[float] = None
|
||||
parametric_rim_fresnel_power_factor: Optional[float] = None
|
||||
parametric_rim_lift_factor: Optional[float] = None
|
||||
outline_width_mode: Optional[str] = None
|
||||
outline_width_factor: Optional[float] = None
|
||||
outline_width_multiply_texture_backup: Optional[TextureInfoBackup] = None
|
||||
outline_color_factor: Optional[tuple[float, float, float]] = None
|
||||
outline_lighting_mix_factor: Optional[float] = None
|
||||
uv_animation_mask_texture_backup: Optional[TextureInfoBackup] = None
|
||||
uv_animation_scroll_x_speed_factor: Optional[float] = None
|
||||
uv_animation_scroll_y_speed_factor: Optional[float] = None
|
||||
uv_animation_rotation_speed_factor: Optional[float] = None
|
||||
if addon_version < (2, 20, 62):
|
||||
pbr_metallic_roughness = mtoon1.pbr_metallic_roughness
|
||||
base_color_factor = convert.float4_or_none(
|
||||
pbr_metallic_roughness.get("base_color_factor")
|
||||
)
|
||||
base_color_texture_backup = backup_texture_info(
|
||||
pbr_metallic_roughness.base_color_texture
|
||||
)
|
||||
normal_texture = mtoon1.normal_texture
|
||||
normal_texture_backup = backup_texture_info(normal_texture)
|
||||
normal_texture_scale = convert.float_or_none(normal_texture.get("scale"))
|
||||
emissive_factor = convert.float3_or_none(mtoon1.get("emissive_factor"))
|
||||
emissive_texture_backup = backup_texture_info(mtoon1.emissive_texture)
|
||||
emissive_strength = convert.float_or_none(
|
||||
mtoon1.extensions.khr_materials_emissive_strength.get("emissive_strength")
|
||||
)
|
||||
|
||||
transparent_with_z_write_object = vrmc_materials_mtoon.get(
|
||||
"transparent_with_z_write"
|
||||
)
|
||||
if isinstance(transparent_with_z_write_object, int):
|
||||
transparent_with_z_write = bool(transparent_with_z_write_object)
|
||||
|
||||
render_queue_offset_number_object = vrmc_materials_mtoon.get(
|
||||
"render_queue_offset_number"
|
||||
)
|
||||
if isinstance(render_queue_offset_number_object, int):
|
||||
render_queue_offset_number = render_queue_offset_number_object
|
||||
|
||||
shade_multiply_texture_backup = backup_texture_info(
|
||||
vrmc_materials_mtoon.shade_multiply_texture
|
||||
)
|
||||
shade_color_factor = convert.float3_or_none(
|
||||
vrmc_materials_mtoon.get("shade_color_factor")
|
||||
)
|
||||
|
||||
shading_shift_texture = vrmc_materials_mtoon.shading_shift_texture
|
||||
shading_shift_texture_backup = backup_texture_info(shading_shift_texture)
|
||||
shading_shift_texture_scale = convert.float_or_none(
|
||||
shading_shift_texture.get("scale")
|
||||
)
|
||||
|
||||
shading_shift_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("shading_shift_factor")
|
||||
)
|
||||
shading_toony_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("shading_toony_factor")
|
||||
)
|
||||
matcap_factor = convert.float3_or_none(
|
||||
vrmc_materials_mtoon.get("matcap_factor")
|
||||
)
|
||||
matcap_texture_backup = backup_texture_info(vrmc_materials_mtoon.matcap_texture)
|
||||
gi_equalization_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("gi_equalization_factor")
|
||||
)
|
||||
parametric_rim_color_factor = convert.float3_or_none(
|
||||
vrmc_materials_mtoon.get("parametric_rim_color_factor")
|
||||
)
|
||||
rim_multiply_texture_backup = backup_texture_info(
|
||||
vrmc_materials_mtoon.rim_multiply_texture
|
||||
)
|
||||
rim_lighting_mix_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("rim_lighting_mix_factor")
|
||||
)
|
||||
parametric_rim_fresnel_power_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("parametric_rim_fresnel_power_factor")
|
||||
)
|
||||
parametric_rim_lift_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("parametric_rim_lift_factor")
|
||||
)
|
||||
outline_width_mode_number = vrmc_materials_mtoon.get("outline_width_mode")
|
||||
if isinstance(outline_width_mode_number, int):
|
||||
outline_width_mode_item = (
|
||||
Mtoon1VrmcMaterialsMtoonPropertyGroup.outline_width_mode_enum
|
||||
)
|
||||
outline_width_mode = outline_width_mode_item.value_to_identifier(
|
||||
outline_width_mode_number,
|
||||
Mtoon1VrmcMaterialsMtoonPropertyGroup.OUTLINE_WIDTH_MODE_NONE.identifier,
|
||||
)
|
||||
outline_width_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("outline_width_factor")
|
||||
)
|
||||
outline_width_multiply_texture_backup = backup_texture_info(
|
||||
vrmc_materials_mtoon.outline_width_multiply_texture
|
||||
)
|
||||
outline_color_factor = convert.float3_or_none(
|
||||
vrmc_materials_mtoon.get("outline_color_factor")
|
||||
)
|
||||
outline_lighting_mix_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("outline_lighting_mix_factor")
|
||||
)
|
||||
uv_animation_mask_texture_backup = backup_texture_info(
|
||||
vrmc_materials_mtoon.uv_animation_mask_texture
|
||||
)
|
||||
uv_animation_scroll_x_speed_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("uv_animation_scroll_x_speed_factor")
|
||||
)
|
||||
uv_animation_scroll_y_speed_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("uv_animation_scroll_y_speed_factor")
|
||||
)
|
||||
uv_animation_rotation_speed_factor = convert.float_or_none(
|
||||
vrmc_materials_mtoon.get("uv_animation_rotation_speed_factor")
|
||||
)
|
||||
|
||||
if (
|
||||
addon_version < shader.LAST_MODIFIED_VERSION
|
||||
# Blender 4.2 has a node specification change, so it will be forcibly reset.
|
||||
or (bpy.app.version >= (4, 2) and tuple(context.blend_data.version) < (4, 2))
|
||||
):
|
||||
reset_shader_node_group(
|
||||
context, material, reset_material_node_tree=True, reset_node_groups=False
|
||||
)
|
||||
|
||||
# From this point on, you can write code that assumes the shader node is up to date.
|
||||
vrmc_materials_mtoon = mtoon1.extensions.vrmc_materials_mtoon
|
||||
if alpha_mode is not None:
|
||||
mtoon1.alpha_mode = alpha_mode
|
||||
if alpha_cutoff is not None:
|
||||
mtoon1.alpha_cutoff = alpha_cutoff
|
||||
if base_color_factor is not None:
|
||||
mtoon1.pbr_metallic_roughness.base_color_factor = base_color_factor
|
||||
if base_color_texture_backup is not None:
|
||||
mtoon1.pbr_metallic_roughness.base_color_texture.restore(
|
||||
base_color_texture_backup
|
||||
)
|
||||
if normal_texture_backup is not None:
|
||||
mtoon1.normal_texture.restore(normal_texture_backup)
|
||||
if normal_texture_scale is not None:
|
||||
mtoon1.normal_texture.scale = normal_texture_scale
|
||||
if emissive_texture_backup is not None:
|
||||
mtoon1.emissive_texture.restore(emissive_texture_backup)
|
||||
if emissive_factor is not None:
|
||||
mtoon1.emissive_factor = emissive_factor
|
||||
if emissive_strength is not None:
|
||||
mtoon1.extensions.khr_materials_emissive_strength.emissive_strength = (
|
||||
emissive_strength
|
||||
)
|
||||
|
||||
if transparent_with_z_write is not None:
|
||||
vrmc_materials_mtoon.transparent_with_z_write = transparent_with_z_write
|
||||
if render_queue_offset_number is not None:
|
||||
vrmc_materials_mtoon.render_queue_offset_number = render_queue_offset_number
|
||||
if shade_multiply_texture_backup is not None:
|
||||
vrmc_materials_mtoon.shade_multiply_texture.restore(
|
||||
shade_multiply_texture_backup
|
||||
)
|
||||
if shade_color_factor is not None:
|
||||
vrmc_materials_mtoon.shade_color_factor = shade_color_factor
|
||||
if shading_shift_texture_backup is not None:
|
||||
vrmc_materials_mtoon.shading_shift_texture.restore(shading_shift_texture_backup)
|
||||
if shading_shift_factor is not None:
|
||||
vrmc_materials_mtoon.shading_shift_factor = shading_shift_factor
|
||||
if shading_shift_texture_scale is not None:
|
||||
vrmc_materials_mtoon.shading_shift_texture.scale = shading_shift_texture_scale
|
||||
if shading_toony_factor is not None:
|
||||
vrmc_materials_mtoon.shading_toony_factor = shading_toony_factor
|
||||
if gi_equalization_factor is not None:
|
||||
vrmc_materials_mtoon.gi_equalization_factor = gi_equalization_factor
|
||||
if matcap_factor is not None:
|
||||
vrmc_materials_mtoon.matcap_factor = matcap_factor
|
||||
if matcap_texture_backup is not None:
|
||||
vrmc_materials_mtoon.matcap_texture.restore(matcap_texture_backup)
|
||||
if parametric_rim_color_factor is not None:
|
||||
vrmc_materials_mtoon.parametric_rim_color_factor = parametric_rim_color_factor
|
||||
if rim_multiply_texture_backup is not None:
|
||||
vrmc_materials_mtoon.rim_multiply_texture.restore(rim_multiply_texture_backup)
|
||||
if rim_lighting_mix_factor is not None:
|
||||
vrmc_materials_mtoon.rim_lighting_mix_factor = rim_lighting_mix_factor
|
||||
if parametric_rim_fresnel_power_factor is not None:
|
||||
vrmc_materials_mtoon.parametric_rim_fresnel_power_factor = (
|
||||
parametric_rim_fresnel_power_factor
|
||||
)
|
||||
if parametric_rim_lift_factor is not None:
|
||||
vrmc_materials_mtoon.parametric_rim_lift_factor = parametric_rim_lift_factor
|
||||
if outline_width_mode is not None:
|
||||
vrmc_materials_mtoon.outline_width_mode = outline_width_mode
|
||||
if outline_width_factor is not None:
|
||||
vrmc_materials_mtoon.outline_width_factor = outline_width_factor
|
||||
if outline_width_multiply_texture_backup is not None:
|
||||
vrmc_materials_mtoon.outline_width_multiply_texture.restore(
|
||||
outline_width_multiply_texture_backup
|
||||
)
|
||||
if outline_color_factor is not None:
|
||||
vrmc_materials_mtoon.outline_color_factor = outline_color_factor
|
||||
if outline_lighting_mix_factor is not None:
|
||||
vrmc_materials_mtoon.outline_lighting_mix_factor = outline_lighting_mix_factor
|
||||
if uv_animation_mask_texture_backup is not None:
|
||||
vrmc_materials_mtoon.uv_animation_mask_texture.restore(
|
||||
uv_animation_mask_texture_backup
|
||||
)
|
||||
if uv_animation_scroll_x_speed_factor is not None:
|
||||
vrmc_materials_mtoon.uv_animation_scroll_x_speed_factor = (
|
||||
uv_animation_scroll_x_speed_factor
|
||||
)
|
||||
if uv_animation_scroll_y_speed_factor is not None:
|
||||
vrmc_materials_mtoon.uv_animation_scroll_y_speed_factor = (
|
||||
uv_animation_scroll_y_speed_factor
|
||||
)
|
||||
if uv_animation_rotation_speed_factor is not None:
|
||||
vrmc_materials_mtoon.uv_animation_rotation_speed_factor = (
|
||||
uv_animation_rotation_speed_factor
|
||||
)
|
||||
|
||||
mtoon1.setup_drivers()
|
||||
updated_addon_version = version.get_addon_version()
|
||||
if tuple(mtoon1.addon_version) != updated_addon_version:
|
||||
mtoon1.addon_version = updated_addon_version
|
||||
|
||||
|
||||
def backup_texture_info(texture_info: object) -> Optional[TextureInfoBackup]:
|
||||
if not isinstance(texture_info, PropertyGroup):
|
||||
return None
|
||||
|
||||
source: Optional[Image] = None
|
||||
wrap_s = Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.identifier
|
||||
wrap_t = Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.identifier
|
||||
offset_x = 0.0
|
||||
offset_y = 0.0
|
||||
scale_x = 1.0
|
||||
scale_y = 1.0
|
||||
|
||||
index = getattr(texture_info, "index", None)
|
||||
if isinstance(index, PropertyGroup):
|
||||
source_object = index.get("source")
|
||||
if isinstance(source_object, Image):
|
||||
source = source_object
|
||||
sampler = getattr(index, "sampler", None)
|
||||
if isinstance(sampler, PropertyGroup):
|
||||
wrap_s_number = sampler.get("wrap_s")
|
||||
if isinstance(wrap_s_number, int):
|
||||
wrap_s = Mtoon1SamplerPropertyGroup.wrap_enum.value_to_identifier(
|
||||
wrap_s_number,
|
||||
wrap_s,
|
||||
)
|
||||
wrap_t_number = sampler.get("wrap_t")
|
||||
if isinstance(wrap_t_number, int):
|
||||
wrap_t = Mtoon1SamplerPropertyGroup.wrap_enum.value_to_identifier(
|
||||
wrap_t_number,
|
||||
wrap_t,
|
||||
)
|
||||
|
||||
extensions = getattr(texture_info, "extensions", None)
|
||||
if isinstance(extensions, PropertyGroup):
|
||||
khr_texture_transform = getattr(extensions, "khr_texture_transform", None)
|
||||
if isinstance(khr_texture_transform, PropertyGroup):
|
||||
offset = convert.float2_or_none(khr_texture_transform.get("offset"))
|
||||
if offset is not None:
|
||||
offset_x, offset_y = offset
|
||||
scale = convert.float2_or_none(khr_texture_transform.get("scale"))
|
||||
if scale is not None:
|
||||
scale_x, scale_y = scale
|
||||
|
||||
return TextureInfoBackup(
|
||||
source=source,
|
||||
mag_filter=None,
|
||||
min_filter=None,
|
||||
wrap_s=wrap_s,
|
||||
wrap_t=wrap_t,
|
||||
offset=(offset_x, offset_y),
|
||||
scale=(scale_x, scale_y),
|
||||
)
|
||||
|
||||
|
||||
def migrate_sampler_filter_node(material: Material) -> None:
|
||||
node_tree = material.node_tree
|
||||
if not node_tree:
|
||||
return
|
||||
|
||||
mtoon1 = get_material_extension(material).mtoon1
|
||||
|
||||
for node_name, attrs in [
|
||||
(
|
||||
"Mtoon1BaseColorTexture.Image",
|
||||
("pbr_metallic_roughness", "base_color_texture"),
|
||||
),
|
||||
("Mtoon1EmissiveTexture.Image", ("emissive_texture",)),
|
||||
("Mtoon1NormalTexture.Image", ("normal_texture",)),
|
||||
(
|
||||
"Mtoon1ShadeMultiplyTexture.Image",
|
||||
("extensions", "vrmc_materials_mtoon", "shade_multiply_texture"),
|
||||
),
|
||||
(
|
||||
"Mtoon1ShadingShiftTexture.Image",
|
||||
("extensions", "vrmc_materials_mtoon", "shading_shift_texture"),
|
||||
),
|
||||
(
|
||||
"Mtoon1OutlineWidthMultiplyTexture.Image",
|
||||
(
|
||||
"extensions",
|
||||
"vrmc_materials_mtoon",
|
||||
"outline_width_multiply_texture",
|
||||
),
|
||||
),
|
||||
(
|
||||
"Mtoon1UvAnimationMaskTexture.Image",
|
||||
("extensions", "vrmc_materials_mtoon", "uv_animation_mask_texture"),
|
||||
),
|
||||
(
|
||||
"Mtoon1MatcapTexture.Image",
|
||||
("extensions", "vrmc_materials_mtoon", "matcap_texture"),
|
||||
),
|
||||
(
|
||||
"Mtoon1RimMultiplyTexture.Image",
|
||||
("extensions", "vrmc_materials_mtoon", "rim_multiply_texture"),
|
||||
),
|
||||
]:
|
||||
sampler = functools.reduce(
|
||||
lambda prop, attr: getattr(prop, attr, None),
|
||||
(*attrs, "index", "sampler"),
|
||||
mtoon1,
|
||||
)
|
||||
if not isinstance(sampler, PropertyGroup):
|
||||
continue
|
||||
|
||||
mag_filter = sampler.get("mag_filter")
|
||||
if (
|
||||
not isinstance(mag_filter, int)
|
||||
or mag_filter not in Mtoon1SamplerPropertyGroup.mag_filter_enum.values()
|
||||
):
|
||||
continue
|
||||
|
||||
node = node_tree.nodes.get(node_name)
|
||||
if not isinstance(node, ShaderNodeTexImage):
|
||||
continue
|
||||
|
||||
if (
|
||||
mag_filter == GL_NEAREST
|
||||
and node.interpolation != IMAGE_INTERPOLATION_CLOSEST
|
||||
):
|
||||
node.interpolation = IMAGE_INTERPOLATION_CLOSEST
|
||||
if (
|
||||
mag_filter == GL_LINEAR
|
||||
and node.interpolation not in GL_LINEAR_IMAGE_INTERPOLATIONS
|
||||
):
|
||||
node.interpolation = IMAGE_INTERPOLATION_LINEAR
|
||||
1158
editor/mtoon1/ops.py
Normal file
1158
editor/mtoon1/ops.py
Normal file
File diff suppressed because it is too large
Load Diff
447
editor/mtoon1/panel.py
Normal file
447
editor/mtoon1/panel.py
Normal file
@@ -0,0 +1,447 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from typing import Optional
|
||||
|
||||
import bpy
|
||||
from bpy.app.translations import pgettext
|
||||
from bpy.types import Context, Panel, PropertyGroup, UILayout
|
||||
|
||||
from ...common.logger import get_logger
|
||||
from .. import search
|
||||
from ..extension import get_material_extension
|
||||
from ..ops import VRM_OT_open_url_in_web_browser, layout_operator
|
||||
from .ops import (
|
||||
VRM_OT_import_mtoon1_texture_image_file,
|
||||
VRM_OT_reset_mtoon1_material_shader_node_tree,
|
||||
)
|
||||
from .property_group import (
|
||||
Mtoon0TexturePropertyGroup,
|
||||
Mtoon1MaterialPropertyGroup,
|
||||
Mtoon1TextureInfoPropertyGroup,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def draw_texture_info(
|
||||
material_name: str,
|
||||
ext: Mtoon1MaterialPropertyGroup,
|
||||
parent_layout: UILayout,
|
||||
base_property_group: PropertyGroup,
|
||||
texture_info_attr_name: str,
|
||||
color_factor_attr_name: Optional[str] = None,
|
||||
*,
|
||||
is_vrm0: bool,
|
||||
) -> UILayout:
|
||||
texture_info = getattr(base_property_group, texture_info_attr_name, None)
|
||||
if not isinstance(texture_info, Mtoon1TextureInfoPropertyGroup):
|
||||
raise TypeError
|
||||
layout = parent_layout.split(factor=0.3)
|
||||
toggle_layout = layout.row()
|
||||
toggle_layout.alignment = "LEFT"
|
||||
toggle_layout.prop(
|
||||
texture_info,
|
||||
"show_expanded",
|
||||
emboss=False,
|
||||
text=texture_info.index.panel_label,
|
||||
translate=False,
|
||||
icon="TRIA_DOWN" if texture_info.show_expanded else "TRIA_RIGHT",
|
||||
)
|
||||
input_layout = layout.row(align=True)
|
||||
|
||||
node_image = texture_info.index.get_connected_node_image()
|
||||
if node_image == texture_info.index.source:
|
||||
source_prop_name = "source"
|
||||
placeholder = ""
|
||||
else:
|
||||
source_prop_name = "source_for_desynced_node_tree"
|
||||
placeholder = node_image.name if node_image else ""
|
||||
|
||||
if bpy.app.version >= (4, 1):
|
||||
input_layout.prop(
|
||||
texture_info.index,
|
||||
source_prop_name,
|
||||
text="",
|
||||
translate=False,
|
||||
placeholder=placeholder,
|
||||
)
|
||||
else:
|
||||
input_layout.prop(
|
||||
texture_info.index,
|
||||
source_prop_name,
|
||||
text="",
|
||||
translate=False,
|
||||
)
|
||||
|
||||
import_image_file_op = layout_operator(
|
||||
input_layout,
|
||||
VRM_OT_import_mtoon1_texture_image_file,
|
||||
text="",
|
||||
translate=False,
|
||||
icon="FILEBROWSER",
|
||||
)
|
||||
import_image_file_op.material_name = material_name
|
||||
import_image_file_op.target_texture = type(texture_info.index).__name__
|
||||
if color_factor_attr_name:
|
||||
input_layout.separator(factor=0.5)
|
||||
input_layout.prop(base_property_group, color_factor_attr_name, text="")
|
||||
|
||||
if not texture_info.show_expanded:
|
||||
return input_layout
|
||||
|
||||
box = parent_layout.box().column()
|
||||
if texture_info.index.source:
|
||||
box.prop(texture_info.index.source.colorspace_settings, "name")
|
||||
if (
|
||||
texture_info.index.source.colorspace_settings.name
|
||||
!= texture_info.index.colorspace
|
||||
):
|
||||
box.box().label(
|
||||
text=pgettext(
|
||||
'It is recommended to set "{colorspace}"'
|
||||
+ ' to "{input_colorspace}" for "{texture_label}"'
|
||||
).format(
|
||||
texture_label=texture_info.index.label,
|
||||
colorspace=pgettext(texture_info.index.colorspace),
|
||||
input_colorspace=pgettext("Input Color Space"),
|
||||
),
|
||||
icon="ERROR",
|
||||
)
|
||||
box.prop(texture_info.index.sampler, "mag_filter")
|
||||
box.prop(texture_info.index.sampler, "min_filter")
|
||||
box.prop(texture_info.index.sampler, "wrap_s")
|
||||
box.prop(texture_info.index.sampler, "wrap_t")
|
||||
box.prop(texture_info.extensions.khr_texture_transform, "offset")
|
||||
box.prop(texture_info.extensions.khr_texture_transform, "scale")
|
||||
|
||||
if is_vrm0:
|
||||
if ext.extensions.vrmc_materials_mtoon.matcap_texture == texture_info:
|
||||
box.box().label(
|
||||
text="Offset and Scale are ignored in VRM 0.0", icon="ERROR"
|
||||
)
|
||||
elif ext.pbr_metallic_roughness.base_color_texture != texture_info:
|
||||
box.box().label(
|
||||
text="Offset and Scale in VRM 0.0 are"
|
||||
+ " the values of the Lit Color Texture",
|
||||
icon="ERROR",
|
||||
)
|
||||
|
||||
return input_layout
|
||||
|
||||
|
||||
def draw_mtoon0_texture(
|
||||
material_name: str,
|
||||
parent_layout: UILayout,
|
||||
base_property_group: PropertyGroup,
|
||||
texture_attr_name: str,
|
||||
scalar_factor_attr_name: str,
|
||||
) -> UILayout:
|
||||
texture = getattr(base_property_group, texture_attr_name, None)
|
||||
if not isinstance(texture, Mtoon0TexturePropertyGroup):
|
||||
raise TypeError
|
||||
layout = parent_layout.split(factor=0.3)
|
||||
toggle_layout = layout.row()
|
||||
toggle_layout.alignment = "LEFT"
|
||||
toggle_layout.prop(
|
||||
texture,
|
||||
"show_expanded",
|
||||
emboss=False,
|
||||
text=texture.panel_label,
|
||||
translate=False,
|
||||
icon="TRIA_DOWN" if texture.show_expanded else "TRIA_RIGHT",
|
||||
)
|
||||
input_layout = layout.row(align=True)
|
||||
input_layout.prop(texture, "source", text="")
|
||||
import_image_file_op = layout_operator(
|
||||
input_layout,
|
||||
VRM_OT_import_mtoon1_texture_image_file,
|
||||
text="",
|
||||
translate=False,
|
||||
icon="FILEBROWSER",
|
||||
)
|
||||
import_image_file_op.material_name = material_name
|
||||
import_image_file_op.target_texture = type(texture).__name__
|
||||
|
||||
input_layout.separator(factor=0.5)
|
||||
input_layout.prop(base_property_group, scalar_factor_attr_name, text="")
|
||||
|
||||
if not texture.show_expanded:
|
||||
return input_layout
|
||||
|
||||
box = parent_layout.box().column()
|
||||
if texture.source:
|
||||
box.prop(texture.source.colorspace_settings, "name")
|
||||
if texture.source.colorspace_settings.name != texture.colorspace:
|
||||
box.box().label(
|
||||
text=pgettext(
|
||||
'It is recommended to set "{colorspace}"'
|
||||
+ ' to "{input_colorspace}" for "{texture_label}"'
|
||||
).format(
|
||||
texture_label=texture.label,
|
||||
colorspace=pgettext(texture.colorspace),
|
||||
input_colorspace=pgettext("Input Color Space"),
|
||||
),
|
||||
icon="ERROR",
|
||||
)
|
||||
box.prop(texture.sampler, "mag_filter")
|
||||
box.prop(texture.sampler, "min_filter")
|
||||
box.prop(texture.sampler, "wrap_s")
|
||||
box.prop(texture.sampler, "wrap_t")
|
||||
return input_layout
|
||||
|
||||
|
||||
def draw_mtoon1_material(context: Context, layout: UILayout) -> None:
|
||||
material = context.material
|
||||
if not material:
|
||||
return
|
||||
ext = get_material_extension(material)
|
||||
layout = layout.column()
|
||||
|
||||
layout.prop(ext.mtoon1, "enabled")
|
||||
if not ext.mtoon1.enabled:
|
||||
return
|
||||
|
||||
is_vrm0 = search.current_armature_is_vrm0(context)
|
||||
gltf = ext.mtoon1
|
||||
mtoon = gltf.extensions.vrmc_materials_mtoon
|
||||
|
||||
# https://github.com/vrm-c/UniVRM/blob/v0.102.0/Assets/VRMShaders/VRM10/MToon10/Editor/MToonInspector.cs#L14
|
||||
layout.label(text="Rendering", translate=False)
|
||||
rendering_box = layout.box().column()
|
||||
rendering_box.prop(gltf, "alpha_mode")
|
||||
if gltf.alpha_mode == gltf.ALPHA_MODE_MASK.identifier:
|
||||
rendering_box.prop(gltf, "alpha_cutoff", slider=True)
|
||||
if gltf.alpha_mode == gltf.ALPHA_MODE_BLEND.identifier:
|
||||
rendering_box.prop(mtoon, "transparent_with_z_write")
|
||||
rendering_box.prop(gltf, "double_sided")
|
||||
rendering_box.prop(mtoon, "render_queue_offset_number", slider=True)
|
||||
|
||||
layout.label(text="Lighting", translate=False)
|
||||
lighting_box = layout.box().column()
|
||||
draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
lighting_box,
|
||||
gltf.pbr_metallic_roughness,
|
||||
"base_color_texture",
|
||||
"base_color_factor",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
lighting_box,
|
||||
mtoon,
|
||||
"shade_multiply_texture",
|
||||
"shade_color_factor",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
normal_texture_layout = draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
lighting_box,
|
||||
gltf,
|
||||
"normal_texture",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
normal_texture_layout.separator(factor=0.5)
|
||||
normal_texture_layout.prop(gltf.normal_texture, "scale")
|
||||
|
||||
lighting_box.prop(mtoon, "shading_toony_factor", slider=True)
|
||||
lighting_box.prop(mtoon, "shading_shift_factor", slider=True)
|
||||
shading_shift_texture_layout = draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
lighting_box,
|
||||
mtoon,
|
||||
"shading_shift_texture",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
shading_shift_texture_layout.separator(factor=0.5)
|
||||
shading_shift_texture_layout.prop(mtoon.shading_shift_texture, "scale")
|
||||
|
||||
# UniVRM (MIT License)
|
||||
# https://github.com/vrm-c/UniVRM/blob/d2b4ad1964b754341873f2a1b093d58e1df1713f/Assets/VRMShaders/VRM10/MToon10/Editor/MToonInspector.cs#L120-L128
|
||||
if (
|
||||
not mtoon.shading_shift_texture.index.source
|
||||
and mtoon.shading_toony_factor - mtoon.shading_shift_factor < 1.0 - 0.001
|
||||
):
|
||||
lighting_box.box().label(
|
||||
text="The lit area includes non-lit area.", icon="ERROR"
|
||||
)
|
||||
|
||||
layout.label(text="Global Illumination", translate=False)
|
||||
gi_box = layout.box()
|
||||
gi_box.prop(mtoon, "gi_equalization_factor", slider=True)
|
||||
|
||||
layout.label(text="Emission", translate=False)
|
||||
emission_box = layout.box().column()
|
||||
emissive_texture_layout = draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
emission_box,
|
||||
gltf,
|
||||
"emissive_texture",
|
||||
is_vrm0=is_vrm0,
|
||||
).row(align=True)
|
||||
emissive_texture_layout.scale_x = 0.71
|
||||
emissive_texture_layout.separator(factor=0.5 / 0.71)
|
||||
emissive_texture_layout.prop(gltf, "emissive_factor", text="")
|
||||
emissive_texture_layout.prop(
|
||||
gltf.extensions.khr_materials_emissive_strength, "emissive_strength"
|
||||
)
|
||||
|
||||
layout.label(text="Rim Lighting", translate=False)
|
||||
rim_lighting_box = layout.box().column()
|
||||
draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
rim_lighting_box,
|
||||
mtoon,
|
||||
"rim_multiply_texture",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
rim_lighting_box.prop(mtoon, "rim_lighting_mix_factor", slider=True)
|
||||
draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
rim_lighting_box,
|
||||
mtoon,
|
||||
"matcap_texture",
|
||||
"matcap_factor",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
rim_lighting_box.row().prop(mtoon, "parametric_rim_color_factor")
|
||||
rim_lighting_box.prop(mtoon, "parametric_rim_fresnel_power_factor", slider=True)
|
||||
rim_lighting_box.prop(mtoon, "parametric_rim_lift_factor", slider=True)
|
||||
|
||||
layout.label(text="Outline", translate=False)
|
||||
outline_box = layout.box().column()
|
||||
outline_box.prop(mtoon, "outline_width_mode")
|
||||
if (
|
||||
bpy.app.version >= (3, 3)
|
||||
and mtoon.outline_width_mode
|
||||
== mtoon.OUTLINE_WIDTH_MODE_SCREEN_COORDINATES.identifier
|
||||
):
|
||||
outline_warning_message = pgettext(
|
||||
'The "Screen Coordinates" display is not yet implemented.\n'
|
||||
+ 'It is displayed in the same way as "World Coordinates".'
|
||||
)
|
||||
outline_warning_column = outline_box.box().column(align=True)
|
||||
for index, outline_warning_line in enumerate(
|
||||
outline_warning_message.splitlines()
|
||||
):
|
||||
outline_warning_column.label(
|
||||
text=outline_warning_line,
|
||||
translate=False,
|
||||
icon="BLANK1" if index else "INFO",
|
||||
)
|
||||
if mtoon.outline_width_mode != mtoon.OUTLINE_WIDTH_MODE_NONE.identifier:
|
||||
outline_width_multiply_texture_layout = draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
outline_box,
|
||||
mtoon,
|
||||
"outline_width_multiply_texture",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
outline_width_multiply_texture_layout.separator(factor=0.5)
|
||||
outline_width_multiply_texture_layout.prop(
|
||||
mtoon, "outline_width_factor", slider=True, text=""
|
||||
)
|
||||
outline_box.row().prop(mtoon, "outline_color_factor")
|
||||
outline_box.prop(mtoon, "outline_lighting_mix_factor", slider=True)
|
||||
outline_box.prop(mtoon, "enable_outline_preview", text="Enable Preview")
|
||||
|
||||
layout.label(text="UV Animation", translate=False)
|
||||
uv_animation_box = layout.box().column()
|
||||
draw_texture_info(
|
||||
material.name,
|
||||
ext.mtoon1,
|
||||
uv_animation_box,
|
||||
mtoon,
|
||||
"uv_animation_mask_texture",
|
||||
is_vrm0=is_vrm0,
|
||||
)
|
||||
uv_animation_box.prop(mtoon, "uv_animation_scroll_x_speed_factor")
|
||||
uv_animation_box.prop(mtoon, "uv_animation_scroll_y_speed_factor")
|
||||
uv_animation_box.prop(mtoon, "uv_animation_rotation_speed_factor")
|
||||
|
||||
layout.prop(gltf, "show_expanded_mtoon0")
|
||||
if gltf.show_expanded_mtoon0:
|
||||
mtoon0_box = layout.box().column()
|
||||
mtoon0_box.prop(gltf, "mtoon0_front_cull_mode")
|
||||
draw_mtoon0_texture(
|
||||
material.name,
|
||||
mtoon0_box,
|
||||
ext.mtoon1,
|
||||
"mtoon0_receive_shadow_texture",
|
||||
"mtoon0_receive_shadow_rate",
|
||||
)
|
||||
draw_mtoon0_texture(
|
||||
material.name,
|
||||
mtoon0_box,
|
||||
ext.mtoon1,
|
||||
"mtoon0_shading_grade_texture",
|
||||
"mtoon0_shading_grade_rate",
|
||||
)
|
||||
mtoon0_box.prop(gltf, "mtoon0_light_color_attenuation", slider=True)
|
||||
mtoon0_box.prop(gltf, "mtoon0_rim_lighting_mix", slider=True)
|
||||
mtoon0_box.prop(gltf, "mtoon0_outline_scaled_max_distance", slider=True)
|
||||
mtoon0_box.prop(gltf, "mtoon0_render_queue_and_clamp", slider=True)
|
||||
|
||||
reset_op = layout_operator(layout, VRM_OT_reset_mtoon1_material_shader_node_tree)
|
||||
reset_op.material_name = material.name
|
||||
|
||||
|
||||
def draw_material(context: Context, layout: UILayout) -> None:
|
||||
material = context.material
|
||||
if not material:
|
||||
return
|
||||
ext = get_material_extension(material)
|
||||
if ext.mtoon1.is_outline_material:
|
||||
layout.box().label(icon="INFO", text="This is an MToon Outline material")
|
||||
return
|
||||
|
||||
draw_mtoon1_material(context, layout)
|
||||
|
||||
node, legacy_shader_name = search.legacy_shader_node(material)
|
||||
if ext.mtoon1.enabled or (node and legacy_shader_name == "MToon_unversioned"):
|
||||
layout.prop(ext.mtoon1, "export_shape_key_normals")
|
||||
return
|
||||
if node and legacy_shader_name in ["TRANSPARENT_ZWRITE", "GLTF"]:
|
||||
return
|
||||
|
||||
help_column = layout.box().column(align=True)
|
||||
help_message = pgettext(
|
||||
"How to export this material to VRM.\n"
|
||||
+ "Meet one of the following conditions.\n"
|
||||
+ " - VRM MToon Material is enabled\n"
|
||||
+ ' - Connect the "Surface" to a "Principled BSDF"\n'
|
||||
+ ' - Connect the "Surface" to a "MToon_unversioned"\n'
|
||||
+ ' - Connect the "Surface" to a "TRANSPARENT_ZWRITE"\n'
|
||||
+ " - Others that are compatible with the glTF 2.0 add-on export\n"
|
||||
)
|
||||
for index, help_line in enumerate(help_message.splitlines()):
|
||||
help_column.label(
|
||||
text=help_line, translate=False, icon="HELP" if index == 0 else "NONE"
|
||||
)
|
||||
url = "https://docs.blender.org/manual/en/2.93/addons/import_export/scene_gltf2.html#exported-materials"
|
||||
link_row = help_column.split(factor=0.8)
|
||||
link_row.label(text=" " + url, translate=False)
|
||||
web_op = layout_operator(link_row, VRM_OT_open_url_in_web_browser, icon="URL")
|
||||
web_op.url = url
|
||||
|
||||
|
||||
class VRM_PT_vrm_material_property(Panel):
|
||||
bl_idname = "VRM_PT_vrm_material_property"
|
||||
bl_label = "VRM Material"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "material"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return bool(context.material)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
draw_material(context, self.layout)
|
||||
3780
editor/mtoon1/property_group.py
Normal file
3780
editor/mtoon1/property_group.py
Normal file
File diff suppressed because it is too large
Load Diff
377
editor/mtoon1/scene_watcher.py
Normal file
377
editor/mtoon1/scene_watcher.py
Normal file
@@ -0,0 +1,377 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Final, Optional
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context, Material, Mesh, ShaderNodeGroup, ShaderNodeOutputMaterial
|
||||
|
||||
from ...common import ops
|
||||
from ...common.logger import get_logger
|
||||
from ...common.scene_watcher import RunState, SceneWatcher
|
||||
from ...common.shader import MTOON1_AUTO_SETUP_GROUP_NODE_TREE_CUSTOM_KEY
|
||||
from ..extension import get_material_extension
|
||||
from .ops import VRM_OT_refresh_mtoon1_outline, generate_mtoon1_outline_material_name
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
HAS_AUTO_SMOOTH: Final = tuple(bpy.app.version) < (4, 1)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComparisonMaterial:
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComparisonObject:
|
||||
use_auto_smooth: Optional[bool] = None
|
||||
comparison_materials: list[Optional[ComparisonMaterial]] = field(
|
||||
default_factory=list[Optional[ComparisonMaterial]]
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutlineUpdater(SceneWatcher):
|
||||
comparison_objects: list[ComparisonObject] = field(
|
||||
default_factory=list[ComparisonObject]
|
||||
)
|
||||
outline_material_key_to_material_name: dict[int, str] = field(
|
||||
default_factory=dict[int, str]
|
||||
)
|
||||
|
||||
object_index: int = 0
|
||||
comparison_object_index: int = 0
|
||||
material_slot_index: int = 0
|
||||
|
||||
@staticmethod
|
||||
def get_outline_material_key(material: Material) -> int:
|
||||
return material.as_pointer() ^ 0x5F5FF5F5
|
||||
|
||||
def reset_run_progress(self) -> None:
|
||||
self.object_index = 0
|
||||
self.comparison_object_index = 0
|
||||
self.material_slot_index = 0
|
||||
self.outline_material_key_to_material_name.clear()
|
||||
|
||||
def run(self, context: Context) -> RunState:
|
||||
"""Detect changes in material assignments to objects and assign outlines."""
|
||||
blend_data = context.blend_data
|
||||
|
||||
# If this value becomes zero, return PREEMPT and interrupt the process.
|
||||
# If a change is detected, set a virtually infinite value so that the
|
||||
# process proceeds to the end.
|
||||
preempt_countdown = 15
|
||||
|
||||
changed = False
|
||||
|
||||
create_modifier = False
|
||||
|
||||
objects = blend_data.objects
|
||||
if not objects:
|
||||
return RunState.FINISH
|
||||
|
||||
# If the number of objects is less than the previous state and the index
|
||||
# range is exceeded, start over from the beginning
|
||||
objects_len = len(objects)
|
||||
if self.object_index >= objects_len:
|
||||
self.object_index = 0
|
||||
|
||||
# Scan objects and check for differences with the comparison object
|
||||
next_object_index = self.object_index
|
||||
for obj in objects[self.object_index : objects_len]:
|
||||
self.object_index = next_object_index
|
||||
next_object_index += 1
|
||||
|
||||
preempt_countdown -= 1
|
||||
if preempt_countdown <= 0:
|
||||
return RunState.PREEMPT
|
||||
|
||||
# Only mesh objects are subject to investigation.
|
||||
# Skip if it is not a mesh object
|
||||
obj_data = obj.data
|
||||
if not isinstance(obj_data, Mesh):
|
||||
continue
|
||||
mesh = obj_data
|
||||
|
||||
# If the number of comparison objects is insufficient, add a new
|
||||
# comparison object
|
||||
while self.comparison_object_index >= len(self.comparison_objects):
|
||||
self.comparison_objects.append(ComparisonObject())
|
||||
|
||||
# Get a comparison object
|
||||
comparison_object = self.comparison_objects[self.comparison_object_index]
|
||||
|
||||
# Comparison of use_auto_smooth
|
||||
if HAS_AUTO_SMOOTH and (
|
||||
(use_auto_smooth := comparison_object.use_auto_smooth) is None
|
||||
or (use_auto_smooth != mesh.use_auto_smooth)
|
||||
):
|
||||
changed, preempt_countdown = True, sys.maxsize
|
||||
# Resolve change differences
|
||||
comparison_object.use_auto_smooth = mesh.use_auto_smooth
|
||||
|
||||
# If the number of MaterialSlots is less than the previous state and
|
||||
# the index range is exceeded, start over from the beginning
|
||||
material_slots = obj.material_slots
|
||||
material_slots_len = len(material_slots)
|
||||
if self.material_slot_index >= material_slots_len:
|
||||
self.material_slot_index = 0
|
||||
|
||||
# Match the number of MaterialSlots and the number of comparison Materials
|
||||
while material_slots_len > len(comparison_object.comparison_materials):
|
||||
comparison_object.comparison_materials.append(None)
|
||||
while material_slots_len < len(comparison_object.comparison_materials):
|
||||
comparison_object.comparison_materials.pop()
|
||||
|
||||
# Scan MaterialSlots and check for differences with the comparison Material
|
||||
next_material_slot_index = self.material_slot_index
|
||||
for material_slot in material_slots[
|
||||
self.material_slot_index : material_slots_len
|
||||
]:
|
||||
material_slot_index = self.material_slot_index = (
|
||||
next_material_slot_index
|
||||
)
|
||||
next_material_slot_index += 1
|
||||
|
||||
preempt_countdown -= 1
|
||||
if preempt_countdown <= 0:
|
||||
return RunState.PREEMPT
|
||||
|
||||
# If the number of comparison objects is insufficient, add a new
|
||||
# comparison object
|
||||
while material_slot_index >= len(
|
||||
comparison_object.comparison_materials
|
||||
):
|
||||
comparison_object.comparison_materials.append(None)
|
||||
comparison_material = comparison_object.comparison_materials[
|
||||
material_slot_index
|
||||
]
|
||||
|
||||
# Difference check
|
||||
if not (
|
||||
(material := material_slot.material)
|
||||
and (mtoon1 := get_material_extension(material).mtoon1)
|
||||
and mtoon1.get_enabled()
|
||||
):
|
||||
# MToon of the material in the material slot is disabled
|
||||
if comparison_material is None:
|
||||
# No updates
|
||||
continue
|
||||
# if the comparison object is enabled, a change is detected
|
||||
changed, preempt_countdown = True, sys.maxsize
|
||||
# Resolve change differences
|
||||
comparison_object.comparison_materials[material_slot_index] = None
|
||||
continue
|
||||
|
||||
# MToon of the material in the material slot is enabled
|
||||
|
||||
outline_material = mtoon1.outline_material
|
||||
outline_material_name = generate_mtoon1_outline_material_name(material)
|
||||
|
||||
if (
|
||||
comparison_material
|
||||
and comparison_material.name == material.name
|
||||
and (
|
||||
outline_material is None
|
||||
or outline_material.name == outline_material_name
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
# MToon of the material in the material slot is enabled,
|
||||
# but if the comparison object does not exist or the name
|
||||
# does not match, a change is detected
|
||||
changed, preempt_countdown = True, sys.maxsize
|
||||
# Resolve change differences
|
||||
comparison_object.comparison_materials[material_slot_index] = (
|
||||
ComparisonMaterial(material.name)
|
||||
)
|
||||
# A material with MToon enabled has been newly assigned
|
||||
# to the object so, if necessary, create a new outline
|
||||
# modifier.
|
||||
# Originally, True/False should be set for each object and
|
||||
# material pair, but for now, I think it's okay to fix it
|
||||
# for practical use.
|
||||
create_modifier = True
|
||||
|
||||
if outline_material is None:
|
||||
continue
|
||||
|
||||
# When a material is copied, a single outline material may be
|
||||
# shared by multiple MToon materials. To resolve this, copy the
|
||||
# outline material and reassign it.
|
||||
outline_material_key = self.get_outline_material_key(outline_material)
|
||||
original_material_name = self.outline_material_key_to_material_name.get(
|
||||
outline_material_key
|
||||
)
|
||||
if original_material_name is None:
|
||||
self.outline_material_key_to_material_name[outline_material_key] = (
|
||||
material.name
|
||||
)
|
||||
elif original_material_name != material.name:
|
||||
outline_material = outline_material.copy()
|
||||
outline_material_key = self.get_outline_material_key(
|
||||
outline_material
|
||||
)
|
||||
self.outline_material_key_to_material_name[outline_material_key] = (
|
||||
material.name
|
||||
)
|
||||
mtoon1.outline_material = outline_material
|
||||
|
||||
# The outline material name follows the naming changes of
|
||||
# the base MToon material.
|
||||
if outline_material.name != outline_material_name:
|
||||
outline_material.name = outline_material_name
|
||||
|
||||
# Since the scanning of MaterialSlots is complete,
|
||||
# reset the next scanning index to 0.
|
||||
self.material_slot_index = 0
|
||||
|
||||
# Before scanning the next object,
|
||||
# advance the index of the next comparison object.
|
||||
self.comparison_object_index += 1
|
||||
|
||||
# Since the number of elements in self.comparison_objects may be
|
||||
# unnecessarily large, reduce it to a sufficient size
|
||||
while len(self.comparison_objects) > self.comparison_object_index:
|
||||
self.comparison_objects.pop()
|
||||
|
||||
if not changed:
|
||||
return RunState.FINISH
|
||||
|
||||
VRM_OT_refresh_mtoon1_outline.refresh(context, create_modifier=create_modifier)
|
||||
return RunState.FINISH
|
||||
|
||||
def create_fast_path_performance_test_objects(self, context: Context) -> None:
|
||||
blend_data = context.blend_data
|
||||
|
||||
for i in range(100):
|
||||
blend_data.materials.new(f"Material#{i}")
|
||||
|
||||
for i in range(100):
|
||||
mesh = blend_data.meshes.new(f"Mesh#{i}")
|
||||
obj = blend_data.objects.new(f"Object#{i}", mesh)
|
||||
context.scene.collection.objects.link(obj)
|
||||
for k in range(50):
|
||||
material = blend_data.materials[(k * 3) % len(blend_data.materials)]
|
||||
mesh.materials.append(material)
|
||||
if k % 5 == 0:
|
||||
get_material_extension(material).mtoon1.enabled = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class MToon1AutoSetup(SceneWatcher):
|
||||
last_material_index: int = 0
|
||||
last_node_index: int = 0
|
||||
|
||||
def reset_run_progress(self) -> None:
|
||||
self.last_material_index: int = 0
|
||||
self.last_node_index: int = 0
|
||||
|
||||
def run(self, context: Context) -> RunState:
|
||||
"""Monitor the appearance of MToon auto-setup node groups and set them up.
|
||||
|
||||
Since this function is called frequently, keep the processing lightweight and
|
||||
be careful to minimize IO and GC Allocation.
|
||||
"""
|
||||
# If this value becomes 0 or less, interrupt the process
|
||||
search_preempt_countdown = 100
|
||||
|
||||
materials = context.blend_data.materials
|
||||
|
||||
# Restore the material traversal start position from the last interrupted state.
|
||||
end_material_index = len(materials)
|
||||
start_material_index = self.last_material_index
|
||||
if start_material_index >= end_material_index:
|
||||
self.last_material_index = 0
|
||||
self.last_node_index = 0
|
||||
start_material_index = 0
|
||||
|
||||
# Traverse the materials and enable MToon if necessary.
|
||||
next_material_index = start_material_index
|
||||
for material in materials[start_material_index:end_material_index]:
|
||||
self.last_material_index = next_material_index
|
||||
next_material_index += 1
|
||||
|
||||
search_preempt_countdown -= 1
|
||||
if search_preempt_countdown <= 0:
|
||||
return RunState.PREEMPT
|
||||
|
||||
if bpy.app.version < (5, 0, 0) and not material.use_nodes:
|
||||
continue
|
||||
node_tree = material.node_tree
|
||||
if node_tree is None:
|
||||
continue
|
||||
|
||||
nodes = node_tree.nodes
|
||||
|
||||
# Restore the node traversal start position from the last interrupted state.
|
||||
end_node_index = len(nodes)
|
||||
start_node_index = self.last_node_index
|
||||
if start_node_index >= end_node_index:
|
||||
start_node_index = 0
|
||||
|
||||
# Traverse the nodes and convert the material to MToon if the MToon
|
||||
# placeholder node is connected to ShaderNodeOutputMaterial.
|
||||
next_node_index = start_node_index
|
||||
for node in nodes[start_node_index:end_node_index]:
|
||||
self.last_node_index = next_node_index
|
||||
next_node_index += 1
|
||||
|
||||
search_preempt_countdown -= 1
|
||||
if search_preempt_countdown <= 0:
|
||||
return RunState.PREEMPT
|
||||
|
||||
if not isinstance(node, ShaderNodeGroup):
|
||||
continue
|
||||
|
||||
group_node_tree = node.node_tree
|
||||
if group_node_tree is None:
|
||||
continue
|
||||
|
||||
if not group_node_tree.get(
|
||||
MTOON1_AUTO_SETUP_GROUP_NODE_TREE_CUSTOM_KEY
|
||||
):
|
||||
continue
|
||||
|
||||
found = False
|
||||
for output in node.outputs:
|
||||
for link in output.links:
|
||||
if isinstance(link.to_node, ShaderNodeOutputMaterial):
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
if not found:
|
||||
continue
|
||||
|
||||
mtoon1 = get_material_extension(material).mtoon1
|
||||
if mtoon1.enabled:
|
||||
ops.vrm.reset_mtoon1_material_shader_node_group(
|
||||
material_name=material.name
|
||||
)
|
||||
else:
|
||||
mtoon1.enabled = True
|
||||
break
|
||||
self.last_node_index = 0
|
||||
self.last_material_index = 0
|
||||
|
||||
return RunState.FINISH
|
||||
|
||||
def create_fast_path_performance_test_objects(self, context: Context) -> None:
|
||||
blend_data = context.blend_data
|
||||
|
||||
for i in range(100):
|
||||
blend_data.materials.new(f"Material#{i}")
|
||||
|
||||
for i in range(100):
|
||||
mesh = blend_data.meshes.new(f"Mesh#{i}")
|
||||
obj = blend_data.objects.new(f"Object#{i}", mesh)
|
||||
context.scene.collection.objects.link(obj)
|
||||
for k in range(50):
|
||||
material = blend_data.materials[(k * 3) % len(blend_data.materials)]
|
||||
mesh.materials.append(material)
|
||||
if k % 5 == 0:
|
||||
get_material_extension(material).mtoon1.enabled = True
|
||||
1
editor/node_constraint1/__init__.py
Normal file
1
editor/node_constraint1/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
BIN
editor/node_constraint1/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
editor/node_constraint1/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/node_constraint1/__pycache__/panel.cpython-311.pyc
Normal file
BIN
editor/node_constraint1/__pycache__/panel.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
316
editor/node_constraint1/panel.py
Normal file
316
editor/node_constraint1/panel.py
Normal file
@@ -0,0 +1,316 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Set as AbstractSet
|
||||
|
||||
from bpy.app.translations import pgettext
|
||||
from bpy.types import (
|
||||
Armature,
|
||||
Context,
|
||||
CopyRotationConstraint,
|
||||
DampedTrackConstraint,
|
||||
Object,
|
||||
Panel,
|
||||
UILayout,
|
||||
)
|
||||
|
||||
from ...common.preferences import get_preferences
|
||||
from .. import search
|
||||
from ..extension import get_armature_extension
|
||||
from ..panel import VRM_PT_vrm_armature_object_property
|
||||
from ..search import active_object_is_vrm1_armature
|
||||
from .property_group import NodeConstraint1NodeConstraintPropertyGroup
|
||||
|
||||
|
||||
def draw_roll_constraint_layout(
|
||||
layout: UILayout,
|
||||
node_constraint: NodeConstraint1NodeConstraintPropertyGroup,
|
||||
object_constraints: dict[str, CopyRotationConstraint],
|
||||
bone_constraints: dict[str, CopyRotationConstraint],
|
||||
) -> None:
|
||||
constraints_box = layout.box()
|
||||
constraints_row = constraints_box.row()
|
||||
constraints_row.alignment = "LEFT"
|
||||
constraints_row.prop(
|
||||
node_constraint,
|
||||
"show_expanded_roll_constraints",
|
||||
icon="TRIA_DOWN"
|
||||
if node_constraint.show_expanded_roll_constraints
|
||||
else "TRIA_RIGHT",
|
||||
emboss=False,
|
||||
)
|
||||
if not node_constraint.show_expanded_roll_constraints:
|
||||
return
|
||||
|
||||
constraints_column = constraints_box.column()
|
||||
if object_constraints or bone_constraints:
|
||||
constraints_expanded_column = constraints_column.column()
|
||||
for object_name, constraint in object_constraints.items():
|
||||
constraint_row = constraints_expanded_column.row()
|
||||
constraint_row.alignment = "LEFT"
|
||||
constraint_row.label(
|
||||
text=object_name + ": " + constraint.name,
|
||||
icon="CONSTRAINT",
|
||||
translate=False,
|
||||
)
|
||||
for bone_name, constraint in bone_constraints.items():
|
||||
constraint_row = constraints_expanded_column.row()
|
||||
constraint_row.alignment = "LEFT"
|
||||
constraint_row.label(
|
||||
text=bone_name + ": " + constraint.name,
|
||||
icon="CONSTRAINT_BONE",
|
||||
translate=False,
|
||||
)
|
||||
|
||||
constraints_help_column = constraints_column.box().column(align=True)
|
||||
help_message = pgettext(
|
||||
"Conditions exported as Roll Constraint\n"
|
||||
+ " - {copy_rotation}\n"
|
||||
+ " - Enabled\n"
|
||||
+ " - No Vertex Group\n"
|
||||
+ " - {axis} is one of X, Y and Z\n"
|
||||
+ " - No Inverted\n"
|
||||
+ " - {mix} is {add}\n"
|
||||
+ " - {target} is {local_space}\n"
|
||||
+ " - {owner} is {local_space}\n"
|
||||
+ " - No circular dependencies\n"
|
||||
+ " - The one at the top of the list of\n"
|
||||
+ " those that meet all the conditions\n",
|
||||
).format(
|
||||
copy_rotation=pgettext("Copy Rotation"),
|
||||
axis=pgettext("Axis"),
|
||||
mix=pgettext("Mix"),
|
||||
add=pgettext("Add"),
|
||||
target=pgettext("Target"),
|
||||
local_space=pgettext("Local Space"),
|
||||
owner=pgettext("Owner"),
|
||||
)
|
||||
for index, help_line in enumerate(help_message.splitlines()):
|
||||
constraints_help_column.label(
|
||||
text=help_line, translate=False, icon="HELP" if index == 0 else "NONE"
|
||||
)
|
||||
|
||||
|
||||
def draw_aim_constraint_layout(
|
||||
layout: UILayout,
|
||||
node_constraint: NodeConstraint1NodeConstraintPropertyGroup,
|
||||
object_constraints: dict[str, DampedTrackConstraint],
|
||||
bone_constraints: dict[str, DampedTrackConstraint],
|
||||
) -> None:
|
||||
constraints_box = layout.box()
|
||||
constraints_row = constraints_box.row()
|
||||
constraints_row.alignment = "LEFT"
|
||||
constraints_row.prop(
|
||||
node_constraint,
|
||||
"show_expanded_aim_constraints",
|
||||
icon="TRIA_DOWN"
|
||||
if node_constraint.show_expanded_aim_constraints
|
||||
else "TRIA_RIGHT",
|
||||
emboss=False,
|
||||
)
|
||||
if not node_constraint.show_expanded_aim_constraints:
|
||||
return
|
||||
|
||||
constraints_column = constraints_box.column()
|
||||
if object_constraints or bone_constraints:
|
||||
constraints_expanded_column = constraints_column.column()
|
||||
for object_name, constraint in object_constraints.items():
|
||||
constraint_row = constraints_expanded_column.row()
|
||||
constraint_row.alignment = "LEFT"
|
||||
constraint_row.label(
|
||||
text=object_name + ": " + constraint.name,
|
||||
icon="CONSTRAINT",
|
||||
translate=False,
|
||||
)
|
||||
for bone_name, constraint in bone_constraints.items():
|
||||
constraint_row = constraints_expanded_column.row()
|
||||
constraint_row.alignment = "LEFT"
|
||||
constraint_row.label(
|
||||
text=bone_name + ": " + constraint.name,
|
||||
icon="CONSTRAINT_BONE",
|
||||
translate=False,
|
||||
)
|
||||
|
||||
constraints_help_column = constraints_column.box().column(align=True)
|
||||
help_message = pgettext(
|
||||
"Conditions exported as Aim Constraint\n"
|
||||
+ " - {damped_track}\n"
|
||||
+ " - Enabled\n"
|
||||
+ " - Target Bone {head_tail} is 0\n"
|
||||
+ " - No Follow Target Bone B-Bone\n"
|
||||
+ " - No circular dependencies\n"
|
||||
+ " - The one at the top of the list of\n"
|
||||
+ " those that meet all the conditions\n"
|
||||
).format(
|
||||
damped_track=pgettext("Damped Track"),
|
||||
head_tail=pgettext("Head/Tail"),
|
||||
)
|
||||
for index, help_line in enumerate(help_message.splitlines()):
|
||||
constraints_help_column.label(
|
||||
text=help_line, translate=False, icon="HELP" if index == 0 else "NONE"
|
||||
)
|
||||
|
||||
|
||||
def draw_rotation_constraint_layout(
|
||||
layout: UILayout,
|
||||
node_constraint: NodeConstraint1NodeConstraintPropertyGroup,
|
||||
object_constraints: dict[str, CopyRotationConstraint],
|
||||
bone_constraints: dict[str, CopyRotationConstraint],
|
||||
) -> None:
|
||||
constraints_box = layout.box()
|
||||
constraints_row = constraints_box.row()
|
||||
constraints_row.alignment = "LEFT"
|
||||
constraints_row.prop(
|
||||
node_constraint,
|
||||
"show_expanded_rotation_constraints",
|
||||
icon="TRIA_DOWN"
|
||||
if node_constraint.show_expanded_rotation_constraints
|
||||
else "TRIA_RIGHT",
|
||||
emboss=False,
|
||||
)
|
||||
if not node_constraint.show_expanded_rotation_constraints:
|
||||
return
|
||||
|
||||
constraints_column = constraints_box.column()
|
||||
if object_constraints or bone_constraints:
|
||||
constraints_expanded_column = constraints_column.column()
|
||||
for object_name, constraint in object_constraints.items():
|
||||
constraint_row = constraints_expanded_column.row()
|
||||
constraint_row.alignment = "LEFT"
|
||||
constraint_row.label(
|
||||
text=object_name + ": " + constraint.name,
|
||||
icon="CONSTRAINT",
|
||||
translate=False,
|
||||
)
|
||||
for bone_name, constraint in bone_constraints.items():
|
||||
constraint_row = constraints_expanded_column.row()
|
||||
constraint_row.alignment = "LEFT"
|
||||
constraint_row.label(
|
||||
text=bone_name + ": " + constraint.name,
|
||||
icon="CONSTRAINT_BONE",
|
||||
translate=False,
|
||||
)
|
||||
|
||||
constraints_help_column = constraints_column.box().column(align=True)
|
||||
help_message = pgettext(
|
||||
"Conditions exported as Rotation Constraint\n"
|
||||
+ " - {copy_rotation}\n"
|
||||
+ " - Enabled\n"
|
||||
+ " - No Vertex Group\n"
|
||||
+ " - {axis} is X, Y and Z\n"
|
||||
+ " - No Inverted\n"
|
||||
+ " - {mix} is {add}\n"
|
||||
+ " - {target} is {local_space}\n"
|
||||
+ " - {owner} is {local_space}\n"
|
||||
+ " - No circular dependencies\n"
|
||||
+ " - The one at the top of the list of\n"
|
||||
+ " those that meet all the conditions\n"
|
||||
).format(
|
||||
copy_rotation=pgettext("Copy Rotation"),
|
||||
axis=pgettext("Axis"),
|
||||
mix=pgettext("Mix"),
|
||||
add=pgettext("Add"),
|
||||
target=pgettext("Target"),
|
||||
local_space=pgettext("Local Space"),
|
||||
owner=pgettext("Owner"),
|
||||
)
|
||||
for index, help_line in enumerate(help_message.splitlines()):
|
||||
constraints_help_column.label(
|
||||
text=help_line, translate=False, icon="HELP" if index == 0 else "NONE"
|
||||
)
|
||||
|
||||
|
||||
def draw_node_constraint1_layout(
|
||||
context: Context,
|
||||
armature: Object,
|
||||
layout: UILayout,
|
||||
node_constraint: NodeConstraint1NodeConstraintPropertyGroup,
|
||||
) -> None:
|
||||
preferences = get_preferences(context)
|
||||
objs = search.export_objects(
|
||||
context,
|
||||
armature_object_name=armature.name,
|
||||
export_invisibles=preferences.export_invisibles,
|
||||
export_only_selections=False,
|
||||
export_lights=False,
|
||||
)
|
||||
object_constraints, bone_constraints, _ = search.export_constraints(objs, armature)
|
||||
draw_roll_constraint_layout(
|
||||
layout,
|
||||
node_constraint,
|
||||
object_constraints.roll_constraints,
|
||||
bone_constraints.roll_constraints,
|
||||
)
|
||||
draw_aim_constraint_layout(
|
||||
layout,
|
||||
node_constraint,
|
||||
object_constraints.aim_constraints,
|
||||
bone_constraints.aim_constraints,
|
||||
)
|
||||
draw_rotation_constraint_layout(
|
||||
layout,
|
||||
node_constraint,
|
||||
object_constraints.rotation_constraints,
|
||||
bone_constraints.rotation_constraints,
|
||||
)
|
||||
|
||||
|
||||
class VRM_PT_node_constraint1_armature_object_property(Panel):
|
||||
bl_idname = "VRM_PT_node_constraint1_armature_object_property"
|
||||
bl_label = "Node Constraint"
|
||||
bl_translation_context = "VRM"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_options: AbstractSet[str] = {"DEFAULT_CLOSED"}
|
||||
bl_parent_id = VRM_PT_vrm_armature_object_property.bl_idname
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return active_object_is_vrm1_armature(context)
|
||||
|
||||
def draw_header(self, _context: Context) -> None:
|
||||
self.layout.label(icon="CONSTRAINT")
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
active_object = context.active_object
|
||||
if not active_object:
|
||||
return
|
||||
armature_data = active_object.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
draw_node_constraint1_layout(
|
||||
context,
|
||||
active_object,
|
||||
self.layout,
|
||||
get_armature_extension(armature_data).node_constraint1,
|
||||
)
|
||||
|
||||
|
||||
class VRM_PT_node_constraint1_ui(Panel):
|
||||
bl_idname = "VRM_PT_node_constraint1_ui"
|
||||
bl_label = "Node Constraint"
|
||||
bl_translation_context = "VRM"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "VRM"
|
||||
bl_options: AbstractSet[str] = {"DEFAULT_CLOSED"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return search.current_armature_is_vrm1(context)
|
||||
|
||||
def draw_header(self, _context: Context) -> None:
|
||||
self.layout.label(icon="CONSTRAINT")
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
armature = search.current_armature(context)
|
||||
if not armature:
|
||||
return
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
draw_node_constraint1_layout(
|
||||
context,
|
||||
armature,
|
||||
self.layout,
|
||||
get_armature_extension(armature_data).node_constraint1,
|
||||
)
|
||||
25
editor/node_constraint1/property_group.py
Normal file
25
editor/node_constraint1/property_group.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bpy.props import BoolProperty
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
|
||||
# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.schema.json
|
||||
class NodeConstraint1NodeConstraintPropertyGroup(PropertyGroup):
|
||||
# for UI
|
||||
show_expanded_roll_constraints: BoolProperty( # type: ignore[valid-type]
|
||||
name="Roll Constraint"
|
||||
)
|
||||
show_expanded_aim_constraints: BoolProperty( # type: ignore[valid-type]
|
||||
name="Aim Constraint"
|
||||
)
|
||||
show_expanded_rotation_constraints: BoolProperty( # type: ignore[valid-type]
|
||||
name="Rotation Constraint"
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
show_expanded_roll_constraints: bool # type: ignore[no-redef]
|
||||
show_expanded_aim_constraints: bool # type: ignore[no-redef]
|
||||
show_expanded_rotation_constraints: bool # type: ignore[no-redef]
|
||||
405
editor/ops.py
Normal file
405
editor/ops.py
Normal file
@@ -0,0 +1,405 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2018 iCyP
|
||||
# SPDX-FileCopyrightText: 2024 saturday06
|
||||
import json
|
||||
import warnings
|
||||
import webbrowser
|
||||
from collections.abc import Set as AbstractSet
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Optional, TypeVar, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from bpy.app.translations import pgettext
|
||||
from bpy.props import StringProperty
|
||||
from bpy.types import (
|
||||
Armature,
|
||||
Context,
|
||||
Event,
|
||||
Operator,
|
||||
UILayout,
|
||||
)
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
|
||||
from ..common.deep import make_json
|
||||
from ..common.human_bone_mapper.vroid_mapping import (
|
||||
FULL_PATTERN,
|
||||
symmetrise_vroid_bone_name,
|
||||
)
|
||||
from ..common.logger import get_logger
|
||||
from ..common.vrm0.human_bone import HumanBoneSpecifications
|
||||
from ..common.workspace import save_workspace
|
||||
from . import search
|
||||
from .extension import get_armature_extension
|
||||
from .t_pose import set_estimated_humanoid_t_pose
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class VRM_OT_simplify_vroid_bones(Operator):
|
||||
bl_idname = "vrm.bones_rename"
|
||||
bl_label = "Symmetrize VRoid Bone Names on X-Axis"
|
||||
bl_description = "Make VRoid bone names editable for X-axis mirroring."
|
||||
bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}
|
||||
|
||||
armature_object_name: StringProperty( # type: ignore[valid-type]
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
def update_armature_name(self, _context: Context) -> None:
|
||||
message = (
|
||||
f"`{type(self).__qualname__}.armature_name` is deprecated"
|
||||
+ " and will be removed in the next major release."
|
||||
+ f" `Please use {type(self).__qualname__}.armature_object_name` instead."
|
||||
)
|
||||
logger.warning(message)
|
||||
warnings.warn(message, DeprecationWarning, stacklevel=5)
|
||||
|
||||
armature_name: StringProperty( # type: ignore[valid-type]
|
||||
options={"HIDDEN"},
|
||||
update=update_armature_name,
|
||||
)
|
||||
"""`armature_name` is deprecated and will be removed in the next major release.
|
||||
Please use `armature_object_name` instead.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def vroid_bones_exist(armature: Armature) -> bool:
|
||||
return any(map(FULL_PATTERN.match, armature.bones.keys()))
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
if not self.armature_object_name and self.armature_name:
|
||||
self.armature_object_name = self.armature_name
|
||||
armature = context.blend_data.objects.get(self.armature_object_name)
|
||||
if armature is None or armature.type != "ARMATURE":
|
||||
return {"CANCELLED"}
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return {"CANCELLED"}
|
||||
|
||||
with save_workspace(context):
|
||||
for bone_name, bone in armature_data.bones.items():
|
||||
bone.name = symmetrise_vroid_bone_name(bone_name)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
armature_object_name: str # type: ignore[no-redef]
|
||||
armature_name: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
class VRM_OT_save_human_bone_mappings(Operator, ExportHelper):
|
||||
bl_idname = "vrm.save_human_bone_mappings"
|
||||
bl_label = "Save Bone Mappings"
|
||||
bl_description = "Save bone mappings."
|
||||
bl_options: AbstractSet[str] = {"REGISTER"}
|
||||
|
||||
filename_ext = ".json"
|
||||
filter_glob: StringProperty( # type: ignore[valid-type]
|
||||
default="*.json",
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return search.armature_exists(context)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
armature = search.current_armature(context)
|
||||
if not armature:
|
||||
return {"CANCELLED"}
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return {"CANCELLED"}
|
||||
|
||||
mappings = {}
|
||||
for human_bone in get_armature_extension(
|
||||
armature_data
|
||||
).vrm0.humanoid.human_bones:
|
||||
if human_bone.bone not in HumanBoneSpecifications.all_names:
|
||||
continue
|
||||
if not human_bone.node.bone_name:
|
||||
continue
|
||||
mappings[human_bone.bone] = human_bone.node.bone_name
|
||||
|
||||
Path(self.filepath).write_bytes(
|
||||
json.dumps(mappings, sort_keys=True, indent=4)
|
||||
.replace("\r\n", "\n")
|
||||
.encode()
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
filter_glob: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
class VRM_OT_load_human_bone_mappings(Operator, ImportHelper):
|
||||
bl_idname = "vrm.load_human_bone_mappings"
|
||||
bl_label = "Load Bone Mappings"
|
||||
bl_description = "Load bone mappings."
|
||||
bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}
|
||||
|
||||
filename_ext = ".json"
|
||||
filter_glob: StringProperty( # type: ignore[valid-type]
|
||||
default="*.json",
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return search.armature_exists(context)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
armature = search.current_armature(context)
|
||||
if not armature:
|
||||
return {"CANCELLED"}
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return {"CANCELLED"}
|
||||
|
||||
obj = make_json(json.loads(Path(self.filepath).read_text(encoding="UTF-8")))
|
||||
if not isinstance(obj, dict):
|
||||
return {"CANCELLED"}
|
||||
|
||||
for human_bone_name, bpy_bone_name in obj.items():
|
||||
if human_bone_name not in HumanBoneSpecifications.all_names:
|
||||
continue
|
||||
if not isinstance(bpy_bone_name, str):
|
||||
continue
|
||||
# INFO@MICROSOFT.COM
|
||||
found = False
|
||||
for human_bone in get_armature_extension(
|
||||
armature_data
|
||||
).vrm0.humanoid.human_bones:
|
||||
if human_bone.bone == human_bone_name:
|
||||
human_bone.node.bone_name = bpy_bone_name
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
continue
|
||||
|
||||
human_bone = get_armature_extension(
|
||||
armature_data
|
||||
).vrm0.humanoid.human_bones.add()
|
||||
human_bone.bone = human_bone_name
|
||||
human_bone.node.bone_name = bpy_bone_name
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
return ImportHelper.invoke(self, context, event)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
filter_glob: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
class VRM_OT_open_url_in_web_browser(Operator):
|
||||
bl_idname = "vrm.open_url_in_web_browser"
|
||||
bl_label = "Open"
|
||||
bl_description = "Open the URL in the default web browser."
|
||||
bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}
|
||||
|
||||
url: StringProperty( # type: ignore[valid-type]
|
||||
options={"HIDDEN"}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def supported(url_str: str) -> bool:
|
||||
try:
|
||||
url = urlparse(url_str)
|
||||
except ValueError:
|
||||
return False
|
||||
return url.scheme in ["http", "https"]
|
||||
|
||||
def execute(self, _context: Context) -> set[str]:
|
||||
url = self.url
|
||||
if not self.supported(url):
|
||||
return {"CANCELLED"}
|
||||
webbrowser.open(self.url)
|
||||
return {"FINISHED"}
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
url: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
class VRM_OT_show_blend_file_compatibility_warning(Operator):
|
||||
bl_idname = "vrm.show_blend_file_compatibility_warning"
|
||||
bl_label = "File Compatibility Warning"
|
||||
bl_description = "Show blend file compatibility warning."
|
||||
bl_options: AbstractSet[str] = {"REGISTER"}
|
||||
|
||||
file_version: StringProperty(options={"HIDDEN"}) # type: ignore[valid-type]
|
||||
app_version: StringProperty(options={"HIDDEN"}) # type: ignore[valid-type]
|
||||
|
||||
def execute(self, _context: Context) -> set[str]:
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, _event: Event) -> set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self, width=500)
|
||||
|
||||
def draw(self, _context: Context) -> None:
|
||||
column = self.layout.row(align=True).column()
|
||||
text = pgettext(
|
||||
"The current file is not compatible with the running Blender.\n"
|
||||
+ "The current file was created in Blender {file_version}, but the running"
|
||||
+ " Blender version is {app_version}.\n"
|
||||
+ "This incompatibility may result in data loss or corruption."
|
||||
).format(
|
||||
app_version=self.app_version,
|
||||
file_version=self.file_version,
|
||||
)
|
||||
description_outer_column = column.column()
|
||||
description_outer_column.emboss = "NONE"
|
||||
description_column = description_outer_column.box().column(align=True)
|
||||
for i, line in enumerate(text.splitlines()):
|
||||
icon = "ERROR" if i == 0 else "NONE"
|
||||
description_column.label(text=line, translate=False, icon=icon)
|
||||
open_url = layout_operator(
|
||||
self.layout,
|
||||
VRM_OT_open_url_in_web_browser,
|
||||
text="Open Documentation",
|
||||
icon="URL",
|
||||
)
|
||||
open_url.url = "https://developer.blender.org/docs/handbook/guidelines/compatibility_handling_for_blend_files/#forward-compatibility"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
file_version: str # type: ignore[no-redef]
|
||||
app_version: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
class VRM_OT_show_blend_file_addon_compatibility_warning(Operator):
|
||||
bl_idname = "vrm.show_blend_file_addon_compatibility_warning"
|
||||
bl_label = "VRM Add-on Compatibility Warning"
|
||||
bl_description = "Show blend file and VRM add-on compatibility warning."
|
||||
bl_options: AbstractSet[str] = {"REGISTER"}
|
||||
|
||||
file_addon_version: StringProperty(options={"HIDDEN"}) # type: ignore[valid-type]
|
||||
installed_addon_version: StringProperty(options={"HIDDEN"}) # type: ignore[valid-type]
|
||||
|
||||
def execute(self, _context: Context) -> set[str]:
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, _event: Event) -> set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self, width=500)
|
||||
|
||||
def draw(self, _context: Context) -> None:
|
||||
column = self.layout.row(align=True).column()
|
||||
text = pgettext(
|
||||
"The current file is not compatible with the installed VRM Add-on.\n"
|
||||
+ "The current file was created in VRM Add-on {file_addon_version}, but the"
|
||||
+ " installed\n"
|
||||
+ "VRM Add-on version is {installed_addon_version}. This incompatibility\n"
|
||||
+ "may result in data loss or corruption."
|
||||
).format(
|
||||
file_addon_version=self.file_addon_version,
|
||||
installed_addon_version=self.installed_addon_version,
|
||||
)
|
||||
description_outer_column = column.column()
|
||||
description_outer_column.emboss = "NONE"
|
||||
description_column = description_outer_column.box().column(align=True)
|
||||
for i, line in enumerate(text.splitlines()):
|
||||
icon = "ERROR" if i == 0 else "NONE"
|
||||
description_column.label(text=line, translate=False, icon=icon)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
file_addon_version: str # type: ignore[no-redef]
|
||||
installed_addon_version: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
__Operator = TypeVar("__Operator", bound=Operator)
|
||||
|
||||
|
||||
def layout_operator(
|
||||
layout: UILayout,
|
||||
operator_type: type[__Operator],
|
||||
*,
|
||||
text: Optional[str] = None,
|
||||
text_ctxt: str = "",
|
||||
translate: bool = True,
|
||||
icon: str = "NONE",
|
||||
emboss: bool = True,
|
||||
depress: bool = False,
|
||||
icon_value: int = 0,
|
||||
) -> __Operator:
|
||||
if text is None:
|
||||
text = operator_type.bl_label
|
||||
operator = layout.operator(
|
||||
operator_type.bl_idname,
|
||||
text=text,
|
||||
text_ctxt=text_ctxt,
|
||||
translate=translate,
|
||||
icon=icon,
|
||||
emboss=emboss,
|
||||
depress=depress,
|
||||
icon_value=icon_value,
|
||||
)
|
||||
|
||||
split = operator_type.bl_idname.split(".")
|
||||
if len(split) != 2:
|
||||
message = f"Unexpected bl_idname: {operator_type.bl_idname}"
|
||||
raise AssertionError(message)
|
||||
name = f"{split[0].encode().upper().decode()}_OT_{split[1]}"
|
||||
if type(operator).__qualname__ != name:
|
||||
raise AssertionError(
|
||||
f"{type(operator)} is not compatible with {operator_type}."
|
||||
+ f"the expected name is {name}"
|
||||
)
|
||||
return cast("__Operator", operator)
|
||||
|
||||
|
||||
class VRM_OT_make_estimated_humanoid_t_pose(Operator):
|
||||
bl_idname = "vrm.make_estimated_humanoid_t_pose"
|
||||
bl_label = "Make Estimated T-Pose"
|
||||
bl_description = "Create VRM estimated humanoid T-pose."
|
||||
bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}
|
||||
|
||||
armature_object_name: StringProperty( # type: ignore[valid-type]
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
def update_armature_name(self, _context: Context) -> None:
|
||||
message = (
|
||||
f"`{type(self).__qualname__}.armature_name` is deprecated"
|
||||
+ " and will be removed in the next major release."
|
||||
+ f" `Please use {type(self).__qualname__}.armature_object_name` instead."
|
||||
)
|
||||
logger.warning(message)
|
||||
warnings.warn(message, DeprecationWarning, stacklevel=5)
|
||||
|
||||
armature_name: StringProperty( # type: ignore[valid-type]
|
||||
options={"HIDDEN"},
|
||||
update=update_armature_name,
|
||||
)
|
||||
"""`armature_name` is deprecated and will be removed in the next major release.
|
||||
Please use `armature_object_name` instead.
|
||||
"""
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
if not self.armature_object_name and self.armature_name:
|
||||
self.armature_object_name = self.armature_name
|
||||
armature = context.blend_data.objects.get(self.armature_object_name)
|
||||
if armature is None or armature.type != "ARMATURE":
|
||||
return {"CANCELLED"}
|
||||
if not set_estimated_humanoid_t_pose(context, armature):
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
armature_object_name: str # type: ignore[no-redef]
|
||||
armature_name: str # type: ignore[no-redef]
|
||||
287
editor/panel.py
Normal file
287
editor/panel.py
Normal file
@@ -0,0 +1,287 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Set as AbstractSet
|
||||
from typing import Callable, Optional, Protocol, TypeVar, Union, runtime_checkable
|
||||
|
||||
from bpy.app.translations import pgettext
|
||||
from bpy.types import (
|
||||
AnyType,
|
||||
Armature,
|
||||
Context,
|
||||
Menu,
|
||||
Operator,
|
||||
Panel,
|
||||
UILayout,
|
||||
UIList,
|
||||
)
|
||||
|
||||
from ..common import version
|
||||
from ..common.preferences import get_preferences
|
||||
from . import make_armature, search, validation
|
||||
from .extension import get_armature_extension
|
||||
from .ops import layout_operator
|
||||
|
||||
__AddOperator = TypeVar("__AddOperator", bound=Operator)
|
||||
__RemoveOperator = TypeVar("__RemoveOperator", bound=Operator)
|
||||
__MoveUpOperator = TypeVar("__MoveUpOperator", bound=Operator)
|
||||
__MoveDownOperator = TypeVar("__MoveDownOperator", bound=Operator)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TemplateListCollectionProtocol(Protocol):
|
||||
def __len__(self) -> int: ...
|
||||
def __getitem__(self, index: int) -> object: ...
|
||||
|
||||
|
||||
def draw_template_list(
|
||||
layout: UILayout,
|
||||
template_list: type[UIList],
|
||||
base_object: AnyType,
|
||||
collection_attribue_name: str,
|
||||
active_index_attribute_name: str,
|
||||
add_operator_type: type[__AddOperator],
|
||||
remove_operator_type: type[__RemoveOperator],
|
||||
move_up_operator_type: type[__MoveUpOperator],
|
||||
move_down_operator_type: type[__MoveDownOperator],
|
||||
*,
|
||||
can_remove: Callable[[int], bool] = lambda _: True,
|
||||
can_move: Callable[[int], bool] = lambda _: True,
|
||||
compact: bool = False,
|
||||
menu: Optional[type[Menu]] = None,
|
||||
) -> tuple[
|
||||
list[Union[__AddOperator, __RemoveOperator, __MoveUpOperator, __MoveDownOperator]],
|
||||
list[Union[__RemoveOperator, __MoveUpOperator, __MoveDownOperator]],
|
||||
int,
|
||||
object,
|
||||
tuple[
|
||||
__AddOperator,
|
||||
__RemoveOperator,
|
||||
Optional[__MoveUpOperator],
|
||||
Optional[__MoveDownOperator],
|
||||
],
|
||||
]:
|
||||
collection = getattr(base_object, collection_attribue_name, None)
|
||||
if not isinstance(collection, TemplateListCollectionProtocol):
|
||||
message = (
|
||||
f"{collection}.{collection_attribue_name}"
|
||||
+ " is not a Template List Collection Protocol."
|
||||
)
|
||||
raise TypeError(message)
|
||||
|
||||
active_index = getattr(base_object, active_index_attribute_name, None)
|
||||
if not isinstance(active_index, int):
|
||||
message = f"{base_object}.{active_index_attribute_name} is not an int."
|
||||
raise TypeError(message)
|
||||
|
||||
if 0 <= active_index < len(collection):
|
||||
active_object = collection[active_index]
|
||||
else:
|
||||
active_object = None
|
||||
|
||||
length = len(collection)
|
||||
list_row_len = 4 if length > int(compact) else 2
|
||||
|
||||
list_row = layout.row()
|
||||
list_row.template_list(
|
||||
template_list.bl_idname,
|
||||
"",
|
||||
base_object,
|
||||
collection_attribue_name,
|
||||
base_object,
|
||||
active_index_attribute_name,
|
||||
rows=list_row_len,
|
||||
)
|
||||
list_side_column = list_row.column(align=True)
|
||||
add_operator = layout_operator(
|
||||
list_side_column, add_operator_type, icon="ADD", text="", translate=False
|
||||
)
|
||||
|
||||
if length >= 1 and 0 <= active_index < length and can_remove(active_index):
|
||||
remove_operator_parent = list_side_column
|
||||
else:
|
||||
remove_operator_parent = list_side_column.column(align=True)
|
||||
remove_operator_parent.enabled = False
|
||||
|
||||
remove_operator = layout_operator(
|
||||
remove_operator_parent,
|
||||
remove_operator_type,
|
||||
icon="REMOVE",
|
||||
text="",
|
||||
translate=False,
|
||||
)
|
||||
|
||||
if menu is not None:
|
||||
list_side_column.separator()
|
||||
list_side_column.menu(menu=menu.bl_idname, text="", icon="DOWNARROW_HLT")
|
||||
|
||||
move_operator_parent = list_side_column.column(align=True)
|
||||
# Without separator, the button width changes mysteriously when list elements are 0
|
||||
move_operator_parent.separator()
|
||||
if length <= 1 or not (0 <= active_index < length) or not can_move(active_index):
|
||||
move_operator_parent.enabled = False
|
||||
|
||||
collection_ops: list[
|
||||
Union[__AddOperator, __RemoveOperator, __MoveUpOperator, __MoveDownOperator]
|
||||
] = [add_operator]
|
||||
collection_item_ops: list[
|
||||
Union[__RemoveOperator, __MoveUpOperator, __MoveDownOperator]
|
||||
] = [remove_operator]
|
||||
|
||||
if length > int(compact):
|
||||
move_up_operator = layout_operator(
|
||||
move_operator_parent,
|
||||
move_up_operator_type,
|
||||
icon="TRIA_UP",
|
||||
text="",
|
||||
translate=False,
|
||||
)
|
||||
collection_item_ops.append(move_up_operator)
|
||||
move_down_operator = layout_operator(
|
||||
move_operator_parent,
|
||||
move_down_operator_type,
|
||||
icon="TRIA_DOWN",
|
||||
text="",
|
||||
translate=False,
|
||||
)
|
||||
collection_item_ops.append(move_down_operator)
|
||||
else:
|
||||
move_up_operator = None
|
||||
move_down_operator = None
|
||||
|
||||
collection_ops.extend(collection_item_ops)
|
||||
|
||||
return (
|
||||
collection_ops,
|
||||
collection_item_ops,
|
||||
active_index,
|
||||
active_object,
|
||||
(add_operator, remove_operator, move_up_operator, move_down_operator),
|
||||
)
|
||||
|
||||
|
||||
class VRM_PT_vrm_armature_object_property(Panel):
|
||||
bl_idname = "VRM_PT_vrm_armature_object_property"
|
||||
bl_label = "VRM"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
active_object = context.active_object
|
||||
if not active_object:
|
||||
return False
|
||||
armature_data = active_object.data
|
||||
return isinstance(armature_data, Armature)
|
||||
|
||||
def draw(self, _context: Context) -> None:
|
||||
warning_message = version.panel_warning_message()
|
||||
if not warning_message:
|
||||
return
|
||||
|
||||
box = self.layout.box()
|
||||
warning_column = box.column()
|
||||
for index, warning_line in enumerate(warning_message.splitlines()):
|
||||
warning_column.label(
|
||||
text=warning_line,
|
||||
translate=False,
|
||||
icon="NONE" if index else "ERROR",
|
||||
)
|
||||
|
||||
|
||||
def add_armature(add_armature_op: Operator, _context: Context) -> None:
|
||||
layout_operator(
|
||||
add_armature_op.layout,
|
||||
make_armature.ICYP_OT_make_armature,
|
||||
text="VRM Humanoid",
|
||||
icon="OUTLINER_OB_ARMATURE",
|
||||
).skip_heavy_armature_setup = True
|
||||
|
||||
|
||||
class VRM_PT_current_selected_armature(Panel):
|
||||
bl_idname = "VRM_PT_current_selected_armature"
|
||||
bl_label = "Current selected armature"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "VRM"
|
||||
bl_options: AbstractSet[str] = {"HIDE_HEADER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return search.multiple_armatures_exist(context)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
armature = search.current_armature(context)
|
||||
if not armature:
|
||||
return
|
||||
layout = self.layout
|
||||
layout.label(text=armature.name, icon="ARMATURE_DATA", translate=False)
|
||||
|
||||
|
||||
class VRM_PT_controller(Panel):
|
||||
bl_idname = "ICYP_PT_ui_controller"
|
||||
bl_label = "Operator"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "VRM"
|
||||
|
||||
def draw_header(self, _context: Context) -> None:
|
||||
self.layout.label(icon="TOOL_SETTINGS")
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
preferences = get_preferences(context)
|
||||
|
||||
# draw_main
|
||||
layout_operator(
|
||||
layout,
|
||||
make_armature.ICYP_OT_make_armature,
|
||||
text=pgettext("Create VRM Model"),
|
||||
icon="OUTLINER_OB_ARMATURE",
|
||||
).skip_heavy_armature_setup = True
|
||||
vrm_validator_op = layout_operator(
|
||||
layout,
|
||||
validation.WM_OT_vrm_validator,
|
||||
text=pgettext("Check VRM Model"),
|
||||
icon="VIEWZOOM",
|
||||
)
|
||||
vrm_validator_op.show_successful_message = True
|
||||
layout.prop(preferences, "export_invisibles")
|
||||
layout.prop(preferences, "export_only_selections")
|
||||
|
||||
armature = search.current_armature(context)
|
||||
if armature:
|
||||
vrm_validator_op.armature_object_name = armature.name
|
||||
armature_data = armature.data
|
||||
if isinstance(armature_data, Armature):
|
||||
layout.prop(
|
||||
get_armature_extension(armature_data),
|
||||
"spec_version",
|
||||
text="",
|
||||
translate=False,
|
||||
)
|
||||
|
||||
|
||||
class VRM_PT_controller_unsupported_blender_version_warning(Panel):
|
||||
bl_idname = "VRM_PT_controller_unsupported_blender_version_warning"
|
||||
bl_label = "Unsupported Blender Version Warning"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "VRM"
|
||||
bl_options: AbstractSet[str] = {"HIDE_HEADER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, _context: Context) -> bool:
|
||||
return bool(version.panel_warning_message())
|
||||
|
||||
def draw(self, _context: Context) -> None:
|
||||
warning_message = version.panel_warning_message()
|
||||
if warning_message is None:
|
||||
return
|
||||
box = self.layout.box()
|
||||
warning_column = box.column()
|
||||
for index, warning_line in enumerate(warning_message.splitlines()):
|
||||
warning_column.label(
|
||||
text=warning_line,
|
||||
translate=False,
|
||||
icon="NONE" if index else "ERROR",
|
||||
)
|
||||
851
editor/property_group.py
Normal file
851
editor/property_group.py
Normal file
@@ -0,0 +1,851 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import uuid
|
||||
import warnings
|
||||
from collections.abc import Iterator, Mapping, Sequence, ValuesView
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
ClassVar,
|
||||
Final,
|
||||
Optional,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
|
||||
import bpy
|
||||
from bpy.app.translations import pgettext
|
||||
from bpy.props import FloatProperty, PointerProperty, StringProperty
|
||||
from bpy.types import Armature, Bone, Context, Material, Object, PropertyGroup, UILayout
|
||||
|
||||
from ..common.logger import get_logger
|
||||
from ..common.vrm0 import human_bone as vrm0_human_bone
|
||||
from ..common.vrm1 import human_bone as vrm1_human_bone
|
||||
|
||||
HumanBoneSpecification = TypeVar(
|
||||
"HumanBoneSpecification",
|
||||
vrm0_human_bone.HumanBoneSpecification,
|
||||
vrm1_human_bone.HumanBoneSpecification,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
# https://docs.blender.org/api/2.93/bpy.types.EnumPropertyItem.html#bpy.types.EnumPropertyItem
|
||||
@dataclass(frozen=True)
|
||||
class PropertyGroupEnumItem:
|
||||
identifier: str
|
||||
name: str
|
||||
description: str
|
||||
icon: str
|
||||
value: int
|
||||
|
||||
@staticmethod
|
||||
def from_enum_property_items(
|
||||
items: tuple[tuple[str, str, str, str, int], ...],
|
||||
) -> tuple[tuple[str, ...], tuple[int, ...], tuple["PropertyGroupEnumItem", ...]]:
|
||||
enum_items = tuple(
|
||||
PropertyGroupEnumItem(
|
||||
identifier=identifier,
|
||||
name=name,
|
||||
description=description,
|
||||
icon=icon,
|
||||
value=value,
|
||||
)
|
||||
for identifier, name, description, icon, value in items
|
||||
)
|
||||
identifiers = tuple(enum_item.identifier for enum_item in enum_items)
|
||||
values = tuple(enum_item.value for enum_item in enum_items)
|
||||
return identifiers, values, enum_items
|
||||
|
||||
|
||||
class PropertyGroupEnum:
|
||||
def __init__(self, enum_items: tuple[tuple[str, str, str, str, int], ...]) -> None:
|
||||
self.enum_items = enum_items
|
||||
|
||||
def items(self) -> tuple[tuple[str, str, str, str, int], ...]:
|
||||
return self.enum_items
|
||||
|
||||
def value_to_identifier(self, value: int, default: str) -> str:
|
||||
return next(
|
||||
(enum.identifier for enum in self if enum.value == value),
|
||||
default,
|
||||
)
|
||||
|
||||
def identifier_to_value(self, identifier: str, default: int) -> int:
|
||||
return next(
|
||||
(enum.value for enum in self if enum.identifier == identifier),
|
||||
default,
|
||||
)
|
||||
|
||||
def identifiers(self) -> tuple[str, ...]:
|
||||
return tuple(enum.identifier for enum in self)
|
||||
|
||||
def values(self) -> tuple[int, ...]:
|
||||
return tuple(enum.value for enum in self)
|
||||
|
||||
def __iter__(self) -> Iterator[PropertyGroupEnumItem]:
|
||||
return (
|
||||
PropertyGroupEnumItem(
|
||||
identifier=identifier,
|
||||
name=name,
|
||||
description=description,
|
||||
icon=icon,
|
||||
value=value,
|
||||
)
|
||||
for identifier, name, description, icon, value in self.items()
|
||||
)
|
||||
|
||||
|
||||
def property_group_enum(
|
||||
*items: tuple[str, str, str, str, int],
|
||||
) -> tuple[PropertyGroupEnum, Iterator[PropertyGroupEnumItem]]:
|
||||
enum = PropertyGroupEnum(items)
|
||||
return enum, enum.__iter__()
|
||||
|
||||
|
||||
class StringPropertyGroup(PropertyGroup):
|
||||
def get_value(self) -> str:
|
||||
value = self.get("value")
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return str(value)
|
||||
|
||||
def set_value(self, value: str) -> None:
|
||||
self.name = value
|
||||
self["value"] = value
|
||||
|
||||
value: StringProperty( # type: ignore[valid-type]
|
||||
name="String Value",
|
||||
get=get_value,
|
||||
set=set_value,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
value: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
class FloatPropertyGroup(PropertyGroup):
|
||||
def get_value(self) -> float:
|
||||
value = self.get("value")
|
||||
if isinstance(value, (float, int)):
|
||||
return float(value)
|
||||
return 0.0
|
||||
|
||||
def set_value(self, value: float) -> None:
|
||||
self.name = str(value)
|
||||
self["value"] = value
|
||||
|
||||
value: FloatProperty( # type: ignore[valid-type]
|
||||
name="Float Value",
|
||||
get=get_value,
|
||||
set=set_value,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
value: float # type: ignore[no-redef]
|
||||
|
||||
|
||||
class MeshObjectPropertyGroup(PropertyGroup):
|
||||
def get_mesh_object_name(self) -> str:
|
||||
bpy_object = self.bpy_object
|
||||
if not bpy_object or not bpy_object.name or bpy_object.type != "MESH":
|
||||
return ""
|
||||
return str(bpy_object.name)
|
||||
|
||||
def set_mesh_object_name(self, value: object) -> None:
|
||||
context = bpy.context
|
||||
|
||||
if (
|
||||
not isinstance(value, str)
|
||||
or not (obj := context.blend_data.objects.get(value))
|
||||
or obj.type != "MESH"
|
||||
):
|
||||
if self.bpy_object:
|
||||
self.bpy_object = None
|
||||
return
|
||||
|
||||
if self.bpy_object != obj:
|
||||
self.bpy_object = obj
|
||||
|
||||
mesh_object_name: StringProperty( # type: ignore[valid-type]
|
||||
get=get_mesh_object_name, set=set_mesh_object_name
|
||||
)
|
||||
|
||||
saved_mesh_object_name_to_restore: StringProperty() # type: ignore[valid-type]
|
||||
|
||||
def get_value(self) -> str:
|
||||
message = (
|
||||
"`MeshObjectPropertyGroup.value` is deprecated and will be removed in the"
|
||||
" next major release. Please use `MeshObjectPropertyGroup.mesh_object_name`"
|
||||
" instead."
|
||||
)
|
||||
logger.warning(message)
|
||||
warnings.warn(message, DeprecationWarning, stacklevel=5)
|
||||
return str(self.mesh_object_name)
|
||||
|
||||
def set_value(self, value: str) -> None:
|
||||
message = (
|
||||
"`MeshObjectPropertyGroup.value` is deprecated and will be removed in the"
|
||||
" next major release. Please use `MeshObjectPropertyGroup.mesh_object_name`"
|
||||
" instead."
|
||||
)
|
||||
logger.warning(message)
|
||||
warnings.warn(message, DeprecationWarning, stacklevel=5)
|
||||
self.mesh_object_name = value
|
||||
|
||||
value: StringProperty( # type: ignore[valid-type]
|
||||
get=get_value,
|
||||
set=set_value,
|
||||
)
|
||||
"""`value` is deprecated and will be removed in the next
|
||||
major release. Please use `mesh_object_name` instead.
|
||||
"""
|
||||
|
||||
def poll_bpy_object(self, obj: object) -> bool:
|
||||
return isinstance(obj, Object) and obj.type == "MESH"
|
||||
|
||||
def update_bpy_object(self, _context: Context) -> None:
|
||||
bpy_object = self.bpy_object
|
||||
self.saved_mesh_object_name_to_restore = bpy_object.name if bpy_object else ""
|
||||
|
||||
bpy_object: PointerProperty( # type: ignore[valid-type]
|
||||
type=Object,
|
||||
poll=poll_bpy_object,
|
||||
update=update_bpy_object,
|
||||
)
|
||||
|
||||
def restore_object_assignment(self, context: Context) -> None:
|
||||
if self.bpy_object:
|
||||
return
|
||||
obj = context.blend_data.objects.get(self.saved_mesh_object_name_to_restore)
|
||||
if not obj:
|
||||
return
|
||||
self.set_mesh_object_name(obj.name)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
mesh_object_name: str # type: ignore[no-redef]
|
||||
saved_mesh_object_name_to_restore: str # type: ignore[no-redef]
|
||||
value: str # type: ignore[no-redef]
|
||||
bpy_object: Optional[Object] # type: ignore[no-redef]
|
||||
|
||||
|
||||
class MaterialPropertyGroup(PropertyGroup):
|
||||
material: PointerProperty( # type: ignore[valid-type]
|
||||
type=Material,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
material: Optional[Material] # type: ignore[no-redef]
|
||||
|
||||
|
||||
class BonePropertyGroupType(Enum):
|
||||
VRM0_FIRST_PERSON = 1
|
||||
VRM0_HUMAN = 2
|
||||
VRM0_COLLIDER_GROUP = 3
|
||||
VRM0_BONE_GROUP_CENTER = 4
|
||||
VRM0_BONE_GROUP = 5
|
||||
VRM1_HUMAN = 6
|
||||
SPRING_BONE1_COLLIDER = 7
|
||||
SPRING_BONE1_SPRING_CENTER = 8
|
||||
SPRING_BONE1_SPRING_JOINT = 9
|
||||
|
||||
@staticmethod
|
||||
def is_vrm0(bone_property_group_type: "BonePropertyGroupType") -> bool:
|
||||
return bone_property_group_type in {
|
||||
BonePropertyGroupType.VRM0_FIRST_PERSON,
|
||||
BonePropertyGroupType.VRM0_HUMAN,
|
||||
BonePropertyGroupType.VRM0_COLLIDER_GROUP,
|
||||
BonePropertyGroupType.VRM0_BONE_GROUP_CENTER,
|
||||
BonePropertyGroupType.VRM0_BONE_GROUP,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def is_vrm1(bone_property_group_type: "BonePropertyGroupType") -> bool:
|
||||
return bone_property_group_type in {
|
||||
BonePropertyGroupType.VRM1_HUMAN,
|
||||
BonePropertyGroupType.SPRING_BONE1_COLLIDER,
|
||||
BonePropertyGroupType.SPRING_BONE1_SPRING_CENTER,
|
||||
BonePropertyGroupType.SPRING_BONE1_SPRING_JOINT,
|
||||
}
|
||||
|
||||
|
||||
class BonePropertyGroup(PropertyGroup):
|
||||
@staticmethod
|
||||
def get_all_bone_property_groups(
|
||||
armature: Union[Object, Armature],
|
||||
) -> Iterator[tuple["BonePropertyGroup", BonePropertyGroupType]]:
|
||||
"""Return (bone: BonePropertyGroup, is_vrm0: bool, is_vrm1: bool)."""
|
||||
from .extension import get_armature_extension
|
||||
|
||||
if isinstance(armature, Object):
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
else:
|
||||
armature_data = armature
|
||||
|
||||
ext = get_armature_extension(armature_data)
|
||||
yield (
|
||||
ext.vrm0.first_person.first_person_bone,
|
||||
BonePropertyGroupType.VRM0_FIRST_PERSON,
|
||||
)
|
||||
for human_bone in ext.vrm0.humanoid.human_bones:
|
||||
yield human_bone.node, BonePropertyGroupType.VRM0_HUMAN
|
||||
for collider_group in ext.vrm0.secondary_animation.collider_groups:
|
||||
yield collider_group.node, BonePropertyGroupType.VRM0_COLLIDER_GROUP
|
||||
for bone_group in ext.vrm0.secondary_animation.bone_groups:
|
||||
yield bone_group.center, BonePropertyGroupType.VRM0_BONE_GROUP_CENTER
|
||||
yield from (
|
||||
(bone, BonePropertyGroupType.VRM0_BONE_GROUP)
|
||||
for bone in bone_group.bones
|
||||
)
|
||||
for (
|
||||
human_bone
|
||||
) in ext.vrm1.humanoid.human_bones.human_bone_name_to_human_bone().values():
|
||||
yield human_bone.node, BonePropertyGroupType.VRM1_HUMAN
|
||||
for collider in ext.spring_bone1.colliders:
|
||||
yield collider.node, BonePropertyGroupType.SPRING_BONE1_COLLIDER
|
||||
for spring in ext.spring_bone1.springs:
|
||||
yield spring.center, BonePropertyGroupType.SPRING_BONE1_SPRING_CENTER
|
||||
for joint in spring.joints:
|
||||
yield joint.node, BonePropertyGroupType.SPRING_BONE1_SPRING_JOINT
|
||||
|
||||
@staticmethod
|
||||
def find_bone_candidates(
|
||||
armature_data: Armature,
|
||||
target: HumanBoneSpecification,
|
||||
bpy_bone_name_to_human_bone_specification: Mapping[str, HumanBoneSpecification],
|
||||
error_bpy_bone_names: Sequence[str],
|
||||
diagnostics_layout: Optional[UILayout] = None,
|
||||
) -> set[str]:
|
||||
bones = armature_data.bones
|
||||
human_bone_name_to_bpy_bone_name = {
|
||||
value.name: key
|
||||
for key, value in bpy_bone_name_to_human_bone_specification.items()
|
||||
}
|
||||
|
||||
search_conditions: list[str] = []
|
||||
|
||||
# If the target's ancestor has a Human Bone assignment, use that as the
|
||||
# search start bone
|
||||
searching_bones: Optional[list[Bone]] = None
|
||||
ancestor: Optional[HumanBoneSpecification] = target.parent()
|
||||
while ancestor:
|
||||
ancestor_bpy_bone_name = human_bone_name_to_bpy_bone_name.get(ancestor.name)
|
||||
if not ancestor_bpy_bone_name or not (
|
||||
ancestor_bpy_bone := bones.get(ancestor_bpy_bone_name)
|
||||
):
|
||||
# If there's no Human Bone assignment, traverse to the parent
|
||||
ancestor = ancestor.parent()
|
||||
continue
|
||||
|
||||
if ancestor_bpy_bone.name in error_bpy_bone_names:
|
||||
# If there's an error in the Human Bone assignment, return empty result
|
||||
if diagnostics_layout:
|
||||
diagnostics_message = pgettext(
|
||||
'The bone assigned to the VRM Human Bone "{human_bone}" must'
|
||||
+ " be descendants of the bones assigned \nto the VRM human"
|
||||
+ ' bone "{parent_human_bone}". However, it cannot retrieve'
|
||||
+ " bone candidates because there\nis an error in the"
|
||||
+ ' assignment of the VRM Human Bone "{parent_human_bone}".'
|
||||
+ " Please resolve the error in the\nassignment of the VRM"
|
||||
+ ' Human Bone "{parent_human_bone}" first.'
|
||||
).format(
|
||||
human_bone=target.title,
|
||||
parent_human_bone=ancestor.title,
|
||||
)
|
||||
diagnostics_column = diagnostics_layout.column(align=True)
|
||||
for i, line in enumerate(diagnostics_message.splitlines()):
|
||||
diagnostics_column.label(
|
||||
text=line,
|
||||
translate=False,
|
||||
icon="ERROR" if i == 0 else "BLANK1",
|
||||
)
|
||||
return set()
|
||||
|
||||
# Use the child bones of the found ancestor bone as the search start bones
|
||||
searching_bones = list(ancestor_bpy_bone.children)
|
||||
if diagnostics_layout:
|
||||
search_conditions.append(
|
||||
pgettext(
|
||||
'Being a descendant of the bone "{bpy_bone}" assigned'
|
||||
+ ' to the VRM Human Bone "{human_bone}"'
|
||||
).format(
|
||||
human_bone=ancestor.title,
|
||||
bpy_bone=ancestor_bpy_bone_name,
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
root_bones: Final[Sequence[Bone]] = [
|
||||
bone for bone in bones.values() if not bone.parent
|
||||
]
|
||||
|
||||
# If no ancestor bone is found, use the root bone as the search start bone
|
||||
if searching_bones is None and len(root_bones) > 1:
|
||||
# If there are multiple root bones,
|
||||
# select the root bone of the already assigned bone
|
||||
human_bone_specification = None
|
||||
for (
|
||||
bpy_bone_name,
|
||||
human_bone_specification,
|
||||
) in bpy_bone_name_to_human_bone_specification.items():
|
||||
if not bpy_bone_name:
|
||||
continue
|
||||
if bpy_bone_name in error_bpy_bone_names:
|
||||
continue
|
||||
bpy_bone = bones.get(bpy_bone_name)
|
||||
if not bpy_bone:
|
||||
continue
|
||||
|
||||
main_root_bone: Optional[Bone] = None
|
||||
parent: Optional[Bone] = bpy_bone
|
||||
while parent:
|
||||
main_root_bone = parent
|
||||
parent = parent.parent
|
||||
if not main_root_bone:
|
||||
continue
|
||||
|
||||
searching_bones = [main_root_bone]
|
||||
if diagnostics_layout:
|
||||
search_conditions.append(
|
||||
pgettext(
|
||||
'Sharing the root bone with the bone "{bpy_bone}" assigned'
|
||||
+ ' to the VRM Human Bone "{human_bone}"'
|
||||
).format(
|
||||
human_bone=human_bone_specification.title,
|
||||
bpy_bone=bpy_bone_name,
|
||||
)
|
||||
)
|
||||
break
|
||||
if searching_bones is None:
|
||||
searching_bones = list(root_bones)
|
||||
|
||||
# Bone candidates
|
||||
bone_candidates: set[Bone] = set(searching_bones)
|
||||
|
||||
# First, register all descendant bones as candidates
|
||||
filling_bones = searching_bones.copy()
|
||||
while filling_bones:
|
||||
filling_bone = filling_bones.pop()
|
||||
bone_candidates.update(filling_bone.children)
|
||||
filling_bones.extend(filling_bone.children)
|
||||
|
||||
removing_bone_tree = set[Bone]()
|
||||
|
||||
# Traverse descendant bones and when we encounter an already assigned bone,
|
||||
# examine its relationship and exclude unnecessary candidates.
|
||||
while searching_bones:
|
||||
searching_bone = searching_bones.pop()
|
||||
human_bone_specification = bpy_bone_name_to_human_bone_specification.get(
|
||||
searching_bone.name
|
||||
)
|
||||
|
||||
# If not assigned to a Human Bone or if it's the target bone,
|
||||
# recursively traverse child bones
|
||||
if (
|
||||
human_bone_specification is None
|
||||
or human_bone_specification == target
|
||||
or searching_bone.name in error_bpy_bone_names
|
||||
):
|
||||
searching_bones.extend(searching_bone.children)
|
||||
continue
|
||||
|
||||
if human_bone_specification.is_ancestor_of(target):
|
||||
# If an ancestor bone has an assignment
|
||||
# This case shouldn't exist since we start from the nearest
|
||||
# ancestor bone
|
||||
continue
|
||||
|
||||
if target.is_ancestor_of(human_bone_specification):
|
||||
# If a descendant bone has an assignment:
|
||||
# - Exclude that bone and its descendants
|
||||
# - Exclude branches when traversing from that bone to the root bone
|
||||
removing_bone_tree.add(searching_bone)
|
||||
|
||||
parent = searching_bone
|
||||
while parent:
|
||||
grand_parent = parent.parent
|
||||
if grand_parent is None:
|
||||
# If parent is a root bone,
|
||||
# exclude other root bones except parent
|
||||
removing_bone_tree.update(
|
||||
root_bone for root_bone in root_bones if root_bone != parent
|
||||
)
|
||||
break
|
||||
# Exclude sibling bones of parent
|
||||
removing_bone_tree.update(
|
||||
parent_sibling
|
||||
for parent_sibling in grand_parent.children
|
||||
if parent_sibling != parent
|
||||
)
|
||||
parent = grand_parent
|
||||
|
||||
if diagnostics_layout:
|
||||
search_conditions.append(
|
||||
pgettext(
|
||||
'Being an ancestor of the bone "{bpy_bone}" assigned'
|
||||
+ ' to the VRM Human Bone "{human_bone}"'
|
||||
).format(
|
||||
human_bone=human_bone_specification.title,
|
||||
bpy_bone=searching_bone.name,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# If a bone that is neither ancestor nor descendant has an assignment:
|
||||
# - Exclude that bone and its descendants
|
||||
# - Exclude that bone and its ancestors
|
||||
removing_bone_tree.add(searching_bone)
|
||||
|
||||
parent = searching_bone
|
||||
while parent:
|
||||
bone_candidates.discard(parent)
|
||||
parent = parent.parent
|
||||
if diagnostics_layout:
|
||||
search_conditions.append(
|
||||
pgettext(
|
||||
'Not being an ancestor of the bone "{bpy_bone}" assigned'
|
||||
+ ' to the VRM Human Bone "{human_bone}"'
|
||||
).format(
|
||||
human_bone=human_bone_specification.title,
|
||||
bpy_bone=searching_bone.name,
|
||||
)
|
||||
)
|
||||
|
||||
bone_candidates.difference_update(removing_bone_tree)
|
||||
while removing_bone_tree:
|
||||
removing_bone = removing_bone_tree.pop()
|
||||
bone_candidates.difference_update(removing_bone.children)
|
||||
removing_bone_tree.update(removing_bone.children)
|
||||
|
||||
if diagnostics_layout and search_conditions:
|
||||
diagnostics_layout.label(
|
||||
text=pgettext(
|
||||
"Bones that meet all of the following conditions will be "
|
||||
+ "candidates for assignment:"
|
||||
),
|
||||
translate=False,
|
||||
icon="INFO",
|
||||
)
|
||||
for search_condition in sorted(search_conditions):
|
||||
diagnostics_layout.label(
|
||||
text=search_condition,
|
||||
translate=False,
|
||||
icon="DOT",
|
||||
)
|
||||
|
||||
return {bone_cancidate.name for bone_cancidate in bone_candidates}
|
||||
|
||||
armature_data_name_and_bone_uuid_to_bone_name_cache: ClassVar[
|
||||
dict[tuple[str, str], str]
|
||||
] = {}
|
||||
|
||||
def get_bone_name(self) -> str:
|
||||
from .extension import get_bone_extension
|
||||
|
||||
if not self.bone_uuid:
|
||||
return ""
|
||||
|
||||
armature_data = self.find_armature()
|
||||
|
||||
# Use cache to speed up this function since profiling showed it was slow
|
||||
cache_key = (armature_data.name, self.bone_uuid)
|
||||
cached_bone_name = self.armature_data_name_and_bone_uuid_to_bone_name_cache.get(
|
||||
cache_key
|
||||
)
|
||||
if cached_bone_name is not None:
|
||||
if (
|
||||
cached_bone := armature_data.bones.get(cached_bone_name)
|
||||
) and get_bone_extension(cached_bone).uuid == self.bone_uuid:
|
||||
return cached_bone_name
|
||||
# If there's even one old value in the cache, clear everything for safety
|
||||
self.armature_data_name_and_bone_uuid_to_bone_name_cache.clear()
|
||||
|
||||
for bone in armature_data.bones:
|
||||
if get_bone_extension(bone).uuid == self.bone_uuid:
|
||||
self.armature_data_name_and_bone_uuid_to_bone_name_cache[cache_key] = (
|
||||
bone.name
|
||||
)
|
||||
return bone.name
|
||||
|
||||
return ""
|
||||
|
||||
def find_armature(self) -> Armature:
|
||||
id_data = self.id_data
|
||||
if isinstance(id_data, Armature):
|
||||
return id_data
|
||||
|
||||
message = (
|
||||
f"{type(self)}/{self}.id_data is not a {Armature}"
|
||||
+ f" but {type(id_data)}/{id_data}"
|
||||
)
|
||||
raise AssertionError(message)
|
||||
|
||||
def get_bone_property_group_type(self) -> BonePropertyGroupType:
|
||||
armature_data = self.find_armature()
|
||||
for (
|
||||
bone_property_group,
|
||||
bone_property_group_type,
|
||||
) in self.get_all_bone_property_groups(armature_data):
|
||||
if bone_property_group == self:
|
||||
return bone_property_group_type
|
||||
|
||||
message = (
|
||||
f"{type(self)}/{self} is not associated with "
|
||||
+ "BonePropertyGroupType.get_all_bone_property_groups"
|
||||
)
|
||||
raise AssertionError(message)
|
||||
|
||||
def set_bone_name(self, value: str) -> None:
|
||||
from .extension import get_armature_extension, get_bone_extension
|
||||
|
||||
context = bpy.context
|
||||
|
||||
armature_data = self.find_armature()
|
||||
armature_objects = [
|
||||
obj for obj in context.blend_data.objects if obj.data == armature_data
|
||||
]
|
||||
bone_property_group_type = self.get_bone_property_group_type()
|
||||
|
||||
# Assign UUIDs and regenerate it if duplication exist
|
||||
found_uuids: set[str] = set()
|
||||
for bone in armature_data.bones:
|
||||
bone_extension = get_bone_extension(bone)
|
||||
found_uuid = bone_extension.uuid
|
||||
if not found_uuid or found_uuid in found_uuids:
|
||||
bone_extension.uuid = uuid.uuid4().hex
|
||||
self.armature_data_name_and_bone_uuid_to_bone_name_cache.clear()
|
||||
found_uuids.add(bone_extension.uuid)
|
||||
|
||||
if not value or not (bone := armature_data.bones.get(value)):
|
||||
if not self.bone_uuid:
|
||||
# Not changed
|
||||
return
|
||||
self.bone_uuid = ""
|
||||
elif self.bone_uuid and self.bone_uuid == get_bone_extension(bone).uuid:
|
||||
# Not changed
|
||||
return
|
||||
else:
|
||||
self.bone_uuid = get_bone_extension(bone).uuid
|
||||
|
||||
ext = get_armature_extension(armature_data)
|
||||
|
||||
if bone_property_group_type in [
|
||||
BonePropertyGroupType.VRM0_COLLIDER_GROUP,
|
||||
BonePropertyGroupType.VRM0_BONE_GROUP,
|
||||
]:
|
||||
for collider_group in ext.vrm0.secondary_animation.collider_groups:
|
||||
for armature_object in armature_objects:
|
||||
collider_group.refresh(armature_object)
|
||||
|
||||
if bone_property_group_type == BonePropertyGroupType.SPRING_BONE1_COLLIDER:
|
||||
for collider in ext.spring_bone1.colliders:
|
||||
for armature_object in armature_objects:
|
||||
collider.reset_bpy_object(context, armature_object)
|
||||
|
||||
if (
|
||||
ext.is_vrm0()
|
||||
and bone_property_group_type == BonePropertyGroupType.VRM0_HUMAN
|
||||
):
|
||||
self.update_all_vrm0_node_candidates(armature_data)
|
||||
if (
|
||||
ext.is_vrm1()
|
||||
and bone_property_group_type == BonePropertyGroupType.VRM1_HUMAN
|
||||
):
|
||||
self.update_all_vrm1_node_candidates(armature_data)
|
||||
|
||||
@staticmethod
|
||||
def update_all_vrm0_node_candidates(armature_data: Armature) -> None:
|
||||
from .extension import get_armature_extension
|
||||
|
||||
ext = get_armature_extension(armature_data)
|
||||
|
||||
bpy_bone_name_to_human_bone_specification: dict[
|
||||
str, vrm0_human_bone.HumanBoneSpecification
|
||||
] = {}
|
||||
for human_bone in ext.vrm0.humanoid.human_bones:
|
||||
if not human_bone.node.bone_name:
|
||||
continue
|
||||
human_bone_name = vrm0_human_bone.HumanBoneName.from_str(human_bone.bone)
|
||||
if human_bone_name is None:
|
||||
continue
|
||||
bpy_bone_name_to_human_bone_specification[human_bone.node.bone_name] = (
|
||||
vrm0_human_bone.HumanBoneSpecifications.get(human_bone_name)
|
||||
)
|
||||
|
||||
# Set from parent to child since child nodes will always have errors
|
||||
# if parent node has errors
|
||||
traversing_human_bone_specifications = [
|
||||
vrm0_human_bone.HumanBoneSpecifications.HIPS
|
||||
]
|
||||
error_bpy_bone_names = []
|
||||
node_candidates_updated = True
|
||||
while traversing_human_bone_specifications:
|
||||
traversing_human_bone_specification = (
|
||||
traversing_human_bone_specifications.pop()
|
||||
)
|
||||
|
||||
if node_candidates_updated:
|
||||
error_bpy_bone_names = [
|
||||
error_human_bone.node.bone_name
|
||||
for error_human_bone in ext.vrm0.humanoid.human_bones
|
||||
if error_human_bone.node.bone_name
|
||||
and error_human_bone.node.bone_name
|
||||
not in error_human_bone.node_candidates
|
||||
]
|
||||
|
||||
human_bone = next(
|
||||
(
|
||||
human_bone
|
||||
for human_bone in ext.vrm0.humanoid.human_bones
|
||||
if vrm0_human_bone.HumanBoneName.from_str(human_bone.bone)
|
||||
== traversing_human_bone_specification.name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if human_bone:
|
||||
node_candidates_updated = human_bone.update_node_candidates(
|
||||
armature_data,
|
||||
bpy_bone_name_to_human_bone_specification,
|
||||
error_bpy_bone_names,
|
||||
)
|
||||
|
||||
traversing_human_bone_specifications.extend(
|
||||
traversing_human_bone_specification.children()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update_all_vrm1_node_candidates(armature_data: Armature) -> None:
|
||||
from .extension import get_armature_extension
|
||||
|
||||
ext = get_armature_extension(armature_data)
|
||||
|
||||
human_bone_name_to_human_bone = (
|
||||
ext.vrm1.humanoid.human_bones.human_bone_name_to_human_bone()
|
||||
)
|
||||
bpy_bone_name_to_human_bone_specification: dict[
|
||||
str, vrm1_human_bone.HumanBoneSpecification
|
||||
] = {}
|
||||
for human_bone_name, human_bone in human_bone_name_to_human_bone.items():
|
||||
if not human_bone.node.bone_name:
|
||||
continue
|
||||
bpy_bone_name_to_human_bone_specification[human_bone.node.bone_name] = (
|
||||
vrm1_human_bone.HumanBoneSpecifications.get(human_bone_name)
|
||||
)
|
||||
|
||||
# Set from parent to child since child nodes will always have errors
|
||||
# if parent node has errors
|
||||
traversing_human_bone_specifications = [
|
||||
vrm1_human_bone.HumanBoneSpecifications.HIPS
|
||||
]
|
||||
error_bpy_bone_names = []
|
||||
node_candidates_updated = True
|
||||
while traversing_human_bone_specifications:
|
||||
traversing_human_bone_specification = (
|
||||
traversing_human_bone_specifications.pop()
|
||||
)
|
||||
|
||||
if node_candidates_updated:
|
||||
error_bpy_bone_names = [
|
||||
error_human_bone.node.bone_name
|
||||
for error_human_bone in human_bone_name_to_human_bone.values()
|
||||
if error_human_bone.node.bone_name
|
||||
and error_human_bone.node.bone_name
|
||||
not in error_human_bone.node_candidates
|
||||
]
|
||||
|
||||
human_bone = human_bone_name_to_human_bone[
|
||||
traversing_human_bone_specification.name
|
||||
]
|
||||
node_candidates_updated = human_bone.update_node_candidates(
|
||||
armature_data,
|
||||
traversing_human_bone_specification,
|
||||
bpy_bone_name_to_human_bone_specification,
|
||||
error_bpy_bone_names,
|
||||
)
|
||||
|
||||
traversing_human_bone_specifications.extend(
|
||||
traversing_human_bone_specification.children()
|
||||
)
|
||||
|
||||
bone_name: StringProperty( # type: ignore[valid-type]
|
||||
name="Bone",
|
||||
get=get_bone_name,
|
||||
set=set_bone_name,
|
||||
)
|
||||
|
||||
def get_value(self) -> str:
|
||||
message = (
|
||||
"`BonePropertyGroup.value` is deprecated and will be removed in the"
|
||||
" next major release. Please use `BonePropertyGroup.bone_name` instead."
|
||||
)
|
||||
logger.warning(message)
|
||||
warnings.warn(message, DeprecationWarning, stacklevel=5)
|
||||
return str(self.bone_name)
|
||||
|
||||
def set_value(self, value: str) -> None:
|
||||
message = (
|
||||
"`BonePropertyGroup.value` is deprecated and will be removed in the"
|
||||
" next major release. Please use `BonePropertyGroup.bone_name` instead."
|
||||
)
|
||||
logger.warning(message)
|
||||
warnings.warn(message, DeprecationWarning, stacklevel=5)
|
||||
self.bone_name = value
|
||||
|
||||
value: StringProperty( # type: ignore[valid-type]
|
||||
name="Bone",
|
||||
get=get_value,
|
||||
set=set_value,
|
||||
)
|
||||
"""`value` is deprecated and will be removed in the next major
|
||||
release. Please use `bone_name` instead."
|
||||
"""
|
||||
|
||||
bone_uuid: StringProperty() # type: ignore[valid-type]
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
bone_name: str # type: ignore[no-redef]
|
||||
value: str # type: ignore[no-redef]
|
||||
bone_uuid: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
T_co = TypeVar("T_co", covariant=True)
|
||||
|
||||
|
||||
# The actual type does not take type arguments.
|
||||
class CollectionPropertyProtocol(Protocol[T_co]):
|
||||
def add(self) -> T_co: ... # TODO: undocumented
|
||||
|
||||
def __len__(self) -> int: ... # TODO: undocumented
|
||||
|
||||
def __iter__(self) -> Iterator[T_co]: ... # TODO: undocumented
|
||||
|
||||
def clear(self) -> None: ... # TODO: undocumented
|
||||
|
||||
@overload
|
||||
def __getitem__(self, index: int) -> T_co: ... # TODO: undocumented
|
||||
|
||||
@overload
|
||||
def __getitem__(
|
||||
self, index: "slice[Optional[int], Optional[int], Optional[int]]"
|
||||
) -> tuple[T_co, ...]: ... # TODO: undocumented
|
||||
|
||||
def remove(self, index: int) -> None: ... # TODO: undocumented
|
||||
|
||||
def values(self) -> ValuesView[T_co]: ... # TODO: undocumented
|
||||
|
||||
def __contains__(self, value: str) -> bool: ... # TODO: undocumented
|
||||
|
||||
def move(self, from_index: int, to_index: int) -> None: ... # TODO: undocumented
|
||||
721
editor/search.py
Normal file
721
editor/search.py
Normal file
@@ -0,0 +1,721 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Final, Optional, Union
|
||||
|
||||
import bpy
|
||||
from bpy.app.translations import pgettext
|
||||
from bpy.types import (
|
||||
Armature,
|
||||
Collection,
|
||||
Constraint,
|
||||
Context,
|
||||
CopyRotationConstraint,
|
||||
Curve,
|
||||
DampedTrackConstraint,
|
||||
Material,
|
||||
Mesh,
|
||||
Object,
|
||||
ObjectConstraints,
|
||||
PoseBoneConstraints,
|
||||
ShaderNodeGroup,
|
||||
ShaderNodeOutputMaterial,
|
||||
)
|
||||
|
||||
from ..common.logger import get_logger
|
||||
from .extension import get_armature_extension
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
MESH_CONVERTIBLE_OBJECT_TYPES = [
|
||||
"CURVE",
|
||||
"FONT",
|
||||
"MESH",
|
||||
# "META", # Disable until the glTF 2.0 add-on supports it
|
||||
"SURFACE",
|
||||
]
|
||||
|
||||
|
||||
def export_materials(context: Context, objects: Sequence[Object]) -> Sequence[Material]:
|
||||
result: list[Material] = []
|
||||
for obj in objects:
|
||||
if obj.type not in MESH_CONVERTIBLE_OBJECT_TYPES:
|
||||
continue
|
||||
|
||||
mesh_convertible = obj.data
|
||||
if isinstance(mesh_convertible, (Curve, Mesh)):
|
||||
for material_ref in mesh_convertible.materials:
|
||||
if not isinstance(material_ref, Material):
|
||||
continue
|
||||
material = context.blend_data.materials.get(material_ref.name)
|
||||
if not isinstance(material, Material):
|
||||
continue
|
||||
result.append(material)
|
||||
else:
|
||||
logger.error(
|
||||
"Unexpected mesh convertible object type: %s", type(mesh_convertible)
|
||||
)
|
||||
|
||||
for material_slot in obj.material_slots:
|
||||
if not material_slot:
|
||||
continue
|
||||
material_ref = material_slot.material
|
||||
if not material_ref:
|
||||
continue
|
||||
material = context.blend_data.materials.get(material_ref.name)
|
||||
if not isinstance(material, Material):
|
||||
continue
|
||||
result.append(material)
|
||||
|
||||
return list(dict.fromkeys(result)) # Remove duplicates
|
||||
|
||||
|
||||
LEGACY_SHADER_NAMES: Final = (
|
||||
"MToon_unversioned",
|
||||
"GLTF",
|
||||
"TRANSPARENT_ZWRITE",
|
||||
)
|
||||
|
||||
|
||||
def legacy_shader_node(
|
||||
material: Material,
|
||||
) -> tuple[Optional[ShaderNodeGroup], Optional[str]]:
|
||||
if bpy.app.version < (5, 0, 0) and not material.use_nodes:
|
||||
return (None, None)
|
||||
|
||||
node_tree = material.node_tree
|
||||
if not node_tree:
|
||||
return (None, None)
|
||||
|
||||
for node in node_tree.nodes:
|
||||
if node.mute:
|
||||
continue
|
||||
|
||||
if not isinstance(node, ShaderNodeOutputMaterial):
|
||||
continue
|
||||
|
||||
surface_socket = node.inputs.get("Surface")
|
||||
if not surface_socket:
|
||||
continue
|
||||
|
||||
links = surface_socket.links
|
||||
if not links:
|
||||
continue
|
||||
|
||||
link = links[0]
|
||||
if not link.is_valid or link.is_muted:
|
||||
continue
|
||||
|
||||
group_node = link.from_node
|
||||
if group_node.mute:
|
||||
continue
|
||||
|
||||
if not isinstance(group_node, ShaderNodeGroup):
|
||||
continue
|
||||
|
||||
group_node_tree = group_node.node_tree
|
||||
if not group_node_tree:
|
||||
continue
|
||||
|
||||
legacy_shader_name = group_node_tree.get("SHADER")
|
||||
if not isinstance(legacy_shader_name, str):
|
||||
continue
|
||||
|
||||
return (group_node, legacy_shader_name)
|
||||
|
||||
return (None, None)
|
||||
|
||||
|
||||
def shader_nodes_and_materials(
|
||||
materials: Sequence[Material],
|
||||
) -> Sequence[tuple[ShaderNodeGroup, str, Material]]:
|
||||
result: list[tuple[ShaderNodeGroup, str, Material]] = []
|
||||
for material in materials:
|
||||
node, legacy_shader_name = legacy_shader_node(material)
|
||||
if node is not None and legacy_shader_name is not None:
|
||||
result.append((node, legacy_shader_name, material))
|
||||
return result
|
||||
|
||||
|
||||
def object_distance(
|
||||
left: Object,
|
||||
right: Object,
|
||||
collection_child_to_parent: dict[Collection, Optional[Collection]],
|
||||
) -> tuple[int, int, int, int]:
|
||||
left_collection_path: list[Collection] = []
|
||||
left_collections = [
|
||||
collection
|
||||
for collection in left.users_collection
|
||||
if collection in collection_child_to_parent
|
||||
]
|
||||
if left_collections:
|
||||
left_collection: Optional[Collection] = left_collections[0]
|
||||
while left_collection:
|
||||
left_collection_path.insert(0, left_collection)
|
||||
left_collection = collection_child_to_parent.get(left_collection)
|
||||
|
||||
right_collection_path: list[Collection] = []
|
||||
right_collections = [
|
||||
collection
|
||||
for collection in right.users_collection
|
||||
if collection in collection_child_to_parent
|
||||
]
|
||||
if right_collections:
|
||||
right_collection: Optional[Collection] = right_collections[0]
|
||||
while right_collection:
|
||||
right_collection_path.insert(0, right_collection)
|
||||
right_collection = collection_child_to_parent.get(right_collection)
|
||||
|
||||
while (
|
||||
left_collection_path
|
||||
and right_collection_path
|
||||
and left_collection_path[0] == right_collection_path[0]
|
||||
):
|
||||
left_collection_path.pop(0)
|
||||
right_collection_path.pop(0)
|
||||
|
||||
left_parent_path: list[Object] = []
|
||||
traversing_left: Optional[Object] = left
|
||||
while traversing_left:
|
||||
left_parent_path.insert(0, traversing_left)
|
||||
traversing_left = traversing_left.parent
|
||||
|
||||
right_parent_path: list[Object] = []
|
||||
traversing_right: Optional[Object] = right
|
||||
while traversing_right:
|
||||
right_parent_path.insert(0, traversing_right)
|
||||
traversing_right = traversing_right.parent
|
||||
|
||||
while (
|
||||
left_parent_path
|
||||
and right_parent_path
|
||||
and left_parent_path[0] == right_parent_path[0]
|
||||
):
|
||||
left_parent_path.pop(0)
|
||||
right_parent_path.pop(0)
|
||||
|
||||
return (
|
||||
len(left_parent_path),
|
||||
len(right_parent_path),
|
||||
len(left_collection_path),
|
||||
len(right_collection_path),
|
||||
)
|
||||
|
||||
|
||||
def armature_exists(context: Context) -> bool:
|
||||
return any(armature.users > 0 for armature in context.blend_data.armatures) and any(
|
||||
obj.type == "ARMATURE" for obj in context.blend_data.objects
|
||||
)
|
||||
|
||||
|
||||
def current_armature_is_vrm0(context: Context) -> bool:
|
||||
live_armature_datum = [
|
||||
armature_data
|
||||
for armature_data in context.blend_data.armatures
|
||||
if armature_data.users
|
||||
]
|
||||
if not live_armature_datum:
|
||||
return False
|
||||
if all(
|
||||
get_armature_extension(armature_data).is_vrm0()
|
||||
for armature_data in live_armature_datum
|
||||
) and any(obj.type == "ARMATURE" for obj in context.blend_data.objects):
|
||||
return True
|
||||
armature = current_armature(context)
|
||||
if armature is None:
|
||||
return False
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return False
|
||||
return get_armature_extension(armature_data).is_vrm0()
|
||||
|
||||
|
||||
def current_armature_is_vrm1(context: Context) -> bool:
|
||||
live_armature_datum = [
|
||||
armature_data
|
||||
for armature_data in context.blend_data.armatures
|
||||
if armature_data.users
|
||||
]
|
||||
if not live_armature_datum:
|
||||
return False
|
||||
if all(
|
||||
get_armature_extension(armature_data).is_vrm1()
|
||||
for armature_data in live_armature_datum
|
||||
) and any(obj.type == "ARMATURE" for obj in context.blend_data.objects):
|
||||
return True
|
||||
armature = current_armature(context)
|
||||
if armature is None:
|
||||
return False
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return False
|
||||
return get_armature_extension(armature_data).is_vrm1()
|
||||
|
||||
|
||||
def multiple_armatures_exist(context: Context) -> bool:
|
||||
first_data_exists = False
|
||||
for data in context.blend_data.armatures:
|
||||
if not data.users:
|
||||
continue
|
||||
if not first_data_exists:
|
||||
first_data_exists = True
|
||||
continue
|
||||
|
||||
first_obj_exists = False
|
||||
for obj in context.blend_data.objects:
|
||||
if obj.type == "ARMATURE":
|
||||
if first_obj_exists:
|
||||
return True
|
||||
first_obj_exists = True
|
||||
break
|
||||
return False
|
||||
|
||||
|
||||
def current_armature(context: Context) -> Optional[Object]:
|
||||
objects = [obj for obj in context.blend_data.objects if obj.type == "ARMATURE"]
|
||||
if not objects:
|
||||
return None
|
||||
|
||||
if len(objects) == 1:
|
||||
return objects[0]
|
||||
|
||||
active_object = getattr(context, "active_object", None)
|
||||
if not isinstance(active_object, Object):
|
||||
active_object = context.view_layer.objects.active
|
||||
if not active_object:
|
||||
return objects[0]
|
||||
|
||||
collection_child_to_parent: dict[Collection, Optional[Collection]] = {
|
||||
context.scene.collection: None
|
||||
}
|
||||
|
||||
collections = [context.scene.collection]
|
||||
while collections:
|
||||
parent = collections.pop()
|
||||
for child in parent.children:
|
||||
collections.append(child)
|
||||
collection_child_to_parent[child] = parent
|
||||
|
||||
min_distance: Optional[tuple[int, int, int, int]] = None
|
||||
nearest_object: Optional[Object] = None
|
||||
for obj in objects:
|
||||
distance = object_distance(active_object, obj, collection_child_to_parent)
|
||||
if min_distance is None or min_distance > distance:
|
||||
min_distance = distance
|
||||
nearest_object = obj
|
||||
|
||||
return objects[0] if nearest_object is None else nearest_object
|
||||
|
||||
|
||||
def export_objects(
|
||||
context: Context,
|
||||
armature_object_name: Optional[str],
|
||||
*,
|
||||
export_invisibles: bool,
|
||||
export_only_selections: bool,
|
||||
export_lights: bool,
|
||||
) -> list[Object]:
|
||||
selected_objects = []
|
||||
if export_only_selections:
|
||||
selected_objects = list(context.selected_objects)
|
||||
elif export_invisibles:
|
||||
selected_objects = list(context.blend_data.objects)
|
||||
else:
|
||||
selected_objects = list(context.selectable_objects)
|
||||
|
||||
objects: list[Object] = []
|
||||
|
||||
# https://projects.blender.org/blender/blender/issues/113378
|
||||
context.view_layer.update()
|
||||
|
||||
armature_object = None
|
||||
if armature_object_name:
|
||||
armature_object = context.blend_data.objects.get(armature_object_name)
|
||||
if armature_object and armature_object.name in context.view_layer.objects:
|
||||
objects.append(armature_object)
|
||||
else:
|
||||
armature_object = None
|
||||
if not armature_object:
|
||||
objects.extend(
|
||||
obj
|
||||
for obj in context.blend_data.objects
|
||||
if obj.type == "ARMATURE" and obj.name in context.view_layer.objects
|
||||
)
|
||||
|
||||
collider_bpy_objects: list[Optional[Object]] = []
|
||||
for armature_data in context.blend_data.armatures:
|
||||
ext = get_armature_extension(armature_data)
|
||||
for spring_bone1_collider in ext.spring_bone1.colliders:
|
||||
spring_bone1_collider_bpy_object = spring_bone1_collider.bpy_object
|
||||
if not spring_bone1_collider_bpy_object:
|
||||
continue
|
||||
collider_bpy_objects.append(spring_bone1_collider_bpy_object)
|
||||
collider_bpy_objects.extend(spring_bone1_collider_bpy_object.children)
|
||||
for collider_group in ext.vrm0.secondary_animation.collider_groups:
|
||||
collider_bpy_objects.extend(
|
||||
collider.bpy_object for collider in collider_group.colliders
|
||||
)
|
||||
|
||||
for obj in selected_objects:
|
||||
if obj.type in ["ARMATURE", "CAMERA"]:
|
||||
continue
|
||||
if obj.type == "LIGHT" and not export_lights:
|
||||
continue
|
||||
if obj.name not in context.view_layer.objects:
|
||||
continue
|
||||
if not export_invisibles and not obj.visible_get():
|
||||
continue
|
||||
objects.append(obj)
|
||||
|
||||
return [
|
||||
# Remove colliders and duplicates
|
||||
obj
|
||||
for obj in dict.fromkeys(objects)
|
||||
if obj not in collider_bpy_objects
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExportConstraint:
|
||||
roll_constraints: dict[str, CopyRotationConstraint]
|
||||
aim_constraints: dict[str, DampedTrackConstraint]
|
||||
rotation_constraints: dict[str, CopyRotationConstraint]
|
||||
|
||||
@property
|
||||
def all_constraints(self) -> list[tuple[str, Constraint]]:
|
||||
all_constraints: list[tuple[str, Constraint]] = []
|
||||
all_constraints.extend(self.roll_constraints.items())
|
||||
all_constraints.extend(self.aim_constraints.items())
|
||||
all_constraints.extend(self.rotation_constraints.items())
|
||||
return all_constraints
|
||||
|
||||
|
||||
def roll_constraint_or_none(
|
||||
constraint: Constraint,
|
||||
objs: Sequence[Object],
|
||||
armature: Object,
|
||||
) -> Optional[CopyRotationConstraint]:
|
||||
if (
|
||||
not isinstance(constraint, CopyRotationConstraint)
|
||||
or not constraint.is_valid
|
||||
or constraint.mute
|
||||
or not (constraint_target := constraint.target)
|
||||
or constraint_target not in objs
|
||||
or constraint.mix_mode != "ADD"
|
||||
or (int(constraint.use_x) + int(constraint.use_y) + int(constraint.use_z)) != 1
|
||||
or constraint.owner_space != "LOCAL"
|
||||
or constraint.target_space != "LOCAL"
|
||||
):
|
||||
return None
|
||||
|
||||
if constraint_target.type != "ARMATURE":
|
||||
return constraint
|
||||
|
||||
if constraint_target != armature:
|
||||
return None
|
||||
|
||||
armature_data = armature.data
|
||||
if (
|
||||
not isinstance(armature_data, Armature)
|
||||
or constraint.subtarget not in armature_data.bones
|
||||
):
|
||||
return None
|
||||
|
||||
return constraint
|
||||
|
||||
|
||||
def aim_constraint_or_none(
|
||||
constraint: Constraint,
|
||||
objs: Sequence[Object],
|
||||
armature: Object,
|
||||
) -> Optional[DampedTrackConstraint]:
|
||||
if (
|
||||
not isinstance(constraint, DampedTrackConstraint)
|
||||
or not constraint.is_valid
|
||||
or constraint.mute
|
||||
or not (constraint_target := constraint.target)
|
||||
or constraint_target not in objs
|
||||
):
|
||||
return None
|
||||
|
||||
if constraint_target.type != "ARMATURE":
|
||||
return constraint
|
||||
|
||||
if constraint_target != armature:
|
||||
return None
|
||||
|
||||
armature_data = armature.data
|
||||
if (
|
||||
not isinstance(armature_data, Armature)
|
||||
or constraint.subtarget not in armature_data.bones
|
||||
or abs(constraint.head_tail) > 0
|
||||
):
|
||||
return None
|
||||
|
||||
return constraint
|
||||
|
||||
|
||||
def rotation_constraint_or_none(
|
||||
constraint: Constraint,
|
||||
objs: Sequence[Object],
|
||||
armature: Object,
|
||||
) -> Optional[CopyRotationConstraint]:
|
||||
if (
|
||||
not isinstance(constraint, CopyRotationConstraint)
|
||||
or not constraint.is_valid
|
||||
or constraint.mute
|
||||
or not (constraint_target := constraint.target)
|
||||
or constraint.target not in objs
|
||||
or constraint.invert_x
|
||||
or constraint.invert_y
|
||||
or constraint.invert_z
|
||||
or constraint.mix_mode != "ADD"
|
||||
or not constraint.use_x
|
||||
or not constraint.use_y
|
||||
or not constraint.use_z
|
||||
or constraint.owner_space != "LOCAL"
|
||||
or constraint.target_space != "LOCAL"
|
||||
):
|
||||
return None
|
||||
|
||||
if constraint_target.type != "ARMATURE":
|
||||
return constraint
|
||||
|
||||
if constraint_target != armature:
|
||||
return None
|
||||
|
||||
armature_data = armature.data
|
||||
if (
|
||||
not isinstance(armature_data, Armature)
|
||||
or constraint.subtarget not in armature_data.bones
|
||||
):
|
||||
return None
|
||||
|
||||
return constraint
|
||||
|
||||
|
||||
def export_object_constraints(
|
||||
objs: Sequence[Object],
|
||||
armature: Object,
|
||||
) -> ExportConstraint:
|
||||
roll_constraints: dict[str, CopyRotationConstraint] = {}
|
||||
aim_constraints: dict[str, DampedTrackConstraint] = {}
|
||||
rotation_constraints: dict[str, CopyRotationConstraint] = {}
|
||||
|
||||
for obj in objs:
|
||||
for constraint in obj.constraints:
|
||||
roll_constraint = roll_constraint_or_none(constraint, objs, armature)
|
||||
if roll_constraint:
|
||||
roll_constraints[obj.name] = roll_constraint
|
||||
break
|
||||
aim_constraint = aim_constraint_or_none(constraint, objs, armature)
|
||||
if aim_constraint:
|
||||
aim_constraints[obj.name] = aim_constraint
|
||||
break
|
||||
rotation_constraint = rotation_constraint_or_none(
|
||||
constraint, objs, armature
|
||||
)
|
||||
if rotation_constraint:
|
||||
rotation_constraints[obj.name] = rotation_constraint
|
||||
break
|
||||
|
||||
return ExportConstraint(
|
||||
roll_constraints=roll_constraints,
|
||||
aim_constraints=aim_constraints,
|
||||
rotation_constraints=rotation_constraints,
|
||||
)
|
||||
|
||||
|
||||
def export_bone_constraints(
|
||||
objs: Sequence[Object],
|
||||
armature: Object,
|
||||
) -> ExportConstraint:
|
||||
roll_constraints: dict[str, CopyRotationConstraint] = {}
|
||||
aim_constraints: dict[str, DampedTrackConstraint] = {}
|
||||
rotation_constraints: dict[str, CopyRotationConstraint] = {}
|
||||
|
||||
for bone in armature.pose.bones:
|
||||
for constraint in bone.constraints:
|
||||
roll_constraint = roll_constraint_or_none(constraint, objs, armature)
|
||||
if roll_constraint:
|
||||
roll_constraints[bone.name] = roll_constraint
|
||||
break
|
||||
aim_constraint = aim_constraint_or_none(constraint, objs, armature)
|
||||
if aim_constraint:
|
||||
aim_constraints[bone.name] = aim_constraint
|
||||
break
|
||||
rotation_constraint = rotation_constraint_or_none(
|
||||
constraint, objs, armature
|
||||
)
|
||||
if rotation_constraint:
|
||||
rotation_constraints[bone.name] = rotation_constraint
|
||||
break
|
||||
|
||||
return ExportConstraint(
|
||||
roll_constraints=roll_constraints,
|
||||
aim_constraints=aim_constraints,
|
||||
rotation_constraints=rotation_constraints,
|
||||
)
|
||||
|
||||
|
||||
def export_constraints(
|
||||
objs: Sequence[Object],
|
||||
armature: Object,
|
||||
) -> tuple[ExportConstraint, ExportConstraint, Sequence[str]]:
|
||||
messages: list[str] = []
|
||||
object_constraints = export_object_constraints(objs, armature)
|
||||
bone_constraints = export_bone_constraints(objs, armature)
|
||||
|
||||
all_roll_constraints: list[CopyRotationConstraint] = list(
|
||||
object_constraints.roll_constraints.values()
|
||||
) + list(bone_constraints.roll_constraints.values())
|
||||
all_rotation_constraints: list[CopyRotationConstraint] = list(
|
||||
object_constraints.rotation_constraints.values()
|
||||
) + list(bone_constraints.rotation_constraints.values())
|
||||
|
||||
# TODO: Aim Constraint's circular dependency detection
|
||||
# + list(object_constraints.aim_constraints.values())
|
||||
# + list(bone_constraints.aim_constraints.values())
|
||||
all_constraints: list[Constraint] = []
|
||||
all_constraints.extend(all_roll_constraints)
|
||||
all_constraints.extend(all_rotation_constraints)
|
||||
|
||||
excluded_constraints: list[
|
||||
Union[
|
||||
CopyRotationConstraint,
|
||||
DampedTrackConstraint,
|
||||
]
|
||||
] = []
|
||||
|
||||
for search_constraint in all_constraints:
|
||||
if not isinstance(search_constraint, CopyRotationConstraint):
|
||||
continue
|
||||
current_constraints = [
|
||||
(
|
||||
search_constraint,
|
||||
(
|
||||
search_constraint.use_x,
|
||||
search_constraint.use_y,
|
||||
search_constraint.use_z,
|
||||
),
|
||||
)
|
||||
]
|
||||
iterated_constraints: set[Constraint] = set()
|
||||
while current_constraints:
|
||||
current_constraint, current_axis = current_constraints.pop()
|
||||
|
||||
if current_constraint in iterated_constraints:
|
||||
break
|
||||
iterated_constraints.add(current_constraint)
|
||||
|
||||
found = False
|
||||
|
||||
target = current_constraint.target
|
||||
if not target:
|
||||
continue
|
||||
|
||||
if target.type == "ARMATURE":
|
||||
bone = target.pose.bones[current_constraint.subtarget]
|
||||
owner_name = bone.name
|
||||
target_constraints: Union[ObjectConstraints, PoseBoneConstraints] = (
|
||||
bone.constraints
|
||||
)
|
||||
else:
|
||||
owner_name = target.name
|
||||
target_constraints = target.constraints
|
||||
|
||||
for target_constraint in target_constraints:
|
||||
if not isinstance(target_constraint, CopyRotationConstraint):
|
||||
continue
|
||||
if target_constraint not in all_constraints:
|
||||
continue
|
||||
|
||||
next_target_axis = (
|
||||
current_axis[0] and target_constraint.use_x,
|
||||
current_axis[1] and target_constraint.use_y,
|
||||
current_axis[2] and target_constraint.use_z,
|
||||
)
|
||||
if next_target_axis == (False, False, False):
|
||||
continue
|
||||
|
||||
if target_constraint != search_constraint:
|
||||
current_constraints.append((target_constraint, next_target_axis))
|
||||
continue
|
||||
|
||||
excluded_constraints.append(search_constraint)
|
||||
messages.append(
|
||||
pgettext(
|
||||
'Node Constraint "{owner_name} / {constraint_name}" has'
|
||||
+ " a circular dependency"
|
||||
).format(
|
||||
owner_name=owner_name,
|
||||
constraint_name=search_constraint.name,
|
||||
)
|
||||
)
|
||||
found = True
|
||||
break
|
||||
|
||||
if found:
|
||||
break
|
||||
|
||||
return (
|
||||
ExportConstraint(
|
||||
roll_constraints={
|
||||
k: v
|
||||
for k, v in object_constraints.roll_constraints.items()
|
||||
if v not in excluded_constraints
|
||||
},
|
||||
aim_constraints={
|
||||
k: v
|
||||
for k, v in object_constraints.aim_constraints.items()
|
||||
if v not in excluded_constraints
|
||||
},
|
||||
rotation_constraints={
|
||||
k: v
|
||||
for k, v in object_constraints.rotation_constraints.items()
|
||||
if v not in excluded_constraints
|
||||
},
|
||||
),
|
||||
ExportConstraint(
|
||||
roll_constraints={
|
||||
k: v
|
||||
for k, v in bone_constraints.roll_constraints.items()
|
||||
if v not in excluded_constraints
|
||||
},
|
||||
aim_constraints={
|
||||
k: v
|
||||
for k, v in bone_constraints.aim_constraints.items()
|
||||
if v not in excluded_constraints
|
||||
},
|
||||
rotation_constraints={
|
||||
k: v
|
||||
for k, v in bone_constraints.rotation_constraints.items()
|
||||
if v not in excluded_constraints
|
||||
},
|
||||
),
|
||||
messages,
|
||||
)
|
||||
|
||||
|
||||
def active_object_is_vrm1_armature(context: Context) -> bool:
|
||||
active_object = context.active_object
|
||||
if not active_object:
|
||||
return False
|
||||
if active_object.type != "ARMATURE":
|
||||
return False
|
||||
armature_data = active_object.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return False
|
||||
return get_armature_extension(armature_data).is_vrm1()
|
||||
|
||||
|
||||
def active_object_is_vrm0_armature(context: Context) -> bool:
|
||||
active_object = context.active_object
|
||||
if not active_object:
|
||||
return False
|
||||
if active_object.type != "ARMATURE":
|
||||
return False
|
||||
armature_data = active_object.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return False
|
||||
return get_armature_extension(armature_data).is_vrm0()
|
||||
1
editor/spring_bone1/__init__.py
Normal file
1
editor/spring_bone1/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
BIN
editor/spring_bone1/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
editor/spring_bone1/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/spring_bone1/__pycache__/handler.cpython-311.pyc
Normal file
BIN
editor/spring_bone1/__pycache__/handler.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/spring_bone1/__pycache__/migration.cpython-311.pyc
Normal file
BIN
editor/spring_bone1/__pycache__/migration.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/spring_bone1/__pycache__/ops.cpython-311.pyc
Normal file
BIN
editor/spring_bone1/__pycache__/ops.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/spring_bone1/__pycache__/panel.cpython-311.pyc
Normal file
BIN
editor/spring_bone1/__pycache__/panel.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/spring_bone1/__pycache__/property_group.cpython-311.pyc
Normal file
BIN
editor/spring_bone1/__pycache__/property_group.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/spring_bone1/__pycache__/ui_list.cpython-311.pyc
Normal file
BIN
editor/spring_bone1/__pycache__/ui_list.cpython-311.pyc
Normal file
Binary file not shown.
858
editor/spring_bone1/handler.py
Normal file
858
editor/spring_bone1/handler.py
Normal file
@@ -0,0 +1,858 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Iterable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from sys import float_info
|
||||
from typing import Optional, Union
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy.types import Armature, Context, Object, PoseBone
|
||||
from mathutils import Matrix, Quaternion, Vector
|
||||
|
||||
from ...common.rotation import (
|
||||
get_rotation_as_quaternion,
|
||||
set_rotation_without_mode_change,
|
||||
)
|
||||
from ..extension import get_armature_extension
|
||||
from ..property_group import CollectionPropertyProtocol
|
||||
from .property_group import (
|
||||
SpringBone1JointPropertyGroup,
|
||||
SpringBone1SpringPropertyGroup,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class State:
|
||||
frame_count: Decimal = Decimal()
|
||||
spring_bone_60_fps_update_count: Decimal = Decimal()
|
||||
last_fps: Optional[Decimal] = None
|
||||
last_fps_base: Optional[Decimal] = None
|
||||
|
||||
def reset(self, context: Context) -> None:
|
||||
self.frame_count = Decimal()
|
||||
self.spring_bone_60_fps_update_count = Decimal()
|
||||
self.last_fps_base = Decimal(context.scene.render.fps_base)
|
||||
self.last_fps = Decimal(context.scene.render.fps)
|
||||
|
||||
|
||||
state = State()
|
||||
|
||||
|
||||
def reset_state(context: Context) -> None:
|
||||
state.reset(context)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SphereWorldCollider:
|
||||
offset: Vector
|
||||
radius: float
|
||||
|
||||
def calculate_collision(
|
||||
self, target: Vector, target_radius: float
|
||||
) -> tuple[Vector, float]:
|
||||
diff = target - self.offset
|
||||
diff_length = diff.length
|
||||
if diff_length < float_info.epsilon:
|
||||
return Vector((0, 0, -1)), -0.01
|
||||
return diff / diff_length, diff_length - target_radius - self.radius
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CapsuleWorldCollider:
|
||||
offset: Vector
|
||||
radius: float
|
||||
tail: Vector
|
||||
offset_to_tail_diff: Vector # Must be non-zero vector
|
||||
offset_to_tail_diff_length_squared: float # Must be non-negative value
|
||||
|
||||
def calculate_collision(
|
||||
self, target: Vector, target_radius: float
|
||||
) -> tuple[Vector, float]:
|
||||
offset_to_target_diff = target - self.offset
|
||||
|
||||
# Find the shortest point on the line containing offset and tail to the target
|
||||
# self.offset + (self.tail - self.offset) * offset_to_tail_ratio_for_nearest
|
||||
# Calculate offset_to_tail_ratio_for_nearest to express it as the above formula
|
||||
offset_to_tail_ratio_for_nearest = (
|
||||
self.offset_to_tail_diff.dot(offset_to_target_diff)
|
||||
/ self.offset_to_tail_diff_length_squared
|
||||
)
|
||||
|
||||
# The line segment from offset to tail has start point 0 and end point 1,
|
||||
# so clamp outside ranges
|
||||
offset_to_tail_ratio_for_nearest = max(
|
||||
0, min(1, offset_to_tail_ratio_for_nearest)
|
||||
)
|
||||
|
||||
# Calculate the shortest point to the target
|
||||
nearest = (
|
||||
self.offset + self.offset_to_tail_diff * offset_to_tail_ratio_for_nearest
|
||||
)
|
||||
|
||||
# Collision detection
|
||||
diff = target - nearest
|
||||
diff_length = diff.length
|
||||
if diff_length < float_info.epsilon:
|
||||
return Vector((0, 0, -1)), -0.01
|
||||
return diff / diff_length, diff_length - target_radius - self.radius
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SphereInsideWorldCollider:
|
||||
offset: Vector
|
||||
radius: float
|
||||
|
||||
def calculate_collision(
|
||||
self, target: Vector, target_radius: float
|
||||
) -> tuple[Vector, float]:
|
||||
diff = self.offset - target
|
||||
diff_length = diff.length
|
||||
if diff_length < float_info.epsilon:
|
||||
return Vector((0, 0, -1)), -0.01
|
||||
return diff / diff_length, -diff_length - target_radius + self.radius
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CapsuleInsideWorldCollider:
|
||||
offset: Vector
|
||||
radius: float
|
||||
tail: Vector
|
||||
offset_to_tail_diff: Vector # Must be non-zero vector
|
||||
offset_to_tail_diff_length_squared: float # Must be non-negative value
|
||||
|
||||
def calculate_collision(
|
||||
self, target: Vector, target_radius: float
|
||||
) -> tuple[Vector, float]:
|
||||
offset_to_target_diff = target - self.offset
|
||||
|
||||
# Find the shortest point on the line containing offset and tail to the target
|
||||
# self.offset + (self.tail - self.offset) * offset_to_tail_ratio_for_nearest
|
||||
# Calculate offset_to_tail_ratio_for_nearest to express it as the above formula
|
||||
offset_to_tail_ratio_for_nearest = (
|
||||
self.offset_to_tail_diff.dot(offset_to_target_diff)
|
||||
/ self.offset_to_tail_diff_length_squared
|
||||
)
|
||||
|
||||
# The line segment from offset to tail has start point 0 and end point 1,
|
||||
# so clamp outside ranges
|
||||
offset_to_tail_ratio_for_nearest = max(
|
||||
0, min(1, offset_to_tail_ratio_for_nearest)
|
||||
)
|
||||
|
||||
# Calculate the shortest point to the target
|
||||
nearest = (
|
||||
self.offset + self.offset_to_tail_diff * offset_to_tail_ratio_for_nearest
|
||||
)
|
||||
|
||||
# Collision detection
|
||||
diff = nearest - target
|
||||
diff_length = diff.length
|
||||
if diff_length < float_info.epsilon:
|
||||
return Vector((0, 0, -1)), -0.01
|
||||
return diff / diff_length, -diff_length - target_radius + self.radius
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlaneWorldCollider:
|
||||
offset: Vector
|
||||
normal: Vector
|
||||
|
||||
def calculate_collision(
|
||||
self, target: Vector, target_radius: float
|
||||
) -> tuple[Vector, float]:
|
||||
distance = (target - self.offset).dot(self.normal) - target_radius
|
||||
return self.normal, distance
|
||||
|
||||
|
||||
# https://github.com/vrm-c/vrm-specification/tree/993a90a5bda9025f3d9e2923ad6dea7506f88553/specification/VRMC_springBone-1.0#update-procedure
|
||||
def update_pose_bone_rotations(context: Context, delta_time: float) -> None:
|
||||
pose_bone_and_rotations: list[tuple[PoseBone, Quaternion]] = []
|
||||
|
||||
for obj in context.blend_data.objects:
|
||||
calculate_object_pose_bone_rotations(delta_time, obj, pose_bone_and_rotations)
|
||||
|
||||
for pose_bone, pose_bone_rotation in pose_bone_and_rotations:
|
||||
# Assigning rotation to pose_bone is expensive, so avoid it as much as possible
|
||||
angle_diff = pose_bone_rotation.rotation_difference(
|
||||
get_rotation_as_quaternion(pose_bone)
|
||||
).angle
|
||||
if abs(angle_diff) < float_info.epsilon:
|
||||
continue
|
||||
set_rotation_without_mode_change(pose_bone, pose_bone_rotation)
|
||||
|
||||
|
||||
def calculate_object_pose_bone_rotations(
|
||||
delta_time: float,
|
||||
obj: Object,
|
||||
pose_bone_and_rotations: list[tuple[PoseBone, Quaternion]],
|
||||
) -> None:
|
||||
if obj.type != "ARMATURE":
|
||||
return
|
||||
armature_data = obj.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
ext = get_armature_extension(armature_data)
|
||||
if not ext.is_vrm1():
|
||||
return
|
||||
spring_bone1 = ext.spring_bone1
|
||||
if not spring_bone1.enable_animation:
|
||||
return
|
||||
|
||||
obj_matrix_world = obj.matrix_world
|
||||
obj_matrix_world_inverted = obj_matrix_world.inverted_safe()
|
||||
obj_matrix_world_quaternion = obj_matrix_world.to_quaternion()
|
||||
|
||||
collider_uuid_to_world_collider: dict[
|
||||
str,
|
||||
Union[
|
||||
SphereWorldCollider,
|
||||
CapsuleWorldCollider,
|
||||
SphereInsideWorldCollider,
|
||||
CapsuleInsideWorldCollider,
|
||||
PlaneWorldCollider,
|
||||
],
|
||||
] = {}
|
||||
for collider in spring_bone1.colliders:
|
||||
pose_bone = obj.pose.bones.get(collider.node.bone_name)
|
||||
if not pose_bone:
|
||||
continue
|
||||
pose_bone_world_matrix = obj_matrix_world @ pose_bone.matrix
|
||||
|
||||
extended_collider = collider.extensions.vrmc_spring_bone_extended_collider
|
||||
world_collider: Union[
|
||||
None,
|
||||
SphereWorldCollider,
|
||||
CapsuleWorldCollider,
|
||||
SphereInsideWorldCollider,
|
||||
CapsuleInsideWorldCollider,
|
||||
PlaneWorldCollider,
|
||||
] = None
|
||||
if extended_collider.enabled:
|
||||
if (
|
||||
extended_collider.shape_type
|
||||
== extended_collider.SHAPE_TYPE_EXTENDED_SPHERE.identifier
|
||||
):
|
||||
offset = pose_bone_world_matrix @ Vector(
|
||||
extended_collider.shape.sphere.offset
|
||||
)
|
||||
radius = extended_collider.shape.sphere.radius
|
||||
if extended_collider.shape.sphere.inside:
|
||||
world_collider = SphereInsideWorldCollider(
|
||||
offset=offset, radius=radius
|
||||
)
|
||||
else:
|
||||
world_collider = SphereWorldCollider(offset=offset, radius=radius)
|
||||
elif (
|
||||
extended_collider.shape_type
|
||||
== extended_collider.SHAPE_TYPE_EXTENDED_CAPSULE.identifier
|
||||
):
|
||||
offset = pose_bone_world_matrix @ Vector(
|
||||
extended_collider.shape.capsule.offset
|
||||
)
|
||||
tail = pose_bone_world_matrix @ Vector(
|
||||
extended_collider.shape.capsule.tail
|
||||
)
|
||||
radius = extended_collider.shape.sphere.radius
|
||||
offset_to_tail_diff = tail - offset
|
||||
offset_to_tail_diff_length_squared = offset_to_tail_diff.length_squared
|
||||
if offset_to_tail_diff_length_squared < float_info.epsilon:
|
||||
# If offset and tail positions are the same, use as sphere collider
|
||||
if extended_collider.shape.capsule.inside:
|
||||
world_collider = SphereInsideWorldCollider(
|
||||
offset=offset, radius=radius
|
||||
)
|
||||
else:
|
||||
world_collider = SphereWorldCollider(
|
||||
offset=offset, radius=radius
|
||||
)
|
||||
elif extended_collider.shape.capsule.inside:
|
||||
world_collider = CapsuleInsideWorldCollider(
|
||||
offset=offset,
|
||||
radius=radius,
|
||||
tail=tail,
|
||||
offset_to_tail_diff=offset_to_tail_diff,
|
||||
offset_to_tail_diff_length_squared=offset_to_tail_diff_length_squared,
|
||||
)
|
||||
else:
|
||||
world_collider = CapsuleWorldCollider(
|
||||
offset=offset,
|
||||
radius=radius,
|
||||
tail=tail,
|
||||
offset_to_tail_diff=offset_to_tail_diff,
|
||||
offset_to_tail_diff_length_squared=offset_to_tail_diff_length_squared,
|
||||
)
|
||||
elif (
|
||||
extended_collider.shape_type
|
||||
== extended_collider.SHAPE_TYPE_EXTENDED_PLANE.identifier
|
||||
):
|
||||
offset = pose_bone_world_matrix @ Vector(
|
||||
extended_collider.shape.plane.offset
|
||||
)
|
||||
normal = pose_bone_world_matrix.to_quaternion() @ Vector(
|
||||
extended_collider.shape.plane.normal
|
||||
)
|
||||
world_collider = PlaneWorldCollider(
|
||||
offset=offset,
|
||||
normal=normal,
|
||||
)
|
||||
elif collider.shape_type == collider.SHAPE_TYPE_SPHERE.identifier:
|
||||
offset = pose_bone_world_matrix @ Vector(collider.shape.sphere.offset)
|
||||
radius = collider.shape.sphere.radius
|
||||
world_collider = SphereWorldCollider(
|
||||
offset=offset,
|
||||
radius=radius,
|
||||
)
|
||||
elif collider.shape_type == collider.SHAPE_TYPE_CAPSULE.identifier:
|
||||
offset = pose_bone_world_matrix @ Vector(collider.shape.capsule.offset)
|
||||
tail = pose_bone_world_matrix @ Vector(collider.shape.capsule.tail)
|
||||
radius = collider.shape.sphere.radius
|
||||
offset_to_tail_diff = tail - offset
|
||||
offset_to_tail_diff_length_squared = offset_to_tail_diff.length_squared
|
||||
if offset_to_tail_diff_length_squared < float_info.epsilon:
|
||||
# If offset and tail positions are the same, use as sphere collider
|
||||
world_collider = SphereWorldCollider(
|
||||
offset=offset,
|
||||
radius=radius,
|
||||
)
|
||||
else:
|
||||
world_collider = CapsuleWorldCollider(
|
||||
offset=offset,
|
||||
radius=radius,
|
||||
tail=tail,
|
||||
offset_to_tail_diff=offset_to_tail_diff,
|
||||
offset_to_tail_diff_length_squared=offset_to_tail_diff_length_squared,
|
||||
)
|
||||
|
||||
if world_collider:
|
||||
collider_uuid_to_world_collider[collider.uuid] = world_collider
|
||||
|
||||
collider_group_uuid_to_world_colliders: dict[
|
||||
str,
|
||||
list[
|
||||
Union[
|
||||
SphereWorldCollider,
|
||||
CapsuleWorldCollider,
|
||||
SphereInsideWorldCollider,
|
||||
CapsuleInsideWorldCollider,
|
||||
PlaneWorldCollider,
|
||||
]
|
||||
],
|
||||
] = {}
|
||||
for collider_group in spring_bone1.collider_groups:
|
||||
for collider_reference in collider_group.colliders:
|
||||
world_collider = collider_uuid_to_world_collider.get(
|
||||
collider_reference.collider_uuid
|
||||
)
|
||||
if world_collider is None:
|
||||
continue
|
||||
world_colliders = collider_group_uuid_to_world_colliders.get(
|
||||
collider_group.uuid
|
||||
)
|
||||
if world_colliders is None:
|
||||
world_colliders = []
|
||||
collider_group_uuid_to_world_colliders[collider_group.uuid] = (
|
||||
world_colliders
|
||||
)
|
||||
world_colliders.append(world_collider)
|
||||
|
||||
for spring in spring_bone1.springs:
|
||||
joints = spring.joints
|
||||
if not joints:
|
||||
continue
|
||||
|
||||
calculate_spring_pose_bone_rotations(
|
||||
delta_time,
|
||||
obj,
|
||||
obj_matrix_world,
|
||||
obj_matrix_world_inverted,
|
||||
obj_matrix_world_quaternion,
|
||||
spring,
|
||||
pose_bone_and_rotations,
|
||||
collider_group_uuid_to_world_colliders,
|
||||
)
|
||||
|
||||
|
||||
def calculate_spring_pose_bone_rotations(
|
||||
delta_time: float,
|
||||
obj: Object,
|
||||
obj_matrix_world: Matrix,
|
||||
obj_matrix_world_inverted: Matrix,
|
||||
obj_matrix_world_quaternion: Quaternion,
|
||||
spring: SpringBone1SpringPropertyGroup,
|
||||
pose_bone_and_rotations: list[tuple[PoseBone, Quaternion]],
|
||||
collider_group_uuid_to_world_colliders: dict[
|
||||
str,
|
||||
list[
|
||||
Union[
|
||||
SphereWorldCollider,
|
||||
CapsuleWorldCollider,
|
||||
SphereInsideWorldCollider,
|
||||
CapsuleInsideWorldCollider,
|
||||
PlaneWorldCollider,
|
||||
]
|
||||
],
|
||||
],
|
||||
) -> None:
|
||||
world_collider_groups: Sequence[
|
||||
Sequence[
|
||||
Union[
|
||||
SphereWorldCollider,
|
||||
CapsuleWorldCollider,
|
||||
SphereInsideWorldCollider,
|
||||
CapsuleInsideWorldCollider,
|
||||
PlaneWorldCollider,
|
||||
]
|
||||
]
|
||||
] = [
|
||||
collider_group_world_colliders
|
||||
for collider_group_reference in spring.collider_groups
|
||||
if (
|
||||
collider_group_world_colliders
|
||||
:= collider_group_uuid_to_world_colliders.get(
|
||||
collider_group_reference.collider_group_uuid
|
||||
)
|
||||
)
|
||||
and collider_group_world_colliders
|
||||
]
|
||||
|
||||
center_pose_bone = obj.pose.bones.get(spring.center.bone_name)
|
||||
if center_pose_bone:
|
||||
current_center_world_translation = (
|
||||
obj_matrix_world @ center_pose_bone.matrix
|
||||
).to_translation()
|
||||
previous_center_world_translation = Vector(
|
||||
spring.animation_state.previous_center_world_translation
|
||||
)
|
||||
previous_to_current_center_world_translation = (
|
||||
current_center_world_translation - previous_center_world_translation
|
||||
)
|
||||
if not spring.animation_state.use_center_space:
|
||||
spring.animation_state.previous_center_world_translation = (
|
||||
current_center_world_translation.copy()
|
||||
)
|
||||
spring.animation_state.use_center_space = True
|
||||
else:
|
||||
current_center_world_translation = Vector((0, 0, 0))
|
||||
previous_to_current_center_world_translation = Vector((0, 0, 0))
|
||||
if spring.animation_state.use_center_space:
|
||||
spring.animation_state.use_center_space = False
|
||||
|
||||
for sorted_joint_and_bones in sort_spring_bone_joints(obj, spring.joints):
|
||||
joints: list[
|
||||
tuple[
|
||||
SpringBone1JointPropertyGroup,
|
||||
PoseBone,
|
||||
Matrix,
|
||||
]
|
||||
] = [
|
||||
(
|
||||
joint,
|
||||
pose_bone,
|
||||
pose_bone.bone.convert_local_to_pose(
|
||||
Matrix(), pose_bone.bone.matrix_local
|
||||
),
|
||||
)
|
||||
for joint, pose_bone in sorted_joint_and_bones
|
||||
]
|
||||
|
||||
# https://github.com/vrm-c/vrm-specification/blob/7279e169ac0dcf37e7d81b2adcad9107101d7e25/specification/VRMC_springBone-1.0/README.md#center-space
|
||||
enable_center_space = False
|
||||
if center_pose_bone:
|
||||
first_pose_bone = next((pose_bone for (_, pose_bone, _) in joints), None)
|
||||
ancestor_of_first_pose_bone: Optional[PoseBone] = first_pose_bone
|
||||
while ancestor_of_first_pose_bone:
|
||||
if center_pose_bone == ancestor_of_first_pose_bone:
|
||||
enable_center_space = True
|
||||
break
|
||||
ancestor_of_first_pose_bone = ancestor_of_first_pose_bone.parent
|
||||
|
||||
next_head_pose_bone_before_rotation_matrix = None
|
||||
for (
|
||||
head_joint,
|
||||
head_pose_bone,
|
||||
head_rest_object_matrix,
|
||||
), (
|
||||
tail_joint,
|
||||
tail_pose_bone,
|
||||
tail_rest_object_matrix,
|
||||
) in zip(joints, joints[1:]):
|
||||
head_tail_parented = False
|
||||
searching_tail_parent = tail_pose_bone.parent
|
||||
while searching_tail_parent:
|
||||
if searching_tail_parent.name == head_pose_bone.name:
|
||||
head_tail_parented = True
|
||||
break
|
||||
searching_tail_parent = searching_tail_parent.parent
|
||||
if not head_tail_parented:
|
||||
break
|
||||
|
||||
(
|
||||
head_pose_bone_rotation,
|
||||
next_head_pose_bone_before_rotation_matrix,
|
||||
) = calculate_joint_pair_head_pose_bone_rotations(
|
||||
delta_time,
|
||||
obj_matrix_world,
|
||||
obj_matrix_world_inverted,
|
||||
obj_matrix_world_quaternion,
|
||||
head_joint,
|
||||
head_pose_bone,
|
||||
head_rest_object_matrix,
|
||||
tail_joint,
|
||||
tail_pose_bone,
|
||||
tail_rest_object_matrix,
|
||||
next_head_pose_bone_before_rotation_matrix,
|
||||
world_collider_groups,
|
||||
previous_to_current_center_world_translation
|
||||
if enable_center_space
|
||||
else Vector((0, 0, 0)),
|
||||
)
|
||||
pose_bone_and_rotations.append((head_pose_bone, head_pose_bone_rotation))
|
||||
|
||||
spring.animation_state.previous_center_world_translation = (
|
||||
current_center_world_translation
|
||||
)
|
||||
|
||||
|
||||
def calculate_joint_pair_head_pose_bone_rotations(
|
||||
delta_time: float,
|
||||
obj_matrix_world: Matrix,
|
||||
obj_matrix_world_inverted: Matrix,
|
||||
obj_matrix_world_quaternion: Quaternion,
|
||||
head_joint: SpringBone1JointPropertyGroup,
|
||||
head_pose_bone: PoseBone,
|
||||
current_head_rest_object_matrix: Matrix,
|
||||
tail_joint: SpringBone1JointPropertyGroup,
|
||||
tail_pose_bone: PoseBone,
|
||||
current_tail_rest_object_matrix: Matrix,
|
||||
next_head_pose_bone_before_rotation_matrix: Optional[Matrix],
|
||||
world_collider_groups: Sequence[
|
||||
Sequence[
|
||||
Union[
|
||||
SphereWorldCollider,
|
||||
CapsuleWorldCollider,
|
||||
SphereInsideWorldCollider,
|
||||
CapsuleInsideWorldCollider,
|
||||
PlaneWorldCollider,
|
||||
]
|
||||
]
|
||||
],
|
||||
previous_to_current_center_world_translation: Vector,
|
||||
) -> tuple[Quaternion, Matrix]:
|
||||
current_head_pose_bone_matrix = head_pose_bone.matrix
|
||||
current_tail_pose_bone_matrix = tail_pose_bone.matrix
|
||||
|
||||
if next_head_pose_bone_before_rotation_matrix is None:
|
||||
if head_pose_bone_parent := head_pose_bone.parent:
|
||||
current_head_parent_matrix = head_pose_bone_parent.matrix
|
||||
current_head_parent_rest_object_matrix = (
|
||||
head_pose_bone_parent.bone.convert_local_to_pose(
|
||||
Matrix(), head_pose_bone_parent.bone.matrix_local
|
||||
)
|
||||
)
|
||||
next_head_pose_bone_before_rotation_matrix = current_head_parent_matrix @ (
|
||||
current_head_parent_rest_object_matrix.inverted_safe()
|
||||
@ current_head_rest_object_matrix
|
||||
)
|
||||
else:
|
||||
next_head_pose_bone_before_rotation_matrix = (
|
||||
current_head_rest_object_matrix.copy()
|
||||
)
|
||||
(
|
||||
next_head_pose_bone_translation,
|
||||
next_head_parent_pose_bone_object_rotation,
|
||||
next_head_pose_bone_scale,
|
||||
) = next_head_pose_bone_before_rotation_matrix.decompose()
|
||||
|
||||
next_head_world_translation = obj_matrix_world @ next_head_pose_bone_translation
|
||||
|
||||
if not tail_joint.animation_state.initialized_as_tail:
|
||||
initial_tail_world_translation = (
|
||||
obj_matrix_world @ current_tail_pose_bone_matrix
|
||||
).to_translation()
|
||||
tail_joint.animation_state.initialized_as_tail = True
|
||||
tail_joint.animation_state.previous_world_translation = list(
|
||||
initial_tail_world_translation
|
||||
)
|
||||
tail_joint.animation_state.current_world_translation = list(
|
||||
initial_tail_world_translation
|
||||
)
|
||||
|
||||
previous_tail_world_translation = (
|
||||
Vector(tail_joint.animation_state.previous_world_translation)
|
||||
+ previous_to_current_center_world_translation
|
||||
)
|
||||
current_tail_world_translation = (
|
||||
Vector(tail_joint.animation_state.current_world_translation)
|
||||
+ previous_to_current_center_world_translation
|
||||
)
|
||||
|
||||
inertia = (current_tail_world_translation - previous_tail_world_translation) * (
|
||||
1.0 - head_joint.drag_force
|
||||
)
|
||||
|
||||
current_head_rest_object_matrix_inverted = (
|
||||
current_head_rest_object_matrix.inverted_safe()
|
||||
)
|
||||
next_head_rotation_start_target_local_translation = (
|
||||
current_head_rest_object_matrix_inverted
|
||||
@ current_tail_rest_object_matrix.to_translation()
|
||||
)
|
||||
stiffness_direction = (
|
||||
obj_matrix_world_quaternion
|
||||
@ next_head_parent_pose_bone_object_rotation
|
||||
@ next_head_rotation_start_target_local_translation
|
||||
).normalized()
|
||||
stiffness = stiffness_direction * delta_time * head_joint.stiffness
|
||||
external = Vector(head_joint.gravity_dir) * delta_time * head_joint.gravity_power
|
||||
|
||||
next_tail_world_translation = (
|
||||
current_tail_world_translation + inertia + stiffness + external
|
||||
)
|
||||
|
||||
head_to_tail_world_distance = (
|
||||
obj_matrix_world @ current_head_pose_bone_matrix.to_translation()
|
||||
- (obj_matrix_world @ current_tail_pose_bone_matrix.to_translation())
|
||||
).length
|
||||
|
||||
# Apply distance constraint to next Tail
|
||||
next_tail_world_translation = (
|
||||
next_head_world_translation
|
||||
+ (next_tail_world_translation - next_head_world_translation).normalized()
|
||||
* head_to_tail_world_distance
|
||||
)
|
||||
# Calculate collider collision
|
||||
for world_colliders in world_collider_groups:
|
||||
for world_collider in world_colliders:
|
||||
direction, distance = world_collider.calculate_collision(
|
||||
next_tail_world_translation,
|
||||
head_joint.hit_radius,
|
||||
)
|
||||
if distance >= 0:
|
||||
continue
|
||||
# Push away
|
||||
next_tail_world_translation = (
|
||||
next_tail_world_translation - direction * distance
|
||||
)
|
||||
# Apply distance constraint to next Tail
|
||||
next_tail_world_translation = (
|
||||
next_head_world_translation
|
||||
+ (
|
||||
next_tail_world_translation - next_head_world_translation
|
||||
).normalized()
|
||||
* head_to_tail_world_distance
|
||||
)
|
||||
|
||||
next_tail_object_local_translation = (
|
||||
obj_matrix_world_inverted @ next_tail_world_translation
|
||||
)
|
||||
next_head_rotation_end_target_local_translation = (
|
||||
next_head_pose_bone_before_rotation_matrix.inverted_safe()
|
||||
@ next_tail_object_local_translation
|
||||
)
|
||||
|
||||
next_head_pose_bone_rotation = Quaternion(
|
||||
next_head_rotation_start_target_local_translation.cross(
|
||||
next_head_rotation_end_target_local_translation
|
||||
),
|
||||
next_head_rotation_start_target_local_translation.angle(
|
||||
next_head_rotation_end_target_local_translation, 0
|
||||
),
|
||||
)
|
||||
|
||||
next_head_pose_bone_object_rotation = (
|
||||
next_head_parent_pose_bone_object_rotation @ next_head_pose_bone_rotation
|
||||
)
|
||||
next_head_pose_bone_matrix = (
|
||||
Matrix.Translation(next_head_pose_bone_translation)
|
||||
@ next_head_pose_bone_object_rotation.to_matrix().to_4x4()
|
||||
@ Matrix.Diagonal(next_head_pose_bone_scale).to_4x4()
|
||||
)
|
||||
|
||||
next_tail_pose_bone_before_rotation_matrix = (
|
||||
next_head_pose_bone_matrix
|
||||
@ current_head_rest_object_matrix_inverted
|
||||
@ current_tail_rest_object_matrix
|
||||
)
|
||||
|
||||
tail_joint.animation_state.previous_world_translation = list(
|
||||
current_tail_world_translation
|
||||
)
|
||||
tail_joint.animation_state.current_world_translation = list(
|
||||
next_tail_world_translation
|
||||
)
|
||||
|
||||
return (
|
||||
next_head_pose_bone_rotation
|
||||
if head_pose_bone.bone.use_inherit_rotation
|
||||
else next_head_pose_bone_object_rotation,
|
||||
next_tail_pose_bone_before_rotation_matrix,
|
||||
)
|
||||
|
||||
|
||||
@persistent
|
||||
def depsgraph_update_pre(_unused: object) -> None:
|
||||
context = bpy.context
|
||||
|
||||
state.reset(context)
|
||||
|
||||
|
||||
@persistent
|
||||
def frame_change_pre(_unused: object) -> None:
|
||||
context = bpy.context
|
||||
|
||||
fps = Decimal(context.scene.render.fps)
|
||||
last_fps = state.last_fps
|
||||
fps_base = Decimal(context.scene.render.fps_base)
|
||||
last_fps_base = state.last_fps_base
|
||||
if (
|
||||
last_fps_base is None
|
||||
or (fps_base - last_fps_base).copy_abs() > 0.00001
|
||||
or fps != last_fps
|
||||
):
|
||||
state.reset(context)
|
||||
|
||||
state.frame_count += 1
|
||||
|
||||
# If the current time is future than the next SpringBone calculation
|
||||
# time, move the SpringBone
|
||||
# To minimize floating-point rounding errors, multiply numerator by
|
||||
# common denominator to minimize decimal handling
|
||||
frame_time_x_60_x_fps = state.frame_count * Decimal(60) * fps_base
|
||||
while True:
|
||||
next_spring_bone_60_fps_update_count = (
|
||||
state.spring_bone_60_fps_update_count + Decimal(1)
|
||||
)
|
||||
|
||||
next_spring_bone_update_time_x_60_x_fps = (
|
||||
next_spring_bone_60_fps_update_count * fps
|
||||
)
|
||||
if next_spring_bone_update_time_x_60_x_fps > frame_time_x_60_x_fps:
|
||||
break
|
||||
|
||||
# To accumulate float rounding errors, don't hardcode delta_time as 1.0/60.0
|
||||
# Use the difference between previous and next times
|
||||
next_spring_bone_update_time = next_spring_bone_60_fps_update_count / Decimal(
|
||||
60
|
||||
)
|
||||
current_spring_bone_update_time = (
|
||||
state.spring_bone_60_fps_update_count / Decimal(60)
|
||||
)
|
||||
delta_time = float(next_spring_bone_update_time) - float(
|
||||
current_spring_bone_update_time
|
||||
)
|
||||
update_pose_bone_rotations(context, delta_time)
|
||||
|
||||
state.spring_bone_60_fps_update_count += 1
|
||||
|
||||
|
||||
def sort_spring_bone_joints(
|
||||
obj: Object, joints: CollectionPropertyProtocol[SpringBone1JointPropertyGroup]
|
||||
) -> Sequence[Iterable[tuple[SpringBone1JointPropertyGroup, PoseBone]]]:
|
||||
bones = obj.pose.bones
|
||||
|
||||
# Check if it's sorted and return as-is if already sorted.
|
||||
# This is logically unnecessary but done for simulation efficiency.
|
||||
already_sorted = True
|
||||
sorted_pose_bones: list[PoseBone] = []
|
||||
for joint in joints:
|
||||
joint_bone = bones.get(joint.node.bone_name)
|
||||
if not joint_bone:
|
||||
already_sorted = False
|
||||
break
|
||||
if not sorted_pose_bones:
|
||||
sorted_pose_bones.append(joint_bone)
|
||||
continue
|
||||
parent_bone = sorted_pose_bones[-1]
|
||||
sorted_pose_bones.append(joint_bone)
|
||||
|
||||
traversing_bone = joint_bone.parent
|
||||
connected = False
|
||||
while traversing_bone:
|
||||
if traversing_bone == parent_bone:
|
||||
connected = True
|
||||
break
|
||||
traversing_bone = traversing_bone.parent
|
||||
if not connected:
|
||||
already_sorted = False
|
||||
break
|
||||
|
||||
if already_sorted:
|
||||
return [zip(joints, sorted_pose_bones)]
|
||||
|
||||
# Perform sorting
|
||||
chains = list[list[tuple[SpringBone1JointPropertyGroup, PoseBone]]]()
|
||||
for joint in joints:
|
||||
joint_bone = bones.get(joint.node.bone_name)
|
||||
if not joint_bone:
|
||||
continue
|
||||
|
||||
if not chains:
|
||||
chains.append([(joint, joint_bone)])
|
||||
continue
|
||||
|
||||
# Skip if already registered in chain
|
||||
if any(joint_bone == bone for chain in chains for _, bone in chain):
|
||||
continue
|
||||
|
||||
# If ancestor of chain head, or descendant of chain tail,
|
||||
# or descendant of chain head and ancestor of chain tail,
|
||||
# add to that chain
|
||||
# Otherwise, create a new chain
|
||||
assigned = False
|
||||
for chain in chains:
|
||||
if not chain:
|
||||
# This should not happen
|
||||
continue
|
||||
|
||||
# Check if it's an ancestor of the chain head
|
||||
_, chain_head_bone = chain[0]
|
||||
traversing_bone = chain_head_bone.parent
|
||||
assigned = False
|
||||
while traversing_bone:
|
||||
if traversing_bone == joint_bone:
|
||||
chain.insert(0, (joint, joint_bone))
|
||||
assigned = True
|
||||
break
|
||||
traversing_bone = traversing_bone.parent
|
||||
if assigned:
|
||||
break
|
||||
|
||||
# Check if it's an ancestor of the chain tail
|
||||
_, chain_tail_bone = chain[-1]
|
||||
traversing_bone = joint_bone.parent
|
||||
assigned = False
|
||||
while traversing_bone:
|
||||
if traversing_bone == chain_tail_bone:
|
||||
chain.append((joint, joint_bone))
|
||||
assigned = True
|
||||
break
|
||||
traversing_bone = traversing_bone.parent
|
||||
if assigned:
|
||||
break
|
||||
|
||||
# Check if it's a descendant of the chain head and ancestor of
|
||||
# the chain tail
|
||||
assigned = False
|
||||
for i in range(len(chain) - 1):
|
||||
_, chain_parent_bone = chain[i]
|
||||
_, chain_child_bone = chain[i + 1]
|
||||
|
||||
traversing_bone = chain_child_bone.parent
|
||||
while traversing_bone:
|
||||
if traversing_bone == joint_bone:
|
||||
chain.insert(i + 1, (joint, joint_bone))
|
||||
assigned = True
|
||||
break
|
||||
if traversing_bone == chain_parent_bone:
|
||||
break
|
||||
traversing_bone = traversing_bone.parent
|
||||
if assigned:
|
||||
break
|
||||
if assigned:
|
||||
break
|
||||
|
||||
if not assigned:
|
||||
chains.append([(joint, joint_bone)])
|
||||
|
||||
return chains
|
||||
61
editor/spring_bone1/migration.py
Normal file
61
editor/spring_bone1/migration.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from bpy.types import Armature, Context, Object
|
||||
|
||||
from ..extension import get_armature_extension
|
||||
|
||||
|
||||
def migrate_blender_object(armature: Armature) -> None:
|
||||
ext = get_armature_extension(armature)
|
||||
if tuple(ext.addon_version) >= (2, 3, 27):
|
||||
return
|
||||
|
||||
for collider in ext.spring_bone1.colliders:
|
||||
bpy_object = collider.pop("blender_object", None)
|
||||
if isinstance(bpy_object, Object):
|
||||
collider.bpy_object = bpy_object
|
||||
|
||||
|
||||
def fixup_gravity_dir(armature: Armature) -> None:
|
||||
ext = get_armature_extension(armature)
|
||||
|
||||
if tuple(ext.addon_version) <= (2, 14, 3):
|
||||
for spring in ext.spring_bone1.springs:
|
||||
for joint in spring.joints:
|
||||
joint.gravity_dir = [
|
||||
joint.gravity_dir[0],
|
||||
joint.gravity_dir[2],
|
||||
joint.gravity_dir[1],
|
||||
]
|
||||
|
||||
if tuple(ext.addon_version) <= (2, 14, 10):
|
||||
for spring in ext.spring_bone1.springs:
|
||||
for joint in spring.joints:
|
||||
joint.gravity_dir = [
|
||||
joint.gravity_dir[0],
|
||||
-joint.gravity_dir[1],
|
||||
joint.gravity_dir[2],
|
||||
]
|
||||
|
||||
if tuple(ext.addon_version) <= (2, 15, 3):
|
||||
for spring in ext.spring_bone1.springs:
|
||||
for joint in spring.joints:
|
||||
gravity_dir = list(joint.gravity_dir)
|
||||
joint.gravity_dir = (gravity_dir[0] + 1, 0, 0) # Make a change
|
||||
joint.gravity_dir = gravity_dir
|
||||
|
||||
|
||||
def fixup_collider_group_name(armature: Armature) -> None:
|
||||
ext = get_armature_extension(armature)
|
||||
if tuple(ext.addon_version) <= (2, 20, 38):
|
||||
spring_bone = get_armature_extension(armature).spring_bone1
|
||||
for collider_group in spring_bone.collider_groups:
|
||||
collider_group.fix_index()
|
||||
|
||||
|
||||
def migrate(_context: Context, armature: Object) -> None:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
migrate_blender_object(armature_data)
|
||||
fixup_gravity_dir(armature_data)
|
||||
fixup_collider_group_name(armature_data)
|
||||
1601
editor/spring_bone1/ops.py
Normal file
1601
editor/spring_bone1/ops.py
Normal file
File diff suppressed because it is too large
Load Diff
599
editor/spring_bone1/panel.py
Normal file
599
editor/spring_bone1/panel.py
Normal file
@@ -0,0 +1,599 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Set as AbstractSet
|
||||
from typing import Optional
|
||||
|
||||
from bpy.types import Armature, Context, Object, Panel, UILayout
|
||||
|
||||
from .. import search
|
||||
from ..extension import get_armature_extension
|
||||
from ..migration import defer_migrate
|
||||
from ..panel import VRM_PT_vrm_armature_object_property, draw_template_list
|
||||
from ..search import active_object_is_vrm1_armature
|
||||
from . import ops
|
||||
from .property_group import (
|
||||
SpringBone1ColliderGroupPropertyGroup,
|
||||
SpringBone1ColliderPropertyGroup,
|
||||
SpringBone1JointPropertyGroup,
|
||||
SpringBone1SpringBonePropertyGroup,
|
||||
SpringBone1SpringPropertyGroup,
|
||||
)
|
||||
from .ui_list import (
|
||||
VRM_UL_spring_bone1_collider,
|
||||
VRM_UL_spring_bone1_collider_group,
|
||||
VRM_UL_spring_bone1_collider_group_collider,
|
||||
VRM_UL_spring_bone1_joint,
|
||||
VRM_UL_spring_bone1_spring,
|
||||
VRM_UL_spring_bone1_spring_collider_group,
|
||||
)
|
||||
|
||||
|
||||
def draw_spring_bone1_collider_sphere_layout(
|
||||
layout: UILayout,
|
||||
armature_data: Armature,
|
||||
collider: SpringBone1ColliderPropertyGroup,
|
||||
) -> None:
|
||||
layout.prop_search(collider.node, "bone_name", armature_data, "bones")
|
||||
layout.prop(collider, "ui_collider_type")
|
||||
bpy_object = collider.bpy_object
|
||||
if bpy_object:
|
||||
layout.prop(bpy_object, "name", icon="MESH_UVSPHERE", text="Name")
|
||||
layout.prop(collider.shape.sphere, "offset", text="Offset")
|
||||
layout.separator(factor=0.5)
|
||||
layout.prop(collider.shape.sphere, "radius", slider=True)
|
||||
layout.prop(collider.extensions.vrmc_spring_bone_extended_collider, "enabled")
|
||||
|
||||
|
||||
def draw_spring_bone1_collider_capsule_layout(
|
||||
layout: UILayout,
|
||||
armature_data: Armature,
|
||||
collider: SpringBone1ColliderPropertyGroup,
|
||||
) -> None:
|
||||
layout.prop_search(collider.node, "bone_name", armature_data, "bones")
|
||||
layout.prop(collider, "ui_collider_type")
|
||||
bpy_object = collider.bpy_object
|
||||
if bpy_object:
|
||||
layout.prop(bpy_object, "name", icon="MESH_UVSPHERE", text="Head")
|
||||
layout.prop(collider.shape.capsule, "offset", text="")
|
||||
layout.separator(factor=0.5)
|
||||
layout.prop(collider.shape.capsule, "radius", slider=True)
|
||||
layout.separator(factor=0.5)
|
||||
if bpy_object and bpy_object.children:
|
||||
layout.prop(
|
||||
bpy_object.children[0],
|
||||
"name",
|
||||
icon="MESH_UVSPHERE",
|
||||
text="Tail",
|
||||
)
|
||||
layout.prop(collider.shape.capsule, "tail", text="")
|
||||
layout.prop(collider.extensions.vrmc_spring_bone_extended_collider, "enabled")
|
||||
|
||||
|
||||
def draw_spring_bone1_collider_extended_fallback_layout(
|
||||
layout: UILayout,
|
||||
_armature_data: Armature,
|
||||
collider: SpringBone1ColliderPropertyGroup,
|
||||
) -> None:
|
||||
extended = collider.extensions.vrmc_spring_bone_extended_collider
|
||||
layout.prop(extended, "automatic_fallback_generation")
|
||||
if extended.automatic_fallback_generation:
|
||||
return
|
||||
layout.prop(collider, "shape_type", text="Fallback")
|
||||
if collider.shape_type == collider.SHAPE_TYPE_SPHERE.identifier:
|
||||
layout.prop(collider.shape.sphere, "fallback_offset")
|
||||
layout.separator(factor=0.5)
|
||||
layout.prop(collider.shape.sphere, "fallback_radius", slider=True)
|
||||
elif collider.shape_type == collider.SHAPE_TYPE_CAPSULE.identifier:
|
||||
layout.prop(collider.shape.capsule, "fallback_offset")
|
||||
layout.separator(factor=0.5)
|
||||
layout.prop(collider.shape.capsule, "fallback_radius", slider=True)
|
||||
layout.separator(factor=0.5)
|
||||
layout.prop(collider.shape.capsule, "fallback_tail")
|
||||
|
||||
|
||||
def draw_spring_bone1_collider_extended_sphere_layout(
|
||||
layout: UILayout,
|
||||
armature_data: Armature,
|
||||
collider: SpringBone1ColliderPropertyGroup,
|
||||
) -> None:
|
||||
extended = collider.extensions.vrmc_spring_bone_extended_collider
|
||||
layout.prop_search(collider.node, "bone_name", armature_data, "bones")
|
||||
layout.prop(collider, "ui_collider_type")
|
||||
bpy_object = collider.bpy_object
|
||||
if bpy_object:
|
||||
layout.prop(bpy_object, "name", icon="MESH_UVSPHERE", text="Offset")
|
||||
layout.prop(extended.shape.sphere, "offset", text="")
|
||||
layout.separator(factor=0.5)
|
||||
layout.prop(extended.shape.sphere, "radius", slider=True)
|
||||
if not (extended.enabled and extended.shape.sphere.inside):
|
||||
layout.prop(extended, "enabled")
|
||||
draw_spring_bone1_collider_extended_fallback_layout(
|
||||
layout,
|
||||
armature_data,
|
||||
collider,
|
||||
)
|
||||
|
||||
|
||||
def draw_spring_bone1_collider_extended_capsule_layout(
|
||||
layout: UILayout,
|
||||
armature_data: Armature,
|
||||
collider: SpringBone1ColliderPropertyGroup,
|
||||
) -> None:
|
||||
extended = collider.extensions.vrmc_spring_bone_extended_collider
|
||||
layout.prop_search(collider.node, "bone_name", armature_data, "bones")
|
||||
layout.prop(collider, "ui_collider_type")
|
||||
bpy_object = collider.bpy_object
|
||||
if bpy_object:
|
||||
layout.prop(bpy_object, "name", icon="MESH_UVSPHERE", text="Head")
|
||||
layout.prop(extended.shape.capsule, "offset", text="")
|
||||
layout.separator(factor=0.5)
|
||||
layout.prop(extended.shape.capsule, "radius", slider=True)
|
||||
layout.separator(factor=0.5)
|
||||
if bpy_object and bpy_object.children:
|
||||
layout.prop(
|
||||
bpy_object.children[0],
|
||||
"name",
|
||||
icon="MESH_UVSPHERE",
|
||||
text="Tail",
|
||||
)
|
||||
layout.prop(extended.shape.capsule, "tail", text="")
|
||||
if not (extended.enabled and extended.shape.capsule.inside):
|
||||
layout.prop(extended, "enabled")
|
||||
draw_spring_bone1_collider_extended_fallback_layout(
|
||||
layout,
|
||||
armature_data,
|
||||
collider,
|
||||
)
|
||||
|
||||
|
||||
def draw_spring_bone1_collider_extended_plane_layout(
|
||||
layout: UILayout,
|
||||
armature_data: Armature,
|
||||
collider: SpringBone1ColliderPropertyGroup,
|
||||
) -> None:
|
||||
extended = collider.extensions.vrmc_spring_bone_extended_collider
|
||||
layout.prop_search(collider.node, "bone_name", armature_data, "bones")
|
||||
layout.prop(collider, "ui_collider_type")
|
||||
bpy_object = collider.bpy_object
|
||||
if bpy_object:
|
||||
layout.prop(bpy_object, "name", icon="MESH_UVSPHERE", text="Offset")
|
||||
layout.prop(extended.shape.plane, "offset", text="")
|
||||
layout.separator(factor=0.5)
|
||||
layout.prop(extended.shape.plane, "normal")
|
||||
|
||||
draw_spring_bone1_collider_extended_fallback_layout(
|
||||
layout,
|
||||
armature_data,
|
||||
collider,
|
||||
)
|
||||
|
||||
|
||||
def draw_spring_bone1_collider_layout(
|
||||
armature: Object,
|
||||
layout: UILayout,
|
||||
collider: SpringBone1ColliderPropertyGroup,
|
||||
) -> None:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
extended = collider.extensions.vrmc_spring_bone_extended_collider
|
||||
if extended.enabled:
|
||||
if extended.shape_type == extended.SHAPE_TYPE_EXTENDED_PLANE.identifier:
|
||||
draw_spring_bone1_collider_extended_plane_layout(
|
||||
layout, armature_data, collider
|
||||
)
|
||||
elif extended.shape_type == extended.SHAPE_TYPE_EXTENDED_SPHERE.identifier:
|
||||
draw_spring_bone1_collider_extended_sphere_layout(
|
||||
layout, armature_data, collider
|
||||
)
|
||||
elif extended.shape_type == extended.SHAPE_TYPE_EXTENDED_CAPSULE.identifier:
|
||||
draw_spring_bone1_collider_extended_capsule_layout(
|
||||
layout, armature_data, collider
|
||||
)
|
||||
elif collider.shape_type == collider.SHAPE_TYPE_SPHERE.identifier:
|
||||
draw_spring_bone1_collider_sphere_layout(layout, armature_data, collider)
|
||||
elif collider.shape_type == collider.SHAPE_TYPE_CAPSULE.identifier:
|
||||
draw_spring_bone1_collider_capsule_layout(layout, armature_data, collider)
|
||||
layout.separator(factor=0.5)
|
||||
|
||||
|
||||
def draw_spring_bone1_spring_bone_layout(
|
||||
armature: Object,
|
||||
layout: UILayout,
|
||||
spring_bone: SpringBone1SpringBonePropertyGroup,
|
||||
) -> None:
|
||||
defer_migrate(armature.name)
|
||||
|
||||
layout.prop(spring_bone, "enable_animation")
|
||||
# layout.operator(ops.VRM_OT_reset_spring_bone1_animation_state.bl_idname)
|
||||
|
||||
draw_spring_bone1_colliders_layout(armature, layout, spring_bone)
|
||||
draw_spring_bone1_collider_groups_layout(armature, layout, spring_bone)
|
||||
draw_spring_bone1_springs_layout(armature, layout, spring_bone)
|
||||
|
||||
|
||||
def draw_spring_bone1_colliders_layout(
|
||||
armature: Object,
|
||||
layout: UILayout,
|
||||
spring_bone: SpringBone1SpringBonePropertyGroup,
|
||||
) -> None:
|
||||
colliders_box = layout.box()
|
||||
colliders_header_row = colliders_box.row()
|
||||
colliders_header_row.alignment = "LEFT"
|
||||
colliders_header_row.prop(
|
||||
spring_bone,
|
||||
"show_expanded_colliders",
|
||||
icon="TRIA_DOWN" if spring_bone.show_expanded_colliders else "TRIA_RIGHT",
|
||||
emboss=False,
|
||||
)
|
||||
|
||||
if not spring_bone.show_expanded_colliders:
|
||||
return
|
||||
|
||||
(
|
||||
collider_collection_ops,
|
||||
collider_collection_item_ops,
|
||||
collider_index,
|
||||
collider,
|
||||
_,
|
||||
) = draw_template_list(
|
||||
colliders_box,
|
||||
VRM_UL_spring_bone1_collider,
|
||||
spring_bone,
|
||||
"colliders",
|
||||
"active_collider_index",
|
||||
ops.VRM_OT_add_spring_bone1_collider,
|
||||
ops.VRM_OT_remove_spring_bone1_collider,
|
||||
ops.VRM_OT_move_up_spring_bone1_collider,
|
||||
ops.VRM_OT_move_down_spring_bone1_collider,
|
||||
)
|
||||
|
||||
for collider_collection_op in collider_collection_ops:
|
||||
collider_collection_op.armature_object_name = armature.name
|
||||
|
||||
for collider_collection_item_op in collider_collection_item_ops:
|
||||
collider_collection_item_op.collider_index = collider_index
|
||||
|
||||
if not isinstance(collider, SpringBone1ColliderPropertyGroup):
|
||||
return
|
||||
|
||||
draw_spring_bone1_collider_layout(armature, colliders_box.column(), collider)
|
||||
|
||||
|
||||
def draw_spring_bone1_collider_groups_layout(
|
||||
armature: Object,
|
||||
layout: UILayout,
|
||||
spring_bone: SpringBone1SpringBonePropertyGroup,
|
||||
) -> None:
|
||||
collider_groups_box = layout.box()
|
||||
collider_groups_header_row = collider_groups_box.row()
|
||||
collider_groups_header_row.alignment = "LEFT"
|
||||
collider_groups_header_row.prop(
|
||||
spring_bone,
|
||||
"show_expanded_collider_groups",
|
||||
icon="TRIA_DOWN" if spring_bone.show_expanded_collider_groups else "TRIA_RIGHT",
|
||||
emboss=False,
|
||||
)
|
||||
|
||||
if not spring_bone.show_expanded_collider_groups:
|
||||
return
|
||||
|
||||
(
|
||||
collider_group_collection_ops,
|
||||
collider_group_collection_item_ops,
|
||||
collider_group_index,
|
||||
collider_group,
|
||||
_,
|
||||
) = draw_template_list(
|
||||
collider_groups_box,
|
||||
VRM_UL_spring_bone1_collider_group,
|
||||
spring_bone,
|
||||
"collider_groups",
|
||||
"active_collider_group_index",
|
||||
ops.VRM_OT_add_spring_bone1_collider_group,
|
||||
ops.VRM_OT_remove_spring_bone1_collider_group,
|
||||
ops.VRM_OT_move_up_spring_bone1_collider_group,
|
||||
ops.VRM_OT_move_down_spring_bone1_collider_group,
|
||||
)
|
||||
|
||||
for collider_group_collection_op in collider_group_collection_ops:
|
||||
collider_group_collection_op.armature_object_name = armature.name
|
||||
|
||||
for collider_group_collection_item_op in collider_group_collection_item_ops:
|
||||
collider_group_collection_item_op.collider_group_index = collider_group_index
|
||||
|
||||
if not isinstance(collider_group, SpringBone1ColliderGroupPropertyGroup):
|
||||
return
|
||||
|
||||
collider_group_column = collider_groups_box.column()
|
||||
collider_group_column.prop(
|
||||
collider_group,
|
||||
"vrm_name",
|
||||
)
|
||||
|
||||
colliders_box = collider_group_column.box()
|
||||
colliders_column = colliders_box.column()
|
||||
colliders_column.label(text="Colliders:")
|
||||
|
||||
(
|
||||
collider_collection_ops,
|
||||
collider_collection_item_ops,
|
||||
collider_index,
|
||||
_collider,
|
||||
_,
|
||||
) = draw_template_list(
|
||||
colliders_column,
|
||||
VRM_UL_spring_bone1_collider_group_collider,
|
||||
collider_group,
|
||||
"colliders",
|
||||
"active_collider_index",
|
||||
ops.VRM_OT_add_spring_bone1_collider_group_collider,
|
||||
ops.VRM_OT_remove_spring_bone1_collider_group_collider,
|
||||
ops.VRM_OT_move_up_spring_bone1_collider_group_collider,
|
||||
ops.VRM_OT_move_down_spring_bone1_collider_group_collider,
|
||||
)
|
||||
|
||||
for collider_collection_op in collider_collection_ops:
|
||||
collider_collection_op.armature_object_name = armature.name
|
||||
collider_collection_op.collider_group_index = collider_group_index
|
||||
|
||||
for collider_collection_item_op in collider_collection_item_ops:
|
||||
collider_collection_item_op.collider_index = collider_index
|
||||
|
||||
|
||||
def draw_spring_bone1_springs_layout(
|
||||
armature: Object,
|
||||
layout: UILayout,
|
||||
spring_bone: SpringBone1SpringBonePropertyGroup,
|
||||
) -> None:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
|
||||
springs_box = layout.box()
|
||||
springs_header_row = springs_box.row()
|
||||
springs_header_row.alignment = "LEFT"
|
||||
springs_header_row.prop(
|
||||
spring_bone,
|
||||
"show_expanded_springs",
|
||||
icon="TRIA_DOWN" if spring_bone.show_expanded_springs else "TRIA_RIGHT",
|
||||
emboss=False,
|
||||
)
|
||||
|
||||
if not spring_bone.show_expanded_springs:
|
||||
return
|
||||
|
||||
(
|
||||
spring_collection_ops,
|
||||
spring_collection_item_ops,
|
||||
spring_index,
|
||||
spring,
|
||||
_,
|
||||
) = draw_template_list(
|
||||
springs_box,
|
||||
VRM_UL_spring_bone1_spring,
|
||||
spring_bone,
|
||||
"springs",
|
||||
"active_spring_index",
|
||||
ops.VRM_OT_add_spring_bone1_spring,
|
||||
ops.VRM_OT_remove_spring_bone1_spring,
|
||||
ops.VRM_OT_move_up_spring_bone1_spring,
|
||||
ops.VRM_OT_move_down_spring_bone1_spring,
|
||||
)
|
||||
|
||||
for spring_collection_op in spring_collection_ops:
|
||||
spring_collection_op.armature_object_name = armature.name
|
||||
|
||||
for spring_collection_item_op in spring_collection_item_ops:
|
||||
spring_collection_item_op.spring_index = spring_index
|
||||
|
||||
if not isinstance(spring, SpringBone1SpringPropertyGroup):
|
||||
return
|
||||
|
||||
spring_column = springs_box.column()
|
||||
spring_column.prop(
|
||||
spring,
|
||||
"vrm_name",
|
||||
)
|
||||
spring_column.prop_search(
|
||||
spring.center,
|
||||
"bone_name",
|
||||
armature_data,
|
||||
"bones",
|
||||
text="Center",
|
||||
)
|
||||
|
||||
joints_box = spring_column.box()
|
||||
joints_column = joints_box.column()
|
||||
joints_column.label(text="Joints:")
|
||||
|
||||
(
|
||||
joint_collection_ops,
|
||||
joint_collection_item_ops,
|
||||
joint_index,
|
||||
joint,
|
||||
(add_joint_op, _, _, _),
|
||||
) = draw_template_list(
|
||||
joints_column,
|
||||
VRM_UL_spring_bone1_joint,
|
||||
spring,
|
||||
"joints",
|
||||
"active_joint_index",
|
||||
ops.VRM_OT_add_spring_bone1_joint,
|
||||
ops.VRM_OT_remove_spring_bone1_joint,
|
||||
ops.VRM_OT_move_up_spring_bone1_joint,
|
||||
ops.VRM_OT_move_down_spring_bone1_joint,
|
||||
)
|
||||
|
||||
for joint_collection_op in joint_collection_ops:
|
||||
joint_collection_op.armature_object_name = armature.name
|
||||
joint_collection_op.spring_index = spring_index
|
||||
|
||||
for joint_collection_item_op in joint_collection_item_ops:
|
||||
joint_collection_item_op.joint_index = joint_index
|
||||
|
||||
add_joint_op.guess_properties = True
|
||||
|
||||
if isinstance(joint, SpringBone1JointPropertyGroup):
|
||||
joints_column.prop_search(joint.node, "bone_name", armature_data, "bones")
|
||||
joints_column.prop(joint, "stiffness", slider=True)
|
||||
joints_column.prop(joint, "gravity_power", slider=True)
|
||||
joints_column.prop(joint, "gravity_dir")
|
||||
joints_column.prop(joint, "drag_force", slider=True)
|
||||
joints_column.prop(joint, "hit_radius", slider=True)
|
||||
|
||||
collider_groups_box = spring_column.box()
|
||||
collider_groups_column = collider_groups_box.column()
|
||||
collider_groups_column.label(text="Collider Groups:")
|
||||
|
||||
(
|
||||
collider_group_collection_ops,
|
||||
collider_group_collection_item_ops,
|
||||
collider_group_index,
|
||||
_collider_group,
|
||||
_,
|
||||
) = draw_template_list(
|
||||
collider_groups_column,
|
||||
VRM_UL_spring_bone1_spring_collider_group,
|
||||
spring,
|
||||
"collider_groups",
|
||||
"active_collider_group_index",
|
||||
ops.VRM_OT_add_spring_bone1_spring_collider_group,
|
||||
ops.VRM_OT_remove_spring_bone1_spring_collider_group,
|
||||
ops.VRM_OT_move_up_spring_bone1_spring_collider_group,
|
||||
ops.VRM_OT_move_down_spring_bone1_spring_collider_group,
|
||||
)
|
||||
|
||||
for collider_group_collection_op in collider_group_collection_ops:
|
||||
collider_group_collection_op.armature_object_name = armature.name
|
||||
collider_group_collection_op.spring_index = spring_index
|
||||
|
||||
for collider_group_collection_item_op in collider_group_collection_item_ops:
|
||||
collider_group_collection_item_op.collider_group_index = collider_group_index
|
||||
|
||||
|
||||
class VRM_PT_spring_bone1_armature_object_property(Panel):
|
||||
bl_idname = "VRM_PT_vrm1_spring_bone_armature_object_property"
|
||||
bl_label = "Spring Bone"
|
||||
bl_translation_context = "VRM"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "object"
|
||||
bl_options: AbstractSet[str] = {"DEFAULT_CLOSED"}
|
||||
bl_parent_id = VRM_PT_vrm_armature_object_property.bl_idname
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return active_object_is_vrm1_armature(context)
|
||||
|
||||
def draw_header(self, _context: Context) -> None:
|
||||
self.layout.label(icon="PHYSICS")
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
active_object = context.active_object
|
||||
if not active_object:
|
||||
return
|
||||
armature_data = active_object.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
draw_spring_bone1_spring_bone_layout(
|
||||
active_object,
|
||||
self.layout,
|
||||
get_armature_extension(armature_data).spring_bone1,
|
||||
)
|
||||
|
||||
|
||||
class VRM_PT_spring_bone1_ui(Panel):
|
||||
bl_idname = "VRM_PT_vrm1_spring_bone_ui"
|
||||
bl_label = "Spring Bone"
|
||||
bl_translation_context = "VRM"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "VRM"
|
||||
bl_options: AbstractSet[str] = {"DEFAULT_CLOSED"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return search.current_armature_is_vrm1(context)
|
||||
|
||||
def draw_header(self, _context: Context) -> None:
|
||||
self.layout.label(icon="PHYSICS")
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
armature = search.current_armature(context)
|
||||
if not armature:
|
||||
return
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
draw_spring_bone1_spring_bone_layout(
|
||||
armature,
|
||||
self.layout,
|
||||
get_armature_extension(armature_data).spring_bone1,
|
||||
)
|
||||
|
||||
|
||||
class VRM_PT_spring_bone1_collider_property(Panel):
|
||||
bl_idname = "VRM_PT_spring_bone1_collider_property"
|
||||
bl_label = "VRM Spring Bone Collider"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "data"
|
||||
|
||||
@classmethod
|
||||
def active_armature_and_collider(
|
||||
cls, context: Context
|
||||
) -> Optional[tuple[Object, SpringBone1ColliderPropertyGroup]]:
|
||||
active_object = context.active_object
|
||||
if not active_object:
|
||||
return None
|
||||
if active_object.type != "EMPTY":
|
||||
return None
|
||||
parent = active_object.parent
|
||||
if not parent:
|
||||
return None
|
||||
|
||||
if active_object.parent_type == "BONE":
|
||||
collider_object = context.active_object
|
||||
elif active_object.parent_type == "OBJECT":
|
||||
if parent.type == "ARMATURE":
|
||||
collider_object = active_object
|
||||
elif parent.parent_type == "BONE" or (
|
||||
parent.parent_type == "OBJECT"
|
||||
and parent.parent
|
||||
and parent.parent.type == "ARMATURE"
|
||||
):
|
||||
collider_object = parent
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
for obj in context.blend_data.objects:
|
||||
if obj.type != "ARMATURE":
|
||||
continue
|
||||
armature_data = obj.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
continue
|
||||
for collider in get_armature_extension(
|
||||
armature_data
|
||||
).spring_bone1.colliders:
|
||||
if collider.bpy_object == collider_object:
|
||||
return (obj, collider)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return cls.active_armature_and_collider(context) is not None
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
armature_and_collider = self.active_armature_and_collider(context)
|
||||
if armature_and_collider is None:
|
||||
return
|
||||
armature, collider = armature_and_collider
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
if get_armature_extension(armature_data).is_vrm1():
|
||||
draw_spring_bone1_collider_layout(armature, self.layout.column(), collider)
|
||||
return
|
||||
|
||||
self.layout.label(text="This is a VRM 1.0 Spring Bone Collider", icon="INFO")
|
||||
1271
editor/spring_bone1/property_group.py
Normal file
1271
editor/spring_bone1/property_group.py
Normal file
File diff suppressed because it is too large
Load Diff
246
editor/spring_bone1/ui_list.py
Normal file
246
editor/spring_bone1/ui_list.py
Normal file
@@ -0,0 +1,246 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from bpy.types import Armature, Context, UILayout, UIList
|
||||
|
||||
from ...common.logger import get_logger
|
||||
from ..extension import get_armature_extension
|
||||
from .property_group import (
|
||||
SpringBone1ColliderGroupPropertyGroup,
|
||||
SpringBone1ColliderGroupReferencePropertyGroup,
|
||||
SpringBone1ColliderPropertyGroup,
|
||||
SpringBone1ColliderReferencePropertyGroup,
|
||||
SpringBone1JointPropertyGroup,
|
||||
SpringBone1SpringPropertyGroup,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class VRM_UL_spring_bone1_collider(UIList):
|
||||
bl_idname = "VRM_UL_spring_bone1_collider"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
collider: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
_index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(collider, SpringBone1ColliderPropertyGroup):
|
||||
return
|
||||
|
||||
icon = "SPHERE"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
name = ""
|
||||
bpy_object = collider.bpy_object
|
||||
if bpy_object:
|
||||
name = bpy_object.name
|
||||
layout.label(text=name, translate=False, icon=icon)
|
||||
|
||||
|
||||
class VRM_UL_spring_bone1_collider_group(UIList):
|
||||
bl_idname = "VRM_UL_spring_bone1_collider_group"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
collider_group: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
_index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(collider_group, SpringBone1ColliderGroupPropertyGroup):
|
||||
return
|
||||
|
||||
icon = "PIVOT_INDIVIDUAL"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
layout.label(text=collider_group.vrm_name, translate=False, icon=icon)
|
||||
|
||||
|
||||
class VRM_UL_spring_bone1_collider_group_collider(UIList):
|
||||
bl_idname = "VRM_UL_spring_bone1_collider_group_collider"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
collider_group: object,
|
||||
collider: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(collider_group, SpringBone1ColliderGroupPropertyGroup):
|
||||
return
|
||||
if not isinstance(collider, SpringBone1ColliderReferencePropertyGroup):
|
||||
return
|
||||
icon = "SPHERE"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
armature_data = collider_group.id_data
|
||||
if not isinstance(armature_data, Armature):
|
||||
logger.error("Failed to find armature")
|
||||
return
|
||||
spring_bone = get_armature_extension(armature_data).spring_bone1
|
||||
|
||||
if index == collider_group.active_collider_index:
|
||||
layout.prop_search(
|
||||
collider,
|
||||
"collider_name",
|
||||
spring_bone,
|
||||
"colliders",
|
||||
text="",
|
||||
translate=False,
|
||||
icon=icon,
|
||||
)
|
||||
else:
|
||||
layout.label(text=collider.collider_name, translate=False, icon=icon)
|
||||
|
||||
|
||||
class VRM_UL_spring_bone1_spring(UIList):
|
||||
bl_idname = "VRM_UL_spring_bone1_spring"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
spring: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
_index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(spring, SpringBone1SpringPropertyGroup):
|
||||
return
|
||||
|
||||
icon = "PHYSICS"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
layout.label(text=spring.vrm_name, translate=False, icon=icon)
|
||||
|
||||
|
||||
class VRM_UL_spring_bone1_joint(UIList):
|
||||
bl_idname = "VRM_UL_spring_bone1_joint"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
joint: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
_index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(joint, SpringBone1JointPropertyGroup):
|
||||
return
|
||||
|
||||
icon = "BONE_DATA"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
layout.label(text=joint.node.bone_name, translate=False, icon=icon)
|
||||
|
||||
|
||||
class VRM_UL_spring_bone1_spring_collider_group(UIList):
|
||||
bl_idname = "VRM_UL_spring_bone1_spring_collider_group"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
spring: object,
|
||||
collider_group: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(spring, SpringBone1SpringPropertyGroup):
|
||||
return
|
||||
if not isinstance(
|
||||
collider_group, SpringBone1ColliderGroupReferencePropertyGroup
|
||||
):
|
||||
return
|
||||
|
||||
icon = "PIVOT_INDIVIDUAL"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
armature_data = spring.id_data
|
||||
if not isinstance(armature_data, Armature):
|
||||
logger.error("Failed to find armature")
|
||||
return
|
||||
spring_bone = get_armature_extension(armature_data).spring_bone1
|
||||
|
||||
if index == spring.active_collider_group_index:
|
||||
layout.prop_search(
|
||||
collider_group,
|
||||
"collider_group_name",
|
||||
spring_bone,
|
||||
"collider_groups",
|
||||
text="",
|
||||
translate=False,
|
||||
icon=icon,
|
||||
)
|
||||
else:
|
||||
layout.label(
|
||||
text=collider_group.collider_group_name, translate=False, icon=icon
|
||||
)
|
||||
147
editor/subscription.py
Normal file
147
editor/subscription.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from dataclasses import dataclass
|
||||
|
||||
import bpy
|
||||
from bpy.types import Armature, Bone, Object
|
||||
|
||||
from ..common.logger import get_logger
|
||||
from .extension import VrmAddonArmatureExtensionPropertyGroup, get_armature_extension
|
||||
from .migration import migrate_all_objects
|
||||
from .mtoon1 import ops as mtoon1_ops
|
||||
from .vrm0.property_group import Vrm0HumanoidPropertyGroup
|
||||
from .vrm1 import property_group as vrm1_property_group
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Subscription:
|
||||
object_name_subscription_owner = object()
|
||||
object_mode_subscription_owner = object()
|
||||
object_location_subscription_owner = object()
|
||||
bone_name_subscription_owner = object()
|
||||
armature_name_subscription_owner = object()
|
||||
setup_once: bool = False
|
||||
|
||||
|
||||
subscription = Subscription()
|
||||
|
||||
|
||||
def setup_subscription(*, load_post: bool) -> None:
|
||||
if subscription.setup_once:
|
||||
if load_post:
|
||||
# If called by load_post, unsubscribe and setup again.
|
||||
teardown_subscription()
|
||||
else:
|
||||
# If a subscription is already setup, do nothing.
|
||||
return
|
||||
|
||||
subscription.setup_once = True
|
||||
|
||||
object_name_subscribe_to = (Object, "name")
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=object_name_subscribe_to,
|
||||
owner=subscription.object_name_subscription_owner,
|
||||
args=(),
|
||||
notify=on_change_bpy_object_name,
|
||||
)
|
||||
|
||||
object_mode_subscribe_to = (Object, "mode")
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=object_mode_subscribe_to,
|
||||
owner=subscription.object_mode_subscription_owner,
|
||||
args=(),
|
||||
notify=on_change_bpy_object_mode,
|
||||
)
|
||||
|
||||
object_location_subscribe_to = (Object, "location")
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=object_location_subscribe_to,
|
||||
owner=subscription.object_location_subscription_owner,
|
||||
args=(),
|
||||
notify=on_change_bpy_object_location,
|
||||
)
|
||||
|
||||
bone_name_subscribe_to = (Bone, "name")
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=bone_name_subscribe_to,
|
||||
owner=subscription.bone_name_subscription_owner,
|
||||
args=(),
|
||||
notify=on_change_bpy_bone_name,
|
||||
)
|
||||
|
||||
armature_name_subscribe_to = (Armature, "name")
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=armature_name_subscribe_to,
|
||||
owner=subscription.armature_name_subscription_owner,
|
||||
args=(),
|
||||
notify=on_change_bpy_armature_name,
|
||||
)
|
||||
|
||||
bpy.msgbus.publish_rna(key=object_name_subscribe_to)
|
||||
bpy.msgbus.publish_rna(key=object_mode_subscribe_to)
|
||||
bpy.msgbus.publish_rna(key=object_location_subscribe_to)
|
||||
bpy.msgbus.publish_rna(key=bone_name_subscribe_to)
|
||||
bpy.msgbus.publish_rna(key=armature_name_subscribe_to)
|
||||
|
||||
|
||||
def teardown_subscription() -> None:
|
||||
subscription.setup_once = False
|
||||
bpy.msgbus.clear_by_owner(subscription.armature_name_subscription_owner)
|
||||
bpy.msgbus.clear_by_owner(subscription.bone_name_subscription_owner)
|
||||
bpy.msgbus.clear_by_owner(subscription.object_location_subscription_owner)
|
||||
bpy.msgbus.clear_by_owner(subscription.object_mode_subscription_owner)
|
||||
bpy.msgbus.clear_by_owner(subscription.object_name_subscription_owner)
|
||||
|
||||
|
||||
def on_change_bpy_object_name() -> None:
|
||||
context = bpy.context
|
||||
|
||||
for armature in context.blend_data.armatures:
|
||||
ext = get_armature_extension(armature)
|
||||
if (
|
||||
tuple(ext.addon_version)
|
||||
== VrmAddonArmatureExtensionPropertyGroup.INITIAL_ADDON_VERSION
|
||||
):
|
||||
continue
|
||||
|
||||
# TODO: Needs optimization!
|
||||
for collider in ext.spring_bone1.colliders:
|
||||
collider.broadcast_bpy_object_name()
|
||||
|
||||
|
||||
def on_change_bpy_object_mode() -> None:
|
||||
context = bpy.context
|
||||
|
||||
active_object = context.active_object
|
||||
if not active_object:
|
||||
return
|
||||
mtoon1_ops.VRM_OT_refresh_mtoon1_outline.refresh_object(context, active_object)
|
||||
|
||||
|
||||
def on_change_bpy_object_location() -> None:
|
||||
context = bpy.context
|
||||
active_object = context.active_object
|
||||
if not active_object:
|
||||
return
|
||||
vrm1_property_group.Vrm1LookAtPropertyGroup.update_all_previews(context)
|
||||
|
||||
|
||||
def on_change_bpy_bone_name() -> None:
|
||||
context = bpy.context
|
||||
|
||||
for armature in context.blend_data.armatures:
|
||||
ext = get_armature_extension(armature)
|
||||
if (
|
||||
tuple(ext.addon_version)
|
||||
== VrmAddonArmatureExtensionPropertyGroup.INITIAL_ADDON_VERSION
|
||||
):
|
||||
continue
|
||||
|
||||
Vrm0HumanoidPropertyGroup.update_all_node_candidates(context, armature.name)
|
||||
|
||||
|
||||
def on_change_bpy_armature_name() -> None:
|
||||
context = bpy.context
|
||||
|
||||
migrate_all_objects(context, skip_non_migrated_armatures=True)
|
||||
716
editor/t_pose.py
Normal file
716
editor/t_pose.py
Normal file
@@ -0,0 +1,716 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import math
|
||||
from collections.abc import Iterator, Mapping
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from sys import float_info
|
||||
from typing import Optional, Union
|
||||
|
||||
import bpy
|
||||
from bpy.types import (
|
||||
Armature,
|
||||
Context,
|
||||
Object,
|
||||
Pose,
|
||||
PoseBone,
|
||||
)
|
||||
from mathutils import Euler, Matrix, Quaternion, Vector
|
||||
|
||||
from ..common import ops
|
||||
from ..common.logger import get_logger
|
||||
from ..common.rotation import (
|
||||
get_rotation_as_quaternion,
|
||||
set_rotation_without_mode_change,
|
||||
)
|
||||
from ..common.vrm0.human_bone import HumanBoneName as Vrm0HumanBoneName
|
||||
from ..common.vrm1.human_bone import HumanBoneName as Vrm1HumanBoneName
|
||||
from ..common.workspace import save_workspace
|
||||
from .extension import VrmAddonArmatureExtensionPropertyGroup, get_armature_extension
|
||||
from .vrm0.property_group import Vrm0HumanoidPropertyGroup
|
||||
from .vrm1.property_group import Vrm1HumanoidPropertyGroup
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def set_bone_direction_to_align_child_bone(
|
||||
context: Context,
|
||||
armature: Object,
|
||||
direction: Vector,
|
||||
bone: PoseBone,
|
||||
child_bone: PoseBone,
|
||||
) -> None:
|
||||
world_bone_matrix = armature.matrix_world @ bone.matrix
|
||||
world_child_bone_matrix = armature.matrix_world @ child_bone.matrix
|
||||
|
||||
world_bone_length = (
|
||||
world_child_bone_matrix.translation - world_bone_matrix.translation
|
||||
).length
|
||||
|
||||
world_child_bone_from_translation = world_child_bone_matrix.translation
|
||||
world_child_bone_to_translation = (
|
||||
world_bone_matrix.translation + direction * world_bone_length
|
||||
)
|
||||
|
||||
bone_local_child_bone_from_translation = (
|
||||
armature.matrix_world @ bone.matrix
|
||||
).inverted_safe() @ world_child_bone_from_translation
|
||||
bone_local_child_bone_to_translation = (
|
||||
armature.matrix_world @ bone.matrix
|
||||
).inverted_safe() @ world_child_bone_to_translation
|
||||
|
||||
if bone_local_child_bone_from_translation.length_squared < float_info.epsilon:
|
||||
return
|
||||
if bone_local_child_bone_to_translation.length_squared < float_info.epsilon:
|
||||
return
|
||||
|
||||
rotation = bone_local_child_bone_from_translation.rotation_difference(
|
||||
bone_local_child_bone_to_translation
|
||||
)
|
||||
|
||||
set_rotation_without_mode_change(bone, rotation @ get_rotation_as_quaternion(bone))
|
||||
|
||||
context.view_layer.update()
|
||||
|
||||
|
||||
def set_bone_direction_to_align_z_world_location(
|
||||
context: Context,
|
||||
armature: Object,
|
||||
direction: Vector,
|
||||
bone: PoseBone,
|
||||
from_world_location: Vector,
|
||||
) -> None:
|
||||
world_bone_translation = (armature.matrix_world @ bone.matrix).to_translation()
|
||||
aligned_from_world_location = Vector(
|
||||
(
|
||||
from_world_location.x,
|
||||
world_bone_translation.y,
|
||||
from_world_location.z,
|
||||
)
|
||||
)
|
||||
aligned_to_world_location = world_bone_translation + direction
|
||||
|
||||
bone_local_aligned_from_world_location = (
|
||||
armature.matrix_world @ bone.matrix
|
||||
).inverted_safe() @ aligned_from_world_location
|
||||
bone_local_aligned_to_world_location = (
|
||||
armature.matrix_world @ bone.matrix
|
||||
).inverted_safe() @ aligned_to_world_location
|
||||
|
||||
if bone_local_aligned_from_world_location.length_squared < float_info.epsilon:
|
||||
return
|
||||
if bone_local_aligned_to_world_location.length_squared < float_info.epsilon:
|
||||
return
|
||||
|
||||
rotation = bone_local_aligned_from_world_location.rotation_difference(
|
||||
bone_local_aligned_to_world_location
|
||||
)
|
||||
|
||||
set_rotation_without_mode_change(bone, rotation @ get_rotation_as_quaternion(bone))
|
||||
|
||||
context.view_layer.update()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChainSingleChild:
|
||||
direction: Vector
|
||||
vrm0_human_bone_names: tuple[Vrm0HumanBoneName, ...]
|
||||
vrm1_human_bone_names: tuple[Vrm1HumanBoneName, ...]
|
||||
|
||||
def execute(self, context: Context, armature: Object) -> None:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
raise TypeError
|
||||
|
||||
ext = get_armature_extension(armature_data)
|
||||
if ext.is_vrm0():
|
||||
humanoid = ext.vrm0.humanoid
|
||||
bones: list[PoseBone] = []
|
||||
for human_bone in humanoid.human_bones:
|
||||
human_bone_name = Vrm0HumanBoneName.from_str(human_bone.bone)
|
||||
if not human_bone_name:
|
||||
continue
|
||||
if human_bone_name not in self.vrm0_human_bone_names:
|
||||
continue
|
||||
bone = armature.pose.bones.get(human_bone.node.bone_name)
|
||||
if not bone:
|
||||
continue
|
||||
bones.append(bone)
|
||||
else:
|
||||
human_bones = ext.vrm1.humanoid.human_bones
|
||||
human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
|
||||
bones = [
|
||||
bone
|
||||
for bone in [
|
||||
armature.pose.bones.get(human_bone.node.bone_name)
|
||||
for human_bone in [
|
||||
human_bone_name_to_human_bone.get(human_bone_name)
|
||||
for human_bone_name in self.vrm1_human_bone_names
|
||||
]
|
||||
if human_bone
|
||||
]
|
||||
if bone
|
||||
]
|
||||
|
||||
if len(bones) < 2:
|
||||
return
|
||||
|
||||
root_bone = bones[0]
|
||||
tip_bone = bones[-1]
|
||||
|
||||
chained_bones: list[PoseBone] = []
|
||||
searching_bone: Optional[PoseBone] = tip_bone
|
||||
while True:
|
||||
if not searching_bone:
|
||||
return
|
||||
chained_bones.insert(0, searching_bone)
|
||||
if searching_bone == root_bone:
|
||||
break
|
||||
searching_bone = searching_bone.parent
|
||||
|
||||
for bone, child_bone in zip(chained_bones, chained_bones[1:]):
|
||||
set_bone_direction_to_align_child_bone(
|
||||
context, armature, self.direction, bone, child_bone
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChainHorizontalMultipleChildren:
|
||||
direction: Vector
|
||||
vrm0_parent_human_bone_name: Vrm0HumanBoneName
|
||||
vrm0_human_bone_names: tuple[Vrm0HumanBoneName, ...]
|
||||
vrm1_parent_human_bone_name: Vrm1HumanBoneName
|
||||
vrm1_human_bone_names: tuple[Vrm1HumanBoneName, ...]
|
||||
|
||||
def execute(self, context: Context, armature: Object) -> None:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
raise TypeError
|
||||
|
||||
ext = get_armature_extension(armature_data)
|
||||
if ext.is_vrm0():
|
||||
humanoid = ext.vrm0.humanoid
|
||||
bones: list[PoseBone] = []
|
||||
parent_bone: Optional[PoseBone] = None
|
||||
for human_bone in humanoid.human_bones:
|
||||
human_bone_name = Vrm0HumanBoneName.from_str(human_bone.bone)
|
||||
if not human_bone_name:
|
||||
continue
|
||||
if human_bone_name == self.vrm0_parent_human_bone_name:
|
||||
parent_bone = armature.pose.bones.get(human_bone.node.bone_name)
|
||||
elif human_bone_name in self.vrm0_human_bone_names:
|
||||
bone = armature.pose.bones.get(human_bone.node.bone_name)
|
||||
if bone:
|
||||
bones.append(bone)
|
||||
else:
|
||||
human_bones = ext.vrm1.humanoid.human_bones
|
||||
human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
|
||||
bones = [
|
||||
bone
|
||||
for bone in [
|
||||
armature.pose.bones.get(human_bone.node.bone_name)
|
||||
for human_bone in [
|
||||
human_bone_name_to_human_bone.get(human_bone_name)
|
||||
for human_bone_name in self.vrm1_human_bone_names
|
||||
]
|
||||
if human_bone
|
||||
]
|
||||
if bone
|
||||
]
|
||||
parent_human_bone = human_bone_name_to_human_bone.get(
|
||||
self.vrm1_parent_human_bone_name
|
||||
)
|
||||
parent_bone = None
|
||||
if parent_human_bone:
|
||||
parent_bone = armature.pose.bones.get(parent_human_bone.node.bone_name)
|
||||
if not bones:
|
||||
return
|
||||
world_location = Vector((0, 0, 0))
|
||||
for bone in bones:
|
||||
world_location += (armature.matrix_world @ bone.matrix).to_translation()
|
||||
world_location /= len(bones)
|
||||
|
||||
if not parent_bone:
|
||||
return
|
||||
|
||||
set_bone_direction_to_align_z_world_location(
|
||||
context, armature, self.direction, parent_bone, world_location
|
||||
)
|
||||
|
||||
|
||||
def reset_root_to_human_bone_translation(
|
||||
pose: Pose, ext: VrmAddonArmatureExtensionPropertyGroup
|
||||
) -> None:
|
||||
# Reset positions from root bone to any Human bone
|
||||
# No particular reason for not resetting all bones, just intuition
|
||||
bones: list[PoseBone] = [bone for bone in pose.bones if not bone.parent]
|
||||
while bones:
|
||||
bone = bones.pop()
|
||||
bone.location = Vector((0, 0, 0))
|
||||
|
||||
if ext.is_vrm1():
|
||||
human_bone_name_to_human_bones = (
|
||||
ext.vrm1.humanoid.human_bones.human_bone_name_to_human_bone()
|
||||
)
|
||||
if any(
|
||||
True
|
||||
for human_bone in human_bone_name_to_human_bones.values()
|
||||
if human_bone.node.bone_name == bone.name
|
||||
):
|
||||
continue
|
||||
|
||||
if ext.is_vrm0() and any(
|
||||
True
|
||||
for human_bone in ext.vrm0.humanoid.human_bones
|
||||
if human_bone.node.bone_name == bone.name
|
||||
):
|
||||
continue
|
||||
|
||||
bones.extend(bone.children)
|
||||
|
||||
|
||||
def set_estimated_humanoid_t_pose(context: Context, armature: Object) -> bool:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return False
|
||||
|
||||
ext = get_armature_extension(armature_data)
|
||||
if ext.is_vrm0():
|
||||
if not ext.vrm0.humanoid.all_required_bones_are_assigned():
|
||||
return False
|
||||
else:
|
||||
human_bones = ext.vrm1.humanoid.human_bones
|
||||
if not human_bones.all_required_bones_are_assigned():
|
||||
return False
|
||||
|
||||
for bone in armature.pose.bones:
|
||||
set_rotation_without_mode_change(bone, Quaternion())
|
||||
|
||||
reset_root_to_human_bone_translation(armature.pose, ext)
|
||||
|
||||
context.view_layer.update()
|
||||
|
||||
# https://github.com/vrm-c/vrm-specification/blob/73855bb77d431a3374212551a4fa48e043be3ced/specification/VRMC_vrm-1.0/tpose.md
|
||||
chains: tuple[Union[ChainSingleChild, ChainHorizontalMultipleChildren], ...] = (
|
||||
ChainSingleChild(
|
||||
Vector((-1, 0, 0)),
|
||||
(
|
||||
Vrm0HumanBoneName.RIGHT_UPPER_ARM,
|
||||
Vrm0HumanBoneName.RIGHT_LOWER_ARM,
|
||||
Vrm0HumanBoneName.RIGHT_HAND,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.RIGHT_UPPER_ARM,
|
||||
Vrm1HumanBoneName.RIGHT_LOWER_ARM,
|
||||
Vrm1HumanBoneName.RIGHT_HAND,
|
||||
),
|
||||
),
|
||||
ChainHorizontalMultipleChildren(
|
||||
Vector((-1, 0, 0)),
|
||||
Vrm0HumanBoneName.RIGHT_HAND,
|
||||
(
|
||||
Vrm0HumanBoneName.RIGHT_INDEX_PROXIMAL,
|
||||
Vrm0HumanBoneName.RIGHT_MIDDLE_PROXIMAL,
|
||||
Vrm0HumanBoneName.RIGHT_RING_PROXIMAL,
|
||||
Vrm0HumanBoneName.RIGHT_LITTLE_PROXIMAL,
|
||||
),
|
||||
Vrm1HumanBoneName.RIGHT_HAND,
|
||||
(
|
||||
Vrm1HumanBoneName.RIGHT_INDEX_PROXIMAL,
|
||||
Vrm1HumanBoneName.RIGHT_MIDDLE_PROXIMAL,
|
||||
Vrm1HumanBoneName.RIGHT_RING_PROXIMAL,
|
||||
Vrm1HumanBoneName.RIGHT_LITTLE_PROXIMAL,
|
||||
),
|
||||
),
|
||||
ChainSingleChild(
|
||||
Vector((-math.sqrt(0.5), -math.sqrt(0.5), 0)),
|
||||
(
|
||||
Vrm0HumanBoneName.RIGHT_THUMB_PROXIMAL,
|
||||
Vrm0HumanBoneName.RIGHT_THUMB_INTERMEDIATE,
|
||||
Vrm0HumanBoneName.RIGHT_THUMB_DISTAL,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.RIGHT_THUMB_METACARPAL,
|
||||
Vrm1HumanBoneName.RIGHT_THUMB_PROXIMAL,
|
||||
Vrm1HumanBoneName.RIGHT_THUMB_DISTAL,
|
||||
),
|
||||
),
|
||||
ChainSingleChild(
|
||||
Vector((-1, 0, 0)),
|
||||
(
|
||||
Vrm0HumanBoneName.RIGHT_INDEX_PROXIMAL,
|
||||
Vrm0HumanBoneName.RIGHT_INDEX_INTERMEDIATE,
|
||||
Vrm0HumanBoneName.RIGHT_INDEX_DISTAL,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.RIGHT_INDEX_PROXIMAL,
|
||||
Vrm1HumanBoneName.RIGHT_INDEX_INTERMEDIATE,
|
||||
Vrm1HumanBoneName.RIGHT_INDEX_DISTAL,
|
||||
),
|
||||
),
|
||||
ChainSingleChild(
|
||||
Vector((-1, 0, 0)),
|
||||
(
|
||||
Vrm0HumanBoneName.RIGHT_MIDDLE_PROXIMAL,
|
||||
Vrm0HumanBoneName.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
Vrm0HumanBoneName.RIGHT_MIDDLE_DISTAL,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.RIGHT_MIDDLE_PROXIMAL,
|
||||
Vrm1HumanBoneName.RIGHT_MIDDLE_INTERMEDIATE,
|
||||
Vrm1HumanBoneName.RIGHT_MIDDLE_DISTAL,
|
||||
),
|
||||
),
|
||||
ChainSingleChild(
|
||||
Vector((-1, 0, 0)),
|
||||
(
|
||||
Vrm0HumanBoneName.RIGHT_RING_PROXIMAL,
|
||||
Vrm0HumanBoneName.RIGHT_RING_INTERMEDIATE,
|
||||
Vrm0HumanBoneName.RIGHT_RING_DISTAL,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.RIGHT_RING_PROXIMAL,
|
||||
Vrm1HumanBoneName.RIGHT_RING_INTERMEDIATE,
|
||||
Vrm1HumanBoneName.RIGHT_RING_DISTAL,
|
||||
),
|
||||
),
|
||||
ChainSingleChild(
|
||||
Vector((-1, 0, 0)),
|
||||
(
|
||||
Vrm0HumanBoneName.RIGHT_LITTLE_PROXIMAL,
|
||||
Vrm0HumanBoneName.RIGHT_LITTLE_INTERMEDIATE,
|
||||
Vrm0HumanBoneName.RIGHT_LITTLE_DISTAL,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.RIGHT_LITTLE_PROXIMAL,
|
||||
Vrm1HumanBoneName.RIGHT_LITTLE_INTERMEDIATE,
|
||||
Vrm1HumanBoneName.RIGHT_LITTLE_DISTAL,
|
||||
),
|
||||
),
|
||||
ChainSingleChild(
|
||||
Vector((0, 0, -1)),
|
||||
(
|
||||
Vrm0HumanBoneName.RIGHT_UPPER_LEG,
|
||||
Vrm0HumanBoneName.RIGHT_LOWER_LEG,
|
||||
Vrm0HumanBoneName.RIGHT_FOOT,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.RIGHT_UPPER_LEG,
|
||||
Vrm1HumanBoneName.RIGHT_LOWER_LEG,
|
||||
Vrm1HumanBoneName.RIGHT_FOOT,
|
||||
),
|
||||
),
|
||||
ChainSingleChild(
|
||||
Vector((1, 0, 0)),
|
||||
(
|
||||
Vrm0HumanBoneName.LEFT_UPPER_ARM,
|
||||
Vrm0HumanBoneName.LEFT_LOWER_ARM,
|
||||
Vrm0HumanBoneName.LEFT_HAND,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.LEFT_UPPER_ARM,
|
||||
Vrm1HumanBoneName.LEFT_LOWER_ARM,
|
||||
Vrm1HumanBoneName.LEFT_HAND,
|
||||
),
|
||||
),
|
||||
ChainHorizontalMultipleChildren(
|
||||
Vector((1, 0, 0)),
|
||||
Vrm0HumanBoneName.LEFT_HAND,
|
||||
(
|
||||
Vrm0HumanBoneName.LEFT_INDEX_PROXIMAL,
|
||||
Vrm0HumanBoneName.LEFT_MIDDLE_PROXIMAL,
|
||||
Vrm0HumanBoneName.LEFT_RING_PROXIMAL,
|
||||
Vrm0HumanBoneName.LEFT_LITTLE_PROXIMAL,
|
||||
),
|
||||
Vrm1HumanBoneName.LEFT_HAND,
|
||||
(
|
||||
Vrm1HumanBoneName.LEFT_INDEX_PROXIMAL,
|
||||
Vrm1HumanBoneName.LEFT_MIDDLE_PROXIMAL,
|
||||
Vrm1HumanBoneName.LEFT_RING_PROXIMAL,
|
||||
Vrm1HumanBoneName.LEFT_LITTLE_PROXIMAL,
|
||||
),
|
||||
),
|
||||
ChainSingleChild(
|
||||
Vector((math.sqrt(0.5), -math.sqrt(0.5), 0)),
|
||||
(
|
||||
Vrm0HumanBoneName.LEFT_THUMB_PROXIMAL,
|
||||
Vrm0HumanBoneName.LEFT_THUMB_INTERMEDIATE,
|
||||
Vrm0HumanBoneName.LEFT_THUMB_DISTAL,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.LEFT_THUMB_METACARPAL,
|
||||
Vrm1HumanBoneName.LEFT_THUMB_PROXIMAL,
|
||||
Vrm1HumanBoneName.LEFT_THUMB_DISTAL,
|
||||
),
|
||||
),
|
||||
ChainSingleChild(
|
||||
Vector((1, 0, 0)),
|
||||
(
|
||||
Vrm0HumanBoneName.LEFT_INDEX_PROXIMAL,
|
||||
Vrm0HumanBoneName.LEFT_INDEX_INTERMEDIATE,
|
||||
Vrm0HumanBoneName.LEFT_INDEX_DISTAL,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.LEFT_INDEX_PROXIMAL,
|
||||
Vrm1HumanBoneName.LEFT_INDEX_INTERMEDIATE,
|
||||
Vrm1HumanBoneName.LEFT_INDEX_DISTAL,
|
||||
),
|
||||
),
|
||||
ChainSingleChild(
|
||||
Vector((1, 0, 0)),
|
||||
(
|
||||
Vrm0HumanBoneName.LEFT_MIDDLE_PROXIMAL,
|
||||
Vrm0HumanBoneName.LEFT_MIDDLE_INTERMEDIATE,
|
||||
Vrm0HumanBoneName.LEFT_MIDDLE_DISTAL,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.LEFT_MIDDLE_PROXIMAL,
|
||||
Vrm1HumanBoneName.LEFT_MIDDLE_INTERMEDIATE,
|
||||
Vrm1HumanBoneName.LEFT_MIDDLE_DISTAL,
|
||||
),
|
||||
),
|
||||
ChainSingleChild(
|
||||
Vector((1, 0, 0)),
|
||||
(
|
||||
Vrm0HumanBoneName.LEFT_RING_PROXIMAL,
|
||||
Vrm0HumanBoneName.LEFT_RING_INTERMEDIATE,
|
||||
Vrm0HumanBoneName.LEFT_RING_DISTAL,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.LEFT_RING_PROXIMAL,
|
||||
Vrm1HumanBoneName.LEFT_RING_INTERMEDIATE,
|
||||
Vrm1HumanBoneName.LEFT_RING_DISTAL,
|
||||
),
|
||||
),
|
||||
ChainSingleChild(
|
||||
Vector((1, 0, 0)),
|
||||
(
|
||||
Vrm0HumanBoneName.LEFT_LITTLE_PROXIMAL,
|
||||
Vrm0HumanBoneName.LEFT_LITTLE_INTERMEDIATE,
|
||||
Vrm0HumanBoneName.LEFT_LITTLE_DISTAL,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.LEFT_LITTLE_PROXIMAL,
|
||||
Vrm1HumanBoneName.LEFT_LITTLE_INTERMEDIATE,
|
||||
Vrm1HumanBoneName.LEFT_LITTLE_DISTAL,
|
||||
),
|
||||
),
|
||||
ChainSingleChild(
|
||||
Vector((0, 0, -1)),
|
||||
(
|
||||
Vrm0HumanBoneName.LEFT_UPPER_LEG,
|
||||
Vrm0HumanBoneName.LEFT_LOWER_LEG,
|
||||
Vrm0HumanBoneName.LEFT_FOOT,
|
||||
),
|
||||
(
|
||||
Vrm1HumanBoneName.LEFT_UPPER_LEG,
|
||||
Vrm1HumanBoneName.LEFT_LOWER_LEG,
|
||||
Vrm1HumanBoneName.LEFT_FOOT,
|
||||
),
|
||||
),
|
||||
)
|
||||
for chain in chains:
|
||||
chain.execute(context, armature)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PoseBonePose:
|
||||
matrix_basis: Matrix
|
||||
rotation_mode: str
|
||||
rotation_quaternion: Quaternion
|
||||
rotation_axis_angle: tuple[float, float, float, float]
|
||||
rotation_euler: Euler
|
||||
scale: Vector
|
||||
location: Vector
|
||||
bone_select: bool
|
||||
|
||||
@staticmethod
|
||||
def save(pose: Pose) -> Mapping[str, "PoseBonePose"]:
|
||||
return {
|
||||
bone.name: PoseBonePose(
|
||||
matrix_basis=bone.matrix_basis.copy(),
|
||||
rotation_mode=bone.rotation_mode,
|
||||
rotation_quaternion=bone.rotation_quaternion.copy(),
|
||||
rotation_axis_angle=(
|
||||
bone.rotation_axis_angle[0],
|
||||
bone.rotation_axis_angle[1],
|
||||
bone.rotation_axis_angle[2],
|
||||
bone.rotation_axis_angle[3],
|
||||
),
|
||||
rotation_euler=bone.rotation_euler.copy(),
|
||||
scale=bone.scale.copy(),
|
||||
location=bone.location.copy(),
|
||||
bone_select=(bone.bone.select if bpy.app.version < (5, 0) else False),
|
||||
)
|
||||
for bone in pose.bones
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def load(
|
||||
context: Context,
|
||||
pose: Pose,
|
||||
bone_name_to_pose_bone_pose: Mapping[str, "PoseBonePose"],
|
||||
) -> None:
|
||||
context.view_layer.update()
|
||||
|
||||
bones = [bone for bone in pose.bones if not bone.parent]
|
||||
while bones:
|
||||
bone = bones.pop()
|
||||
pose_bone_pose = bone_name_to_pose_bone_pose.get(bone.name)
|
||||
if pose_bone_pose:
|
||||
bone.matrix_basis = pose_bone_pose.matrix_basis.copy()
|
||||
# Directly restoring bone.matrix seems more efficient, but doing so
|
||||
# can cause problems when constraints are attached
|
||||
# https://github.com/saturday06/VRM-Addon-for-Blender/issues/671
|
||||
bone.rotation_mode = pose_bone_pose.rotation_mode
|
||||
bone.rotation_axis_angle = list(pose_bone_pose.rotation_axis_angle)
|
||||
bone.rotation_quaternion = pose_bone_pose.rotation_quaternion.copy()
|
||||
bone.rotation_euler = pose_bone_pose.rotation_euler.copy()
|
||||
bone.scale = pose_bone_pose.scale.copy()
|
||||
bone.location = pose_bone_pose.location.copy()
|
||||
if bpy.app.version < (5, 0):
|
||||
bone.bone.select = pose_bone_pose.bone_select
|
||||
bones.extend(bone.children)
|
||||
|
||||
context.view_layer.update()
|
||||
|
||||
|
||||
def leave_setup_humanoid_t_pose(
|
||||
context: Context,
|
||||
armature: Object,
|
||||
saved_pose_bone_pose: Mapping[str, PoseBonePose],
|
||||
saved_pose_position: str,
|
||||
*,
|
||||
saved_vrm1_look_at_preview: bool,
|
||||
) -> None:
|
||||
with save_workspace(context, armature):
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
PoseBonePose.load(context, armature.pose, saved_pose_bone_pose)
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
armature_data.pose_position = saved_pose_position
|
||||
ext = get_armature_extension(armature_data)
|
||||
if (
|
||||
ext.is_vrm1()
|
||||
and ext.vrm1.look_at.enable_preview != saved_vrm1_look_at_preview
|
||||
):
|
||||
ext.vrm1.look_at.enable_preview = saved_vrm1_look_at_preview
|
||||
|
||||
|
||||
@contextmanager
|
||||
def setup_humanoid_t_pose(
|
||||
context: Context,
|
||||
armature: Object,
|
||||
) -> Iterator[None]:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
raise TypeError
|
||||
ext = get_armature_extension(armature_data)
|
||||
if ext.is_vrm0():
|
||||
humanoid: Union[Vrm0HumanoidPropertyGroup, Vrm1HumanoidPropertyGroup] = (
|
||||
ext.vrm0.humanoid
|
||||
)
|
||||
else:
|
||||
humanoid = ext.vrm1.humanoid
|
||||
pose = humanoid.pose
|
||||
action = humanoid.pose_library
|
||||
pose_marker_name = humanoid.pose_marker_name
|
||||
|
||||
if pose != humanoid.POSE_CUSTOM_POSE.identifier:
|
||||
action = None
|
||||
pose_marker_name = ""
|
||||
|
||||
if (
|
||||
pose == humanoid.POSE_CURRENT_POSE.identifier
|
||||
and armature_data.pose_position == "REST"
|
||||
):
|
||||
yield
|
||||
return
|
||||
|
||||
if pose == humanoid.POSE_REST_POSITION_POSE.identifier or (
|
||||
pose == humanoid.POSE_CUSTOM_POSE.identifier
|
||||
and not (action and action.name in context.blend_data.actions)
|
||||
):
|
||||
saved_pose_position = armature_data.pose_position
|
||||
armature_data.pose_position = "REST"
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
armature_data.pose_position = saved_pose_position
|
||||
return
|
||||
|
||||
with save_workspace(context, armature):
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
|
||||
saved_pose_position = armature_data.pose_position
|
||||
if armature_data.pose_position != "POSE":
|
||||
armature_data.pose_position = "POSE"
|
||||
|
||||
context.view_layer.update()
|
||||
|
||||
saved_pose_bone_pose = PoseBonePose.save(armature.pose)
|
||||
|
||||
if bpy.app.version < (5, 0):
|
||||
for bone in armature.pose.bones:
|
||||
bone.bone.select = False
|
||||
|
||||
ext = get_armature_extension(armature_data)
|
||||
saved_vrm1_look_at_preview = ext.vrm1.look_at.enable_preview
|
||||
if ext.is_vrm1() and ext.vrm1.look_at.enable_preview:
|
||||
# TODO: It would be helpful to warn in advance if this is
|
||||
# reached during export
|
||||
ext.vrm1.look_at.enable_preview = False
|
||||
if ext.vrm1.look_at.type == ext.vrm1.look_at.TYPE_BONE.identifier:
|
||||
human_bones = ext.vrm1.humanoid.human_bones
|
||||
|
||||
left_eye_bone_name = human_bones.left_eye.node.bone_name
|
||||
left_eye_bone = armature.pose.bones.get(left_eye_bone_name)
|
||||
if left_eye_bone:
|
||||
set_rotation_without_mode_change(left_eye_bone, Quaternion())
|
||||
|
||||
right_eye_bone_name = human_bones.right_eye.node.bone_name
|
||||
right_eye_bone = armature.pose.bones.get(right_eye_bone_name)
|
||||
if right_eye_bone:
|
||||
set_rotation_without_mode_change(right_eye_bone, Quaternion())
|
||||
|
||||
if pose == humanoid.POSE_AUTO_POSE.identifier:
|
||||
ops.vrm.make_estimated_humanoid_t_pose(armature_object_name=armature.name)
|
||||
elif pose == humanoid.POSE_CUSTOM_POSE.identifier:
|
||||
if action and action.name in context.blend_data.actions:
|
||||
pose_marker_frame = 0
|
||||
if pose_marker_name:
|
||||
for search_pose_marker in action.pose_markers.values():
|
||||
if search_pose_marker.name == pose_marker_name:
|
||||
pose_marker_frame = search_pose_marker.frame
|
||||
break
|
||||
armature.pose.apply_pose_from_action(
|
||||
action, evaluation_time=pose_marker_frame
|
||||
)
|
||||
else:
|
||||
# TODO: It would be helpful to warn in advance if this is
|
||||
# reached during export
|
||||
ops.vrm.make_estimated_humanoid_t_pose(
|
||||
armature_object_name=armature.name
|
||||
)
|
||||
|
||||
context.view_layer.update()
|
||||
|
||||
try:
|
||||
yield
|
||||
# After yield, bpy native objects may be deleted or frames may advance
|
||||
# making them invalid. Accessing them in this state can cause crashes,
|
||||
# so be careful not to access potentially invalid native objects after yield
|
||||
finally:
|
||||
leave_setup_humanoid_t_pose(
|
||||
context,
|
||||
armature,
|
||||
saved_pose_bone_pose,
|
||||
saved_pose_position,
|
||||
saved_vrm1_look_at_preview=saved_vrm1_look_at_preview,
|
||||
)
|
||||
1006
editor/validation.py
Normal file
1006
editor/validation.py
Normal file
File diff suppressed because it is too large
Load Diff
1
editor/vrm0/__init__.py
Normal file
1
editor/vrm0/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
BIN
editor/vrm0/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
editor/vrm0/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm0/__pycache__/handler.cpython-311.pyc
Normal file
BIN
editor/vrm0/__pycache__/handler.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm0/__pycache__/menu.cpython-311.pyc
Normal file
BIN
editor/vrm0/__pycache__/menu.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm0/__pycache__/migration.cpython-311.pyc
Normal file
BIN
editor/vrm0/__pycache__/migration.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm0/__pycache__/ops.cpython-311.pyc
Normal file
BIN
editor/vrm0/__pycache__/ops.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm0/__pycache__/panel.cpython-311.pyc
Normal file
BIN
editor/vrm0/__pycache__/panel.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm0/__pycache__/property_group.cpython-311.pyc
Normal file
BIN
editor/vrm0/__pycache__/property_group.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm0/__pycache__/ui_list.cpython-311.pyc
Normal file
BIN
editor/vrm0/__pycache__/ui_list.cpython-311.pyc
Normal file
Binary file not shown.
62
editor/vrm0/gizmo_group.py
Normal file
62
editor/vrm0/gizmo_group.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Set as AbstractSet
|
||||
|
||||
from bpy.types import Armature, Context, GizmoGroup
|
||||
|
||||
from ..extension import get_armature_extension
|
||||
|
||||
|
||||
# https://gist.github.com/FujiSunflower/09fdabc7ca991f8292657abc4ef001b0
|
||||
class Vrm0FirstPersonBoneOffsetGizmoGroup(GizmoGroup):
|
||||
bl_idname = "VRM_GGT_vrm0_first_person_bone_offset"
|
||||
bl_label = "First Person Bone Offset Gizmo"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_options: AbstractSet[str] = {"3D", "PERSISTENT"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
active_object = context.active_object
|
||||
if not active_object:
|
||||
return False
|
||||
return active_object.type == "ARMATURE"
|
||||
|
||||
def setup(self, context: Context) -> None:
|
||||
active_object = context.active_object
|
||||
if not active_object:
|
||||
return
|
||||
armature_data = active_object.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
ext = get_armature_extension(armature_data)
|
||||
first_person = ext.vrm0.first_person
|
||||
first_person_bone = armature_data.bones[
|
||||
first_person.first_person_bone.bone_name
|
||||
]
|
||||
gizmo = self.gizmos.new("GIZMO_GT_move_3d")
|
||||
gizmo.target_set_prop("offset", first_person, "first_person_bone_offset")
|
||||
gizmo.matrix_basis = first_person_bone.matrix_local
|
||||
gizmo.draw_style = "CROSS_2D"
|
||||
gizmo.draw_options = {"ALIGN_VIEW"}
|
||||
gizmo.color = 1.0, 0.5, 0.0
|
||||
gizmo.alpha = 0.5
|
||||
gizmo.color_highlight = 1.0, 0.5, 1.0
|
||||
gizmo.alpha_highlight = 0.5
|
||||
gizmo.scale_basis = 0.25
|
||||
|
||||
self.first_person_gizmo = gizmo
|
||||
|
||||
def refresh(self, context: Context) -> None:
|
||||
active_object = context.active_object
|
||||
if not active_object:
|
||||
return
|
||||
armature_data = active_object.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
ext = get_armature_extension(armature_data)
|
||||
gizmo = self.first_person_gizmo
|
||||
first_person = ext.vrm0.first_person
|
||||
first_person_bone = armature_data.bones[
|
||||
first_person.first_person_bone.bone_name
|
||||
]
|
||||
gizmo.matrix_basis = first_person_bone.matrix_local
|
||||
37
editor/vrm0/handler.py
Normal file
37
editor/vrm0/handler.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from ...common.logger import get_logger
|
||||
from .property_group import Vrm0BlendShapeGroupPropertyGroup
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@persistent
|
||||
def frame_change_pre(_unused: object) -> None:
|
||||
Vrm0BlendShapeGroupPropertyGroup.frame_change_post_shape_key_updates.clear()
|
||||
|
||||
|
||||
@persistent
|
||||
def frame_change_post(_unused: object) -> None:
|
||||
context = bpy.context
|
||||
|
||||
for (
|
||||
(
|
||||
shape_key_name,
|
||||
key_block_name,
|
||||
),
|
||||
value,
|
||||
) in Vrm0BlendShapeGroupPropertyGroup.frame_change_post_shape_key_updates.items():
|
||||
shape_key = context.blend_data.shape_keys.get(shape_key_name)
|
||||
if not shape_key:
|
||||
continue
|
||||
key_blocks = shape_key.key_blocks
|
||||
if not key_blocks:
|
||||
continue
|
||||
key_block = key_blocks.get(key_block_name)
|
||||
if not key_block:
|
||||
continue
|
||||
key_block.value = value
|
||||
Vrm0BlendShapeGroupPropertyGroup.frame_change_post_shape_key_updates.clear()
|
||||
24
editor/vrm0/menu.py
Normal file
24
editor/vrm0/menu.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from bpy.types import Context, Menu
|
||||
|
||||
from ...common.logger import get_logger
|
||||
from ..ops import layout_operator
|
||||
from ..search import current_armature
|
||||
from .ops import VRM_OT_restore_vrm0_blend_shape_group_bind_object
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class VRM_MT_vrm0_blend_shape_master(Menu):
|
||||
bl_label = "Blend Shape Proxy Menu"
|
||||
bl_idname = "VRM_MT_vrm0_blend_shape_master"
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
|
||||
armature = current_armature(context)
|
||||
if not armature:
|
||||
return
|
||||
|
||||
op = layout_operator(layout, VRM_OT_restore_vrm0_blend_shape_group_bind_object)
|
||||
op.armature_object_name = armature.name
|
||||
731
editor/vrm0/migration.py
Normal file
731
editor/vrm0/migration.py
Normal file
@@ -0,0 +1,731 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import contextlib
|
||||
import json
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from bpy.types import ID, Armature, Context, Mesh, Object, Text
|
||||
|
||||
from ...common import convert, ops
|
||||
from ...common.convert import Json
|
||||
from ...common.deep import make_json
|
||||
from ...common.vrm0.human_bone import HumanBoneSpecifications
|
||||
from ..extension import get_armature_extension, get_bone_extension
|
||||
from ..property_group import BonePropertyGroup
|
||||
from .property_group import (
|
||||
Vrm0BlendShapeMasterPropertyGroup,
|
||||
Vrm0FirstPersonPropertyGroup,
|
||||
Vrm0HumanoidPropertyGroup,
|
||||
Vrm0MeshAnnotationPropertyGroup,
|
||||
Vrm0MetaPropertyGroup,
|
||||
Vrm0PropertyGroup,
|
||||
Vrm0SecondaryAnimationPropertyGroup,
|
||||
)
|
||||
|
||||
|
||||
def read_textblock_json(context: Context, armature: Object, armature_key: str) -> Json:
|
||||
text_key = armature.get(armature_key)
|
||||
if isinstance(text_key, Text):
|
||||
textblock: Optional[Text] = text_key
|
||||
elif not isinstance(text_key, str):
|
||||
return None
|
||||
else:
|
||||
textblock = context.blend_data.texts.get(text_key)
|
||||
if not isinstance(textblock, Text):
|
||||
return None
|
||||
textblock_str = "".join(line.body for line in textblock.lines)
|
||||
with contextlib.suppress(json.JSONDecodeError):
|
||||
return make_json(json.loads(textblock_str))
|
||||
return None
|
||||
|
||||
|
||||
def migrate_vrm0_meta(
|
||||
context: Context, meta: Vrm0MetaPropertyGroup, armature: Object
|
||||
) -> None:
|
||||
allowed_user_name = armature.get("allowedUserName")
|
||||
if (
|
||||
isinstance(allowed_user_name, str)
|
||||
and allowed_user_name
|
||||
in Vrm0MetaPropertyGroup.allowed_user_name_enum.identifiers()
|
||||
):
|
||||
meta.allowed_user_name = allowed_user_name
|
||||
|
||||
author = armature.get("author")
|
||||
if isinstance(author, str):
|
||||
meta.author = author
|
||||
|
||||
commercial_ussage_name = armature.get("commercialUssageName")
|
||||
if (
|
||||
isinstance(commercial_ussage_name, str)
|
||||
and commercial_ussage_name
|
||||
in Vrm0MetaPropertyGroup.commercial_ussage_name_enum.identifiers()
|
||||
):
|
||||
meta.commercial_ussage_name = commercial_ussage_name
|
||||
|
||||
contact_information = armature.get("contactInformation")
|
||||
if isinstance(contact_information, str):
|
||||
meta.contact_information = contact_information
|
||||
|
||||
license_name = armature.get("licenseName")
|
||||
if (
|
||||
isinstance(license_name, str)
|
||||
and license_name in Vrm0MetaPropertyGroup.license_name_enum.identifiers()
|
||||
):
|
||||
meta.license_name = license_name
|
||||
|
||||
other_license_url = armature.get("otherLicenseUrl")
|
||||
if isinstance(other_license_url, str):
|
||||
meta.other_license_url = other_license_url
|
||||
|
||||
other_permission_url = armature.get("otherPermissionUrl")
|
||||
if isinstance(other_permission_url, str):
|
||||
meta.other_permission_url = other_permission_url
|
||||
|
||||
reference = armature.get("reference")
|
||||
if isinstance(reference, str):
|
||||
meta.reference = reference
|
||||
|
||||
sexual_ussage_name = armature.get("sexualUssageName")
|
||||
if (
|
||||
isinstance(sexual_ussage_name, str)
|
||||
and sexual_ussage_name
|
||||
in Vrm0MetaPropertyGroup.sexual_ussage_name_enum.identifiers()
|
||||
):
|
||||
meta.sexual_ussage_name = sexual_ussage_name
|
||||
|
||||
title = armature.get("title")
|
||||
if isinstance(title, str):
|
||||
meta.title = title
|
||||
|
||||
version = armature.get("version")
|
||||
if isinstance(version, str):
|
||||
meta.version = version
|
||||
|
||||
violent_ussage_name = armature.get("violentUssageName")
|
||||
if (
|
||||
isinstance(violent_ussage_name, str)
|
||||
and violent_ussage_name
|
||||
in Vrm0MetaPropertyGroup.violent_ussage_name_enum.identifiers()
|
||||
):
|
||||
meta.violent_ussage_name = violent_ussage_name
|
||||
|
||||
texture = armature.get("texture")
|
||||
if isinstance(texture, str):
|
||||
texture_image = context.blend_data.images.get(texture)
|
||||
if texture_image:
|
||||
meta.texture = texture_image
|
||||
|
||||
|
||||
def migrate_vrm0_humanoid(
|
||||
humanoid: Vrm0HumanoidPropertyGroup, humanoid_dict: Json
|
||||
) -> None:
|
||||
if not isinstance(humanoid_dict, dict):
|
||||
return
|
||||
|
||||
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 migrate_vrm0_first_person(
|
||||
context: Context,
|
||||
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, str):
|
||||
first_person.first_person_bone.bone_name = first_person_bone
|
||||
|
||||
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, str):
|
||||
if mesh in context.blend_data.meshes:
|
||||
for obj in context.blend_data.objects:
|
||||
if obj.data == context.blend_data.meshes[mesh]:
|
||||
mesh_annotation.mesh.mesh_object_name = obj.name
|
||||
break
|
||||
elif (
|
||||
mesh in context.blend_data.objects
|
||||
and context.blend_data.objects[mesh].type == "MESH"
|
||||
):
|
||||
mesh_annotation.mesh.mesh_object_name = context.blend_data.objects[
|
||||
mesh
|
||||
].name
|
||||
|
||||
first_person_flag = mesh_annotation_dict.get("firstPersonFlag")
|
||||
if (
|
||||
isinstance(first_person_flag, str)
|
||||
and first_person_flag
|
||||
in Vrm0MeshAnnotationPropertyGroup.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 migrate_vrm0_blend_shape_groups(
|
||||
context: Context,
|
||||
blend_shape_master: Vrm0BlendShapeMasterPropertyGroup,
|
||||
blend_shape_group_dicts: Json,
|
||||
) -> None:
|
||||
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:
|
||||
bind = blend_shape_group.binds.add()
|
||||
|
||||
if not isinstance(bind_dict, dict):
|
||||
continue
|
||||
|
||||
mesh_name = bind_dict.get("mesh")
|
||||
if isinstance(mesh_name, str):
|
||||
if mesh_name in context.blend_data.meshes:
|
||||
mesh: Optional[ID] = context.blend_data.meshes[mesh_name]
|
||||
for obj in context.blend_data.objects:
|
||||
if obj.data == mesh:
|
||||
bind.mesh.mesh_object_name = obj.name
|
||||
break
|
||||
elif (
|
||||
mesh_name in context.blend_data.objects
|
||||
and context.blend_data.objects[mesh_name].type == "MESH"
|
||||
):
|
||||
obj = context.blend_data.objects[mesh_name]
|
||||
bind.mesh.mesh_object_name = obj.name
|
||||
mesh = obj.data
|
||||
else:
|
||||
mesh = None
|
||||
|
||||
if isinstance(mesh, Mesh):
|
||||
index = bind_dict.get("index")
|
||||
shape_keys = mesh.shape_keys
|
||||
if (
|
||||
isinstance(index, str)
|
||||
and shape_keys
|
||||
and index in shape_keys.key_blocks
|
||||
):
|
||||
bind.index = index
|
||||
|
||||
weight = convert.float_or_none(bind_dict.get("weight"))
|
||||
if weight is not None:
|
||||
bind.weight = weight
|
||||
|
||||
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 context.blend_data.materials
|
||||
):
|
||||
material_value.material = 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 migrate_vrm0_secondary_animation(
|
||||
secondary_animation: Vrm0SecondaryAnimationPropertyGroup,
|
||||
bone_group_dicts: Json,
|
||||
armature: Object,
|
||||
armature_data: Armature,
|
||||
) -> None:
|
||||
bone_name_to_collider_objects: dict[str, list[Object]] = {}
|
||||
for collider_object in [
|
||||
child
|
||||
for child in armature.children
|
||||
if child.type == "EMPTY"
|
||||
and child.empty_display_type == "SPHERE"
|
||||
and child.parent_type == "BONE"
|
||||
and child.parent_bone in armature_data.bones
|
||||
]:
|
||||
if collider_object.parent_bone not in bone_name_to_collider_objects:
|
||||
bone_name_to_collider_objects[collider_object.parent_bone] = []
|
||||
bone_name_to_collider_objects[collider_object.parent_bone].append(
|
||||
collider_object
|
||||
)
|
||||
|
||||
for bone_name, collider_objects in bone_name_to_collider_objects.items():
|
||||
collider_group = secondary_animation.collider_groups.add()
|
||||
collider_group.uuid = uuid.uuid4().hex
|
||||
collider_group.node.bone_name = bone_name
|
||||
for collider_object in collider_objects:
|
||||
collider_prop = collider_group.colliders.add()
|
||||
collider_prop.bpy_object = collider_object
|
||||
|
||||
for collider_group in secondary_animation.collider_groups:
|
||||
collider_group.refresh(armature)
|
||||
|
||||
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):
|
||||
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, str):
|
||||
bone_group.center.bone_name = center
|
||||
|
||||
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, str):
|
||||
continue
|
||||
bone_prop.bone_name = bone
|
||||
|
||||
collider_group_node_names = bone_group_dict.get("colliderGroups")
|
||||
if not isinstance(collider_group_node_names, list):
|
||||
continue
|
||||
|
||||
for collider_group_node_name in collider_group_node_names:
|
||||
if not isinstance(collider_group_node_name, str):
|
||||
continue
|
||||
for collider_group in secondary_animation.collider_groups:
|
||||
if collider_group.node.bone_name != collider_group_node_name:
|
||||
continue
|
||||
collider_group_name = bone_group.collider_groups.add()
|
||||
collider_group_name.value = collider_group.name
|
||||
break
|
||||
|
||||
for bone_group in secondary_animation.bone_groups:
|
||||
bone_group.refresh(armature)
|
||||
|
||||
|
||||
def migrate_legacy_custom_properties(
|
||||
context: Context, armature: Object, armature_data: Armature
|
||||
) -> None:
|
||||
ext = get_armature_extension(armature_data)
|
||||
if tuple(ext.addon_version) >= (2, 0, 1):
|
||||
return
|
||||
|
||||
migrate_vrm0_meta(context, ext.vrm0.meta, armature)
|
||||
migrate_vrm0_blend_shape_groups(
|
||||
context,
|
||||
ext.vrm0.blend_shape_master,
|
||||
read_textblock_json(context, armature, "blendshape_group"),
|
||||
)
|
||||
migrate_vrm0_first_person(
|
||||
context,
|
||||
ext.vrm0.first_person,
|
||||
read_textblock_json(context, armature, "firstPerson_params"),
|
||||
)
|
||||
migrate_vrm0_humanoid(
|
||||
ext.vrm0.humanoid, read_textblock_json(context, armature, "humanoid_params")
|
||||
)
|
||||
migrate_vrm0_secondary_animation(
|
||||
ext.vrm0.secondary_animation,
|
||||
read_textblock_json(context, armature, "spring_bone"),
|
||||
armature,
|
||||
armature_data,
|
||||
)
|
||||
|
||||
assigned_bpy_bone_names: list[str] = []
|
||||
for human_bone_name in HumanBoneSpecifications.all_names:
|
||||
bpy_bone_name = armature_data.get(human_bone_name)
|
||||
if (
|
||||
not isinstance(bpy_bone_name, str)
|
||||
or not bpy_bone_name
|
||||
or bpy_bone_name in assigned_bpy_bone_names
|
||||
):
|
||||
continue
|
||||
assigned_bpy_bone_names.append(bpy_bone_name)
|
||||
|
||||
for human_bone in ext.vrm0.humanoid.human_bones:
|
||||
if human_bone.bone == human_bone_name:
|
||||
human_bone.node.bone_name = bpy_bone_name
|
||||
break
|
||||
|
||||
|
||||
def migrate_blender_object(armature_data: Armature) -> None:
|
||||
ext = get_armature_extension(armature_data)
|
||||
if tuple(ext.addon_version) >= (2, 3, 27):
|
||||
return
|
||||
|
||||
for collider_group in ext.vrm0.secondary_animation.collider_groups:
|
||||
for collider in collider_group.colliders:
|
||||
bpy_object = collider.pop("blender_object", None)
|
||||
if isinstance(bpy_object, Object):
|
||||
collider.bpy_object = bpy_object
|
||||
|
||||
|
||||
def migrate_link_to_bone_object(
|
||||
context: Context, armature: Object, armature_data: Armature
|
||||
) -> None:
|
||||
ext = get_armature_extension(armature_data)
|
||||
if tuple(ext.addon_version) >= (2, 3, 27):
|
||||
return
|
||||
|
||||
for (
|
||||
bone_property_group,
|
||||
_bone_property_group_type,
|
||||
) in BonePropertyGroup.get_all_bone_property_groups(armature):
|
||||
link_to_bone = bone_property_group.get("link_to_bone")
|
||||
if not isinstance(link_to_bone, Object) or not link_to_bone.parent_bone:
|
||||
continue
|
||||
parent = link_to_bone.parent
|
||||
if not parent or not parent.name or parent.type != "ARMATURE":
|
||||
continue
|
||||
parent_data = parent.data
|
||||
if not isinstance(parent_data, Armature):
|
||||
continue
|
||||
bone = parent_data.bones.get(link_to_bone.parent_bone)
|
||||
if not bone:
|
||||
continue
|
||||
bone_extension = get_bone_extension(bone)
|
||||
if not bone_extension.uuid:
|
||||
bone_extension.uuid = uuid.uuid4().hex
|
||||
bone_property_group.bone_uuid = bone_extension.uuid
|
||||
|
||||
for (
|
||||
bone_property_group,
|
||||
_bone_property_group_type,
|
||||
) in BonePropertyGroup.get_all_bone_property_groups(armature):
|
||||
link_to_bone = bone_property_group.pop("link_to_bone", None)
|
||||
if not isinstance(link_to_bone, Object):
|
||||
continue
|
||||
if link_to_bone.parent_type != "OBJECT":
|
||||
link_to_bone.parent_type = "OBJECT"
|
||||
if link_to_bone.parent_bone:
|
||||
link_to_bone.parent_bone = ""
|
||||
if link_to_bone.parent is not None:
|
||||
link_to_bone.parent = None
|
||||
|
||||
Vrm0HumanoidPropertyGroup.update_all_node_candidates(
|
||||
context,
|
||||
armature_data.name,
|
||||
force=True,
|
||||
)
|
||||
|
||||
|
||||
def migrate_link_to_mesh_object(armature_data: Armature) -> None:
|
||||
ext = get_armature_extension(armature_data)
|
||||
if tuple(ext.addon_version) >= (2, 3, 23):
|
||||
return
|
||||
|
||||
meshes = [
|
||||
mesh_annotation.mesh
|
||||
for mesh_annotation in ext.vrm0.first_person.mesh_annotations
|
||||
] + [
|
||||
bind.mesh
|
||||
for blend_shape_group in ext.vrm0.blend_shape_master.blend_shape_groups
|
||||
for bind in blend_shape_group.binds
|
||||
]
|
||||
|
||||
for mesh in meshes:
|
||||
if not mesh:
|
||||
continue
|
||||
link_to_mesh = mesh.get("link_to_mesh")
|
||||
if (
|
||||
not isinstance(link_to_mesh, Object)
|
||||
or not link_to_mesh.parent
|
||||
or not link_to_mesh.parent.name
|
||||
or link_to_mesh.parent.type != "MESH"
|
||||
):
|
||||
continue
|
||||
mesh.mesh_object_name = link_to_mesh.parent.name
|
||||
|
||||
|
||||
def remove_link_to_mesh_object(armature_data: Armature) -> None:
|
||||
ext = get_armature_extension(armature_data)
|
||||
if tuple(ext.addon_version) >= (2, 3, 27):
|
||||
return
|
||||
|
||||
meshes = [
|
||||
mesh_annotation.mesh
|
||||
for mesh_annotation in ext.vrm0.first_person.mesh_annotations
|
||||
] + [
|
||||
bind.mesh
|
||||
for blend_shape_group in ext.vrm0.blend_shape_master.blend_shape_groups
|
||||
for bind in blend_shape_group.binds
|
||||
]
|
||||
|
||||
for mesh in meshes:
|
||||
if not mesh:
|
||||
continue
|
||||
link_to_mesh = mesh.pop("link_to_mesh", None)
|
||||
if not isinstance(link_to_mesh, Object):
|
||||
continue
|
||||
if link_to_mesh.parent_type != "OBJECT":
|
||||
link_to_mesh.parent_type = "OBJECT"
|
||||
if link_to_mesh.parent_bone:
|
||||
link_to_mesh.parent_bone = ""
|
||||
if link_to_mesh.parent is not None:
|
||||
link_to_mesh.parent = None
|
||||
|
||||
|
||||
def fixup_gravity_dir(armature_data: Armature) -> None:
|
||||
ext = get_armature_extension(armature_data)
|
||||
if tuple(ext.addon_version) >= (2, 15, 4):
|
||||
return
|
||||
|
||||
for bone_group in ext.vrm0.secondary_animation.bone_groups:
|
||||
gravity_dir = list(bone_group.gravity_dir)
|
||||
bone_group.gravity_dir = (gravity_dir[0] + 1, 0, 0) # Make a change
|
||||
bone_group.gravity_dir = gravity_dir
|
||||
|
||||
|
||||
def fixup_humanoid_feet_spacing(armature_data: Armature) -> None:
|
||||
ext = get_armature_extension(armature_data)
|
||||
if tuple(ext.addon_version) >= (2, 18, 2):
|
||||
return
|
||||
humanoid = ext.vrm0.humanoid
|
||||
feet_spacing = convert.float_or_none(humanoid.get("feet_spacing"))
|
||||
if feet_spacing is not None:
|
||||
humanoid.feet_spacing = float(feet_spacing)
|
||||
|
||||
|
||||
def migrate_pose(context: Context, armature: Object, armature_data: Armature) -> None:
|
||||
ext = get_armature_extension(armature_data)
|
||||
if tuple(ext.addon_version) >= (2, 20, 34):
|
||||
return
|
||||
|
||||
humanoid = ext.vrm0.humanoid
|
||||
if isinstance(humanoid.get("pose"), int):
|
||||
return
|
||||
|
||||
if tuple(ext.addon_version) == ext.INITIAL_ADDON_VERSION:
|
||||
if ext.has_vrm_model_metadata(armature):
|
||||
humanoid.pose = humanoid.POSE_CURRENT_POSE.identifier
|
||||
return
|
||||
|
||||
action = humanoid.pose_library
|
||||
if action and action.name in context.blend_data.actions:
|
||||
humanoid.pose = humanoid.POSE_CUSTOM_POSE.identifier
|
||||
elif armature_data.pose_position == "REST":
|
||||
humanoid.pose = humanoid.POSE_REST_POSITION_POSE.identifier
|
||||
else:
|
||||
humanoid.pose = humanoid.POSE_CURRENT_POSE.identifier
|
||||
|
||||
|
||||
def migrate_auto_pose(_context: Context, armature_data: Armature) -> None:
|
||||
ext = get_armature_extension(armature_data)
|
||||
if tuple(ext.addon_version) == ext.INITIAL_ADDON_VERSION or tuple(
|
||||
ext.addon_version
|
||||
) >= (2, 20, 81):
|
||||
return
|
||||
|
||||
humanoid = ext.vrm0.humanoid
|
||||
if not isinstance(humanoid.get("pose"), int):
|
||||
humanoid.pose = humanoid.POSE_CURRENT_POSE.identifier
|
||||
|
||||
|
||||
def migrate_saved_mesh_object_name_to_restore(
|
||||
_context: Context, armature_data: Armature
|
||||
) -> None:
|
||||
ext = get_armature_extension(armature_data)
|
||||
if tuple(ext.addon_version) == ext.INITIAL_ADDON_VERSION or tuple(
|
||||
ext.addon_version
|
||||
) >= (3, 9, 0):
|
||||
return
|
||||
|
||||
for blend_shape_group in ext.vrm0.blend_shape_master.blend_shape_groups:
|
||||
for bind in blend_shape_group.binds:
|
||||
bind.mesh.saved_mesh_object_name_to_restore = bind.mesh.name
|
||||
|
||||
|
||||
def is_unnecessary(vrm0: Vrm0PropertyGroup) -> bool:
|
||||
if vrm0.humanoid.initial_automatic_bone_assignment:
|
||||
return False
|
||||
if vrm0.first_person.first_person_bone.bone_name:
|
||||
return True
|
||||
return all(
|
||||
(human_bone.bone != "head" or not human_bone.node.bone_name)
|
||||
for human_bone in vrm0.humanoid.human_bones
|
||||
)
|
||||
|
||||
|
||||
def migrate(context: Context, vrm0: Vrm0PropertyGroup, armature: Object) -> None:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
|
||||
migrate_blender_object(armature_data)
|
||||
migrate_link_to_bone_object(context, armature, armature_data)
|
||||
Vrm0HumanoidPropertyGroup.fixup_human_bones(armature)
|
||||
|
||||
for collider_group in vrm0.secondary_animation.collider_groups:
|
||||
collider_group.refresh(armature)
|
||||
for bone_group in vrm0.secondary_animation.bone_groups:
|
||||
bone_group.refresh(armature)
|
||||
|
||||
if not vrm0.first_person.first_person_bone.bone_name:
|
||||
for human_bone in vrm0.humanoid.human_bones:
|
||||
if human_bone.bone == "head":
|
||||
vrm0.first_person.first_person_bone.bone_name = (
|
||||
human_bone.node.bone_name
|
||||
)
|
||||
break
|
||||
|
||||
migrate_legacy_custom_properties(context, armature, armature_data)
|
||||
migrate_link_to_mesh_object(armature_data)
|
||||
remove_link_to_mesh_object(armature_data)
|
||||
fixup_gravity_dir(armature_data)
|
||||
fixup_humanoid_feet_spacing(armature_data)
|
||||
migrate_pose(context, armature, armature_data)
|
||||
migrate_auto_pose(context, armature_data)
|
||||
migrate_saved_mesh_object_name_to_restore(context, armature_data)
|
||||
|
||||
Vrm0HumanoidPropertyGroup.update_all_node_candidates(
|
||||
context,
|
||||
armature_data.name,
|
||||
force=True,
|
||||
)
|
||||
|
||||
if vrm0.humanoid.initial_automatic_bone_assignment:
|
||||
vrm0.humanoid.initial_automatic_bone_assignment = False
|
||||
if all(not b.node.bone_name for b in vrm0.humanoid.human_bones):
|
||||
ops.vrm.assign_vrm0_humanoid_human_bones_automatically(
|
||||
armature_object_name=armature.name
|
||||
)
|
||||
2767
editor/vrm0/ops.py
Normal file
2767
editor/vrm0/ops.py
Normal file
File diff suppressed because it is too large
Load Diff
1249
editor/vrm0/panel.py
Normal file
1249
editor/vrm0/panel.py
Normal file
File diff suppressed because it is too large
Load Diff
1117
editor/vrm0/property_group.py
Normal file
1117
editor/vrm0/property_group.py
Normal file
File diff suppressed because it is too large
Load Diff
419
editor/vrm0/ui_list.py
Normal file
419
editor/vrm0/ui_list.py
Normal file
@@ -0,0 +1,419 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from bpy.types import Armature, Context, Mesh, UILayout, UIList
|
||||
|
||||
from ...common.logger import get_logger
|
||||
from ..extension import get_armature_extension
|
||||
from ..property_group import BonePropertyGroup, StringPropertyGroup
|
||||
from .property_group import (
|
||||
Vrm0BlendShapeBindPropertyGroup,
|
||||
Vrm0BlendShapeGroupPropertyGroup,
|
||||
Vrm0FirstPersonPropertyGroup,
|
||||
Vrm0MaterialValueBindPropertyGroup,
|
||||
Vrm0MeshAnnotationPropertyGroup,
|
||||
Vrm0SecondaryAnimationColliderGroupPropertyGroup,
|
||||
Vrm0SecondaryAnimationColliderPropertyGroup,
|
||||
Vrm0SecondaryAnimationGroupPropertyGroup,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class VRM_UL_vrm0_first_person_mesh_annotation(UIList):
|
||||
bl_idname = "VRM_UL_vrm0_first_person_mesh_annotation"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
first_person: object,
|
||||
mesh_annotation: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(first_person, Vrm0FirstPersonPropertyGroup):
|
||||
return
|
||||
if not isinstance(mesh_annotation, Vrm0MeshAnnotationPropertyGroup):
|
||||
return
|
||||
|
||||
icon = "OUTLINER_OB_MESH"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
row = layout.split(factor=0.6, align=True)
|
||||
if index == first_person.active_mesh_annotation_index:
|
||||
row.prop(
|
||||
mesh_annotation.mesh,
|
||||
"bpy_object",
|
||||
icon=icon,
|
||||
text="",
|
||||
translate=False,
|
||||
)
|
||||
else:
|
||||
row.label(
|
||||
text=mesh_annotation.mesh.mesh_object_name,
|
||||
translate=False,
|
||||
icon=icon,
|
||||
)
|
||||
row.prop(mesh_annotation, "first_person_flag", text="", translate=False)
|
||||
|
||||
|
||||
class VRM_UL_vrm0_secondary_animation_group(UIList):
|
||||
bl_idname = "VRM_UL_vrm0_secondary_animation_group"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
bone_group: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
_index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(bone_group, Vrm0SecondaryAnimationGroupPropertyGroup):
|
||||
return
|
||||
|
||||
icon = "BONE_DATA"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
text = ""
|
||||
if bone_group.bones:
|
||||
text = (
|
||||
"(" + ", ".join(str(bone.bone_name) for bone in bone_group.bones) + ")"
|
||||
)
|
||||
|
||||
if bone_group.center.bone_name:
|
||||
if text:
|
||||
text = " - " + text
|
||||
text = bone_group.center.bone_name + text
|
||||
|
||||
if bone_group.comment:
|
||||
if text:
|
||||
text = " / " + text
|
||||
text = bone_group.comment + text
|
||||
|
||||
if not text:
|
||||
text = "(EMPTY)"
|
||||
|
||||
layout.label(text=text, translate=False, icon=icon)
|
||||
|
||||
|
||||
class VRM_UL_vrm0_secondary_animation_group_bone(UIList):
|
||||
bl_idname = "VRM_UL_vrm0_secondary_animation_group_bone"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
bone_group: object,
|
||||
bone: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(bone_group, Vrm0SecondaryAnimationGroupPropertyGroup):
|
||||
return
|
||||
if not isinstance(bone, BonePropertyGroup):
|
||||
return
|
||||
armature = bone.find_armature()
|
||||
|
||||
icon = "BONE_DATA"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
if index == bone_group.active_bone_index:
|
||||
layout.prop_search(
|
||||
bone,
|
||||
"bone_name",
|
||||
armature,
|
||||
"bones",
|
||||
text="",
|
||||
translate=False,
|
||||
icon=icon,
|
||||
)
|
||||
else:
|
||||
layout.label(text=bone.bone_name, translate=False, icon=icon)
|
||||
|
||||
|
||||
class VRM_UL_vrm0_secondary_animation_group_collider_group(UIList):
|
||||
bl_idname = "VRM_UL_vrm0_secondary_animation_group_collider_group"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
bone_group: object,
|
||||
collider_group: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(bone_group, Vrm0SecondaryAnimationGroupPropertyGroup):
|
||||
return
|
||||
if not isinstance(collider_group, StringPropertyGroup):
|
||||
return
|
||||
|
||||
armature_data = bone_group.id_data
|
||||
if not isinstance(armature_data, Armature):
|
||||
logger.error("Failed to find armature")
|
||||
return
|
||||
|
||||
secondary_animation = get_armature_extension(
|
||||
armature_data
|
||||
).vrm0.secondary_animation
|
||||
|
||||
icon = "PIVOT_INDIVIDUAL"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
if index == bone_group.active_collider_group_index:
|
||||
layout.prop_search(
|
||||
collider_group,
|
||||
"value",
|
||||
secondary_animation,
|
||||
"collider_groups",
|
||||
text="",
|
||||
translate=False,
|
||||
icon=icon,
|
||||
)
|
||||
else:
|
||||
layout.label(text=collider_group.value, translate=False, icon=icon)
|
||||
|
||||
|
||||
class VRM_UL_vrm0_secondary_animation_collider_group(UIList):
|
||||
bl_idname = "VRM_UL_vrm0_secondary_animation_collider_group"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
collider_group: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
_index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(
|
||||
collider_group, Vrm0SecondaryAnimationColliderGroupPropertyGroup
|
||||
):
|
||||
return
|
||||
|
||||
icon = "SPHERE"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
layout.label(text=collider_group.name, translate=False, icon=icon)
|
||||
|
||||
|
||||
class VRM_UL_vrm0_secondary_animation_collider_group_collider(UIList):
|
||||
bl_idname = "VRM_UL_vrm0_secondary_animation_collider_group_collider"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
collider_group: object,
|
||||
collider: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(
|
||||
collider_group, Vrm0SecondaryAnimationColliderGroupPropertyGroup
|
||||
):
|
||||
return
|
||||
if not isinstance(collider, Vrm0SecondaryAnimationColliderPropertyGroup):
|
||||
return
|
||||
|
||||
icon = "MESH_UVSPHERE"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
bpy_object = collider.bpy_object
|
||||
if bpy_object is None:
|
||||
return
|
||||
|
||||
row = layout.split(align=True, factor=0.7)
|
||||
if index == collider_group.active_collider_index:
|
||||
row.prop(
|
||||
bpy_object,
|
||||
"name",
|
||||
icon=icon,
|
||||
translate=False,
|
||||
text="",
|
||||
)
|
||||
row.prop(bpy_object, "empty_display_size", text="")
|
||||
else:
|
||||
row.label(text=bpy_object.name, icon=icon, translate=False)
|
||||
row.prop(bpy_object, "empty_display_size", text="", emboss=False)
|
||||
|
||||
|
||||
class VRM_UL_vrm0_blend_shape_group(UIList):
|
||||
bl_idname = "VRM_UL_vrm0_blend_shape_group"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
item: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
_index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
blend_shape_group = item
|
||||
if not isinstance(blend_shape_group, Vrm0BlendShapeGroupPropertyGroup):
|
||||
return
|
||||
|
||||
preset = next(
|
||||
(
|
||||
preset
|
||||
for preset in blend_shape_group.preset_name_enum
|
||||
if preset.identifier == blend_shape_group.preset_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not preset:
|
||||
return
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=preset.icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
text = blend_shape_group.name + " / " + preset.name
|
||||
split = layout.split(align=True, factor=0.55)
|
||||
split.label(text=text, translate=False, icon=preset.icon)
|
||||
split.prop(blend_shape_group, "preview", text="Preview")
|
||||
|
||||
|
||||
class VRM_UL_vrm0_blend_shape_bind(UIList):
|
||||
bl_idname = "VRM_UL_vrm0_blend_shape_bind"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
item: object,
|
||||
icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
_index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
blend_data = context.blend_data
|
||||
blend_shape_bind = item
|
||||
if not isinstance(blend_shape_bind, Vrm0BlendShapeBindPropertyGroup):
|
||||
return
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon_value=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
name = blend_shape_bind.mesh.mesh_object_name
|
||||
mesh_object = blend_data.objects.get(blend_shape_bind.mesh.mesh_object_name)
|
||||
if mesh_object:
|
||||
mesh_data = mesh_object.data
|
||||
if isinstance(mesh_data, Mesh):
|
||||
shape_keys = mesh_data.shape_keys
|
||||
if shape_keys:
|
||||
keys = shape_keys.key_blocks.keys()
|
||||
if blend_shape_bind.index in keys:
|
||||
name += " / " + blend_shape_bind.index
|
||||
layout.label(text=name, translate=False, icon="MESH_DATA")
|
||||
|
||||
|
||||
class VRM_UL_vrm0_material_value_bind(UIList):
|
||||
bl_idname = "VRM_UL_vrm0_material_value_bind"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
item: object,
|
||||
icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
_index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
material_value_bind = item
|
||||
if not isinstance(material_value_bind, Vrm0MaterialValueBindPropertyGroup):
|
||||
return
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon_value=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
name = ""
|
||||
material = material_value_bind.material
|
||||
if material:
|
||||
name = material.name
|
||||
if material_value_bind.property_name:
|
||||
name += " / " + material_value_bind.property_name
|
||||
layout.label(text=name, translate=False, icon="MATERIAL")
|
||||
1
editor/vrm1/__init__.py
Normal file
1
editor/vrm1/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
BIN
editor/vrm1/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
editor/vrm1/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm1/__pycache__/handler.cpython-311.pyc
Normal file
BIN
editor/vrm1/__pycache__/handler.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm1/__pycache__/menu.cpython-311.pyc
Normal file
BIN
editor/vrm1/__pycache__/menu.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm1/__pycache__/migration.cpython-311.pyc
Normal file
BIN
editor/vrm1/__pycache__/migration.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm1/__pycache__/ops.cpython-311.pyc
Normal file
BIN
editor/vrm1/__pycache__/ops.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm1/__pycache__/panel.cpython-311.pyc
Normal file
BIN
editor/vrm1/__pycache__/panel.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm1/__pycache__/property_group.cpython-311.pyc
Normal file
BIN
editor/vrm1/__pycache__/property_group.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm1/__pycache__/scene_watcher.cpython-311.pyc
Normal file
BIN
editor/vrm1/__pycache__/scene_watcher.cpython-311.pyc
Normal file
Binary file not shown.
BIN
editor/vrm1/__pycache__/ui_list.cpython-311.pyc
Normal file
BIN
editor/vrm1/__pycache__/ui_list.cpython-311.pyc
Normal file
Binary file not shown.
45
editor/vrm1/handler.py
Normal file
45
editor/vrm1/handler.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from ...common.logger import get_logger
|
||||
from ...common.scene_watcher import trigger_scene_watcher
|
||||
from .property_group import Vrm1ExpressionPropertyGroup
|
||||
from .scene_watcher import LookAtPreviewUpdater
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@persistent
|
||||
def frame_change_pre(_unused: object) -> None:
|
||||
Vrm1ExpressionPropertyGroup.frame_change_post_shape_key_updates.clear()
|
||||
|
||||
|
||||
@persistent
|
||||
def frame_change_post(_unused: object) -> None:
|
||||
context = bpy.context
|
||||
|
||||
for (
|
||||
shape_key_name,
|
||||
key_block_name,
|
||||
), value in Vrm1ExpressionPropertyGroup.frame_change_post_shape_key_updates.items():
|
||||
shape_key = context.blend_data.shape_keys.get(shape_key_name)
|
||||
if not shape_key:
|
||||
continue
|
||||
key_blocks = shape_key.key_blocks
|
||||
if not key_blocks:
|
||||
continue
|
||||
key_block = key_blocks.get(key_block_name)
|
||||
if not key_block:
|
||||
continue
|
||||
key_block.value = value
|
||||
Vrm1ExpressionPropertyGroup.frame_change_post_shape_key_updates.clear()
|
||||
|
||||
# Update materials
|
||||
Vrm1ExpressionPropertyGroup.update_materials(context)
|
||||
|
||||
|
||||
@persistent
|
||||
def depsgraph_update_pre(_unused: object) -> None:
|
||||
trigger_scene_watcher(LookAtPreviewUpdater)
|
||||
26
editor/vrm1/menu.py
Normal file
26
editor/vrm1/menu.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from bpy.types import Context, Menu
|
||||
|
||||
from ...common.logger import get_logger
|
||||
from ..ops import layout_operator
|
||||
from ..search import current_armature
|
||||
from .ops import VRM_OT_restore_vrm1_expression_morph_target_bind_object
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class VRM_MT_vrm1_expression(Menu):
|
||||
bl_label = "Expression Menu"
|
||||
bl_idname = "VRM_MT_vrm1_expression"
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
|
||||
armature = current_armature(context)
|
||||
if not armature:
|
||||
return
|
||||
|
||||
op = layout_operator(
|
||||
layout, VRM_OT_restore_vrm1_expression_morph_target_bind_object
|
||||
)
|
||||
op.armature_object_name = armature.name
|
||||
281
editor/vrm1/migration.py
Normal file
281
editor/vrm1/migration.py
Normal file
@@ -0,0 +1,281 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from bpy.types import Armature, Context, Material, Object
|
||||
from idprop.types import IDPropertyGroup
|
||||
from mathutils import Vector
|
||||
|
||||
from ...common import convert, ops, shader
|
||||
from ..extension import get_armature_extension
|
||||
from .property_group import (
|
||||
Vrm1ExpressionPropertyGroup,
|
||||
Vrm1ExpressionsPropertyGroup,
|
||||
Vrm1HumanBonesPropertyGroup,
|
||||
Vrm1MaterialColorBindPropertyGroup,
|
||||
Vrm1PropertyGroup,
|
||||
)
|
||||
|
||||
|
||||
def migrate_old_expression_layout(
|
||||
old_expression: object, expression: Vrm1ExpressionPropertyGroup
|
||||
) -> None:
|
||||
if not isinstance(old_expression, IDPropertyGroup):
|
||||
return
|
||||
|
||||
old_morph_target_binds = convert.iterator_or_none(
|
||||
old_expression.get("morph_target_binds")
|
||||
)
|
||||
if old_morph_target_binds is not None:
|
||||
for old_morph_target_bind in old_morph_target_binds:
|
||||
if not isinstance(old_morph_target_bind, IDPropertyGroup):
|
||||
continue
|
||||
|
||||
morph_target_bind = expression.morph_target_binds.add()
|
||||
old_node = old_morph_target_bind.get("node")
|
||||
if isinstance(old_node, IDPropertyGroup):
|
||||
old_bpy_object = old_node.get("bpy_object")
|
||||
if isinstance(old_bpy_object, Object) and old_bpy_object.type == "MESH":
|
||||
morph_target_bind.node.mesh_object_name = old_bpy_object.name
|
||||
old_node.clear()
|
||||
|
||||
old_index = old_morph_target_bind.get("index")
|
||||
if isinstance(old_index, str):
|
||||
morph_target_bind.index = old_index
|
||||
|
||||
old_weight = old_morph_target_bind.get("weight")
|
||||
if isinstance(old_weight, (float, int)):
|
||||
morph_target_bind.weight = old_weight
|
||||
|
||||
old_morph_target_bind.clear()
|
||||
|
||||
old_material_color_binds = convert.iterator_or_none(
|
||||
old_expression.get("material_color_binds")
|
||||
)
|
||||
if old_material_color_binds is not None:
|
||||
for old_material_color_bind in old_material_color_binds:
|
||||
if not isinstance(old_material_color_bind, IDPropertyGroup):
|
||||
continue
|
||||
|
||||
material_color_bind = expression.material_color_binds.add()
|
||||
old_material = old_material_color_bind.get("material")
|
||||
if isinstance(old_material, Material):
|
||||
material_color_bind.material = old_material
|
||||
|
||||
old_type = next(
|
||||
(
|
||||
enum.identifier
|
||||
for enum in Vrm1MaterialColorBindPropertyGroup.type_enum
|
||||
if old_material_color_bind.get("type") == enum.value
|
||||
),
|
||||
None,
|
||||
)
|
||||
if old_type is not None:
|
||||
material_color_bind.type = old_type
|
||||
|
||||
old_target_value = shader.rgba_or_none(
|
||||
old_material_color_bind.get("target_value"), 0.0, 1.0
|
||||
)
|
||||
if old_target_value:
|
||||
material_color_bind.target_value = old_target_value
|
||||
|
||||
old_material_color_bind.clear()
|
||||
|
||||
old_texture_transform_binds = convert.iterator_or_none(
|
||||
old_expression.get("texture_transform_binds")
|
||||
)
|
||||
if old_texture_transform_binds is not None:
|
||||
for old_texture_transform_bind in old_texture_transform_binds:
|
||||
if not isinstance(old_texture_transform_bind, IDPropertyGroup):
|
||||
continue
|
||||
|
||||
texture_transform_bind = expression.texture_transform_binds.add()
|
||||
old_material = old_texture_transform_bind.get("material")
|
||||
if isinstance(old_material, Material):
|
||||
texture_transform_bind.material = old_material
|
||||
|
||||
old_scale = convert.float2_or_none(old_texture_transform_bind.get("scale"))
|
||||
if old_scale:
|
||||
texture_transform_bind.scale = old_scale
|
||||
|
||||
old_offset = convert.float2_or_none(
|
||||
old_texture_transform_bind.get("offset")
|
||||
)
|
||||
if old_offset:
|
||||
texture_transform_bind.offset = old_offset
|
||||
|
||||
old_texture_transform_bind.clear()
|
||||
|
||||
old_is_binary = old_expression.get("is_binary")
|
||||
if isinstance(old_is_binary, int):
|
||||
expression.is_binary = bool(old_is_binary)
|
||||
|
||||
old_override_blink = next(
|
||||
(
|
||||
enum.identifier
|
||||
for enum in Vrm1ExpressionPropertyGroup.expression_override_type_enum
|
||||
if old_expression.get("override_blink") == enum.value
|
||||
),
|
||||
None,
|
||||
)
|
||||
if old_override_blink is not None:
|
||||
expression.override_blink = old_override_blink
|
||||
|
||||
old_override_look_at = next(
|
||||
(
|
||||
enum.identifier
|
||||
for enum in Vrm1ExpressionPropertyGroup.expression_override_type_enum
|
||||
if old_expression.get("override_look_at") == enum.value
|
||||
),
|
||||
None,
|
||||
)
|
||||
if old_override_look_at is not None:
|
||||
expression.override_look_at = old_override_look_at
|
||||
|
||||
old_override_mouth = next(
|
||||
(
|
||||
enum.identifier
|
||||
for enum in Vrm1ExpressionPropertyGroup.expression_override_type_enum
|
||||
if old_expression.get("override_mouth") == enum.value
|
||||
),
|
||||
None,
|
||||
)
|
||||
if old_override_mouth is not None:
|
||||
expression.override_mouth = old_override_mouth
|
||||
|
||||
|
||||
def migrate_old_expressions_layout(expressions: Vrm1ExpressionsPropertyGroup) -> None:
|
||||
for name, expression in expressions.preset.name_to_expression_dict().items():
|
||||
property_name = {
|
||||
"blinkLeft": "blink_left",
|
||||
"blinkRight": "blink_right",
|
||||
"lookUp": "look_up",
|
||||
"lookDown": "look_down",
|
||||
"lookLeft": "look_left",
|
||||
"lookRight": "look_right",
|
||||
}.get(name)
|
||||
if property_name is None:
|
||||
property_name = name
|
||||
old_expression = expressions.get(property_name)
|
||||
migrate_old_expression_layout(old_expression, expression)
|
||||
expression.name = name
|
||||
for expression in expressions.custom:
|
||||
old_expression = expression.get("expression")
|
||||
migrate_old_expression_layout(old_expression, expression)
|
||||
|
||||
|
||||
def migrate_pose(context: Context, armature: Object, armature_data: Armature) -> None:
|
||||
ext = get_armature_extension(armature_data)
|
||||
if tuple(ext.addon_version) >= (2, 20, 34):
|
||||
return
|
||||
|
||||
humanoid = ext.vrm1.humanoid
|
||||
if isinstance(humanoid.get("pose"), int):
|
||||
return
|
||||
|
||||
if tuple(ext.addon_version) == ext.INITIAL_ADDON_VERSION:
|
||||
if ext.has_vrm_model_metadata(armature):
|
||||
humanoid.pose = humanoid.POSE_CURRENT_POSE.identifier
|
||||
return
|
||||
|
||||
action = humanoid.pose_library
|
||||
if action and action.name in context.blend_data.actions:
|
||||
humanoid.pose = humanoid.POSE_CUSTOM_POSE.identifier
|
||||
elif armature_data.pose_position == "REST":
|
||||
humanoid.pose = humanoid.POSE_REST_POSITION_POSE.identifier
|
||||
else:
|
||||
humanoid.pose = humanoid.POSE_CURRENT_POSE.identifier
|
||||
|
||||
|
||||
def migrate_auto_pose(_context: Context, armature_data: Armature) -> None:
|
||||
ext = get_armature_extension(armature_data)
|
||||
if tuple(ext.addon_version) == ext.INITIAL_ADDON_VERSION or tuple(
|
||||
ext.addon_version
|
||||
) >= (2, 20, 81):
|
||||
return
|
||||
|
||||
humanoid = ext.vrm1.humanoid
|
||||
if not isinstance(humanoid.get("pose"), int):
|
||||
humanoid.pose = humanoid.POSE_CURRENT_POSE.identifier
|
||||
|
||||
|
||||
def migrate(context: Context, vrm1: Vrm1PropertyGroup, armature: Object) -> None:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
|
||||
human_bones = vrm1.humanoid.human_bones
|
||||
human_bones.last_bone_names_str = ""
|
||||
Vrm1HumanBonesPropertyGroup.fixup_human_bones(armature)
|
||||
Vrm1HumanBonesPropertyGroup.update_all_node_candidates(context, armature_data.name)
|
||||
|
||||
if human_bones.initial_automatic_bone_assignment:
|
||||
human_bones.initial_automatic_bone_assignment = False
|
||||
human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
|
||||
if all(not b.node.bone_name for b in human_bone_name_to_human_bone.values()):
|
||||
ops.vrm.assign_vrm1_humanoid_human_bones_automatically(
|
||||
armature_object_name=armature.name
|
||||
)
|
||||
|
||||
if tuple(get_armature_extension(armature_data).addon_version) <= (2, 14, 10):
|
||||
ext = get_armature_extension(armature_data)
|
||||
head_bone_name = ext.vrm1.humanoid.human_bones.head.node.bone_name
|
||||
head_bone = armature_data.bones.get(head_bone_name)
|
||||
if head_bone:
|
||||
look_at = get_armature_extension(armature_data).vrm1.look_at
|
||||
world_translation = (
|
||||
armature.matrix_world @ head_bone.matrix_local
|
||||
).to_quaternion() @ Vector(look_at.offset_from_head_bone)
|
||||
look_at.offset_from_head_bone = list(world_translation)
|
||||
|
||||
if tuple(get_armature_extension(armature_data).addon_version) <= (2, 15, 5):
|
||||
# Apply lower limit value
|
||||
look_at = get_armature_extension(armature_data).vrm1.look_at
|
||||
look_at.range_map_horizontal_inner.input_max_value = (
|
||||
look_at.range_map_horizontal_inner.input_max_value
|
||||
)
|
||||
look_at.range_map_horizontal_outer.input_max_value = (
|
||||
look_at.range_map_horizontal_outer.input_max_value
|
||||
)
|
||||
look_at.range_map_vertical_down.input_max_value = (
|
||||
look_at.range_map_vertical_down.input_max_value
|
||||
)
|
||||
look_at.range_map_vertical_up.input_max_value = (
|
||||
look_at.range_map_vertical_up.input_max_value
|
||||
)
|
||||
|
||||
if tuple(get_armature_extension(armature_data).addon_version) < (2, 18, 0):
|
||||
migrate_old_expressions_layout(
|
||||
get_armature_extension(armature_data).vrm1.expressions
|
||||
)
|
||||
|
||||
if tuple(get_armature_extension(armature_data).addon_version) < (2, 20, 0):
|
||||
look_at = get_armature_extension(armature_data).vrm1.look_at
|
||||
look_at.offset_from_head_bone = (
|
||||
look_at.offset_from_head_bone[0],
|
||||
look_at.offset_from_head_bone[2],
|
||||
-look_at.offset_from_head_bone[1],
|
||||
)
|
||||
|
||||
migrate_pose(context, armature, armature_data)
|
||||
migrate_auto_pose(context, armature_data)
|
||||
|
||||
# Set a name for the expression preset.
|
||||
# It's not necessary for management, but I want to set it because it's
|
||||
# displayed in the animation keyframes.
|
||||
expressions = get_armature_extension(armature_data).vrm1.expressions
|
||||
preset_name_to_expression_dict = expressions.preset.name_to_expression_dict()
|
||||
for preset_name, preset_expression in preset_name_to_expression_dict.items():
|
||||
if preset_expression.name != preset_name:
|
||||
preset_expression.name = preset_name
|
||||
|
||||
if tuple(get_armature_extension(armature_data).addon_version) < (3, 9, 0):
|
||||
for expression in expressions.preset.name_to_expression_dict().values():
|
||||
for morph_target_bind in expression.morph_target_binds:
|
||||
morph_target_bind.node.saved_mesh_object_name_to_restore = (
|
||||
morph_target_bind.node.mesh_object_name
|
||||
)
|
||||
|
||||
Vrm1HumanBonesPropertyGroup.update_all_node_candidates(
|
||||
context,
|
||||
armature_data.name,
|
||||
force=True,
|
||||
)
|
||||
ops.vrm.update_vrm1_expression_ui_list_elements()
|
||||
2730
editor/vrm1/ops.py
Normal file
2730
editor/vrm1/ops.py
Normal file
File diff suppressed because it is too large
Load Diff
1222
editor/vrm1/panel.py
Normal file
1222
editor/vrm1/panel.py
Normal file
File diff suppressed because it is too large
Load Diff
1812
editor/vrm1/property_group.py
Normal file
1812
editor/vrm1/property_group.py
Normal file
File diff suppressed because it is too large
Load Diff
93
editor/vrm1/scene_watcher.py
Normal file
93
editor/vrm1/scene_watcher.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bpy.types import Armature, Context
|
||||
from mathutils import Vector
|
||||
|
||||
from ...common import ops
|
||||
from ...common.logger import get_logger
|
||||
from ...common.scene_watcher import RunState, SceneWatcher
|
||||
from ..extension import get_armature_extension
|
||||
from .property_group import Vrm1LookAtPropertyGroup
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LookAtPreviewUpdater(SceneWatcher):
|
||||
armature_index: int = 0
|
||||
|
||||
def reset_run_progress(self) -> None:
|
||||
self.armature_index = 0
|
||||
|
||||
def run(self, context: Context) -> RunState:
|
||||
"""Detect updates to the target object of Look At and update the state."""
|
||||
blend_data = context.blend_data
|
||||
|
||||
if not blend_data.armatures:
|
||||
return RunState.FINISH
|
||||
|
||||
count = 20
|
||||
|
||||
armatures_len = len(blend_data.armatures)
|
||||
if self.armature_index >= armatures_len:
|
||||
self.armature_index = 0
|
||||
|
||||
start_armature_index = self.armature_index
|
||||
end_armature_index = min(self.armature_index + count, armatures_len)
|
||||
changed = False
|
||||
for armature in blend_data.armatures[start_armature_index:end_armature_index]:
|
||||
ext = get_armature_extension(armature)
|
||||
if not ext.is_vrm1():
|
||||
continue
|
||||
look_at = ext.vrm1.look_at
|
||||
if not look_at.enable_preview:
|
||||
continue
|
||||
preview_target_bpy_object = look_at.preview_target_bpy_object
|
||||
if not preview_target_bpy_object:
|
||||
continue
|
||||
if (
|
||||
Vector(look_at.previous_preview_target_bpy_object_location)
|
||||
- preview_target_bpy_object.location
|
||||
).length_squared > 0:
|
||||
changed = True
|
||||
break
|
||||
|
||||
if changed:
|
||||
Vrm1LookAtPropertyGroup.update_all_previews(context)
|
||||
return RunState.FINISH
|
||||
|
||||
self.armature_index += count
|
||||
|
||||
if end_armature_index < armatures_len:
|
||||
return RunState.PREEMPT
|
||||
|
||||
return RunState.FINISH
|
||||
|
||||
def create_fast_path_performance_test_objects(self, context: Context) -> None:
|
||||
blend_data = context.blend_data
|
||||
|
||||
for i in range(300):
|
||||
if i % 3 != 0:
|
||||
mesh = blend_data.meshes.new(f"Mesh#{i}")
|
||||
obj = blend_data.objects.new(f"Object#{i}", mesh)
|
||||
context.scene.collection.objects.link(obj)
|
||||
continue
|
||||
|
||||
ops.icyp.make_basic_armature()
|
||||
active_object = context.active_object
|
||||
if not active_object:
|
||||
message = f"Not an armature: {active_object}"
|
||||
raise ValueError(message)
|
||||
armature = active_object.data
|
||||
if not isinstance(armature, Armature):
|
||||
raise TypeError
|
||||
|
||||
ext = get_armature_extension(armature)
|
||||
ext.spec_version = ext.SPEC_VERSION_VRM1
|
||||
look_at = ext.vrm1.look_at
|
||||
look_at.type = ext.vrm1.look_at.TYPE_BONE.identifier
|
||||
look_at.preview_target_bpy_object = blend_data.objects[
|
||||
i * 5 % len(blend_data.objects)
|
||||
]
|
||||
ext.vrm1.look_at.enable_preview = True
|
||||
302
editor/vrm1/ui_list.py
Normal file
302
editor/vrm1/ui_list.py
Normal file
@@ -0,0 +1,302 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from bpy.types import Context, Mesh, UILayout, UIList
|
||||
|
||||
from ...common.logger import get_logger
|
||||
from ..property_group import StringPropertyGroup
|
||||
from .property_group import (
|
||||
Vrm1ExpressionsPropertyGroup,
|
||||
Vrm1FirstPersonPropertyGroup,
|
||||
Vrm1MaterialColorBindPropertyGroup,
|
||||
Vrm1MeshAnnotationPropertyGroup,
|
||||
Vrm1MetaPropertyGroup,
|
||||
Vrm1MorphTargetBindPropertyGroup,
|
||||
Vrm1TextureTransformBindPropertyGroup,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class VRM_UL_vrm1_meta_author(UIList):
|
||||
bl_idname = "VRM_UL_vrm1_meta_author"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
meta: object,
|
||||
author: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(meta, Vrm1MetaPropertyGroup):
|
||||
return
|
||||
if not isinstance(author, StringPropertyGroup):
|
||||
return
|
||||
|
||||
icon = "USER"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
if index == meta.active_author_index:
|
||||
layout.prop(author, "value", icon=icon, text="", translate=False)
|
||||
else:
|
||||
layout.label(text=author.value, icon=icon, translate=False)
|
||||
|
||||
|
||||
class VRM_UL_vrm1_meta_reference(UIList):
|
||||
bl_idname = "VRM_UL_vrm1_meta_reference"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
meta: object,
|
||||
reference: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(meta, Vrm1MetaPropertyGroup):
|
||||
return
|
||||
if not isinstance(reference, StringPropertyGroup):
|
||||
return
|
||||
|
||||
icon = "URL"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
if index == meta.active_reference_index:
|
||||
layout.prop(reference, "value", icon=icon, text="", translate=False)
|
||||
else:
|
||||
layout.label(text=reference.value, icon=icon, translate=False)
|
||||
|
||||
|
||||
class VRM_UL_vrm1_first_person_mesh_annotation(UIList):
|
||||
bl_idname = "VRM_UL_vrm1_first_person_mesh_annotation"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
first_person: object,
|
||||
mesh_annotation: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(first_person, Vrm1FirstPersonPropertyGroup):
|
||||
return
|
||||
if not isinstance(mesh_annotation, Vrm1MeshAnnotationPropertyGroup):
|
||||
return
|
||||
|
||||
icon = "OUTLINER_OB_MESH"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
row = layout.split(factor=0.6, align=True)
|
||||
if index == first_person.active_mesh_annotation_index:
|
||||
row.prop(
|
||||
mesh_annotation.node,
|
||||
"bpy_object",
|
||||
text="",
|
||||
translate=False,
|
||||
icon=icon,
|
||||
)
|
||||
else:
|
||||
row.label(
|
||||
text=mesh_annotation.node.mesh_object_name,
|
||||
translate=False,
|
||||
icon=icon,
|
||||
)
|
||||
row.prop(mesh_annotation, "type", text="", translate=False)
|
||||
|
||||
|
||||
class VRM_UL_vrm1_expression(UIList):
|
||||
bl_idname = "VRM_UL_vrm1_expression"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
expressions: object,
|
||||
_item: object,
|
||||
_icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(expressions, Vrm1ExpressionsPropertyGroup):
|
||||
return
|
||||
preset_expression_items = list(
|
||||
expressions.preset.name_to_expression_dict().items()
|
||||
)
|
||||
if index < len(preset_expression_items):
|
||||
name, expression = preset_expression_items[index]
|
||||
icon = expressions.preset.get_icon(name)
|
||||
if not icon:
|
||||
logger.error("Unknown preset expression: %s", name)
|
||||
icon = "SHAPEKEY_DATA"
|
||||
else:
|
||||
custom_expressions = expressions.custom
|
||||
custom_index = index - len(preset_expression_items)
|
||||
if custom_index >= len(custom_expressions):
|
||||
return
|
||||
expression = custom_expressions[custom_index]
|
||||
name = expression.custom_name
|
||||
icon = "SHAPEKEY_DATA"
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
split = layout.split(align=True, factor=0.55)
|
||||
split.label(text=name, translate=False, icon=icon)
|
||||
split.prop(expression, "preview", text="Preview")
|
||||
|
||||
|
||||
class VRM_UL_vrm1_morph_target_bind(UIList):
|
||||
bl_idname = "VRM_UL_vrm1_morph_target_bind"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
morph_target_bind: object,
|
||||
icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
_index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
blend_data = context.blend_data
|
||||
if not isinstance(morph_target_bind, Vrm1MorphTargetBindPropertyGroup):
|
||||
return
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", icon_value=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
name = morph_target_bind.node.mesh_object_name
|
||||
mesh_object = blend_data.objects.get(morph_target_bind.node.mesh_object_name)
|
||||
if mesh_object:
|
||||
mesh_data = mesh_object.data
|
||||
if isinstance(mesh_data, Mesh):
|
||||
shape_keys = mesh_data.shape_keys
|
||||
if shape_keys:
|
||||
keys = shape_keys.key_blocks.keys()
|
||||
if morph_target_bind.index in keys:
|
||||
name += " / " + morph_target_bind.index
|
||||
layout.label(text=name, translate=False, icon="MESH_DATA")
|
||||
|
||||
|
||||
class VRM_UL_vrm1_material_color_bind(UIList):
|
||||
bl_idname = "VRM_UL_vrm0_material_color_bind"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
material_color_bind: object,
|
||||
icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
_index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(material_color_bind, Vrm1MaterialColorBindPropertyGroup):
|
||||
return
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon_value=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
name = ""
|
||||
material = material_color_bind.material
|
||||
if material:
|
||||
name = material.name
|
||||
type_name = next(
|
||||
(
|
||||
enum.name
|
||||
for enum in Vrm1MaterialColorBindPropertyGroup.type_enum
|
||||
if enum.identifier == material_color_bind.type
|
||||
),
|
||||
None,
|
||||
)
|
||||
if type_name:
|
||||
name += " / " + type_name
|
||||
layout.label(text=name, translate=False, icon="MATERIAL")
|
||||
|
||||
|
||||
class VRM_UL_vrm1_texture_transform_bind(UIList):
|
||||
bl_idname = "VRM_UL_vrm1_texture_transform_bind"
|
||||
|
||||
def draw_item(
|
||||
self,
|
||||
_context: Context,
|
||||
layout: UILayout,
|
||||
_data: object,
|
||||
texture_transform_bind: object,
|
||||
icon: int,
|
||||
_active_data: object,
|
||||
_active_prop_name: str,
|
||||
_index: int,
|
||||
_flt_flag: int,
|
||||
) -> None:
|
||||
if not isinstance(
|
||||
texture_transform_bind, Vrm1TextureTransformBindPropertyGroup
|
||||
):
|
||||
return
|
||||
|
||||
if self.layout_type == "GRID":
|
||||
layout.alignment = "CENTER"
|
||||
layout.label(text="", translate=False, icon_value=icon)
|
||||
return
|
||||
|
||||
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
||||
return
|
||||
|
||||
name = ""
|
||||
material = texture_transform_bind.material
|
||||
if material:
|
||||
name = material.name
|
||||
layout.label(text=name, translate=False, icon="MATERIAL")
|
||||
Reference in New Issue
Block a user