Files
blender-vrm/editor/make_armature.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

713 lines
25 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
from collections.abc import Set as AbstractSet
from math import radians
from sys import float_info
from typing import TYPE_CHECKING, Optional
import bpy
from bpy.props import BoolProperty, FloatProperty, StringProperty
from bpy.types import Armature, Context, EditBone, Object, Operator
from mathutils import Matrix, Vector
from ..common.version import get_addon_version
from ..common.workspace import save_workspace
from . import migration
from .extension import get_armature_extension
from .vrm0.property_group import (
Vrm0BlendShapeGroupPropertyGroup,
Vrm0HumanoidPropertyGroup,
)
MIN_BONE_LENGTH = 0.00001 # 10μm
AUTO_BONE_CONNECTION_DISTANCE = 0.000001 # 1μm
class ICYP_OT_make_armature(Operator):
bl_idname = "icyp.make_basic_armature"
bl_label = "Add VRM Humanoid"
bl_description = "make armature and simple setup for VRM export"
bl_options: AbstractSet[str] = {"REGISTER", "UNDO"}
skip_heavy_armature_setup: BoolProperty( # type: ignore[valid-type]
default=False,
options={"HIDDEN"},
)
wip_with_template_mesh: BoolProperty( # type: ignore[valid-type]
default=False
)
# Height in meters
tall: FloatProperty( # type: ignore[valid-type]
default=1.70,
min=0.3,
step=1,
name="Bone tall",
)
# Head-to-body ratio
head_ratio: FloatProperty( # type: ignore[valid-type]
default=8.0,
min=4,
step=5,
description="height per heads",
)
head_width_ratio: FloatProperty( # type: ignore[valid-type]
default=2 / 3,
min=0.3,
max=1.2,
step=5,
description="height per heads",
)
# Leg-to-torso ratio: 0: child-like, 1: adult-like (effective for low head count)
aging_ratio: FloatProperty( # type: ignore[valid-type]
default=0.5, min=0, max=1, step=10
)
# Eye depth
eye_depth: FloatProperty( # type: ignore[valid-type]
default=-0.03, min=-0.1, max=0, step=1
)
# Shoulder width
shoulder_in_width: FloatProperty( # type: ignore[valid-type]
default=0.05,
min=0.01,
step=1,
description="Inner shoulder position",
)
shoulder_width: FloatProperty( # type: ignore[valid-type]
default=0.08,
min=0.01,
step=1,
description="shoulder roll position",
)
# Arm length ratio
arm_length_ratio: FloatProperty( # type: ignore[valid-type]
default=1, min=0.5, step=1
)
# Hand
hand_ratio: FloatProperty( # type: ignore[valid-type]
default=1, min=0.5, max=2.0, step=5
)
finger_1_2_ratio: FloatProperty( # type: ignore[valid-type]
default=0.75,
min=0.5,
max=1,
step=1,
description="proximal / intermediate",
)
finger_2_3_ratio: FloatProperty( # type: ignore[valid-type]
default=0.75,
min=0.5,
max=1,
step=1,
description="intermediate / distal",
)
nail_bone: BoolProperty( # type: ignore[valid-type]
default=False,
description="may need for finger collider",
) # Needed for fingertip collision detection
# Foot
leg_length_ratio: FloatProperty( # type: ignore[valid-type]
default=0.5,
min=0.3,
max=0.6,
step=1,
description="upper body/lower body",
)
leg_width_ratio: FloatProperty( # type: ignore[valid-type]
default=1, min=0.01, step=1
)
leg_size: FloatProperty( # type: ignore[valid-type]
default=0.26, min=0.05, step=1
)
custom_property_name: StringProperty( # type: ignore[valid-type]
options={"HIDDEN"}
)
armature_obj: Optional[Object] = None
def execute(self, context: Context) -> set[str]:
with save_workspace(context):
self.armature_obj, compare_dict = self.make_armature(context)
self.setup_as_vrm(context, self.armature_obj, compare_dict)
if self.custom_property_name:
self.armature_obj[self.custom_property_name] = True
context.view_layer.objects.active = self.armature_obj
return {"FINISHED"}
def float_prop(self, name: str) -> float:
prop = getattr(self, name)
if not isinstance(prop, float):
message = f"prop {name} is not float"
raise TypeError(message)
return prop
def head_size(self) -> float:
return self.float_prop("tall") / self.float_prop("head_ratio")
def hand_size(self) -> float:
return self.head_size() * 0.75 * self.float_prop("hand_ratio")
def make_armature(self, context: Context) -> tuple[Object, dict[str, str]]:
def bone_add(
armature_data: Armature,
name: str,
head_pos: Vector,
tail_pos: Vector,
parent_bone: Optional[EditBone] = None,
radius: float = 0.1,
roll: float = 0,
) -> EditBone:
added_bone = armature_data.edit_bones.new(name)
added_bone.head = head_pos
added_bone.tail = tail_pos
added_bone.head_radius = radius
added_bone.tail_radius = radius
added_bone.envelope_distance = 0.01
added_bone.roll = radians(roll)
if parent_bone is not None:
added_bone.parent = parent_bone
bone_dict.update({name: added_bone})
return added_bone
def x_mirror_bones_add(
armature_data: Armature,
base_name: str,
right_head_pos: Vector,
right_tail_pos: Vector,
parent_bones: tuple[EditBone, EditBone],
radius: float = 0.1,
bone_type: str = "other",
) -> tuple[EditBone, EditBone]:
right_roll = 0
left_roll = 0
if bone_type == "arm":
right_roll = 0
elif bone_type == "leg":
right_roll = 0
left_roll = 0
left_bone = bone_add(
armature_data,
base_name + ".L",
right_head_pos,
right_tail_pos,
parent_bones[0],
radius=radius,
roll=left_roll,
)
head_pos = [pos * axis for pos, axis in zip(right_head_pos, (-1, 1, 1))]
tail_pos = [pos * axis for pos, axis in zip(right_tail_pos, (-1, 1, 1))]
right_bone = bone_add(
armature_data,
base_name + ".R",
Vector((head_pos[0], head_pos[1], head_pos[2])),
Vector((tail_pos[0], tail_pos[1], tail_pos[2])),
parent_bones[1],
radius=radius,
roll=right_roll,
)
return left_bone, right_bone
def x_add(pos_a: Vector, add_x: float) -> Vector:
pos = [p_a + _add for p_a, _add in zip(pos_a, [add_x, 0, 0])]
return Vector((pos[0], pos[1], pos[2]))
def y_add(pos_a: Vector, add_y: float) -> Vector:
pos = [p_a + _add for p_a, _add in zip(pos_a, [0, add_y, 0])]
return Vector((pos[0], pos[1], pos[2]))
def z_add(pos_a: Vector, add_z: float) -> Vector:
pos = [p_a + _add for p_a, _add in zip(pos_a, [0, 0, add_z])]
return Vector((pos[0], pos[1], pos[2]))
def fingers(
armature_data: Armature,
finger_name: str,
proximal_pos: Vector,
finger_len_sum: float,
) -> tuple[
tuple[EditBone, EditBone],
tuple[EditBone, EditBone],
tuple[EditBone, EditBone],
]:
finger_normalize = 1 / (
self.finger_1_2_ratio * self.finger_2_3_ratio
+ self.finger_1_2_ratio
+ 1
)
proximal_finger_len = finger_len_sum * finger_normalize
intermediate_finger_len = (
finger_len_sum * finger_normalize * self.finger_1_2_ratio
)
distal_finger_len = (
finger_len_sum
* finger_normalize
* self.finger_1_2_ratio
* self.finger_2_3_ratio
)
proximal_bones = x_mirror_bones_add(
armature_data,
f"{finger_name}_proximal",
proximal_pos,
x_add(proximal_pos, proximal_finger_len),
hands,
self.hand_size() / 18,
bone_type="arm",
)
intermediate_bones = x_mirror_bones_add(
armature_data,
f"{finger_name}_intermediate",
proximal_bones[0].tail,
x_add(proximal_bones[0].tail, intermediate_finger_len),
proximal_bones,
self.hand_size() / 18,
bone_type="arm",
)
distal_bones = x_mirror_bones_add(
armature_data,
f"{finger_name}_distal",
intermediate_bones[0].tail,
x_add(intermediate_bones[0].tail, distal_finger_len),
intermediate_bones,
self.hand_size() / 18,
bone_type="arm",
)
if self.nail_bone:
x_mirror_bones_add(
armature_data,
f"{finger_name}_nail",
distal_bones[0].tail,
x_add(distal_bones[0].tail, distal_finger_len),
distal_bones,
self.hand_size() / 20,
bone_type="arm",
)
return proximal_bones, intermediate_bones, distal_bones
bpy.ops.object.add(type="ARMATURE", enter_editmode=True, location=(0, 0, 0))
armature = context.object
if not armature:
message = "armature is not created"
raise ValueError(message)
armature_data = armature.data
if not isinstance(armature_data, Armature):
message = "armature data is not an Armature"
raise TypeError(message)
get_armature_extension(armature_data).addon_version = get_addon_version()
bone_dict: dict[str, EditBone] = {}
# bone_type = "leg" or "arm" for roll setting
head_size = self.head_size()
# down side (previously the lower leg ratio of upper leg/lower leg for
# 8-head proportions, later linearly interpolated with age factor for
# 4-head proportions)(breaks if upper leg is too high)
eight_upside_ratio, four_upside_ratio = (
1 - self.leg_length_ratio,
(2.5 / 4) * (1 - self.aging_ratio)
+ (1 - self.leg_length_ratio) * self.aging_ratio,
)
hip_up_down_ratio = (
eight_upside_ratio * (1 - (8 - self.head_ratio) / 4)
+ four_upside_ratio * (8 - self.head_ratio) / 4
)
# Torso
# Groin
body_separate = self.tall * (1 - hip_up_down_ratio)
# Neck length
neck_len = head_size * 2 / 3
# Sacrum (pelvic spine base)
hips_tall = body_separate + head_size * 3 / 4
# Thoracic spine total length # 1/3 of neck is hidden behind the jaw
backbone_len = self.tall - hips_tall - head_size - neck_len / 2
# TODO: Verify the ratio of thoracic spine to vertebrae
# Main flexion point located at the base of the spine, and another flexion
# point located at the base of the thoracic cage
# by Humanoid Doc
spine_len = backbone_len * 5 / 17
root = bone_add(armature_data, "root", Vector((0, 0, 0)), Vector((0, 0, 0.3)))
# Sacrum base
hips = bone_add(
armature_data,
"hips",
Vector((0, 0, body_separate)),
Vector((0, 0, hips_tall)),
root,
roll=0,
)
# Pelvic base -> Thoracic cage base
spine = bone_add(
armature_data, "spine", hips.tail, z_add(hips.tail, spine_len), hips, roll=0
)
# Thoracic cage base -> Neck base
chest = bone_add(
armature_data,
"chest",
spine.tail,
z_add(hips.tail, backbone_len),
spine,
roll=0,
)
neck = bone_add(
armature_data,
"neck",
Vector((0, 0, self.tall - head_size - neck_len / 2)),
Vector((0, 0, self.tall - head_size + neck_len / 2)),
chest,
roll=0,
)
# Half of the neck is hidden behind the jaw
head = bone_add(
armature_data,
"head",
Vector((0, 0, self.tall - head_size + neck_len / 2)),
Vector((0, 0, self.tall)),
neck,
roll=0,
)
# Eyes
eye_depth = self.eye_depth
eyes = x_mirror_bones_add(
armature_data,
"eye",
Vector(
(head_size * self.head_width_ratio / 5, 0, self.tall - head_size / 2)
),
Vector(
(
head_size * self.head_width_ratio / 5,
eye_depth,
self.tall - head_size / 2,
)
),
(head, head),
)
# Legs
leg_width = head_size / 4 * self.leg_width_ratio
leg_size = self.leg_size
leg_bone_length = (body_separate + head_size * 3 / 8 - self.tall * 0.05) / 2
upside_legs = x_mirror_bones_add(
armature_data,
"upper_leg",
x_add(Vector((0, 0, body_separate + head_size * 3 / 8)), leg_width),
x_add(
Vector(
z_add(
Vector((0, 0, body_separate + head_size * 3 / 8)),
-leg_bone_length,
)
),
leg_width,
),
(hips, hips),
radius=leg_width * 0.9,
bone_type="leg",
)
lower_legs = x_mirror_bones_add(
armature_data,
"lower_leg",
upside_legs[0].tail,
Vector((leg_width, 0, self.tall * 0.05)),
upside_legs,
radius=leg_width * 0.9,
bone_type="leg",
)
foots = x_mirror_bones_add(
armature_data,
"foot",
lower_legs[0].tail,
Vector((leg_width, -leg_size * (2 / 3), 0)),
lower_legs,
radius=leg_width * 0.9,
bone_type="leg",
)
toes = x_mirror_bones_add(
armature_data,
"toes",
foots[0].tail,
Vector((leg_width, -leg_size, 0)),
foots,
radius=leg_width * 0.5,
bone_type="leg",
)
# Shoulder to fingers
shoulder_in_pos = self.shoulder_in_width / 2
shoulder_parent = chest
shoulders = x_mirror_bones_add(
armature_data,
"shoulder",
x_add(shoulder_parent.tail, shoulder_in_pos),
x_add(shoulder_parent.tail, shoulder_in_pos + self.shoulder_width),
(shoulder_parent, shoulder_parent),
radius=self.hand_size() * 0.4,
bone_type="arm",
)
arm_length = (
head_size
* (1 * (1 - (self.head_ratio - 6) / 2) + 1.5 * ((self.head_ratio - 6) / 2))
* self.arm_length_ratio
)
arms = x_mirror_bones_add(
armature_data,
"upper_arm",
shoulders[0].tail,
x_add(shoulders[0].tail, arm_length),
shoulders,
radius=self.hand_size() * 0.4,
bone_type="arm",
)
# When making a fist, it becomes about half the size of an open palm.
# When making a fist, the length of the forearm including the hand
# is roughly the same as the length of the upper arm,
# but it breaks down if the hand is too big.
forearm_length = max(arm_length - self.hand_size() / 2, arm_length * 0.8)
forearms = x_mirror_bones_add(
armature_data,
"lower_arm",
arms[0].tail,
x_add(arms[0].tail, forearm_length),
arms,
radius=self.hand_size() * 0.4,
bone_type="arm",
)
hands = x_mirror_bones_add(
armature_data,
"hand",
forearms[0].tail,
x_add(forearms[0].tail, self.hand_size() / 2),
forearms,
radius=self.hand_size() / 4,
bone_type="arm",
)
finger_y_offset = -self.hand_size() / 16
thumbs = fingers(
armature_data,
"thumb",
y_add(hands[0].head, finger_y_offset * 3),
self.hand_size() / 2,
)
mats = [
Matrix.Translation(vec)
for vec in [thumbs[0][i].matrix.translation for i in [0, 1]]
]
for j in range(3):
for n, angle in enumerate([-45, 45]):
thumbs[j][n].transform(mats[n].inverted(), scale=False, roll=False)
thumbs[j][n].transform(Matrix.Rotation(radians(angle), 4, "Z"))
thumbs[j][n].transform(mats[n], scale=False, roll=False)
thumbs[j][n].roll = 0
index_fingers = fingers(
armature_data,
"index",
y_add(hands[0].tail, finger_y_offset * 3),
(self.hand_size() / 2) - (1 / 2.3125) * (self.hand_size() / 2) / 3,
)
middle_fingers = fingers(
armature_data,
"middle",
y_add(hands[0].tail, finger_y_offset),
self.hand_size() / 2,
)
ring_fingers = fingers(
armature_data,
"ring",
y_add(hands[0].tail, -finger_y_offset),
(self.hand_size() / 2) - (1 / 2.3125) * (self.hand_size() / 2) / 3,
)
little_fingers = fingers(
armature_data,
"little",
y_add(hands[0].tail, -finger_y_offset * 3),
((self.hand_size() / 2) - (1 / 2.3125) * (self.hand_size() / 2) / 3)
* ((1 / 2.3125) + (1 / 2.3125) * 0.75),
)
body_dict = {
"hips": hips.name,
"spine": spine.name,
"chest": chest.name,
"neck": neck.name,
"head": head.name,
}
left_right_body_dict = {
f"{left_right}{bone_name}": bones[lr].name
for bone_name, bones in {
"Eye": eyes,
"UpperLeg": upside_legs,
"LowerLeg": lower_legs,
"Foot": foots,
"Toes": toes,
"Shoulder": shoulders,
"UpperArm": arms,
"LowerArm": forearms,
"Hand": hands,
}.items()
for lr, left_right in enumerate(["left", "right"])
}
# VRM finger like name key
fingers_dict = {
f"{left_right}{finger_name}{position}": finger[i][lr].name
for finger_name, finger in zip(
["Thumb", "Index", "Middle", "Ring", "Little"],
[thumbs, index_fingers, middle_fingers, ring_fingers, little_fingers],
)
for i, position in enumerate(["Proximal", "Intermediate", "Distal"])
for lr, left_right in enumerate(["left", "right"])
}
# VRM bone name : blender bone name
bone_name_all_dict: dict[str, str] = {}
bone_name_all_dict.update(body_dict)
bone_name_all_dict.update(left_right_body_dict)
bone_name_all_dict.update(fingers_dict)
armature_data = armature.data
if isinstance(armature_data, Armature):
connect_parent_tail_and_child_head_if_very_close_position(armature_data)
context.scene.view_layers.update()
bpy.ops.object.mode_set(mode="OBJECT")
context.scene.view_layers.update()
return armature, bone_name_all_dict
def setup_as_vrm(
self, context: Context, armature: Object, compare_dict: dict[str, str]
) -> None:
armature_data = armature.data
if not isinstance(armature_data, Armature):
message = "armature data is not an Armature"
raise TypeError(message)
Vrm0HumanoidPropertyGroup.fixup_human_bones(armature)
ext = get_armature_extension(armature_data)
vrm0_humanoid = ext.vrm0.humanoid
vrm1_humanoid = ext.vrm1.humanoid
if not self.skip_heavy_armature_setup:
for vrm_bone_name, bpy_bone_name in compare_dict.items():
for human_bone in vrm0_humanoid.human_bones:
if human_bone.bone == vrm_bone_name:
human_bone.node.bone_name = bpy_bone_name
break
vrm0_humanoid.pose = vrm0_humanoid.POSE_REST_POSITION_POSE.identifier
vrm1_humanoid.pose = vrm1_humanoid.POSE_REST_POSITION_POSE.identifier
self.make_extension_setting_and_metas(
armature,
offset_from_head_bone=(-self.eye_depth, self.head_size() / 6, 0),
)
if not self.skip_heavy_armature_setup:
migration.migrate(context, armature.name)
@classmethod
def make_extension_setting_and_metas(
cls,
armature: Object,
offset_from_head_bone: tuple[float, float, float] = (0, 0, 0),
) -> None:
armature_data = armature.data
if not isinstance(armature_data, Armature):
return
vrm0 = get_armature_extension(armature_data).vrm0
vrm1 = get_armature_extension(armature_data).vrm1
vrm0.first_person.first_person_bone.bone_name = "head"
vrm0.first_person.first_person_bone_offset = (0, 0, 0.06)
vrm1.look_at.offset_from_head_bone = offset_from_head_bone
vrm0.first_person.look_at_horizontal_inner.y_range = 8
vrm0.first_person.look_at_horizontal_outer.y_range = 12
vrm0.meta.author = "undefined"
vrm0.meta.contact_information = "undefined"
vrm0.meta.other_license_url = "undefined"
vrm0.meta.other_permission_url = "undefined"
vrm0.meta.reference = "undefined"
vrm0.meta.title = "undefined"
vrm0.meta.version = "undefined"
for preset in Vrm0BlendShapeGroupPropertyGroup.preset_name_enum:
if (
preset.identifier
== Vrm0BlendShapeGroupPropertyGroup.PRESET_NAME_UNKNOWN.identifier
):
continue
blend_shape_group = vrm0.blend_shape_master.blend_shape_groups.add()
blend_shape_group.name = preset.name.replace(" ", "")
blend_shape_group.preset_name = preset.identifier
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
skip_heavy_armature_setup: bool # type: ignore[no-redef]
wip_with_template_mesh: bool # type: ignore[no-redef]
tall: float # type: ignore[no-redef]
head_ratio: float # type: ignore[no-redef]
head_width_ratio: float # type: ignore[no-redef]
aging_ratio: float # type: ignore[no-redef]
eye_depth: float # type: ignore[no-redef]
shoulder_in_width: float # type: ignore[no-redef]
shoulder_width: float # type: ignore[no-redef]
arm_length_ratio: float # type: ignore[no-redef]
hand_ratio: float # type: ignore[no-redef]
finger_1_2_ratio: float # type: ignore[no-redef]
finger_2_3_ratio: float # type: ignore[no-redef]
nail_bone: bool # type: ignore[no-redef]
leg_length_ratio: float # type: ignore[no-redef]
leg_width_ratio: float # type: ignore[no-redef]
leg_size: float # type: ignore[no-redef]
custom_property_name: str # type: ignore[no-redef]
def connect_parent_tail_and_child_head_if_very_close_position(
armature: Armature,
) -> None:
bones = [bone for bone in armature.edit_bones if not bone.parent]
while bones:
bone = bones.pop()
children_by_distance = sorted(
bone.children,
key=lambda child: (child.parent.tail - child.head).length_squared
if child.parent
else 0.0,
)
for child in children_by_distance:
if (bone.tail - child.head).length < AUTO_BONE_CONNECTION_DISTANCE and (
bone.head - child.head
).length >= MIN_BONE_LENGTH:
bone.tail = child.head
break
bones.extend(bone.children)
bones = [bone for bone in armature.edit_bones if not bone.parent]
while bones:
bone = bones.pop()
for child in bone.children:
if (bone.tail - child.head).length < float_info.epsilon:
child.use_connect = True
bones.append(child)