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

1223 lines
42 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
from collections.abc import Set as AbstractSet
import bpy
from bpy.app.translations import pgettext
from bpy.types import Armature, Context, Mesh, Object, Panel, UILayout
from ...common.logger import get_logger
from ...common.vrm1.human_bone import HumanBoneSpecifications
from .. import ops, search
from ..extension import get_armature_extension, get_bone_extension
from ..migration import defer_migrate
from ..ops import layout_operator
from ..panel import VRM_PT_vrm_armature_object_property, draw_template_list
from ..search import active_object_is_vrm1_armature
from . import ops as vrm1_ops
from .menu import VRM_MT_vrm1_expression
from .ops import draw_bone_prop_search
from .property_group import (
Vrm1CustomExpressionPropertyGroup,
Vrm1ExpressionsPropertyGroup,
Vrm1FirstPersonPropertyGroup,
Vrm1HumanBonesPropertyGroup,
Vrm1HumanoidPropertyGroup,
Vrm1LookAtPropertyGroup,
Vrm1MaterialColorBindPropertyGroup,
Vrm1MetaPropertyGroup,
Vrm1MorphTargetBindPropertyGroup,
Vrm1TextureTransformBindPropertyGroup,
)
from .ui_list import (
VRM_UL_vrm1_expression,
VRM_UL_vrm1_first_person_mesh_annotation,
VRM_UL_vrm1_material_color_bind,
VRM_UL_vrm1_meta_author,
VRM_UL_vrm1_meta_reference,
VRM_UL_vrm1_morph_target_bind,
VRM_UL_vrm1_texture_transform_bind,
)
logger = get_logger(__name__)
def draw_vrm1_humanoid_required_bones_layout(
armature: Object,
layout: UILayout,
) -> None:
split_factor = 0.2
layout.label(text="Required VRM Human Bones", icon="ARMATURE_DATA")
row = layout.row(align=True).split(factor=split_factor, align=True)
label_column = row.column(align=True)
label_column.label(text=HumanBoneSpecifications.HEAD.label)
label_column.label(text=HumanBoneSpecifications.SPINE.label)
label_column.label(text=HumanBoneSpecifications.HIPS.label)
search_column = row.column(align=True)
draw_bone_prop_search(search_column, HumanBoneSpecifications.HEAD, armature)
draw_bone_prop_search(search_column, HumanBoneSpecifications.SPINE, armature)
draw_bone_prop_search(search_column, HumanBoneSpecifications.HIPS, armature)
row = layout.row(align=True).split(factor=split_factor, align=True)
label_column = row.column(align=True)
label_column.label(text="")
label_column.label(text=HumanBoneSpecifications.LEFT_UPPER_ARM.label_no_left_right)
label_column.label(text=HumanBoneSpecifications.LEFT_LOWER_ARM.label_no_left_right)
label_column.label(text=HumanBoneSpecifications.LEFT_HAND.label_no_left_right)
label_column.separator()
label_column.label(text=HumanBoneSpecifications.LEFT_UPPER_LEG.label_no_left_right)
label_column.label(text=HumanBoneSpecifications.LEFT_LOWER_LEG.label_no_left_right)
label_column.label(text=HumanBoneSpecifications.LEFT_FOOT.label_no_left_right)
search_column = row.column(align=True)
right_left_row = search_column.row(align=True)
right_left_row.label(text="Right")
right_left_row.label(text="Left")
right_left_row = search_column.row(align=True)
draw_bone_prop_search(
right_left_row, HumanBoneSpecifications.RIGHT_UPPER_ARM, armature
)
draw_bone_prop_search(
right_left_row, HumanBoneSpecifications.LEFT_UPPER_ARM, armature
)
right_left_row = search_column.row(align=True)
draw_bone_prop_search(
right_left_row, HumanBoneSpecifications.RIGHT_LOWER_ARM, armature
)
draw_bone_prop_search(
right_left_row, HumanBoneSpecifications.LEFT_LOWER_ARM, armature
)
right_left_row = search_column.row(align=True)
draw_bone_prop_search(right_left_row, HumanBoneSpecifications.RIGHT_HAND, armature)
draw_bone_prop_search(right_left_row, HumanBoneSpecifications.LEFT_HAND, armature)
search_column.separator()
right_left_row = search_column.row(align=True)
draw_bone_prop_search(
right_left_row, HumanBoneSpecifications.RIGHT_UPPER_LEG, armature
)
draw_bone_prop_search(
right_left_row, HumanBoneSpecifications.LEFT_UPPER_LEG, armature
)
right_left_row = search_column.row(align=True)
draw_bone_prop_search(
right_left_row, HumanBoneSpecifications.RIGHT_LOWER_LEG, armature
)
draw_bone_prop_search(
right_left_row, HumanBoneSpecifications.LEFT_LOWER_LEG, armature
)
right_left_row = search_column.row(align=True)
draw_bone_prop_search(right_left_row, HumanBoneSpecifications.RIGHT_FOOT, armature)
draw_bone_prop_search(right_left_row, HumanBoneSpecifications.LEFT_FOOT, armature)
def draw_vrm1_humanoid_optional_bones_layout(
armature: Object,
layout: UILayout,
) -> None:
split_factor = 0.2
layout.label(text="Optional VRM Human Bones", icon="BONE_DATA")
row = layout.row(align=True).split(factor=split_factor, align=True)
label_column = row.column(align=True)
label_column.label(text="")
label_column.label(text=HumanBoneSpecifications.LEFT_EYE.label_no_left_right)
label_column.label(text=HumanBoneSpecifications.JAW.label)
label_column.label(text=HumanBoneSpecifications.NECK.label)
label_column.label(text=HumanBoneSpecifications.RIGHT_SHOULDER.label_no_left_right)
label_column.label(text=HumanBoneSpecifications.UPPER_CHEST.label)
label_column.label(text=HumanBoneSpecifications.CHEST.label)
label_column.label(text=HumanBoneSpecifications.RIGHT_TOES.label_no_left_right)
search_column = row.column(align=True)
right_left_row = search_column.row(align=True)
right_left_row.label(text="Right")
right_left_row.label(text="Left")
right_left_row = search_column.row(align=True)
draw_bone_prop_search(right_left_row, HumanBoneSpecifications.RIGHT_EYE, armature)
draw_bone_prop_search(right_left_row, HumanBoneSpecifications.LEFT_EYE, armature)
draw_bone_prop_search(search_column, HumanBoneSpecifications.JAW, armature)
draw_bone_prop_search(search_column, HumanBoneSpecifications.NECK, armature)
right_left_row = search_column.row(align=True)
draw_bone_prop_search(
right_left_row, HumanBoneSpecifications.RIGHT_SHOULDER, armature
)
draw_bone_prop_search(
right_left_row, HumanBoneSpecifications.LEFT_SHOULDER, armature
)
draw_bone_prop_search(search_column, HumanBoneSpecifications.UPPER_CHEST, armature)
draw_bone_prop_search(search_column, HumanBoneSpecifications.CHEST, armature)
right_left_row = search_column.row(align=True)
draw_bone_prop_search(right_left_row, HumanBoneSpecifications.RIGHT_TOES, armature)
draw_bone_prop_search(right_left_row, HumanBoneSpecifications.LEFT_TOES, armature)
row = layout.row(align=True).split(factor=split_factor, align=True)
label_column = row.column(align=True)
label_column.label(text="", translate=False)
label_column.label(text="Left Thumb:")
label_column.label(text="Left Index:")
label_column.label(text="Left Middle:")
label_column.label(text="Left Ring:")
label_column.label(text="Left Little:")
label_column.separator()
label_column.label(text="Right Thumb:")
label_column.label(text="Right Index:")
label_column.label(text="Right Middle:")
label_column.label(text="Right Ring:")
label_column.label(text="Right Little:")
search_column = row.column(align=True)
finger_row = search_column.row(align=True)
finger_row.label(text="Root")
finger_row.label(text="", translate=False)
finger_row.label(text="Tip")
finger_row = search_column.row(align=True)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_THUMB_METACARPAL, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_THUMB_PROXIMAL, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_THUMB_DISTAL, armature
)
finger_row = search_column.row(align=True)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_INDEX_PROXIMAL, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_INDEX_INTERMEDIATE, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_INDEX_DISTAL, armature
)
finger_row = search_column.row(align=True)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_MIDDLE_PROXIMAL, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_MIDDLE_INTERMEDIATE, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_MIDDLE_DISTAL, armature
)
finger_row = search_column.row(align=True)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_RING_PROXIMAL, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_RING_INTERMEDIATE, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_RING_DISTAL, armature
)
finger_row = search_column.row(align=True)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_LITTLE_PROXIMAL, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_LITTLE_INTERMEDIATE, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.LEFT_LITTLE_DISTAL, armature
)
finger_row = search_column.row(align=True)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_THUMB_METACARPAL, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_THUMB_PROXIMAL, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_THUMB_DISTAL, armature
)
finger_row = search_column.row(align=True)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_INDEX_PROXIMAL, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_INDEX_INTERMEDIATE, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_INDEX_DISTAL, armature
)
finger_row = search_column.row(align=True)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_MIDDLE_PROXIMAL, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_MIDDLE_INTERMEDIATE, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_MIDDLE_DISTAL, armature
)
finger_row = search_column.row(align=True)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_RING_PROXIMAL, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_RING_INTERMEDIATE, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_RING_DISTAL, armature
)
finger_row = search_column.row(align=True)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_LITTLE_PROXIMAL, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_LITTLE_INTERMEDIATE, armature
)
draw_bone_prop_search(
finger_row, HumanBoneSpecifications.RIGHT_LITTLE_DISTAL, armature
)
def draw_vrm1_humanoid_layout(
context: Context,
armature: Object,
layout: UILayout,
humanoid: Vrm1HumanoidPropertyGroup,
) -> None:
if defer_migrate(armature.name):
data = armature.data
if not isinstance(data, Armature):
return
Vrm1HumanBonesPropertyGroup.defer_update_all_node_candidates(data.name)
data = armature.data
if not isinstance(data, Armature):
return
human_bones = humanoid.human_bones
armature_box = layout
t_pose_box = armature_box.box()
column = t_pose_box.row().column()
column.label(text="VRM T-Pose", icon="OUTLINER_OB_ARMATURE")
column.prop(humanoid, "pose")
if humanoid.pose == humanoid.POSE_CUSTOM_POSE.identifier:
label = "Pose Library" if bpy.app.version < (3, 0) else "Pose Asset"
column.prop_search(
humanoid,
"pose_library",
context.blend_data,
"actions",
text=label,
translate=False,
)
pose_library = humanoid.pose_library
if pose_library and pose_library.pose_markers:
column.prop_search(
humanoid,
"pose_marker_name",
pose_library,
"pose_markers",
text="Pose",
translate=False,
)
bone_operator_column = layout.column()
layout_operator(
bone_operator_column,
vrm1_ops.VRM_OT_assign_vrm1_humanoid_human_bones_automatically,
icon="ARMATURE_DATA",
).armature_object_name = armature.name
if ops.VRM_OT_simplify_vroid_bones.vroid_bones_exist(data):
simplify_vroid_bones_op = layout_operator(
armature_box,
ops.VRM_OT_simplify_vroid_bones,
text=pgettext(ops.VRM_OT_simplify_vroid_bones.bl_label),
icon="GREASEPENCIL",
)
simplify_vroid_bones_op.armature_object_name = armature.name
draw_vrm1_humanoid_required_bones_layout(armature, armature_box.box())
draw_vrm1_humanoid_optional_bones_layout(armature, armature_box.box())
non_humanoid_export_column = layout.column()
non_humanoid_export_column.prop(human_bones, "allow_non_humanoid_rig")
if human_bones.allow_non_humanoid_rig:
non_humanoid_warnings_box = non_humanoid_export_column.box()
non_humanoid_warnings_column = non_humanoid_warnings_box.column(align=True)
text = pgettext(
"VRMs exported as Non-Humanoid\n"
+ "Rigs can not have animations applied\n"
+ "for humanoid avatars."
)
for index, message in enumerate(pgettext(text).splitlines()):
non_humanoid_warnings_column.label(
text=message,
translate=False,
icon="ERROR" if index == 0 else "NONE",
)
class VRM_PT_vrm1_humanoid_armature_object_property(Panel):
bl_idname = "VRM_PT_vrm1_humanoid_armature_object_property"
bl_label = "Humanoid"
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="ARMATURE_DATA")
def draw(self, context: Context) -> None:
active_object = context.active_object
if not active_object:
return
if active_object.type != "ARMATURE":
return
armature_data = active_object.data
if not isinstance(armature_data, Armature):
return
draw_vrm1_humanoid_layout(
context,
active_object,
self.layout,
get_armature_extension(armature_data).vrm1.humanoid,
)
class VRM_PT_vrm1_humanoid_ui(Panel):
bl_idname = "VRM_PT_vrm1_humanoid_ui"
bl_label = "Humanoid"
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="ARMATURE_DATA")
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_vrm1_humanoid_layout(
context,
armature,
self.layout,
get_armature_extension(armature_data).vrm1.humanoid,
)
def draw_vrm1_first_person_layout(
armature: Object,
_context: Context,
layout: UILayout,
first_person: Vrm1FirstPersonPropertyGroup,
) -> None:
defer_migrate(armature.name)
column = layout.column()
column.label(text="Mesh Annotations", icon="FULLSCREEN_EXIT")
(
mesh_annotation_collection_ops,
mesh_annotation_collection_item_ops,
mesh_annotation_index,
_,
_,
) = draw_template_list(
column,
VRM_UL_vrm1_first_person_mesh_annotation,
first_person,
"mesh_annotations",
"active_mesh_annotation_index",
vrm1_ops.VRM_OT_add_vrm1_first_person_mesh_annotation,
vrm1_ops.VRM_OT_remove_vrm1_first_person_mesh_annotation,
vrm1_ops.VRM_OT_move_up_vrm1_first_person_mesh_annotation,
vrm1_ops.VRM_OT_move_down_vrm1_first_person_mesh_annotation,
)
for mesh_annotation_collection_op in mesh_annotation_collection_ops:
mesh_annotation_collection_op.armature_object_name = armature.name
for mesh_annotation_collection_item_op in mesh_annotation_collection_item_ops:
mesh_annotation_collection_item_op.mesh_annotation_index = mesh_annotation_index
class VRM_PT_vrm1_first_person_armature_object_property(Panel):
bl_idname = "VRM_PT_vrm1_first_person_armature_object_property"
bl_label = "First Person"
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="HIDE_OFF")
def draw(self, context: Context) -> None:
active_object = context.active_object
if not active_object:
return
if active_object.type != "ARMATURE":
return
armature_data = active_object.data
if not isinstance(armature_data, Armature):
return
draw_vrm1_first_person_layout(
active_object,
context,
self.layout,
get_armature_extension(armature_data).vrm1.first_person,
)
class VRM_PT_vrm1_first_person_ui(Panel):
bl_idname = "VRM_PT_vrm1_first_person_ui"
bl_label = "First Person"
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="USER")
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_vrm1_first_person_layout(
armature,
context,
self.layout,
get_armature_extension(armature_data).vrm1.first_person,
)
def draw_vrm1_look_at_layout(
armature: Object,
_context: Context,
layout: UILayout,
look_at: Vrm1LookAtPropertyGroup,
) -> None:
defer_migrate(armature.name)
layout.prop(look_at, "enable_preview")
label_input_split = layout.split(factor=0.4)
label_column = label_input_split.column()
label_column.label(text="Preview Target:")
label_column.label(text="Type:")
input_column = label_input_split.column()
input_column.prop(look_at, "preview_target_bpy_object", text="", translate=False)
input_column.prop(look_at, "type", text="", translate=False)
offset_from_head_bone_column = layout.column()
offset_from_head_bone_column.label(text="Offset from Head Bone:")
offset_from_head_bone_column.row().prop(
look_at, "offset_from_head_bone", icon="BONE_DATA", text="", translate=False
)
column = layout.box().column(align=True)
column.label(text="Range Map Horizontal Inner", icon="FULLSCREEN_EXIT")
column.prop(look_at.range_map_horizontal_inner, "input_max_value")
column.prop(look_at.range_map_horizontal_inner, "output_scale")
column = layout.box().column(align=True)
column.label(text="Range Map Horizontal Outer", icon="FULLSCREEN_ENTER")
column.prop(look_at.range_map_horizontal_outer, "input_max_value")
column.prop(look_at.range_map_horizontal_outer, "output_scale")
column = layout.box().column(align=True)
column.label(text="Range Map Vertical Up", icon="TRIA_UP")
column.prop(look_at.range_map_vertical_up, "input_max_value")
column.prop(look_at.range_map_vertical_up, "output_scale")
column = layout.box().column(align=True)
column.label(text="Range Map Vertical Down", icon="TRIA_DOWN")
column.prop(look_at.range_map_vertical_down, "input_max_value")
column.prop(look_at.range_map_vertical_down, "output_scale")
class VRM_PT_vrm1_look_at_armature_object_property(Panel):
bl_idname = "VRM_PT_vrm1_look_at_armature_object_property"
bl_label = "Look At"
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="HIDE_OFF")
def draw(self, context: Context) -> None:
active_object = context.active_object
if not active_object:
return
if active_object.type != "ARMATURE":
return
armature_data = active_object.data
if not isinstance(armature_data, Armature):
return
draw_vrm1_look_at_layout(
active_object,
context,
self.layout,
get_armature_extension(armature_data).vrm1.look_at,
)
class VRM_PT_vrm1_look_at_ui(Panel):
bl_idname = "VRM_PT_vrm1_look_at_ui"
bl_label = "Look At"
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="HIDE_OFF")
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_vrm1_look_at_layout(
armature,
context,
self.layout,
get_armature_extension(armature_data).vrm1.look_at,
)
def draw_vrm1_expressions_morph_target_bind_layout(
context: Context,
layout: UILayout,
bind: Vrm1MorphTargetBindPropertyGroup,
) -> None:
blend_data = context.blend_data
bind_column = layout.column()
bind_column.prop(
bind.node,
"bpy_object",
text="Mesh",
icon="OUTLINER_OB_MESH",
)
mesh_object = blend_data.objects.get(bind.node.mesh_object_name)
if not mesh_object:
return
mesh = mesh_object.data
if not isinstance(mesh, Mesh):
return
shape_keys = mesh.shape_keys
if not shape_keys:
return
key_blocks = shape_keys.key_blocks
if not key_blocks:
return
bind_column.prop_search(
bind,
"index",
shape_keys,
"key_blocks",
text="Shape key",
)
bind_column.prop(bind, "weight", slider=True)
def draw_vrm1_expressions_material_color_bind_layout(
context: Context,
layout: UILayout,
bind: Vrm1MaterialColorBindPropertyGroup,
) -> None:
blend_data = context.blend_data
bind_column = layout.column()
bind_column.prop_search(bind, "material", blend_data, "materials")
bind_column.prop(bind, "type")
target_value_split = bind_column.split(factor=0.5)
target_value_split.label(text="Target Value:")
if bind.type == "color":
target_value_split.prop(bind, "target_value", text="", translate=False)
else:
target_value_split.prop(bind, "target_value_as_rgb", text="", translate=False)
def draw_vrm1_expressions_texture_transform_bind_layout(
context: Context,
layout: UILayout,
bind: Vrm1TextureTransformBindPropertyGroup,
) -> None:
if not bind.show_experimental_preview_feature:
blend_data = context.blend_data
bind_column = layout.column()
bind_column.prop_search(bind, "material", blend_data, "materials")
bind_column.prop(bind, "scale")
bind_column.prop(bind, "offset")
if bpy.app.version < (4, 2):
return
bind_column.separator()
bind_column.prop(bind, "show_experimental_preview_feature")
return
bind_column = layout.column()
bind_column.prop(bind, "show_experimental_preview_feature")
# Find the armature object
armature = search.current_armature(context)
if not armature:
layout.label(text="No VRM Armature found in the scene", icon="ERROR")
return
armature_data = armature.data
if not isinstance(armature_data, Armature):
layout.label(text="Invalid armature data", icon="ERROR")
return
try:
extension = get_armature_extension(armature_data)
vrm1 = extension.vrm1
expressions = vrm1.expressions
all_expressions = list(expressions.all_name_to_expression_dict().values())
if (
0
<= expressions.active_expression_ui_list_element_index
< len(all_expressions)
):
active_expression = all_expressions[
expressions.active_expression_ui_list_element_index
]
if isinstance(active_expression, Vrm1CustomExpressionPropertyGroup):
expression_name = active_expression.custom_name
else:
expression_name = active_expression.name
else:
expression_name = ""
except TypeError:
layout.label(text="Invalid VRM extension", icon="ERROR")
return
# Add refresh preview button
refresh_row = bind_column.row()
refresh_op = layout_operator(
refresh_row,
vrm1_ops.VRM_OT_refresh_vrm1_expression_texture_transform_bind_preview,
text="Refresh Preview",
icon="FILE_REFRESH",
)
refresh_op.armature_object_name = armature.name
refresh_op.expression_name = expression_name
bind_column.separator()
bind_column.prop_search(bind, "material", context.blend_data, "materials")
# Only show scale, and offset properties if there's a valid bind
if bind and bind.material:
bind_column.prop(bind, "scale")
bind_column.prop(bind, "offset")
else:
bind_column.label(text="No texture transform bind for this expression")
def draw_vrm1_expressions_layout(
armature: Object,
context: Context,
layout: UILayout,
expressions: Vrm1ExpressionsPropertyGroup,
) -> None:
defer_migrate(armature.name)
preset_expressions = list(expressions.preset.name_to_expression_dict().values())
(
expression_collection_ops,
expression_collection_item_ops,
expression_ui_list_element_index,
_,
_,
) = draw_template_list(
layout,
VRM_UL_vrm1_expression,
expressions,
"expression_ui_list_elements",
"active_expression_ui_list_element_index",
vrm1_ops.VRM_OT_add_vrm1_expressions_custom_expression,
vrm1_ops.VRM_OT_remove_vrm1_expressions_custom_expression,
vrm1_ops.VRM_OT_move_up_vrm1_expressions_custom_expression,
vrm1_ops.VRM_OT_move_down_vrm1_expressions_custom_expression,
can_remove=lambda active_collection_index: (
active_collection_index >= len(preset_expressions)
),
can_move=lambda active_collection_index: (
len(expressions.custom) >= 2
and active_collection_index >= len(preset_expressions)
),
menu=VRM_MT_vrm1_expression,
)
for expression_collection_op in expression_collection_ops:
expression_collection_op.armature_object_name = armature.name
expression_collection_op.custom_expression_name = "custom"
custom_index = expression_ui_list_element_index - len(preset_expressions)
if 0 <= expression_ui_list_element_index < len(preset_expressions):
expression = preset_expressions[expression_ui_list_element_index]
preset_icon = expressions.preset.get_icon(expression.name)
if not preset_icon:
logger.error("Unknown preset expression: %s", expression.name)
preset_icon = "SHAPEKEY_DATA"
layout.label(text=expression.name, translate=False, icon=preset_icon)
elif 0 <= custom_index < len(expressions.custom):
expression = expressions.custom[custom_index]
for expression_collection_item_op in expression_collection_item_ops:
expression_collection_item_op.custom_expression_name = (
expression.custom_name
)
layout.prop(expression, "custom_name")
else:
return
column = layout.column()
column.prop(expression, "preview", icon="PLAY", text="Preview")
column.prop(expression, "is_binary", icon="IPO_CONSTANT")
column.prop(expression, "override_blink")
column.prop(expression, "override_look_at")
column.prop(expression, "override_mouth")
column.separator(factor=0.5)
morph_target_binds_box = column.box()
morph_target_binds_box.label(text="Morph Target Binds", icon="MESH_DATA")
(
morph_target_bind_collection_ops,
morph_target_bind_collection_item_ops,
morph_target_bind_index,
morph_target_bind,
_,
) = draw_template_list(
morph_target_binds_box,
VRM_UL_vrm1_morph_target_bind,
expression,
"morph_target_binds",
"active_morph_target_bind_index",
vrm1_ops.VRM_OT_add_vrm1_expression_morph_target_bind,
vrm1_ops.VRM_OT_remove_vrm1_expression_morph_target_bind,
vrm1_ops.VRM_OT_move_up_vrm1_expression_morph_target_bind,
vrm1_ops.VRM_OT_move_down_vrm1_expression_morph_target_bind,
)
for morph_target_bind_collection_op in morph_target_bind_collection_ops:
morph_target_bind_collection_op.armature_object_name = armature.name
morph_target_bind_collection_op.expression_name = expression.name
for morph_target_bind_collection_item_op in morph_target_bind_collection_item_ops:
morph_target_bind_collection_item_op.bind_index = morph_target_bind_index
if isinstance(morph_target_bind, Vrm1MorphTargetBindPropertyGroup):
draw_vrm1_expressions_morph_target_bind_layout(
context,
morph_target_binds_box,
morph_target_bind,
)
column.separator(factor=0.2)
material_color_binds_box = column.box()
material_color_binds_box.label(text="Material Color Binds", icon="MATERIAL")
(
material_color_bind_collection_ops,
material_color_bind_collection_item_ops,
material_color_bind_index,
material_color_bind,
_,
) = draw_template_list(
material_color_binds_box,
VRM_UL_vrm1_material_color_bind,
expression,
"material_color_binds",
"active_material_color_bind_index",
vrm1_ops.VRM_OT_add_vrm1_expression_material_color_bind,
vrm1_ops.VRM_OT_remove_vrm1_expression_material_color_bind,
vrm1_ops.VRM_OT_move_up_vrm1_expression_material_color_bind,
vrm1_ops.VRM_OT_move_down_vrm1_expression_material_color_bind,
)
for material_color_bind_collection_op in material_color_bind_collection_ops:
material_color_bind_collection_op.armature_object_name = armature.name
material_color_bind_collection_op.expression_name = expression.name
for (
material_color_bind_collection_item_op
) in material_color_bind_collection_item_ops:
material_color_bind_collection_item_op.bind_index = material_color_bind_index
if isinstance(material_color_bind, Vrm1MaterialColorBindPropertyGroup):
draw_vrm1_expressions_material_color_bind_layout(
context,
material_color_binds_box,
material_color_bind,
)
column.separator(factor=0.2)
texture_transform_binds_box = column.box()
texture_transform_binds_box.label(text="Texture Transform Binds", icon="MATERIAL")
(
texture_transform_bind_collection_ops,
texture_transform_bind_collection_item_ops,
texture_transform_bind_index,
texture_transform_bind,
_,
) = draw_template_list(
texture_transform_binds_box,
VRM_UL_vrm1_texture_transform_bind,
expression,
"texture_transform_binds",
"active_texture_transform_bind_index",
vrm1_ops.VRM_OT_add_vrm1_expression_texture_transform_bind,
vrm1_ops.VRM_OT_remove_vrm1_expression_texture_transform_bind,
vrm1_ops.VRM_OT_move_up_vrm1_expression_texture_transform_bind,
vrm1_ops.VRM_OT_move_down_vrm1_expression_texture_transform_bind,
)
for texture_transform_bind_collection_op in texture_transform_bind_collection_ops:
texture_transform_bind_collection_op.armature_object_name = armature.name
texture_transform_bind_collection_op.expression_name = expression.name
for (
texture_transform_bind_collection_item_op
) in texture_transform_bind_collection_item_ops:
texture_transform_bind_collection_item_op.bind_index = (
texture_transform_bind_index
)
if isinstance(texture_transform_bind, Vrm1TextureTransformBindPropertyGroup):
draw_vrm1_expressions_texture_transform_bind_layout(
context,
texture_transform_binds_box,
texture_transform_bind,
)
class VRM_PT_vrm1_expressions_armature_object_property(Panel):
bl_idname = "VRM_PT_vrm1_expressions_armature_object_property"
bl_label = "Expressions"
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="SHAPEKEY_DATA")
def draw(self, context: Context) -> None:
active_object = context.active_object
if not active_object:
return
if active_object.type != "ARMATURE":
return
armature_data = active_object.data
if not isinstance(armature_data, Armature):
return
draw_vrm1_expressions_layout(
active_object,
context,
self.layout,
get_armature_extension(armature_data).vrm1.expressions,
)
class VRM_PT_vrm1_expressions_ui(Panel):
bl_idname = "VRM_PT_vrm1_expressions_ui"
bl_label = "Expressions"
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="SHAPEKEY_DATA")
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_vrm1_expressions_layout(
armature,
context,
self.layout,
get_armature_extension(armature_data).vrm1.expressions,
)
def draw_vrm1_meta_layout(
armature: Object,
_context: Context,
layout: UILayout,
meta: Vrm1MetaPropertyGroup,
) -> None:
defer_migrate(armature.name)
thumbnail_column = layout.column()
thumbnail_column.label(text="Thumbnail:")
thumbnail_column.template_ID_preview(meta, "thumbnail_image")
layout.prop(meta, "vrm_name", icon="FILE_BLEND")
layout.prop(meta, "version", icon="LINENUMBERS_ON")
authors_column = layout.column()
authors_column.label(text="Authors:")
(
author_collection_ops,
author_collection_item_ops,
author_index,
_,
_,
) = draw_template_list(
authors_column,
VRM_UL_vrm1_meta_author,
meta,
"authors",
"active_author_index",
vrm1_ops.VRM_OT_add_vrm1_meta_author,
vrm1_ops.VRM_OT_remove_vrm1_meta_author,
vrm1_ops.VRM_OT_move_up_vrm1_meta_author,
vrm1_ops.VRM_OT_move_down_vrm1_meta_author,
can_remove=lambda _: len(meta.authors) >= 2,
compact=True,
)
for author_collection_op in author_collection_ops:
author_collection_op.armature_object_name = armature.name
for author_collection_item_op in author_collection_item_ops:
author_collection_item_op.author_index = author_index
layout.prop(meta, "copyright_information")
layout.prop(meta, "contact_information")
references_column = layout.column()
references_column.label(text="References:")
(
reference_collection_ops,
reference_collection_item_ops,
reference_index,
_,
_,
) = draw_template_list(
references_column,
VRM_UL_vrm1_meta_reference,
meta,
"references",
"active_reference_index",
vrm1_ops.VRM_OT_add_vrm1_meta_reference,
vrm1_ops.VRM_OT_remove_vrm1_meta_reference,
vrm1_ops.VRM_OT_move_up_vrm1_meta_reference,
vrm1_ops.VRM_OT_move_down_vrm1_meta_reference,
compact=True,
)
for reference_collection_op in reference_collection_ops:
reference_collection_op.armature_object_name = armature.name
for reference_collection_item_op in reference_collection_item_ops:
reference_collection_item_op.reference_index = reference_index
layout.prop(meta, "third_party_licenses")
# layout.prop(meta, "license_url", icon="URL")
layout.prop(meta, "avatar_permission", icon="MATCLOTH")
layout.prop(meta, "commercial_usage", icon="SOLO_OFF")
layout.prop(meta, "credit_notation")
layout.prop(meta, "modification")
layout.prop(meta, "allow_excessively_violent_usage")
layout.prop(meta, "allow_excessively_sexual_usage")
layout.prop(meta, "allow_political_or_religious_usage")
layout.prop(meta, "allow_antisocial_or_hate_usage")
layout.prop(meta, "allow_redistribution")
layout.prop(meta, "other_license_url", icon="URL")
class VRM_PT_vrm1_meta_armature_object_property(Panel):
bl_idname = "VRM_PT_vrm1_meta_armature_object_property"
bl_label = "Meta"
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="FILE_BLEND")
def draw(self, context: Context) -> None:
active_object = context.active_object
if not active_object:
return
if active_object.type != "ARMATURE":
return
armature_data = active_object.data
if not isinstance(armature_data, Armature):
return
ext = get_armature_extension(armature_data)
draw_vrm1_meta_layout(active_object, context, self.layout, ext.vrm1.meta)
class VRM_PT_vrm1_meta_ui(Panel):
bl_idname = "VRM_PT_vrm1_meta_ui"
bl_label = "Meta"
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="FILE_BLEND")
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_vrm1_meta_layout(
armature,
context,
self.layout,
get_armature_extension(armature_data).vrm1.meta,
)
class VRM_PT_vrm1_bone_property(Panel):
bl_idname = "VRM_PT_vrm1_bone_property"
bl_label = "VRM"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "bone"
@classmethod
def poll(cls, context: Context) -> bool:
active_object = context.active_object
if not active_object:
return False
if active_object.type != "ARMATURE":
return False
armature_data = active_object.data
if not isinstance(armature_data, Armature):
return False
if not armature_data.bones.active:
return False
return search.current_armature_is_vrm1(context)
def draw(self, context: Context) -> None:
active_object = context.active_object
if not active_object:
return
if active_object.type != "ARMATURE":
return
armature_data = active_object.data
if not isinstance(armature_data, Armature):
return
# context.active_bone is a EditBone
bone = armature_data.bones.active
if not bone:
return
ext = get_bone_extension(bone)
layout = self.layout
layout.prop(ext, "axis_translation")