Merge branch 'main' into Texture-Atlasing

This commit is contained in:
Onan Chew
2024-07-24 20:49:25 -04:00
committed by GitHub
6 changed files with 295 additions and 18 deletions
+76 -1
View File
@@ -146,6 +146,9 @@ def get_all_meshes(context: Context) -> List[Object]:
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature] return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
return [] return []
def get_mesh_items(self, context):
return [(obj.name, obj.name, "") for obj in get_all_meshes(context)]
def open_web_after_delay_multi_threaded(delay: typing.Optional[float] = 1.0, url: typing.Union[str, typing.Any] = ""): def open_web_after_delay_multi_threaded(delay: typing.Optional[float] = 1.0, url: typing.Union[str, typing.Any] = ""):
thread = threading.Thread(target=open_web_after_delay,args=[delay,url],name="open_browser_thread") thread = threading.Thread(target=open_web_after_delay,args=[delay,url],name="open_browser_thread")
thread.start() thread.start()
@@ -164,4 +167,76 @@ def duplicatebone(b: bpy.types.EditBone) -> bpy.types.EditBone:
cb.tail = b.tail cb.tail = b.tail
cb.matrix = b.matrix cb.matrix = b.matrix
cb.parent = b.parent cb.parent = b.parent
return cb return cb
def has_shapekeys(mesh_obj: Object) -> bool:
return mesh_obj.data.shape_keys is not None
def sort_shape_keys(mesh: Object) -> None:
print("Starting shape key sorting...")
if not has_shapekeys(mesh):
print("No shape keys found. Exiting sort function.")
return
# Set the mesh as the active object
bpy.context.view_layer.objects.active = mesh
bpy.ops.object.mode_set(mode='OBJECT')
order = [
'Basis',
'vrc.blink_left',
'vrc.blink_right',
'vrc.lowerlid_left',
'vrc.lowerlid_right',
'vrc.v_aa',
'vrc.v_ch',
'vrc.v_dd',
'vrc.v_e',
'vrc.v_ff',
'vrc.v_ih',
'vrc.v_kk',
'vrc.v_nn',
'vrc.v_oh',
'vrc.v_ou',
'vrc.v_pp',
'vrc.v_rr',
'vrc.v_sil',
'vrc.v_ss',
'vrc.v_th',
]
shape_keys = mesh.data.shape_keys.key_blocks
print(f"Total shape keys: {len(shape_keys)}")
# Create a list of shape key names in their current order
current_order = [key.name for key in shape_keys]
# Create a new order list
new_order = []
# First, add all the keys that are in the predefined order
for name in order:
if name in current_order:
new_order.append(name)
current_order.remove(name)
# Then add any remaining keys that weren't in the predefined order
new_order.extend(current_order)
print("New order:", new_order)
# Now, rearrange the shape keys based on the new order
for i, name in enumerate(new_order):
index = shape_keys.find(name)
if index != i:
print(f"Moving {name} from index {index} to {i}")
mesh.active_shape_key_index = index
while mesh.active_shape_key_index > i:
bpy.ops.object.shape_key_move(type='UP')
print("Shape key sorting completed.")
def get_shapekeys(mesh: Object, prefix: str = '') -> List[tuple]:
if not has_shapekeys(mesh):
return []
return [(key.name, key.name, key.name) for key in mesh.data.shape_keys.key_blocks if key.name != 'Basis' and key.name.startswith(prefix)]
+43 -6
View File
@@ -1,13 +1,13 @@
import bpy import bpy
from ..functions.translations import t, get_languages_list, update_ui from ..functions.translations import t, get_languages_list, update_language
from ..core.register import register_property from ..core.register import register_property
from bpy.types import Scene, Object, Material, TextureNode, Context, SceneObjects, PropertyGroup from bpy.types import Scene, Object, Material, TextureNode, Context, SceneObjects, PropertyGroup
from bpy.props import BoolProperty, EnumProperty, FloatProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty from bpy.props import BoolProperty, EnumProperty, FloatProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty
from bpy.utils import register_class from bpy.utils import register_class
from ..core.register import register_wrap from ..core.register import register_wrap
from ..core.addon_preferences import get_preference from ..core.addon_preferences import get_preference
from ..core.common import SceneMatClass, material_list_bool, get_armatures from ..core.common import SceneMatClass, material_list_bool, get_armatures, get_mesh_items
@@ -21,9 +21,35 @@ def register() -> None:
default=default_language, default=default_language,
update=update_language update=update_language
) )
bpy.types.Scene.selected_mesh = bpy.props.EnumProperty(
items=get_mesh_items,
name="Selected Mesh",
description="The currently selected mesh for viseme operations"
)
bpy.types.Scene.avatar_toolkit_language_changed = bpy.props.BoolProperty(default=False) bpy.types.Scene.avatar_toolkit_language_changed = bpy.props.BoolProperty(default=False)
bpy.types.Scene.mouth_a = bpy.props.StringProperty(
name=t("Scene.mouth_a.label"),
description=t("Scene.mouth_a.desc")
)
bpy.types.Scene.mouth_o = bpy.props.StringProperty(
name=t("Scene.mouth_o.label"),
description=t("Scene.mouth_o.desc")
)
bpy.types.Scene.mouth_ch = bpy.props.StringProperty(
name=t("Scene.mouth_ch.label"),
description=t("Scene.mouth_ch.desc")
)
bpy.types.Scene.shape_intensity = bpy.props.FloatProperty(
name=t("Scene.shape_intensity.label"),
description=t("Scene.shape_intensity.desc"),
default=1.0,
min=0.0,
max=2.0
)
bpy.types.Scene.selected_armature = bpy.props.EnumProperty( bpy.types.Scene.selected_armature = bpy.props.EnumProperty(
items=get_armatures, items=get_armatures,
name="Selected Armature", name="Selected Armature",
@@ -55,13 +81,24 @@ def register() -> None:
register_property((Scene, "texture_atlas_Has_Mat_List_Shown", BoolProperty(default=False, get=material_list_bool.get_bool, set=material_list_bool.set_bool))) register_property((Scene, "texture_atlas_Has_Mat_List_Shown", BoolProperty(default=False, get=material_list_bool.get_bool, set=material_list_bool.set_bool)))
def unregister(): def unregister() -> None:
if hasattr(bpy.types.Scene, "avatar_toolkit_language"): if hasattr(bpy.types.Scene, "avatar_toolkit_language"):
del bpy.types.Scene.avatar_toolkit_language del bpy.types.Scene.avatar_toolkit_language
if hasattr(bpy.types.Scene, "avatar_toolkit_language_changed"): if hasattr(bpy.types.Scene, "avatar_toolkit_language_changed"):
del bpy.types.Scene.avatar_toolkit_language_changed del bpy.types.Scene.avatar_toolkit_language_changed
if hasattr(bpy.types.Scene, "mouth_a"):
del bpy.types.Scene.mouth_a
if hasattr(bpy.types.Scene, "mouth_o"):
del bpy.types.Scene.mouth_o
if hasattr(bpy.types.Scene, "mouth_ch"):
del bpy.types.Scene.mouth_ch
if hasattr(bpy.types.Scene, "shape_intensity"):
del bpy.types.Scene.shape_intensity
if hasattr(bpy.types.Scene, "selected_armature"): if hasattr(bpy.types.Scene, "selected_armature"):
del bpy.types.Scene.selected_armature del bpy.types.Scene.selected_armature
+98
View File
@@ -0,0 +1,98 @@
import bpy
from ..core import common
from ..core.register import register_wrap
from ..functions.translations import t
from typing import List, Tuple
from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes
@register_wrap
class AutoVisemeButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.create_visemes'
bl_label = t('AutoVisemeButton.label')
bl_description = t('AutoVisemeButton.desc')
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature) and get_all_meshes(context)
def execute(self, context: bpy.types.Context) -> set:
print("Starting viseme creation...")
mesh = bpy.data.objects.get(context.scene.selected_mesh)
if not mesh or not common.has_shapekeys(mesh):
self.report({'ERROR'}, t('AutoVisemeButton.error.noShapekeys'))
return {'CANCELLED'}
# Remove existing VRC shape keys
self.remove_existing_vrc_shapekeys(mesh)
shape_a = context.scene.mouth_a
shape_o = context.scene.mouth_o
shape_ch = context.scene.mouth_ch
print(f"Selected shapes: A={shape_a}, O={shape_o}, CH={shape_ch}")
if shape_a == "Basis" or shape_o == "Basis" or shape_ch == "Basis":
self.report({'ERROR'}, t('AutoVisemeButton.error.selectShapekeys'))
return {'CANCELLED'}
# Create visemes
visemes: List[Tuple[str, List[Tuple[str, float]]]] = [
('vrc.v_aa', [(shape_a, 0.9998)]),
('vrc.v_ch', [(shape_ch, 0.9996)]),
('vrc.v_dd', [(shape_a, 0.3), (shape_ch, 0.7)]),
('vrc.v_e', [(shape_a, 0.5), (shape_ch, 0.2)]),
('vrc.v_ff', [(shape_a, 0.2), (shape_ch, 0.4)]),
('vrc.v_ih', [(shape_ch, 0.7), (shape_o, 0.3)]),
('vrc.v_kk', [(shape_a, 0.7), (shape_ch, 0.4)]),
('vrc.v_nn', [(shape_a, 0.2), (shape_ch, 0.7)]),
('vrc.v_oh', [(shape_a, 0.2), (shape_o, 0.8)]),
('vrc.v_ou', [(shape_o, 0.9994)]),
('vrc.v_pp', [(shape_a, 0.0004), (shape_o, 0.0004)]),
('vrc.v_rr', [(shape_ch, 0.5), (shape_o, 0.3)]),
('vrc.v_sil', [(shape_a, 0.0002), (shape_ch, 0.0002)]),
('vrc.v_ss', [(shape_ch, 0.8)]),
('vrc.v_th', [(shape_a, 0.4), (shape_o, 0.15)])
]
for viseme_name, shape_mix in visemes:
print(f"Creating viseme: {viseme_name}")
self.create_viseme(mesh, viseme_name, shape_mix, context.scene.shape_intensity)
print("Sorting shape keys...")
common.sort_shape_keys(mesh)
print("Viseme creation completed.")
self.report({'INFO'}, t('AutoVisemeButton.success'))
return {'FINISHED'}
def create_viseme(self, mesh: bpy.types.Object, viseme_name: str, shape_mix: List[Tuple[str, float]], intensity: float) -> None:
print(f" Creating viseme: {viseme_name}")
shape_keys = mesh.data.shape_keys.key_blocks
# Remove existing viseme if it exists
if viseme_name in shape_keys:
print(f" Removing existing viseme: {viseme_name}")
mesh.shape_key_remove(shape_keys[viseme_name])
# Create new viseme
new_key = mesh.shape_key_add(name=viseme_name, from_mix=False)
new_key.value = 0.0
# Mix shapes
for shape_name, value in shape_mix:
if shape_name in shape_keys:
source_shape = shape_keys[shape_name]
print(f" Mixing shape: {shape_name} with value: {value * intensity}")
for i, vert in enumerate(new_key.data):
vert.co += (source_shape.data[i].co - shape_keys['Basis'].data[i].co) * value * intensity
print(f" Viseme {viseme_name} created successfully.")
def remove_existing_vrc_shapekeys(self, mesh: bpy.types.Object) -> None:
vrc_prefixes = ['vrc.v_', 'vrc.blink_', 'vrc.lowerlid_']
shape_keys = mesh.data.shape_keys.key_blocks
for key in reversed(shape_keys):
if any(key.name.startswith(prefix) for prefix in vrc_prefixes):
mesh.shape_key_remove(key)
+14 -2
View File
@@ -10,6 +10,7 @@
"Quick_Access.import_menu.label": "Import Menu", "Quick_Access.import_menu.label": "Import Menu",
"Quick_Access.import": "Import", "Quick_Access.import": "Import",
"Quick_Access.export": "Export", "Quick_Access.export": "Export",
"Quick_Access.import_menu.desc": "Import a Model",
"Quick_Access.import_pmx": "Import PMX", "Quick_Access.import_pmx": "Import PMX",
"Quick_Access.import_pmx.desc": "Import MMD PMX Model", "Quick_Access.import_pmx.desc": "Import MMD PMX Model",
"Quick_Access.import_pmd": "Import PMD", "Quick_Access.import_pmd": "Import PMD",
@@ -30,10 +31,22 @@
"Tools.tools_title.label": "Tools", "Tools.tools_title.label": "Tools",
"Tools.convert_to_resonite.label": "Convert to Resonite", "Tools.convert_to_resonite.label": "Convert to Resonite",
"Tools.convert_to_resonite.desc": "Converts bone names on a model to names compatable with Resonite", "Tools.convert_to_resonite.desc": "Converts bone names on a model to names compatable with Resonite",
"VisemePanel.label": "Visemes",
"VisemePanel.error.noMesh": "No mesh selected",
"VisemePanel.error.noShapekeys": "Selected mesh has no shape keys",
"VisemePanel.info.selectMesh": "Select a mesh to create visemes",
"VisemePanel.mouth_a.label": "Mouth A",
"VisemePanel.mouth_o.label": "Mouth O",
"VisemePanel.mouth_ch.label": "Mouth CH",
"AutoVisemeButton.label": "Create Visemes",
"AutoVisemeButton.desc": "Create visemes automatically, based on shape keys",
"AutoVisemeButton.error.noShapekeys": "No shape keys found",
"AutoVisemeButton.error.selectShapekeys": "Please Select shape keys",
"AutoVisemeButton.success": "Visemes created successfully",
"Settings.translation_restart_popup.label": "Translation Update",
"Settings.label": "Settings", "Settings.label": "Settings",
"Settings.language.label": "Language", "Settings.language.label": "Language",
"Settings.language.desc": "Select the language for the addon's UI", "Settings.language.desc": "Select the language for the addon's UI",
"Settings.translation_restart_popup.label": "Translation Update",
"Settings.translation_restart_popup.description": "Information about translation updates", "Settings.translation_restart_popup.description": "Information about translation updates",
"Settings.translation_restart_popup.message1": "Some translations may not apply", "Settings.translation_restart_popup.message1": "Some translations may not apply",
"Settings.translation_restart_popup.message2": "until you restart Blender.", "Settings.translation_restart_popup.message2": "until you restart Blender.",
@@ -43,7 +56,6 @@
"Importing.importer_search_term":"https://search.brave.com/search?q=blender+{extension}+importer+addon&source=web", "Importing.importer_search_term":"https://search.brave.com/search?q=blender+{extension}+importer+addon&source=web",
"Importer.export_resonite.label":"Export to Resonite", "Importer.export_resonite.label":"Export to Resonite",
"Importer.export_resonite.desc":"Export to Resonite as a GLTF. Make sure your model is to scale in blender, and import as meters in Resonite.", "Importer.export_resonite.desc":"Export to Resonite as a GLTF. Make sure your model is to scale in blender, and import as meters in Resonite.",
"Importer.export_vrchat.label":"Export to VRChat", "Importer.export_vrchat.label":"Export to VRChat",
"Importer.export_vrchat.desc":"Export to VRChat, may also work for ChilloutVR. Is similar to Cats export." "Importer.export_vrchat.desc":"Export to VRChat, may also work for ChilloutVR. Is similar to Cats export."
} }
+22 -9
View File
@@ -1,8 +1,8 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"Language.auto": "Automatic", "Language.auto": "自動",
"Language.en_US": "English", "Language.en_US": "英語",
"Language.ja_JP": "日本語", "Language.ja_JP": "日本語",
"Quick_Access.label": "クイックアクセス", "Quick_Access.label": "クイックアクセス",
"Quick_Access.import_export.label": "インポート/エクスポート", "Quick_Access.import_export.label": "インポート/エクスポート",
@@ -10,26 +10,39 @@
"Quick_Access.import_menu.label": "インポートメニュー", "Quick_Access.import_menu.label": "インポートメニュー",
"Quick_Access.import": "インポート", "Quick_Access.import": "インポート",
"Quick_Access.export": "エクスポート", "Quick_Access.export": "エクスポート",
"Quick_Access.import_pmx": "PMXインポート", "Quick_Access.import_menu.desc": "モデルをインポート",
"Quick_Access.import_pmx": "PMXをインポート",
"Quick_Access.import_pmx.desc": "MMD PMXモデルをインポート", "Quick_Access.import_pmx.desc": "MMD PMXモデルをインポート",
"Quick_Access.import_pmd": "PMDインポート", "Quick_Access.import_pmd": "PMDインポート",
"Quick_Access.import_pmd.desc": "MMD PMDモデルをインポート", "Quick_Access.import_pmd.desc": "MMD PMDモデルをインポート",
"Quick_Access.export_menu.label": "エクスポートメニュー", "Quick_Access.export_menu.label": "エクスポートメニュー",
"Quick_Access.select_export.label": "エクスポート方法を選択", "Quick_Access.select_export.label": "エクスポート方法を選択",
"Quick_Access.select_export_resonite.label": "Resonite", "Quick_Access.select_export_resonite.label": "Resonite",
"Export.resonite.label": "Resoniteにエクスポート", "Export.resonite.label": "Resoniteにエクスポート",
"Export.resonite.desc": "すべてのアニメーションとマテリアルを含むGLBをエクスポートします。アニメーションデータについては以下を参照してください", "Export.resonite.desc": "すべてのアニメーションとマテリアルを含むGLBをエクスポート。アニメーションデータについては以下を参照:",
"Optimization.label": "最適化", "Optimization.label": "最適化",
"Optimization.options.label": "最適化オプション", "Optimization.options.label": "最適化オプション",
"Optimization.combine_materials.label": "マテリアルを結合", "Optimization.combine_materials.label": "マテリアルを結合",
"Optimization.combine_materials.desc": "類似したマテリアルを結合してモデルを最適化します", "Optimization.combine_materials.desc": "類似したマテリアルを結合してモデルを最適化",
"Optimization.join_all_meshes.label": "すべてのメッシュを結合", "Optimization.join_all_meshes.label": "すべてのメッシュを結合",
"Optimization.join_all_meshes.desc": "すべてのメッシュを1つに結合します", "Optimization.join_all_meshes.desc": "すべてのメッシュを1つに結合",
"Optimization.join_selected_meshes.label": "選択したメッシュを結合", "Optimization.join_selected_meshes.label": "選択したメッシュを結合",
"Optimization.join_selected_meshes.desc": "現在選択されているすべてのメッシュを1つに結合します", "Optimization.join_selected_meshes.desc": "現在選択されているすべてのメッシュを1つに結合",
"Tools.tools_title.label": "ツール", "Tools.tools_title.label": "ツール",
"Tools.convert_to_resonite.label": "Resoniteに変換", "Tools.convert_to_resonite.label": "Resoniteに変換",
"Tools.convert_to_resonite.desc": "モデルのボーン名をResoniteと互換性のある名前に変換します", "Tools.convert_to_resonite.desc": "モデルのボーン名をResoniteと互換性のある名前に変換",
"VisemePanel.label": "ビセーム",
"VisemePanel.error.noMesh": "メッシュが選択されていません",
"VisemePanel.error.noShapekeys": "選択されたメッシュにシェイプキーがありません",
"VisemePanel.info.selectMesh": "ビセームを作成するメッシュを選択してください",
"VisemePanel.mouth_a.label": "口 A",
"VisemePanel.mouth_o.label": "口 O",
"VisemePanel.mouth_ch.label": "口 CH",
"AutoVisemeButton.label": "ビセームを作成",
"AutoVisemeButton.desc": "シェイプキーに基づいて自動的にビセームを作成",
"AutoVisemeButton.error.noShapekeys": "シェイプキーが見つかりません",
"AutoVisemeButton.error.selectShapekeys": "シェイプキーを選択してください",
"AutoVisemeButton.success": "ビセームが正常に作成されました",
"Settings.label": "設定", "Settings.label": "設定",
"Settings.language.label": "言語", "Settings.language.label": "言語",
"Settings.language.desc": "アドオンのUI言語を選択してください", "Settings.language.desc": "アドオンのUI言語を選択してください",
+42
View File
@@ -0,0 +1,42 @@
import bpy
from ..core.register import register_wrap
from ..functions.translations import t
from ..core.common import get_selected_armature
@register_wrap
class AvatarToolkitVisemePanel(bpy.types.Panel):
bl_label = t("VisemePanel.label")
bl_idname = "OBJECT_PT_avatar_toolkit_viseme"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Avatar Toolkit"
bl_parent_id = "OBJECT_PT_avatar_toolkit"
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
armature = get_selected_armature(context)
if armature:
layout.prop(context.scene, "selected_mesh", text="Select Mesh")
mesh = bpy.data.objects.get(context.scene.selected_mesh)
if mesh and mesh.type == 'MESH':
if mesh.data.shape_keys:
layout.prop_search(context.scene, "mouth_a", mesh.data.shape_keys, "key_blocks", text=t('VisemePanel.mouth_a.label'))
layout.prop_search(context.scene, "mouth_o", mesh.data.shape_keys, "key_blocks", text=t('VisemePanel.mouth_o.label'))
layout.prop_search(context.scene, "mouth_ch", mesh.data.shape_keys, "key_blocks", text=t('VisemePanel.mouth_ch.label'))
layout.prop(context.scene, 'shape_intensity')
layout.operator("avatar_toolkit.create_visemes", icon='TRIA_RIGHT')
else:
layout.label(text=t('VisemePanel.error.noShapekeys'), icon='ERROR')
else:
layout.label(text=t('VisemePanel.error.selectMesh'), icon='INFO')
else:
layout.label(text=t('VisemePanel.error.noArmature'), icon='ERROR')
layout.separator()
layout.label(text=t('VisemePanel.info.selectMesh'))