├── .gitattributes ├── .gitignore ├── README.md ├── VRCFacetracking.blend └── VRCFacetracking.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **.blend1 2 | Template 3 | [Ee]xport/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VRCFaceTracking-blender-plugin 2 | Creates blendshapes in Blender to be used for VRC FaceTracking 3 | 4 | https://user-images.githubusercontent.com/58948489/149041916-e82236ee-3e95-4a96-8b8e-a83e173b6fbc.mp4 5 | -------------------------------------------------------------------------------- /VRCFacetracking.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adjerry91/VRCFaceTracking-blender-plugin/0e01853af46287866ce600faffc6c16abe73d4d7/VRCFacetracking.blend -------------------------------------------------------------------------------- /VRCFacetracking.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name" : "VRC Facetracking Shapekeys", 3 | "author" : "Adjerry91", 4 | "version" : (1,1,1), 5 | "blender" : (3,0,0), 6 | "location" : "View3d > Tool", 7 | "warning" : "", 8 | "wiki_url" : "", 9 | "category" : "Shape Keys", 10 | } 11 | 12 | import bpy 13 | import math 14 | 15 | from bpy.types import (Scene, Menu, Operator, Panel, PropertyGroup) 16 | from bpy.props import (BoolProperty, IntProperty, StringProperty, BoolVectorProperty, EnumProperty, PointerProperty) 17 | 18 | blender_version = bool(bpy.app.version >= (2, 80, 0)) 19 | 20 | if blender_version: 21 | user_preferences = bpy.context.preferences 22 | else: 23 | bl_info['blender'] = (2, 79, 0) 24 | user_preferences = bpy.context.user_preferences 25 | 26 | # ------------------------------------------------------------------- 27 | # VRChat Facetracking Shapekey List 28 | # ------------------------------------------------------------------- 29 | 30 | VRCFT_Labels = [ 31 | "Eye_Left_squeeze", 32 | "Eye_Right_squeeze", 33 | "Eye_Left_Blink", 34 | "Eye_Left_Right", 35 | "Eye_Left_Left", 36 | "Eye_Left_Down", 37 | "Eye_Left_Up", 38 | "Eye_Right_Blink", 39 | "Eye_Right_Right", 40 | "Eye_Right_Left", 41 | "Eye_Right_Down", 42 | "Eye_Right_Up", 43 | "Eye_Left_Wide", 44 | "Eye_Right_Wide", 45 | "Eye_Left_Dilation", 46 | "Eye_Left_Constrict", 47 | "Eye_Right_Dilation", 48 | "Eye_Right_Constrict", 49 | "Jaw_Right", 50 | "Jaw_Left", 51 | "Jaw_Forward", 52 | "Jaw_Open", 53 | "Mouth_Ape_Shape", 54 | "Mouth_Upper_Right", 55 | "Mouth_Upper_Left", 56 | "Mouth_Lower_Right", 57 | "Mouth_Lower_Left", 58 | "Mouth_Upper_Overturn", 59 | "Mouth_Lower_Overturn", 60 | "Mouth_Pout", 61 | "Mouth_Smile_Right", 62 | "Mouth_Smile_Left", 63 | "Mouth_Sad_Right", 64 | "Mouth_Sad_Left", 65 | "Cheek_Puff_Right", 66 | "Cheek_Puff_Left", 67 | "Cheek_Suck", 68 | "Mouth_Upper_UpRight", 69 | "Mouth_Upper_UpLeft", 70 | "Mouth_Lower_DownRight", 71 | "Mouth_Lower_DownLeft", 72 | "Mouth_Upper_Inside", 73 | "Mouth_Lower_Inside", 74 | "Mouth_Lower_Overlay", 75 | "Tongue_LongStep1", 76 | "Tongue_LongStep2", 77 | "Tongue_Down", 78 | "Tongue_Up", 79 | "Tongue_Right", 80 | "Tongue_Left", 81 | "Tongue_Roll", 82 | "Tongue_UpRight_Morph", 83 | "Tongue_UpLeft_Morph", 84 | "Tongue_DownRight_Morph", 85 | "Tongue_DownLeft_Morph", 86 | ] 87 | 88 | # ------------------------------------------------------------------- 89 | # Functions 90 | # ------------------------------------------------------------------- 91 | 92 | def duplicate_shapekey(string): 93 | active_object = bpy.context.active_object 94 | 95 | #Check shape keys if duplicate 96 | if active_object.data.shape_keys.key_blocks.find(string) >= 0: 97 | # print("Duplicate shape key found!") 98 | return True 99 | else: 100 | return False 101 | 102 | def version_2_79_or_older(): 103 | return bpy.app.version < (2, 80) 104 | 105 | def unselect_all(): 106 | for obj in get_objects(): 107 | select(obj, False) 108 | 109 | def get_objects(): 110 | return bpy.context.scene.objects if version_2_79_or_older() else bpy.context.view_layer.objects 111 | 112 | def set_active(obj, skip_sel=False): 113 | if not skip_sel: 114 | select(obj) 115 | if version_2_79_or_older(): 116 | bpy.context.scene.objects.active = obj 117 | else: 118 | bpy.context.view_layer.objects.active = obj 119 | 120 | def select(obj, sel=True): 121 | if sel: 122 | hide(obj, False) 123 | if version_2_79_or_older(): 124 | obj.select = sel 125 | else: 126 | obj.select_set(sel) 127 | 128 | def hide(obj, val=True): 129 | if hasattr(obj, 'hide'): 130 | obj.hide = val 131 | if not version_2_79_or_older(): 132 | obj.hide_set(val) 133 | 134 | def get_armature(armature_name=None): 135 | if not armature_name: 136 | armature_name = bpy.context.scene.armature 137 | for obj in get_objects(): 138 | if obj.type == 'ARMATURE': 139 | if (armature_name and obj.name == armature_name) or not armature_name: 140 | return obj 141 | return None 142 | 143 | def get_meshes_objects(armature_name=None, mode=2, check=True, visible_only=False): 144 | # Modes: 145 | # 0 = With armatures only 146 | # 1 = Top level only 147 | # 2 = All meshes 148 | # 3 = Selected only 149 | 150 | if not armature_name: 151 | armature = get_armature() 152 | if armature: 153 | armature_name = armature.name 154 | 155 | meshes = [] 156 | for ob in get_objects(): 157 | if ob.type == 'MESH': 158 | if mode == 0 or mode == 5: 159 | if ob.parent: 160 | if ob.parent.type == 'ARMATURE' and ob.parent.name == armature_name: 161 | meshes.append(ob) 162 | elif ob.parent.parent and ob.parent.parent.type == 'ARMATURE' and ob.parent.parent.name == armature_name: 163 | meshes.append(ob) 164 | 165 | elif mode == 1: 166 | if not ob.parent: 167 | meshes.append(ob) 168 | 169 | elif mode == 2: 170 | meshes.append(ob) 171 | 172 | elif mode == 3: 173 | if is_selected(ob): 174 | meshes.append(ob) 175 | 176 | if visible_only: 177 | for mesh in meshes: 178 | if is_hidden(mesh): 179 | meshes.remove(mesh) 180 | 181 | # Check for broken meshes and delete them 182 | if check: 183 | current_active = get_active() 184 | to_remove = [] 185 | for mesh in meshes: 186 | selected = is_selected(mesh) 187 | # print(mesh.name, mesh.users) 188 | set_active(mesh) 189 | 190 | if not get_active(): 191 | to_remove.append(mesh) 192 | 193 | if not selected: 194 | select(mesh, False) 195 | 196 | for mesh in to_remove: 197 | print('DELETED CORRUPTED MESH:', mesh.name, mesh.users) 198 | meshes.remove(mesh) 199 | delete(mesh) 200 | 201 | if current_active: 202 | set_active(current_active) 203 | 204 | return meshes 205 | 206 | def get_shapekeys_vrcft(self, context): 207 | return get_shapekeys(context, [], False, False, False) 208 | 209 | def get_shapekeys(context, names, no_basis, decimation, return_list): 210 | choices = [] 211 | choices_simple = [] 212 | meshes_list = get_meshes_objects(check=False) 213 | 214 | if decimation: 215 | meshes = meshes_list 216 | elif meshes_list: 217 | meshes = [get_objects().get(context.scene.vrcft_mesh)] 218 | else: 219 | bpy.types.Object.Enum = choices 220 | return bpy.types.Object.Enum 221 | 222 | for mesh in meshes: 223 | if not mesh or not has_shapekeys(mesh): 224 | bpy.types.Object.Enum = choices 225 | return bpy.types.Object.Enum 226 | 227 | for shapekey in mesh.data.shape_keys.key_blocks: 228 | name = shapekey.name 229 | if name in choices_simple: 230 | continue 231 | if no_basis and name == 'Basis': 232 | continue 233 | if decimation and name in Decimation.ignore_shapes: 234 | continue 235 | # 1. Will be returned by context.scene 236 | # 2. Will be shown in lists 237 | # 3. will be shown in the hover description (below description) 238 | choices.append((name, name, '')) 239 | choices_simple.append(name) 240 | 241 | # choices.sort(key=lambda x: tuple(x[0].lower())) 242 | 243 | choices2 = [] 244 | for name in names: 245 | if name in choices_simple and len(choices) > 1 and choices[0][0] != name: 246 | if decimation and name in Decimation.ignore_shapes: 247 | continue 248 | choices2.append((name, name, name)) 249 | 250 | for choice in choices: 251 | choices2.append(choice) 252 | 253 | bpy.types.Object.Enum = choices2 254 | 255 | if return_list: 256 | shape_list = [] 257 | for choice in choices2: 258 | shape_list.append(choice[0]) 259 | return shape_list 260 | 261 | return bpy.types.Object.Enum 262 | 263 | def get_meshes(self, context): 264 | # Modes: 265 | # 0 = With Armature only 266 | # 1 = Without armature only 267 | # 2 = All meshes 268 | 269 | choices = [] 270 | 271 | for mesh in get_meshes_objects(mode=2, check=False): 272 | choices.append((mesh.name, mesh.name, mesh.name)) 273 | 274 | bpy.types.Object.Enum = sorted(choices, key=lambda x: tuple(x[0].lower())) 275 | return bpy.types.Object.Enum 276 | 277 | def has_shapekeys(mesh): 278 | if not hasattr(mesh.data, 'shape_keys'): 279 | return False 280 | return hasattr(mesh.data.shape_keys, 'key_blocks') 281 | 282 | 283 | # ------------------------------------------------------------------- 284 | # Shape Key Operators 285 | # ------------------------------------------------------------------- 286 | 287 | class VRCFT_OT_CreateShapeKeys(Operator): 288 | """Creates VRChat Facetracking Shapekeys""" 289 | bl_label = "Create VRChat Facetracking Shape Keys" 290 | bl_idname = "vrcft.create_shapekeys" 291 | 292 | def execute(self, context): 293 | 294 | object = bpy.context.object 295 | scene = context.scene 296 | vrcft_mesh = scene.vrcft_mesh 297 | active_object = bpy.context.active_object 298 | mesh = bpy.ops.mesh 299 | ops = bpy.ops 300 | 301 | #Set the selected mesh to active object 302 | mesh = get_objects()[vrcft_mesh] 303 | self.report({'INFO'}, "Selected mesh is: " + str(vrcft_mesh)) 304 | set_active(mesh) 305 | 306 | 307 | #Check if there is shape keys on the mesh 308 | if object.data.shape_keys: 309 | 310 | #Create beginning seperation marker for VRCFT Shape Keys 311 | if duplicate_shapekey("~~ VRCFacetracking ~~") == False : 312 | object.shape_key_add(name="~~ VRCFacetracking ~~", from_mix=False) 313 | 314 | #Clear all existing values for shape keys 315 | ops.object.shape_key_clear() 316 | 317 | for x in range(len(VRCFT_Labels)): 318 | curr_key = eval("scene.vrcft_shapekeys_" + str(x)) 319 | 320 | #Check if blend with 'Basis' shape key 321 | if curr_key == "Basis": 322 | #Check for duplicates 323 | if duplicate_shapekey(VRCFT_Labels[x]) == False : 324 | object.shape_key_add(name=VRCFT_Labels[x], from_mix=False) 325 | #Do not overwrite if the shape key exists and is on 'Basis' 326 | 327 | else: 328 | #Check for duplicates 329 | if duplicate_shapekey(VRCFT_Labels[x]) == False : 330 | # Find shapekey enterred and mix to create new shapekey 331 | object.active_shape_key_index = active_object.data.shape_keys.key_blocks.find(curr_key) 332 | object.data.shape_keys.key_blocks[curr_key].value = 1 333 | object.shape_key_add(name=VRCFT_Labels[x], from_mix=True) 334 | else: 335 | #Mix to existing shape key duplicate 336 | object.active_shape_key_index = active_object.data.shape_keys.key_blocks.find(VRCFT_Labels[x]) 337 | object.data.shape_keys.key_blocks[curr_key].value = 1 338 | ops.object.mode_set(mode='EDIT', toggle=False) 339 | bpy.ops.mesh.select_mode(type="VERT") 340 | ops.mesh.select_all(action='SELECT') 341 | ops.mesh.blend_from_shape(shape=curr_key, blend=1.0, add=False) 342 | self.report({'INFO'}, "Existing VRC facetracking shape key: " + VRCFT_Labels[x] + " has been overwritten with: " + curr_key) 343 | #Clear shape key weights 344 | ops.object.shape_key_clear() 345 | 346 | 347 | #Create end seperation marker for VRCFT Shape Keys 348 | if duplicate_shapekey("~~ END OF VRCFacetracking ~~") == False : 349 | object.shape_key_add(name="~~ END OF VRCFacetracking ~~",from_mix=False) 350 | self.report({'INFO'}, "VRC facetracking shapekeys have been created on mesh") 351 | 352 | 353 | #Cleanup mode state 354 | ops.object.mode_set(mode='OBJECT', toggle=False) 355 | 356 | #Move active shape to 'Basis' 357 | active_object.active_shape_key_index = 0 358 | 359 | else: 360 | #Error message if basis does not exist 361 | self.report({'WARNING'}, "No shape keys found on mesh") 362 | return{'FINISHED'} 363 | 364 | # ------------------------------------------------------------------- 365 | # User Interface 366 | # ------------------------------------------------------------------- 367 | 368 | class VRCFT_UL(Panel): 369 | bl_label = "VRChat Facetracking" 370 | bl_idname = "VRCFT" 371 | bl_space_type = 'VIEW_3D' 372 | bl_region_type = 'UI' 373 | bl_category = "VRCFT" 374 | 375 | def draw(self, context): 376 | layout = self.layout 377 | scene = context.scene 378 | vrcft_mesh = scene.vrcft_mesh 379 | object = bpy.context.object 380 | 381 | #Start Layout 382 | col = layout.column() 383 | 384 | #Mesh Selection 385 | mesh = get_objects()[vrcft_mesh] 386 | mesh_count = len(get_meshes_objects(check=False, mode=2)) 387 | row = col.row(align=True) 388 | row.scale_y = 1.1 389 | row.prop(context.scene, 'vrcft_mesh', icon='MESH_DATA') 390 | col.separator() 391 | row = col.row(align=True) 392 | 393 | #Check mesh selections 394 | if vrcft_mesh and has_shapekeys(mesh): 395 | #Info 396 | 397 | row = col.row(align=True) 398 | row.scale_y = 1.1 399 | row.label(text='Select shape keys to create VRCFT shape keys.', icon='INFO') 400 | col.separator() 401 | 402 | #Start Box 403 | box = layout.box() 404 | col = box.column(align=True) 405 | 406 | #Start List of Shapekeys from VRCFT labels list 407 | for i in range(len(VRCFT_Labels)): 408 | row = col.row(align=True) 409 | row.scale_y = 1.1 410 | row.label(text = VRCFT_Labels[i] + ":") 411 | row.prop(scene, 'vrcft_shapekeys_' + str(i), icon='SHAPEKEY_DATA') 412 | row = layout.row() 413 | row.operator("vrcft.create_shapekeys", icon='MESH_MONKEY') 414 | else: 415 | row = col.row(align=True) 416 | row.scale_y = 1.1 417 | row.label(text='Select the mesh with face shape keys.', icon='INFO') 418 | col.separator() 419 | 420 | # ------------------------------------------------------------------- 421 | # Register 422 | # ------------------------------------------------------------------- 423 | 424 | classes = ( 425 | VRCFT_OT_CreateShapeKeys, 426 | VRCFT_UL 427 | ) 428 | 429 | def register(): 430 | for cls in classes: 431 | bpy.utils.register_class(cls) 432 | 433 | # Mesh Select 434 | Scene.vrcft_mesh = EnumProperty(name='Mesh',description='Mesh to apply VRCFT shape keys',items=get_meshes) 435 | # Shape Keys 436 | for i, vrcft_shape in enumerate(VRCFT_Labels): 437 | setattr(Scene, "vrcft_shapekeys_" + str(i), EnumProperty(name='',description='Select shapekey to use for VRCFT',items=get_shapekeys_vrcft)) 438 | 439 | def unregister(): 440 | for cls in classes: 441 | bpy.utils.unregister_class(cls) 442 | 443 | del Scene.vrcft_mesh 444 | del Scene.vrcft_shapekeys_0 445 | del Scene.vrcft_shapekeys_1 446 | del Scene.vrcft_shapekeys_2 447 | del Scene.vrcft_shapekeys_3 448 | del Scene.vrcft_shapekeys_4 449 | del Scene.vrcft_shapekeys_5 450 | del Scene.vrcft_shapekeys_6 451 | del Scene.vrcft_shapekeys_7 452 | del Scene.vrcft_shapekeys_8 453 | del Scene.vrcft_shapekeys_9 454 | del Scene.vrcft_shapekeys_10 455 | del Scene.vrcft_shapekeys_11 456 | del Scene.vrcft_shapekeys_12 457 | del Scene.vrcft_shapekeys_13 458 | del Scene.vrcft_shapekeys_14 459 | del Scene.vrcft_shapekeys_15 460 | del Scene.vrcft_shapekeys_16 461 | del Scene.vrcft_shapekeys_17 462 | del Scene.vrcft_shapekeys_18 463 | del Scene.vrcft_shapekeys_19 464 | del Scene.vrcft_shapekeys_20 465 | del Scene.vrcft_shapekeys_21 466 | del Scene.vrcft_shapekeys_22 467 | del Scene.vrcft_shapekeys_23 468 | del Scene.vrcft_shapekeys_24 469 | del Scene.vrcft_shapekeys_25 470 | del Scene.vrcft_shapekeys_26 471 | del Scene.vrcft_shapekeys_27 472 | del Scene.vrcft_shapekeys_28 473 | del Scene.vrcft_shapekeys_29 474 | del Scene.vrcft_shapekeys_30 475 | del Scene.vrcft_shapekeys_31 476 | del Scene.vrcft_shapekeys_32 477 | del Scene.vrcft_shapekeys_33 478 | del Scene.vrcft_shapekeys_34 479 | del Scene.vrcft_shapekeys_35 480 | del Scene.vrcft_shapekeys_36 481 | del Scene.vrcft_shapekeys_37 482 | del Scene.vrcft_shapekeys_38 483 | del Scene.vrcft_shapekeys_39 484 | del Scene.vrcft_shapekeys_40 485 | del Scene.vrcft_shapekeys_41 486 | del Scene.vrcft_shapekeys_42 487 | del Scene.vrcft_shapekeys_43 488 | del Scene.vrcft_shapekeys_44 489 | del Scene.vrcft_shapekeys_45 490 | del Scene.vrcft_shapekeys_46 491 | del Scene.vrcft_shapekeys_47 492 | del Scene.vrcft_shapekeys_48 493 | del Scene.vrcft_shapekeys_49 494 | del Scene.vrcft_shapekeys_50 495 | del Scene.vrcft_shapekeys_51 496 | del Scene.vrcft_shapekeys_52 497 | del Scene.vrcft_shapekeys_53 498 | del Scene.vrcft_shapekeys_54 499 | 500 | if __name__ == "__main__": 501 | register() 502 | --------------------------------------------------------------------------------