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/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
|
||||
)
|
||||
Reference in New Issue
Block a user