Files
blender-vrm/editor/spring_bone1/property_group.py
小煜 091ad6a49a feat: Add VRM Blender addon with complete import/export functionality
- Add core VRM addon infrastructure with manifest and registration
- Add common utilities module with file system, logging, and conversion helpers
- Add human bone mapper with support for multiple rigging standards (Mixamo, MMD, Unreal, Rigify, etc.)
- Add VRM 0.x and 1.x format support with property groups and handlers
- Add editor UI panels for VRM metadata, spring bones, and MToon materials
- Add exporter with glTF2 extension support for VRM format serialization
- Add importer with scene reconstruction and armature generation
- Add MToon shader support with auto-setup and material migration
- Add spring bone physics simulation with constraint handling
- Add node constraint editor for advanced rigging control
- Add comprehensive validation and error handling with user dialogs
- Add scene watcher for real-time property synchronization
- Add workspace management and preference system
- Include Python cache files and Blender manifest configuration
- This is the initial commit establishing the complete VRM addon ecosystem for Blender
2026-01-01 14:21:56 +08:00

1272 lines
48 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import statistics
from collections.abc import Sequence
from sys import float_info
from typing import TYPE_CHECKING, Callable, Optional
import bpy
from bpy.props import (
BoolProperty,
CollectionProperty,
EnumProperty,
FloatProperty,
FloatVectorProperty,
IntProperty,
PointerProperty,
StringProperty,
)
from bpy.types import Armature, Context, Object, PropertyGroup
from mathutils import Matrix, Quaternion, Vector
from ...common import convert, safe_removal
from ...common.logger import get_logger
from ...common.rotation import (
get_rotation_as_quaternion,
set_rotation_without_mode_change,
)
from ..property_group import BonePropertyGroup, property_group_enum
if TYPE_CHECKING:
from ..property_group import CollectionPropertyProtocol
logger = get_logger(__name__)
def find_armature_and_collider(
context: Context,
match_collider: Callable[["SpringBone1ColliderPropertyGroup"], bool],
) -> tuple[Object, "SpringBone1ColliderPropertyGroup"]:
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_spring_bone1_extension(armature_data).colliders:
if match_collider(collider):
return (obj, collider)
message = "No armature"
raise ValueError(message)
# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.shape.schema.json#L7-L27
class SpringBone1ColliderShapeSpherePropertyGroup(PropertyGroup):
def find_armature_and_collider(
self,
context: Context,
) -> tuple[Object, "SpringBone1ColliderPropertyGroup"]:
return find_armature_and_collider(
context, lambda collider: collider.shape.sphere == self
)
def get_offset(self) -> tuple[float, float, float]:
context = bpy.context
armature, collider = self.find_armature_and_collider(context)
if not collider.bpy_object:
logger.error(
'Failed to get bpy object of "%s" in sphere.get_offset()', collider.name
)
return (0, 0, 0)
bone = armature.pose.bones.get(collider.node.bone_name)
if bone:
matrix = (
bone.matrix.inverted()
@ armature.matrix_world.inverted()
@ collider.bpy_object.matrix_world
)
else:
matrix = armature.matrix_world.inverted() @ collider.bpy_object.matrix_world
return convert.float3_or(matrix.to_translation(), (0.0, 0.0, 0.0))
def set_offset(self, offset: Sequence[float]) -> None:
context = bpy.context
armature, collider = self.find_armature_and_collider(context)
collider.reset_bpy_object(context, armature)
bone = armature.pose.bones.get(collider.node.bone_name)
if not bone:
if collider.bpy_object:
collider.bpy_object.matrix_world = (
armature.matrix_world @ Matrix.Translation(offset)
)
return
if collider.bpy_object:
collider.bpy_object.matrix_world = (
armature.matrix_world @ bone.matrix @ Matrix.Translation(offset)
)
def update_offset(self, _context: Context) -> None:
self.fallback_offset = self.offset
def get_radius(self) -> float:
context = bpy.context
_, collider = self.find_armature_and_collider(context)
if not collider.bpy_object:
logger.error(
'Failed to get bpy object of "%s" in sphere.get_radius()', collider.name
)
return 0.0
mean_scale = statistics.mean(
abs(s) for s in collider.bpy_object.matrix_basis.to_scale()
)
empty_display_size: float = collider.bpy_object.empty_display_size
return float(mean_scale) * empty_display_size
def set_radius(self, v: float) -> None:
context = bpy.context
armature, collider = self.find_armature_and_collider(context)
collider.reset_bpy_object(context, armature)
if not collider.bpy_object:
logger.error(
'Failed to reset bpy object of "%s" in sphere.set_radius()',
collider.name,
)
return
location, rotation, _ = collider.bpy_object.matrix_basis.decompose()
collider.bpy_object.matrix_basis = (
Matrix.Translation(location) @ rotation.to_matrix().to_4x4()
)
collider.bpy_object.empty_display_size = v
def update_radius(self, _context: Context) -> None:
self.fallback_radius = self.radius
offset: FloatVectorProperty( # type: ignore[valid-type]
size=3,
subtype="TRANSLATION",
unit="LENGTH",
default=(0, 0, 0),
get=get_offset,
set=set_offset,
update=update_offset,
)
radius: FloatProperty( # type: ignore[valid-type]
name="Radius",
min=0.0,
default=0.0,
soft_max=1.0,
unit="LENGTH",
get=get_radius,
set=set_radius,
update=update_radius,
)
fallback_offset: FloatVectorProperty( # type: ignore[valid-type]
name="Fallback Sphere Offset",
size=3,
subtype="TRANSLATION",
unit="LENGTH",
default=(0, 0, 0),
)
fallback_radius: FloatProperty( # type: ignore[valid-type]
name="Fallback Sphere Radius",
min=0.0,
default=0.0,
soft_max=1.0,
unit="LENGTH",
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
offset: Sequence[float] # type: ignore[no-redef]
radius: float # type: ignore[no-redef]
fallback_offset: Sequence[float] # type: ignore[no-redef]
fallback_radius: float # type: ignore[no-redef]
# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.shape.schema.json#L28-L58
class SpringBone1ColliderShapeCapsulePropertyGroup(PropertyGroup):
def find_armature_and_collider(
self,
context: Context,
) -> tuple[Object, "SpringBone1ColliderPropertyGroup"]:
return find_armature_and_collider(
context, lambda collider: collider.shape.capsule == self
)
def get_offset(self) -> tuple[float, float, float]:
context = bpy.context
armature, collider = self.find_armature_and_collider(context)
if not collider.bpy_object:
logger.error(
'Failed to get bpy object of "%s" in capsule.get_offset()',
collider.name,
)
return (0, 0, 0)
bone = armature.pose.bones.get(collider.node.bone_name)
if bone:
matrix = (
bone.matrix.inverted()
@ armature.matrix_world.inverted()
@ collider.bpy_object.matrix_world
)
else:
matrix = armature.matrix_world.inverted() @ collider.bpy_object.matrix_world
return convert.float3_or(matrix.to_translation(), (0.0, 0.0, 0.0))
def set_offset(self, offset: Sequence[float]) -> None:
context = bpy.context
armature, collider = self.find_armature_and_collider(context)
collider.reset_bpy_object(context, armature)
bone = armature.pose.bones.get(collider.node.bone_name)
if not bone:
if collider.bpy_object:
collider.bpy_object.matrix_world = (
armature.matrix_world @ Matrix.Translation(offset)
)
return
if collider.bpy_object:
collider.bpy_object.matrix_world = (
armature.matrix_world @ bone.matrix @ Matrix.Translation(offset)
)
def update_offset(self, _context: Context) -> None:
self.fallback_offset = self.offset
def get_tail(self) -> tuple[float, float, float]:
context = bpy.context
armature, collider = self.find_armature_and_collider(context)
if not collider.bpy_object or not collider.bpy_object.children:
logger.error(
'Failed to get bpy object of "%s" in capsule.get_tail()', collider.name
)
return (0, 0, 0)
bone = armature.pose.bones.get(collider.node.bone_name)
if bone:
matrix = (
bone.matrix.inverted()
@ armature.matrix_world.inverted()
@ collider.bpy_object.children[0].matrix_world
)
else:
matrix = (
armature.matrix_world.inverted()
@ collider.bpy_object.children[0].matrix_world
)
return convert.float3_or(matrix.to_translation(), (0.0, 0.0, 0.0))
def set_tail(self, offset: Sequence[float]) -> None:
context = bpy.context
armature, collider = self.find_armature_and_collider(context)
collider.reset_bpy_object(context, armature)
bone = armature.pose.bones.get(collider.node.bone_name)
if not bone:
if collider.bpy_object:
collider.bpy_object.children[0].matrix_world = (
armature.matrix_world @ Matrix.Translation(offset)
)
return
if collider.bpy_object:
if not collider.bpy_object.children:
logger.error(
'Failed to set tail bpy object matrix of "%s"'
" in capsule.set_tail()",
collider.name,
)
return
collider.bpy_object.children[0].matrix_world = (
armature.matrix_world @ bone.matrix @ Matrix.Translation(offset)
)
def update_tail(self, _context: Context) -> None:
self.fallback_tail = self.tail
def get_radius(self) -> float:
context = bpy.context
_, collider = self.find_armature_and_collider(context)
if not collider.bpy_object:
logger.error(
'Failed to get bpy object of "%s" in capsule.get_radius()',
collider.name,
)
return 0.0
mean_scale: float = statistics.mean(
abs(s) for s in collider.bpy_object.matrix_basis.to_scale()
)
empty_display_size: float = collider.bpy_object.empty_display_size
return mean_scale * empty_display_size
def set_radius(self, v: float) -> None:
context = bpy.context
armature, collider = self.find_armature_and_collider(context)
collider.reset_bpy_object(context, armature)
if not collider.bpy_object:
logger.error(
'Failed to reset bpy object of "%s" in capsule.set_radius()',
collider.name,
)
return
location, rotation, _ = collider.bpy_object.matrix_basis.decompose()
collider.bpy_object.matrix_basis = (
Matrix.Translation(location) @ rotation.to_matrix().to_4x4()
)
collider.bpy_object.empty_display_size = v
if not collider.bpy_object.children:
logger.error(
'Failed to set tail bpy object size of "%s" in capsule.set_radius()',
collider.name,
)
return
collider.bpy_object.children[0].empty_display_size = v
def update_radius(self, _context: Context) -> None:
self.fallback_radius = self.radius
offset: FloatVectorProperty( # type: ignore[valid-type]
size=3,
subtype="TRANSLATION",
unit="LENGTH",
default=(0, 0, 0),
get=get_offset,
set=set_offset,
update=update_offset,
)
radius: FloatProperty( # type: ignore[valid-type]
name="Radius",
min=0.0,
default=0.0,
soft_max=1.0,
unit="LENGTH",
get=get_radius,
set=set_radius,
update=update_radius,
)
tail: FloatVectorProperty( # type: ignore[valid-type]
size=3,
subtype="TRANSLATION",
unit="LENGTH",
default=(0, 0, 0),
get=get_tail,
set=set_tail,
update=update_tail,
)
fallback_offset: FloatVectorProperty( # type: ignore[valid-type]
name="Fallback Capsule Head",
size=3,
subtype="TRANSLATION",
unit="LENGTH",
default=(0, 0, 0),
)
fallback_radius: FloatProperty( # type: ignore[valid-type]
name="Fallback Capsule Radius",
min=0.0,
default=0.0,
soft_max=1.0,
unit="LENGTH",
)
fallback_tail: FloatVectorProperty( # type: ignore[valid-type]
name="Fallback Capsule Tail",
size=3,
subtype="TRANSLATION",
unit="LENGTH",
default=(0, 0, 0),
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
offset: Sequence[float] # type: ignore[no-redef]
radius: float # type: ignore[no-redef]
tail: Sequence[float] # type: ignore[no-redef]
fallback_offset: Sequence[float] # type: ignore[no-redef]
fallback_radius: float # type: ignore[no-redef]
fallback_tail: Sequence[float] # type: ignore[no-redef]
# https://github.com/vrm-c/vrm-specification/blob/9e8c886a2043639a3c166eb7cc93526be6313147/specification/VRMC_springBone_extended_collider-1.0/schema/VRMC_springBone_extended_collider.shape.schema.json#L9
class SpringBone1ExtendedColliderShapeSpherePropertyGroup(
SpringBone1ColliderShapeSpherePropertyGroup
):
def find_armature_and_collider(
self,
context: Context,
) -> tuple[Object, "SpringBone1ColliderPropertyGroup"]:
return find_armature_and_collider(
context,
lambda collider: (
collider.extensions.vrmc_spring_bone_extended_collider.shape.sphere
== self
),
)
inside: BoolProperty() # type: ignore[valid-type]
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
inside: bool # type: ignore[no-redef]
# https://github.com/vrm-c/vrm-specification/blob/9e8c886a2043639a3c166eb7cc93526be6313147/specification/VRMC_springBone_extended_collider-1.0/schema/VRMC_springBone_extended_collider.shape.schema.json#L36
class SpringBone1ExtendedColliderShapeCapsulePropertyGroup(
SpringBone1ColliderShapeCapsulePropertyGroup
):
def find_armature_and_collider(
self,
context: Context,
) -> tuple[Object, "SpringBone1ColliderPropertyGroup"]:
return find_armature_and_collider(
context,
lambda collider: (
collider.extensions.vrmc_spring_bone_extended_collider.shape.capsule
== self
),
)
inside: BoolProperty() # type: ignore[valid-type]
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
inside: bool # type: ignore[no-redef]
# https://github.com/vrm-c/vrm-specification/blob/9e8c886a2043639a3c166eb7cc93526be6313147/specification/VRMC_springBone_extended_collider-1.0/schema/VRMC_springBone_extended_collider.shape.schema.json#L73
class SpringBone1ExtendedColliderShapePlanePropertyGroup(PropertyGroup):
def find_armature_and_collider(
self,
context: Context,
) -> tuple[Object, "SpringBone1ColliderPropertyGroup"]:
return find_armature_and_collider(
context,
lambda collider: (
collider.extensions.vrmc_spring_bone_extended_collider.shape.plane
== self
),
)
def get_offset(self) -> tuple[float, float, float]:
context = bpy.context
armature, collider = self.find_armature_and_collider(context)
if not collider.bpy_object:
logger.error(
'Failed to get bpy object of "%s" in extended_collider.get_offset()',
collider.name,
)
return (0, 0, 0)
bone = armature.pose.bones.get(collider.node.bone_name)
if bone:
matrix = (
bone.matrix.inverted()
@ armature.matrix_world.inverted()
@ collider.bpy_object.matrix_world
)
else:
matrix = armature.matrix_world.inverted() @ collider.bpy_object.matrix_world
return convert.float3_or(matrix.to_translation(), (0.0, 0.0, 0.0))
def set_offset(self, offset: Sequence[float]) -> None:
context = bpy.context
armature, collider = self.find_armature_and_collider(context)
collider.reset_bpy_object(context, armature)
bone = armature.pose.bones.get(collider.node.bone_name)
if not bone:
if collider.bpy_object:
collider.bpy_object.matrix_world = (
armature.matrix_world @ Matrix.Translation(offset)
)
return
if collider.bpy_object:
collider.bpy_object.matrix_world = (
armature.matrix_world @ bone.matrix @ Matrix.Translation(offset)
)
def get_normal(self) -> tuple[float, float, float]:
context = bpy.context
_, collider = self.find_armature_and_collider(context)
bpy_object = collider.bpy_object
if not bpy_object:
return (0.0, 1.0, 0.0)
result = Vector((0.0, 1.0, 0.0))
result.rotate(get_rotation_as_quaternion(bpy_object))
return (
# use tuple initializer to make type checkers happy
result.x,
result.y,
result.z,
)
def set_normal(self, normal: Sequence[float]) -> None:
context = bpy.context
armature, collider = self.find_armature_and_collider(context)
collider.reset_bpy_object(context, armature)
y_up_vec = Vector((0.0, 1.0, 0.0))
normal_vec = Vector(normal)
if normal_vec.length_squared > 0:
rotation = y_up_vec.rotation_difference(normal_vec)
else:
rotation = Quaternion()
bpy_object = collider.bpy_object
if bpy_object:
set_rotation_without_mode_change(bpy_object, rotation)
offset: FloatVectorProperty( # type: ignore[valid-type]
size=3,
subtype="TRANSLATION",
unit="LENGTH",
default=(0, 0, 0),
get=get_offset,
set=set_offset,
)
normal: FloatVectorProperty( # type: ignore[valid-type]
size=3,
default=(0, 0, 1),
get=get_normal,
set=set_normal,
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
offset: Sequence[float] # type: ignore[no-redef]
normal: Sequence[float] # type: ignore[no-redef]
# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.shape.schema.json
class SpringBone1ColliderShapePropertyGroup(PropertyGroup):
sphere: PointerProperty( # type: ignore[valid-type]
type=SpringBone1ColliderShapeSpherePropertyGroup
)
capsule: PointerProperty( # type: ignore[valid-type]
type=SpringBone1ColliderShapeCapsulePropertyGroup
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
sphere: SpringBone1ColliderShapeSpherePropertyGroup # type: ignore[no-redef]
capsule: SpringBone1ColliderShapeCapsulePropertyGroup # type: ignore[no-redef]
# https://github.com/vrm-c/vrm-specification/blob/9e8c886a2043639a3c166eb7cc93526be6313147/specification/VRMC_springBone_extended_collider-1.0/schema/VRMC_springBone_extended_collider.shape.schema.json
class SpringBone1ExtendedColliderShapePropertyGroup(PropertyGroup):
sphere: PointerProperty( # type: ignore[valid-type]
type=SpringBone1ExtendedColliderShapeSpherePropertyGroup
)
capsule: PointerProperty( # type: ignore[valid-type]
type=SpringBone1ExtendedColliderShapeCapsulePropertyGroup
)
plane: PointerProperty( # type: ignore[valid-type]
type=SpringBone1ExtendedColliderShapePlanePropertyGroup
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
sphere: ( # type: ignore[no-redef]
SpringBone1ExtendedColliderShapeSpherePropertyGroup
)
capsule: ( # type: ignore[no-redef]
SpringBone1ExtendedColliderShapeCapsulePropertyGroup
)
plane: ( # type: ignore[no-redef]
SpringBone1ExtendedColliderShapePlanePropertyGroup
)
# https://github.com/vrm-c/vrm-specification/blob/9e8c886a2043639a3c166eb7cc93526be6313147/specification/VRMC_springBone_extended_collider-1.0/schema/VRMC_springBone_extended_collider.schema.json
class SpringBone1VrmcSpringBoneExtendedColliderPropertyGroup(PropertyGroup):
def update_shape_type(self, context: Context) -> None:
armature, collider = find_armature_and_collider(
context,
lambda collider: (
collider.extensions.vrmc_spring_bone_extended_collider == self
),
)
collider.reset_bpy_object(context, armature)
enabled: BoolProperty( # type: ignore[valid-type]
name="Extended Collider",
update=update_shape_type,
)
automatic_fallback_generation: BoolProperty( # type: ignore[valid-type]
name="Automatic Fallback Generation",
default=True,
)
shape: PointerProperty( # type: ignore[valid-type]
type=SpringBone1ExtendedColliderShapePropertyGroup
)
(
shape_type_enum,
(
SHAPE_TYPE_EXTENDED_SPHERE,
SHAPE_TYPE_EXTENDED_CAPSULE,
SHAPE_TYPE_EXTENDED_PLANE,
),
) = property_group_enum(
("extendedSphere", "Sphere", "", "NONE", 0),
("extendedCapsule", "Capsule", "", "NONE", 1),
("extendedPlane", "Plane", "", "NONE", 2),
)
shape_type: EnumProperty( # type: ignore[valid-type]
items=shape_type_enum.items(),
name="Shape",
update=update_shape_type,
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
enabled: bool # type: ignore[no-redef]
automatic_fallback_generation: bool # type: ignore[no-redef]
shape: SpringBone1ExtendedColliderShapePropertyGroup # type: ignore[no-redef]
shape_type: str # type: ignore[no-redef]
class SpringBone1ColliderExtensionsPropertyGroup(PropertyGroup):
vrmc_spring_bone_extended_collider: PointerProperty( # type: ignore[valid-type]
type=SpringBone1VrmcSpringBoneExtendedColliderPropertyGroup
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
vrmc_spring_bone_extended_collider: ( # type: ignore[no-redef]
SpringBone1VrmcSpringBoneExtendedColliderPropertyGroup
)
# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.collider.schema.json
class SpringBone1ColliderPropertyGroup(PropertyGroup):
def broadcast_bpy_object_name(self) -> None:
armature = self.id_data
if not isinstance(armature, Armature):
message = (
f"{type(self)}/{self}.id_data is not a {Armature}"
+ f" but {type(armature)}/{armature}"
)
raise TypeError(message)
if not self.bpy_object or not self.bpy_object.name:
self.name = ""
return
if self.name == self.bpy_object.name:
return
self.name = self.bpy_object.name
spring_bone = get_armature_spring_bone1_extension(armature)
for collider_group in spring_bone.collider_groups:
for collider_reference in collider_group.colliders:
if self.uuid != collider_reference.collider_uuid:
continue
if collider_reference.collider_name != self.name:
collider_reference.collider_name = self.name
node: PointerProperty(type=BonePropertyGroup) # type: ignore[valid-type]
shape: PointerProperty(type=SpringBone1ColliderShapePropertyGroup) # type: ignore[valid-type]
extensions: PointerProperty(type=SpringBone1ColliderExtensionsPropertyGroup) # type: ignore[valid-type]
# for UI
show_expanded: BoolProperty() # type: ignore[valid-type]
(
shape_type_enum,
(
SHAPE_TYPE_SPHERE,
SHAPE_TYPE_CAPSULE,
),
) = property_group_enum(
("Sphere", "Sphere", "", "NONE", 0),
("Capsule", "Capsule", "", "NONE", 1),
)
(
ui_collider_type_enum,
(
UI_COLLIDER_TYPE_SPHERE,
UI_COLLIDER_TYPE_CAPSULE,
UI_COLLIDER_TYPE_PLANE,
UI_COLLIDER_TYPE_SPHERE_INSIDE,
UI_COLLIDER_TYPE_CAPSULE_INSIDE,
),
) = property_group_enum(
("uiColliderTypeSphere", "Sphere", "", "NONE", 0),
("uiColliderTypeCapsule", "Capsule", "", "NONE", 1),
("uiColliderTypePlane", "Plane", "", "NONE", 2),
("uiColliderTypeSphereInside", "Sphere Inside", "", "NONE", 3),
("uiColliderTypeCapsuleInside", "Capsule Inside", "", "NONE", 4),
)
def get_ui_collider_type(self) -> int:
extended = self.extensions.vrmc_spring_bone_extended_collider
if not extended.enabled:
if self.shape_type == self.SHAPE_TYPE_SPHERE.identifier:
return self.UI_COLLIDER_TYPE_SPHERE.value
if self.shape_type == self.SHAPE_TYPE_CAPSULE.identifier:
return self.UI_COLLIDER_TYPE_CAPSULE.value
return self.UI_COLLIDER_TYPE_SPHERE.value
if extended.shape_type == extended.SHAPE_TYPE_EXTENDED_PLANE.identifier:
return self.UI_COLLIDER_TYPE_PLANE.value
if extended.shape_type == extended.SHAPE_TYPE_EXTENDED_SPHERE.identifier:
if extended.shape.sphere.inside:
return self.UI_COLLIDER_TYPE_SPHERE_INSIDE.value
return self.UI_COLLIDER_TYPE_SPHERE.value
if extended.shape_type == extended.SHAPE_TYPE_EXTENDED_CAPSULE.identifier:
if extended.shape.capsule.inside:
return self.UI_COLLIDER_TYPE_CAPSULE_INSIDE.value
return self.UI_COLLIDER_TYPE_CAPSULE.value
return self.UI_COLLIDER_TYPE_SPHERE.value
def set_ui_collider_type(self, value: int) -> None:
extended = self.extensions.vrmc_spring_bone_extended_collider
if value in [
self.UI_COLLIDER_TYPE_SPHERE.value,
self.UI_COLLIDER_TYPE_SPHERE_INSIDE.value,
]:
if self.shape_type != self.SHAPE_TYPE_SPHERE.identifier:
self.shape_type = self.SHAPE_TYPE_SPHERE.identifier
if extended.shape_type != extended.SHAPE_TYPE_EXTENDED_SPHERE.identifier:
extended.shape_type = extended.SHAPE_TYPE_EXTENDED_SPHERE.identifier
inside = self.UI_COLLIDER_TYPE_SPHERE_INSIDE.value == value
if extended.shape.sphere.inside != inside:
extended.shape.sphere.inside = inside
if extended.enabled != inside:
extended.enabled = inside
return
if value in [
self.UI_COLLIDER_TYPE_CAPSULE.value,
self.UI_COLLIDER_TYPE_CAPSULE_INSIDE.value,
]:
if self.shape_type != self.SHAPE_TYPE_CAPSULE.identifier:
self.shape_type = self.SHAPE_TYPE_CAPSULE.identifier
if extended.shape_type != extended.SHAPE_TYPE_EXTENDED_CAPSULE.identifier:
extended.shape_type = extended.SHAPE_TYPE_EXTENDED_CAPSULE.identifier
inside = self.UI_COLLIDER_TYPE_CAPSULE_INSIDE.value == value
if extended.shape.capsule.inside != inside:
extended.shape.capsule.inside = inside
if extended.enabled != inside:
extended.enabled = inside
return
if self.UI_COLLIDER_TYPE_PLANE.value == value:
if not extended.enabled:
extended.enabled = True
if extended.shape_type != extended.SHAPE_TYPE_EXTENDED_PLANE.identifier:
extended.shape_type = extended.SHAPE_TYPE_EXTENDED_PLANE.identifier
def update_shape_type(self, context: Context) -> None:
if (
self.bpy_object
and self.bpy_object.parent
and self.bpy_object.parent.type == "ARMATURE"
):
self.reset_bpy_object(context, self.bpy_object.parent)
shape_type: EnumProperty( # type: ignore[valid-type]
items=shape_type_enum.items(),
name="Shape",
update=update_shape_type,
)
ui_collider_type: EnumProperty( # type: ignore[valid-type]
items=ui_collider_type_enum.items(),
name="Collider Type",
get=get_ui_collider_type,
set=set_ui_collider_type,
)
# for View3D
bpy_object: PointerProperty( # type: ignore[valid-type]
type=Object
)
# for references
uuid: StringProperty() # type: ignore[valid-type]
def reset_bpy_object(self, context: Context, armature: Object) -> None:
armature_data = armature.data
if not isinstance(armature_data, Armature):
return
collider_prefix = armature_data.name
if self.node and self.node.bone_name:
collider_prefix = self.node.bone_name
if not self.bpy_object or not self.bpy_object.name:
obj = context.blend_data.objects.new(
name=f"{collider_prefix} Collider", object_data=None
)
obj.empty_display_size = 0.125
context.scene.collection.objects.link(obj)
self.bpy_object = obj
self.bpy_object.select_set(True)
if self.bpy_object.parent != armature:
self.bpy_object.parent = armature
extended = self.extensions.vrmc_spring_bone_extended_collider
if (
not extended.enabled
and self.shape_type == self.SHAPE_TYPE_SPHERE.identifier
) or (
extended.enabled
and extended.shape_type
in [
extended.SHAPE_TYPE_EXTENDED_SPHERE.identifier,
extended.SHAPE_TYPE_EXTENDED_PLANE.identifier,
]
):
for child in list(self.bpy_object.children):
if not safe_removal.remove_object(context, child):
logger.warning(
'Failed to remove "%s" with %d users'
" while changing spring bone collider type",
child.name,
child.users,
)
empty_display_type = "SPHERE"
if extended.shape_type == extended.SHAPE_TYPE_EXTENDED_PLANE.identifier:
empty_display_type = "CIRCLE"
if self.bpy_object.empty_display_type != empty_display_type:
self.bpy_object.empty_display_type = empty_display_type
elif (
not extended.enabled
and self.shape_type == self.SHAPE_TYPE_CAPSULE.identifier
) or (
extended.enabled
and extended.shape_type == extended.SHAPE_TYPE_EXTENDED_CAPSULE.identifier
):
if self.bpy_object.children:
end_object = self.bpy_object.children[0]
else:
end_object = context.blend_data.objects.new(
name=f"{self.bpy_object.name} End", object_data=None
)
end_object.empty_display_size = self.bpy_object.empty_display_size
context.scene.collection.objects.link(end_object)
end_object.parent = self.bpy_object
end_object.matrix_local = Matrix.Translation(Vector((0.0, 0.2, 0.0)))
end_object.select_set(True)
if self.bpy_object.empty_display_type != "SPHERE":
self.bpy_object.empty_display_type = "SPHERE"
if end_object.empty_display_type != "SPHERE":
end_object.empty_display_type = "SPHERE"
if end_object.empty_display_size != self.bpy_object.empty_display_size:
end_object.empty_display_size = self.bpy_object.empty_display_size
if end_object.scale != Vector((1, 1, 1)):
end_object.scale = Vector((1, 1, 1))
if self.node.bone_name:
if self.bpy_object.parent_type != "BONE":
self.bpy_object.parent_type = "BONE"
if self.bpy_object.parent_bone != self.node.bone_name:
self.bpy_object.parent_bone = self.node.bone_name
else:
if self.bpy_object.parent_type != "OBJECT":
self.bpy_object.parent_type = "OBJECT"
if self.bpy_object.parent_bone:
self.bpy_object.parent_bone = ""
self.broadcast_bpy_object_name()
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
node: BonePropertyGroup # type: ignore[no-redef]
shape: SpringBone1ColliderShapePropertyGroup # type: ignore[no-redef]
extensions: SpringBone1ColliderExtensionsPropertyGroup # type: ignore[no-redef]
show_expanded: bool # type: ignore[no-redef]
shape_type: str # type: ignore[no-redef]
ui_collider_type: str # type: ignore[no-redef]
bpy_object: Optional[Object] # type: ignore[no-redef]
uuid: str # type: ignore[no-redef]
class SpringBone1ColliderReferencePropertyGroup(PropertyGroup):
def get_collider_name(self) -> str:
value = self.get("collider_name", "")
return value if isinstance(value, str) else str(value)
def set_collider_name(self, value: object) -> None:
armature = self.id_data
if not isinstance(armature, Armature):
message = (
f"{type(self)}/{self}.id_data is not a {Armature}"
+ f" but {type(armature)}/{armature}"
)
raise TypeError(message)
if not isinstance(value, str):
value = str(value)
self.name = value
if self.get("collider_name") == value:
return
self["collider_name"] = value
spring_bone = get_armature_spring_bone1_extension(armature)
for collider in spring_bone.colliders:
if collider.name != value:
continue
if self.collider_uuid != collider.uuid:
self.collider_uuid = collider.uuid
collider_name: StringProperty( # type: ignore[valid-type]
get=get_collider_name, set=set_collider_name
)
collider_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.
collider_name: str # type: ignore[no-redef]
collider_uuid: str # type: ignore[no-redef]
# https://github.com/vrm-c/vrm-specification/blob/f2d8f158297fc883aef9c3071ca68fbe46b03f45/specification/0.0/schema/vrm.secondaryanimation.collidergroup.schema.json
class SpringBone1ColliderGroupPropertyGroup(PropertyGroup):
def get_vrm_name(self) -> str:
value = self.get("vrm_name", "")
return value if isinstance(value, str) else str(value)
def set_vrm_name(self, vrm_name: object) -> None:
if not isinstance(vrm_name, str):
vrm_name = str(vrm_name)
self["vrm_name"] = vrm_name
self.fix_index()
def fix_index(self) -> None:
armature = self.id_data
if not isinstance(armature, Armature):
message = (
f"{type(self)}/{self}.id_data is not a {Armature}"
+ f" but {type(armature)}/{armature}"
)
raise TypeError(message)
spring_bone = get_armature_spring_bone1_extension(armature)
for index, collider_group in enumerate(spring_bone.collider_groups):
if collider_group.path_from_id() != self.path_from_id():
continue
name = f"{self.vrm_name} #{index + 1}"
self.name = name
for spring in spring_bone.springs:
for collider_group_reference in spring.collider_groups:
if collider_group_reference.collider_group_uuid == self.uuid:
collider_group_reference.collider_group_name = name
return
vrm_name: StringProperty( # type: ignore[valid-type]
name="Name",
get=get_vrm_name,
set=set_vrm_name,
)
colliders: CollectionProperty( # type: ignore[valid-type]
name="Colliders",
type=SpringBone1ColliderReferencePropertyGroup,
)
# for UI
show_expanded: BoolProperty() # type: ignore[valid-type]
active_collider_index: IntProperty(min=0) # type: ignore[valid-type]
# for reference
# When copying objects, there may be multiple objects with the same uuid
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.
vrm_name: str # type: ignore[no-redef]
colliders: CollectionPropertyProtocol[ # type: ignore[no-redef]
SpringBone1ColliderReferencePropertyGroup
]
show_expanded: bool # type: ignore[no-redef]
active_collider_index: int # type: ignore[no-redef]
uuid: str # type: ignore[no-redef]
# https://github.com/vrm-c/vrm-specification/tree/993a90a5bda9025f3d9e2923ad6dea7506f88553/specification/VRMC_springBone-1.0#initialization
class SpringBone1JointAnimationStatePropertyGroup(PropertyGroup):
initialized_as_tail: BoolProperty() # type: ignore[valid-type]
previous_world_translation: FloatVectorProperty(size=3) # type: ignore[valid-type]
current_world_translation: FloatVectorProperty(size=3) # type: ignore[valid-type]
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
initialized_as_tail: bool # type: ignore[no-redef]
previous_world_translation: Sequence[float] # type: ignore[no-redef]
current_world_translation: Sequence[float] # type: ignore[no-redef]
# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.joint.schema.json
class SpringBone1JointPropertyGroup(PropertyGroup):
node: PointerProperty(type=BonePropertyGroup) # type: ignore[valid-type]
hit_radius: FloatProperty( # type: ignore[valid-type]
name="Joint Radius",
min=0.0,
default=0.0,
soft_max=0.5,
)
stiffness: FloatProperty( # type: ignore[valid-type]
name="Stiffness Force",
min=0.0,
default=1.0,
soft_max=4.0,
)
gravity_power: FloatProperty( # type: ignore[valid-type]
name="Gravity Power",
min=0.0,
default=0.0,
soft_max=2.0,
)
def update_gravity_dir(self, _context: Context) -> None:
gravity_dir = Vector(self.gravity_dir)
normalized_gravity_dir = gravity_dir.normalized()
if abs(normalized_gravity_dir.length) < float_info.epsilon:
self.gravity_dir = (0, 0, -1)
elif (gravity_dir - normalized_gravity_dir).length > 0.0001:
self.gravity_dir = normalized_gravity_dir
gravity_dir: FloatVectorProperty( # type: ignore[valid-type]
name="Gravity Dir",
size=3,
min=-1,
max=1,
default=(0, 0, -1),
subtype="XYZ",
update=update_gravity_dir,
)
drag_force: FloatProperty( # type: ignore[valid-type]
name="Drag Force",
default=0.5,
min=0,
max=1.0,
)
animation_state: PointerProperty( # type: ignore[valid-type]
type=SpringBone1JointAnimationStatePropertyGroup,
)
# for UI
show_expanded: BoolProperty() # type: ignore[valid-type]
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
node: BonePropertyGroup # type: ignore[no-redef]
hit_radius: float # type: ignore[no-redef]
stiffness: float # type: ignore[no-redef]
gravity_power: float # type: ignore[no-redef]
gravity_dir: Sequence[float] # type: ignore[no-redef]
drag_force: float # type: ignore[no-redef]
animation_state: ( # type: ignore[no-redef]
SpringBone1JointAnimationStatePropertyGroup
)
show_expanded: bool # type: ignore[no-redef]
class SpringBone1ColliderGroupReferencePropertyGroup(PropertyGroup):
def get_collider_group_name(self) -> str:
value = self.get("collider_group_name", "")
return value if isinstance(value, str) else str(value)
def set_collider_group_name(self, value: object) -> None:
armature = self.id_data
if not isinstance(armature, Armature):
message = (
f"{type(self)}/{self}.id_data is not a {Armature}"
+ f" but {type(armature)}/{armature}"
)
raise TypeError(message)
if not isinstance(value, str):
value = str(value)
self.name = value
if self.get("collider_group_name") != value:
self["collider_group_name"] = value
spring_bone = get_armature_spring_bone1_extension(armature)
for collider_group in spring_bone.collider_groups:
if collider_group.name != value:
continue
if self.collider_group_uuid != collider_group.uuid:
self.collider_group_uuid = collider_group.uuid
break
collider_group_name: StringProperty( # type: ignore[valid-type]
get=get_collider_group_name, set=set_collider_group_name
)
collider_group_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.
collider_group_name: str # type: ignore[no-redef]
collider_group_uuid: str # type: ignore[no-redef]
class SpringBone1SpringAnimationStatePropertyGroup(PropertyGroup):
use_center_space: BoolProperty() # type: ignore[valid-type]
previous_center_world_translation: FloatVectorProperty(size=3) # type: ignore[valid-type]
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
use_center_space: bool # type: ignore[no-redef]
previous_center_world_translation: Sequence[float] # type: ignore[no-redef]
# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.spring.schema.json
class SpringBone1SpringPropertyGroup(PropertyGroup):
vrm_name: StringProperty( # type: ignore[valid-type]
name="Name"
)
joints: CollectionProperty( # type: ignore[valid-type]
type=SpringBone1JointPropertyGroup
)
collider_groups: CollectionProperty( # type: ignore[valid-type]
type=SpringBone1ColliderGroupReferencePropertyGroup,
)
center: PointerProperty( # type: ignore[valid-type]
type=BonePropertyGroup,
)
# for UI
show_expanded: BoolProperty() # type: ignore[valid-type]
show_expanded_bones: BoolProperty( # type: ignore[valid-type]
name="Bones"
)
show_expanded_collider_groups: BoolProperty( # type: ignore[valid-type]
name="Collider Groups"
)
active_joint_index: IntProperty(min=0) # type: ignore[valid-type]
active_collider_group_index: IntProperty(min=0) # type: ignore[valid-type]
animation_state: PointerProperty( # type: ignore[valid-type]
type=SpringBone1SpringAnimationStatePropertyGroup
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
vrm_name: str # type: ignore[no-redef]
joints: CollectionPropertyProtocol[ # type: ignore[no-redef]
SpringBone1JointPropertyGroup
]
collider_groups: CollectionPropertyProtocol[ # type: ignore[no-redef]
SpringBone1ColliderGroupReferencePropertyGroup
]
center: BonePropertyGroup # type: ignore[no-redef]
show_expanded: bool # type: ignore[no-redef]
show_expanded_bones: bool # type: ignore[no-redef]
show_expanded_collider_groups: bool # type: ignore[no-redef]
active_joint_index: int # type: ignore[no-redef]
active_collider_group_index: int # type: ignore[no-redef]
animation_state: ( # type: ignore[no-redef]
SpringBone1SpringAnimationStatePropertyGroup
)
# https://github.com/vrm-c/vrm-specification/blob/6fb6baaf9b9095a84fb82c8384db36e1afeb3558/specification/VRMC_springBone-1.0-beta/schema/VRMC_springBone.schema.json
class SpringBone1SpringBonePropertyGroup(PropertyGroup):
colliders: CollectionProperty( # type: ignore[valid-type]
type=SpringBone1ColliderPropertyGroup,
)
collider_groups: CollectionProperty( # type: ignore[valid-type]
type=SpringBone1ColliderGroupPropertyGroup,
)
springs: CollectionProperty( # type: ignore[valid-type]
type=SpringBone1SpringPropertyGroup,
)
def update_enable_animation(self, _context: Context) -> None:
for spring in self.springs:
for joint in spring.joints:
joint.animation_state.initialized_as_tail = False
enable_animation: BoolProperty( # type: ignore[valid-type]
name="Enable Animation",
update=update_enable_animation,
)
# for UI
show_expanded_colliders: BoolProperty( # type: ignore[valid-type]
name="Spring Bone Colliders"
)
show_expanded_collider_groups: BoolProperty( # type: ignore[valid-type]
name="Spring Bone Collider Groups"
)
show_expanded_springs: BoolProperty( # type: ignore[valid-type]
name="Spring Bone Springs"
)
active_collider_index: IntProperty(min=0) # type: ignore[valid-type]
active_collider_group_index: IntProperty(min=0) # type: ignore[valid-type]
active_spring_index: IntProperty(min=0) # type: ignore[valid-type]
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
colliders: CollectionPropertyProtocol[ # type: ignore[no-redef]
SpringBone1ColliderPropertyGroup
]
collider_groups: CollectionPropertyProtocol[ # type: ignore[no-redef]
SpringBone1ColliderGroupPropertyGroup
]
springs: CollectionPropertyProtocol[ # type: ignore[no-redef]
SpringBone1SpringPropertyGroup
]
enable_animation: bool # type: ignore[no-redef]
show_expanded_colliders: bool # type: ignore[no-redef]
show_expanded_collider_groups: bool # type: ignore[no-redef]
show_expanded_springs: bool # type: ignore[no-redef]
active_collider_index: int # type: ignore[no-redef]
active_collider_group_index: int # type: ignore[no-redef]
active_spring_index: int # type: ignore[no-redef]
def get_armature_spring_bone1_extension(
armature: Armature,
) -> SpringBone1SpringBonePropertyGroup:
from ..extension import get_armature_extension
ext = get_armature_extension(armature)
spring_bone1: SpringBone1SpringBonePropertyGroup = ext.spring_bone1
return spring_bone1