import bpy import struct import base64 import os import subprocess import bmesh import sys import ctypes import time VERSION = 1.0 bl_info = {"name": "Viva Project Model Exporter v1", "category": "Object"} class VivaProjectHeadExporter(bpy.types.Operator): """Export as .viva3d""" # blender will use this as a tooltip for menu items and buttons. bl_idname = "object.export_viva_project_mesh" # unique identifier for buttons and menu items to reference. bl_label = "Export mesh for Viva Project" # display name in the interface. bl_options = {'REGISTER', 'UNDO'} # enable undo for the operator. def execute(self, context): # execute() is called by blender when running the operator. # The original script executeExportVivaProjectHead() return {'FINISHED'}# this lets blender know the operator finished successfully. class VivaProjectArmatureMatch(bpy.types.Operator): """Match Viva Project Skeleton""" # blender will use this as a tooltip for menu items and buttons. bl_idname = "object.match_viva_project_armature" # unique identifier for buttons and menu items to reference. bl_label = "Match Viva Project Skeleton" # display name in the interface. bl_options = {'REGISTER', 'UNDO'} # enable undo for the operator. def execute(self, context): # execute() is called by blender when running the operator. # The original script matchVivaProjectSkeleton() return {'FINISHED'}# this lets blender know the operator finished successfully. def register(): bpy.utils.register_class(VivaProjectHeadExporter) bpy.utils.register_class(VivaProjectArmatureMatch) def unregister(): bpy.utils.unregister_class(VivaProjectHeadExporter) bpy.utils.unregister_class(VivaProjectArmatureMatch) # This allows you to run the script directly from blenders text editor # to test the addon without having to install it. register() #*******************************# #************ PACK *************# #*******************************# #max vertices: 65535 #max faces: 65535 #max bones: 255 #max vertices per face: 255 #max bones per vertex: 4 bytes = [] info = [] def ShowMessageBox(text = "", title = "Viva Project Head Expoter v"+str(VERSION), icon = 'ERROR'): def draw(self, context): self.layout.label(text=text) bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) errors = [] def addError(msg): global errors print(msg) errors.append(msg) def unsignedIntTo2Bytes(v): return bytearray(struct.pack("H",v)) def unsignedIntTo1Byte(v): return bytearray(struct.pack("B",v)) def floatTo4Bytes(v): return bytearray(struct.pack("f",v)) def normalFloatTo1Byte(v): return bytearray(struct.pack("B",max(0,int(v*128.0+127.0 )))) def unsignedNormalFloatTo1Byte(v): return bytearray(struct.pack("B",min(255,int( v*256.0 )))) def mirrorAppend(l): orig = len(l) for i in range(0,orig): entry = l[i] if entry.find("_r") != -1: l.append( entry.replace("_r","_l") ) return l baseSkeleton = ['spine1', None, [0.0, 1.43027663230896, 20.829814910888672], [0.0, 0.47541430592536926, 23.557783126831055], 0.0, [['spine2', 'spine1', [0.0, 0.47541430592536926, 23.557783126831055], [0.0, 0.23448880016803741, 25.945756912231445], 0.0, [['spine3', 'spine2', [0.0, 0.23448880016803741, 25.945756912231445], [0.0, 1.0527253150939941, 31.89455795288086], 2.384185791015625e-07, [['shoulder_l', 'spine3', [0.34273192286491394, 1.0527253150939941, 31.407487869262695], [2.7051873207092285, 0.7438520789146423, 31.15633773803711], 0.0, [['armControl_l', 'shoulder_l', [2.7051873207092285, 0.7438520789146423, 31.15633773803711], [5.898359775543213, 1.2883739471435547, 27.46017074584961], 4.4703490686970326e-08, [['upperArm_l', 'armControl_l', [2.7051873207092285, 0.7438520789146423, 31.15633773803711], [4.301773548126221, 1.016113042831421, 29.30825424194336], 7.450581307466564e-08, []], ['bicep_l', 'armControl_l', [5.898355007171631, 1.2883739471435547, 27.46017074584961], [4.301773548126221, 1.016113042831421, 29.30825424194336], 1.4901159417490817e-08, []], ['forearmControl_l', 'armControl_l', [5.898359775543213, 1.2883739471435547, 27.46017074584961], [9.099026679992676, 0.9459040760993958, 23.733680725097656], 0.7853981852531433, [['hand_l', 'forearmControl_l', [9.099026679992676, 0.9459040760993958, 23.733680725097656], [9.876893997192383, 0.9950312376022339, 22.690710067749023], -2.980232594040899e-08, [['thumb1_l', 'hand_l', [9.311381340026855, 0.5909335017204285, 23.329681396484375], [9.326107025146484, -0.035463713109493256, 22.82261085510254], -0.3839723765850067, [['thumb2_l', 'thumb1_l', [9.326107025146484, -0.035463713109493256, 22.82261085510254], [9.369035720825195, -0.42945563793182373, 22.462879180908203], -0.3682214021682739, [['thumb3_l', 'thumb2_l', [9.369035720825195, -0.42945563793182373, 22.462879180908203], [9.414382934570312, -0.7093737125396729, 22.129837036132812], -0.3826146125793457, []]]]]], ['index1_l', 'hand_l', [10.282362937927246, 0.31196972727775574, 22.517505645751953], [10.756875991821289, 0.15218861401081085, 21.89153480529785], -2.181661605834961, [['index2_l', 'index1_l', [10.756875991821289, 0.15218861401081085, 21.89153480529785], [10.994132041931152, 0.07229805737733841, 21.578550338745117], -2.181661605834961, [['index3_l', 'index2_l', [10.994132041931152, 0.07229805737733841, 21.578550338745117], [11.231389045715332, -0.007592441514134407, 21.26556396484375], -2.181661605834961, []]]]]], ['middle1_l', 'hand_l', [10.301751136779785, 0.7227813601493835, 22.51811981201172], [10.869375228881836, 0.6472422480583191, 21.835247039794922], -2.356194496154785, [['middle2_l', 'middle1_l', [10.869375228881836, 0.6472422480583191, 21.835247039794922], [11.153186798095703, 0.6094726920127869, 21.493812561035156], -2.356194496154785, [['middle3_l', 'middle2_l', [11.153186798095703, 0.6094726920127869, 21.493812561035156], [11.43699836730957, 0.5717031955718994, 21.152376174926758], -2.356194496154785, []]]]]], ['ring1_l', 'hand_l', [10.180306434631348, 1.152927041053772, 22.507169723510742], [10.681703567504883, 1.177601933479309, 21.873558044433594], -2.478367567062378, [['ring2_l', 'ring1_l', [10.681703567504883, 1.177601933479309, 21.873558044433594], [10.932401657104492, 1.1899393796920776, 21.556751251220703], -2.478367567062378, [['ring3_l', 'ring2_l', [10.932401657104492, 1.1899393796920776, 21.556751251220703], [11.183098793029785, 1.2022769451141357, 21.239946365356445], -2.478365898132324, []]]]]], ['pinky1_l', 'hand_l', [10.002487182617188, 1.5079398155212402, 22.53680992126465], [10.392721176147461, 1.6533280611038208, 22.023563385009766], -2.57907772064209, [['pinky2_l', 'pinky1_l', [10.392721176147461, 1.6533280611038208, 22.023563385009766], [10.587837219238281, 1.7260223627090454, 21.766937255859375], -2.579075574874878, [['pinky3_l', 'pinky2_l', [10.587837219238281, 1.7260223627090454, 21.766937255859375], [10.782954216003418, 1.7987163066864014, 21.510313034057617], -2.579075813293457, []]]]]]]], ['arm_l', 'forearmControl_l', [5.898355007171631, 1.2883739471435547, 27.46017074584961], [7.498690605163574, 1.1171389818191528, 25.596925735473633], 1.4901161193847656e-08, []], ['wrist_l', 'forearmControl_l', [7.498690605163574, 1.1171389818191528, 25.596925735473633], [9.099026679992676, 0.9459040760993958, 23.733680725097656], 1.4901161193847656e-08, []]]]]]]], ['neck', 'spine3', [0.0, 1.0527253150939941, 31.89455795288086], [-1.8477439578390431e-09, 0.3884499669075012, 33.066734313964844], -2.220446049250313e-16, [['head', 'neck', [-1.8477439578390431e-09, 0.3884499669075012, 33.066734313964844], [-1.8477439578390431e-09, 0.3884499669075012, 35.50340270996094], 0.0, [['eyeball_l', 'head', [1.4120454788208008, -1.529252052307129, 35.87043762207031], [1.4120454788208008, -2.0360159873962402, 35.87043762207031], -0.0, []], ['eyeball_r', 'head', [-1.4120454788208008, -1.529252052307129, 35.87043762207031], [-1.4120454788208008, -2.0360159873962402, 35.87043762207031], 0.0, []]]]]], ['shoulder_r', 'spine3', [-0.34273192286491394, 1.0527253150939941, 31.407487869262695], [-2.7051873207092285, 0.7438520789146423, 31.15633773803711], 0.0, [['armControl_r', 'shoulder_r', [-2.7051873207092285, 0.7438520789146423, 31.15633773803711], [-5.898355007171631, 1.2883739471435547, 27.46017074584961], 0.0, [['forearmControl_r', 'armControl_r', [-5.898355007171631, 1.2883739471435547, 27.46017074584961], [-9.099026679992676, 0.945904016494751, 23.733680725097656], -0.7853981256484985, [['arm_r', 'forearmControl_r', [-5.898355007171631, 1.2883739471435547, 27.46017074584961], [-7.498690605163574, 1.1171391010284424, 25.596925735473633], 1.4901161193847656e-08, []], ['wrist_r', 'forearmControl_r', [-7.498690605163574, 1.1171391010284424, 25.596925735473633], [-9.099026679992676, 0.945904016494751, 23.733680725097656], 4.470348358154297e-08, []], ['hand_r', 'forearmControl_r', [-9.099026679992676, 0.945904016494751, 23.733680725097656], [-9.876893997192383, 0.9950312376022339, 22.690710067749023], -2.9802322387695312e-08, [['thumb1_r', 'hand_r', [-9.311381340026855, 0.5909335017204285, 23.329681396484375], [-9.326107025146484, -0.035463713109493256, 22.82261085510254], 0.3839723765850067, [['thumb2_r', 'thumb1_r', [-9.326107025146484, -0.035463713109493256, 22.82261085510254], [-9.369035720825195, -0.42945563793182373, 22.462879180908203], 0.3682214021682739, [['thumb3_r', 'thumb2_r', [-9.369035720825195, -0.42945563793182373, 22.462879180908203], [-9.414382934570312, -0.7093737125396729, 22.129837036132812], 0.3826146125793457, []]]]]], ['index1_r', 'hand_r', [-10.282362937927246, 0.31196972727775574, 22.517505645751953], [-10.756875991821289, 0.15218861401081085, 21.89153480529785], 2.181661605834961, [['index2_r', 'index1_r', [-10.756875991821289, 0.15218861401081085, 21.89153480529785], [-10.994132041931152, 0.07229805737733841, 21.578550338745117], 2.181661605834961, [['index3_r', 'index2_r', [-10.994132041931152, 0.07229805737733841, 21.578550338745117], [-11.231389045715332, -0.007592441514134407, 21.26556396484375], 2.181661605834961, []]]]]], ['middle1_r', 'hand_r', [-10.301751136779785, 0.7227813601493835, 22.51811981201172], [-10.869375228881836, 0.6472422480583191, 21.835247039794922], 2.356194496154785, [['middle2_r', 'middle1_r', [-10.869375228881836, 0.6472422480583191, 21.835247039794922], [-11.153186798095703, 0.6094726920127869, 21.493812561035156], 2.356194496154785, [['middle3_r', 'middle2_r', [-11.153186798095703, 0.6094726920127869, 21.493812561035156], [-11.43699836730957, 0.5717031955718994, 21.152376174926758], 2.356194496154785, []]]]]], ['ring1_r', 'hand_r', [-10.180306434631348, 1.152927041053772, 22.507169723510742], [-10.681703567504883, 1.177601933479309, 21.873558044433594], 2.478367567062378, [['ring2_r', 'ring1_r', [-10.681703567504883, 1.177601933479309, 21.873558044433594], [-10.932401657104492, 1.1899393796920776, 21.556751251220703], 2.478367567062378, [['ring3_r', 'ring2_r', [-10.932401657104492, 1.1899393796920776, 21.556751251220703], [-11.183098793029785, 1.2022769451141357, 21.239946365356445], 2.478365898132324, []]]]]], ['pinky1_r', 'hand_r', [-10.002487182617188, 1.5079398155212402, 22.53680992126465], [-10.392721176147461, 1.6533280611038208, 22.023563385009766], 2.57907772064209, [['pinky2_r', 'pinky1_r', [-10.392721176147461, 1.6533280611038208, 22.023563385009766], [-10.587837219238281, 1.7260223627090454, 21.766937255859375], 2.579075574874878, [['pinky3_r', 'pinky2_r', [-10.587837219238281, 1.7260223627090454, 21.766937255859375], [-10.782954216003418, 1.7987163066864014, 21.510313034057617], 2.579075813293457, []]]]]]]]]], ['upperArm_r', 'armControl_r', [-2.7051873207092285, 0.7438520789146423, 31.15633773803711], [-4.301773548126221, 1.016113042831421, 29.30825424194336], -7.450581307466564e-08, []], ['bicep_r', 'armControl_r', [-5.898355007171631, 1.2883739471435547, 27.46017074584961], [-4.301773548126221, 1.016113042831421, 29.30825424194336], -1.4901159417490817e-08, []]]]]]]]]], ['pelvis_l', 'spine1', [-0.0, 1.43027663230896, 20.829814910888672], [1.738000750541687, 1.3061156272888184, 21.816553115844727], 1.1920928955078125e-07, [['upperLegControl_l', 'pelvis_l', [1.738000750541687, 1.3061156272888184, 21.816553115844727], [1.5406512022018433, 1.49922513961792, 12.127400398254395], 5.529728341890916e-10, [['leg_l', 'upperLegControl_l', [1.5406512022018433, 1.49922513961792, 12.127400398254395], [1.3060163259506226, 2.6924078464508057, 1.67356538772583], -2.3283067140944524e-10, [['foot_l', 'leg_l', [1.3060163259506226, 2.6924078464508057, 1.67356538772583], [1.5047242641448975, -0.17014926671981812, 0.3285523056983948], 4.321347546465404e-07, [['foot_end_l', 'foot_l', [1.5047242641448975, -0.17014926671981812, 0.3285523056983948], [1.5530214309692383, -0.6672050356864929, 0.3285523056983948], 0.0, [['toeBig1_l', 'foot_end_l', [0.9427100419998169, 0.04511302709579468, 0.34542733430862427], [0.9557243585586548, -0.3566070795059204, 0.3539293706417084], -1.320361614227295, [['toeBig2_l', 'toeBig1_l', [0.9557243585586548, -0.3566070795059204, 0.3539293706417084], [0.9851030111312866, -0.8717422485351562, 0.22089330852031708], -0.4052252471446991, []]]], ['toeIndex1_l', 'foot_end_l', [1.3653970956802368, 0.0337616428732872, 0.3321574628353119], [1.4088189601898193, -0.4166068136692047, 0.3147052824497223], 0.46949681639671326, [['toeIndex2_l', 'toeIndex1_l', [1.4088189601898193, -0.4166068136692047, 0.3147052824497223], [1.4470016956329346, -0.7784968614578247, 0.16579969227313995], -0.5277117490768433, []]]], ['toeMiddle1_l', 'foot_end_l', [1.715632438659668, 0.1567046195268631, 0.2961054742336273], [1.7413853406906128, -0.3078361451625824, 0.2708389163017273], 1.327994465827942, [['toeMiddle2_l', 'toeMiddle1_l', [1.7413853406906128, -0.3078361451625824, 0.2708389163017273], [1.7757751941680908, -0.5985118746757507, 0.16194979846477509], -0.6065555810928345, []]]], ['toeRing1_l', 'foot_end_l', [1.9914512634277344, 0.3421282470226288, 0.263614296913147], [2.0230166912078857, -0.07829537987709045, 0.25555017590522766], 0.18290433287620544, [['toeRing2_l', 'toeRing1_l', [2.0230166912078857, -0.07829537987709045, 0.25555017590522766], [2.0597429275512695, -0.3707708716392517, 0.14917781949043274], -0.734302282333374, []]]], ['toePinky_l', 'foot_end_l', [2.2249598503112793, 0.5017977356910706, 0.2437739223241806], [2.2967140674591064, 0.13283739984035492, 0.18862110376358032], 1.000266194343567, []]]]]]]], ['thigh_l', 'upperLegControl_l', [1.6393259763717651, 1.4026703834533691, 16.97197723388672], [1.5406512022018433, 1.49922513961792, 12.127400398254395], -3.608875864813399e-09, []], ['upperThigh_l', 'upperLegControl_l', [1.738000750541687, 1.3061156272888184, 21.816553115844727], [1.6393259763717651, 1.4026703834533691, 16.97197723388672], -3.6379796952701327e-09, []], ['kneecap_l', 'upperLegControl_l', [1.5406512022018433, 1.49922513961792, 12.127400398254395], [1.5406512022018433, 0.4545128345489502, 12.127400398254395], 0.0, []]]], ['glute_l', 'pelvis_l', [1.738000750541687, 1.3061156272888184, 21.816553115844727], [1.738000750541687, 3.246829032897949, 21.816553115844727], -0.0, []], ['hip_l', 'pelvis_l', [1.2957403659820557, 0.47541436553001404, 23.142574310302734], [2.691704034805298, 0.47541433572769165, 23.156892776489258], 9.313225746154785e-10, []]]], ['pelvis_r', 'spine1', [0.0, 1.43027663230896, 20.829814910888672], [-1.738000750541687, 1.3061156272888184, 21.816553115844727], -1.1920928955078125e-07, [['upperLegControl_r', 'pelvis_r', [-1.738000750541687, 1.3061156272888184, 21.816553115844727], [-1.5527788400650024, 1.49922513961792, 12.127400398254395], 1.3678801424887865e-09, [['thigh_r', 'upperLegControl_r', [-1.6453897953033447, 1.4026703834533691, 16.97197723388672], [-1.5527788400650024, 1.49922513961792, 12.127400398254395], -9.313225746154785e-10, []], ['upperThigh_r', 'upperLegControl_r', [-1.738000750541687, 1.3061156272888184, 21.816553115844727], [-1.6453897953033447, 1.4026703834533691, 16.97197723388672], 1.426087803402254e-09, []], ['leg_r', 'upperLegControl_r', [-1.5527788400650024, 1.49922513961792, 12.127400398254395], [-1.3060163259506226, 2.6924078464508057, 1.67356538772583], 2.0954757928848267e-09, [['foot_r', 'leg_r', [-1.3060163259506226, 2.6924078464508057, 1.67356538772583], [-1.5047242641448975, -0.17014926671981812, 0.3285523056983948], -4.321347546465404e-07, [['foot_end_r', 'foot_r', [-1.5047242641448975, -0.17014926671981812, 0.3285523056983948], [-1.5530214309692383, -0.6672050356864929, 0.3285523056983948], 0.0, [['toeBig1_r', 'foot_end_r', [-0.9427100419998169, 0.04511302709579468, 0.34542733430862427], [-0.9557243585586548, -0.3566070795059204, 0.3539293706417084], 1.320361614227295, [['toeBig2_r', 'toeBig1_r', [-0.9557243585586548, -0.3566070795059204, 0.3539293706417084], [-0.9851030111312866, -0.8717422485351562, 0.22089330852031708], 0.4052252471446991, []]]], ['toeIndex1_r', 'foot_end_r', [-1.3653970956802368, 0.0337616428732872, 0.3321574628353119], [-1.4088189601898193, -0.4166068136692047, 0.3147052824497223], -0.46949681639671326, [['toeIndex2_r', 'toeIndex1_r', [-1.4088189601898193, -0.4166068136692047, 0.3147052824497223], [-1.4470016956329346, -0.7784968614578247, 0.16579969227313995], 0.5277117490768433, []]]], ['toeMiddle1_r', 'foot_end_r', [-1.715632438659668, 0.1567046195268631, 0.2961054742336273], [-1.7413853406906128, -0.3078361451625824, 0.2708389163017273], -1.327994465827942, [['toeMiddle2_r', 'toeMiddle1_r', [-1.7413853406906128, -0.3078361451625824, 0.2708389163017273], [-1.7757751941680908, -0.5985118746757507, 0.16194979846477509], 0.6065555810928345, []]]], ['toeRing1_r', 'foot_end_r', [-1.9914512634277344, 0.3421282470226288, 0.263614296913147], [-2.0230166912078857, -0.07829537987709045, 0.25555017590522766], -0.18290433287620544, [['toeRing2_r', 'toeRing1_r', [-2.0230166912078857, -0.07829537987709045, 0.25555017590522766], [-2.0597429275512695, -0.3707708716392517, 0.14917781949043274], 0.734302282333374, []]]], ['toePinky_r', 'foot_end_r', [-2.2249598503112793, 0.5017977356910706, 0.2437739223241806], [-2.2967140674591064, 0.13283739984035492, 0.18862110376358032], -1.000266194343567, []]]]]]]], ['kneecap_r', 'upperLegControl_r', [-1.5527788400650024, 1.49922513961792, 12.127400398254395], [-1.5406512022018433, 0.4545128345489502, 12.127400398254395], 0.0, []]]], ['glute_r', 'pelvis_r', [-1.738000750541687, 1.3061156272888184, 21.816553115844727], [-1.738000750541687, 3.246829032897949, 21.816553115844727], 0.0, []], ['hip_r', 'pelvis_r', [-1.2957403659820557, 0.47541436553001404, 23.142574310302734], [-2.691704034805298, 0.47541433572769165, 23.156892776489258], -9.313225746154785e-10, []]]]]] def searchBaseSkeletonBone(name,boneEntry): if boneEntry[0] == name: return boneEntry for childEntry in boneEntry[5]: r = searchBaseSkeletonBone(name,childEntry) if r != None: return r return None def FindBaseSkeletonBone(name): return searchBaseSkeletonBone(name,baseSkeleton) keepBones = [ 'pupil_r', 'pupil_l' ] allowedShapeKeys = mirrorAppend([ \ 'jawDown', 'platysma', 'centerLip', 'madEyebrow_r', 'sadEyebrow_r', 'madEye_r', 'lowerEyelid_r', 'upperEyelid_r', 'smile_r', 'upperLip_r', 'lowerLip_r', 'frown_r', 'pupilShrink', 'happyClosedEyelids', 'cheek_r', 'puckerLips', 'showBotTeeth', "lipsShut" ]) def resetShapeKeys( targetObj ): for key in targetObj.data.shape_keys.key_blocks: key.value = 0 def applyAllModifiers( targetObj ): for modifier in targetObj.modifiers: if modifier.name != 'Armature': bpy.ops.object.modifier_apply(apply_as='DATA', modifier=modifier.name) def select(obj): bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj def remove_shape_keys(): shape_key_count = len(get_active_block().items()) for i in range(0, shape_key_count): bpy.ops.object.shape_key_remove(all=False) def super_apply_modifiers(targetObj): shapeKeyMeshes = [] #skip "basis" because targetObj will include a basis for key in targetObj.data.shape_keys.key_blocks[1:]: print(key.name) shapeKeyMesh = clone(targetObj) select(shapeKeyMesh) resetShapeKeys(shapeKeyMesh) shapeKeyMesh.data.shape_keys.key_blocks[key.name].value = 1.0 targetObj.data.shape_keys.key_blocks[key.name].value = 0.0 shapeKeyMesh.name=key.name bpy.ops.object.convert(target='MESH') shapeKeyMeshes.append(shapeKeyMesh) select(targetObj) targetObj.shape_key_clear() applyAllModifiers(targetObj) for shapeKeyMesh in shapeKeyMeshes: shapeKeyMesh.select_set(True) targetObj.select_set(True) bpy.ops.object.join_shapes() for shapeKeyMesh in shapeKeyMeshes: select(shapeKeyMesh) bpy.ops.object.delete() def deleteCopy(copy): bpy.ops.object.delete() return None def clone(obj): newObj = obj.copy() newObj.data = obj.data.copy() newObj.animation_data_clear() bpy.context.collection.objects.link(newObj) return newObj def clearAllShapeKeyValues(meshClone): for key in meshClone.data.shape_keys.key_blocks[1:]: meshClone.data.shape_keys.key_blocks[key.name].value = 0.0 def addErrorIfShapeKeyExists(meshClone,baseName,name): try: k = meshClone.data.shape_keys.key_blocks[name] addError("[SHAPE KEY]\t Cannot automirror \""+baseName+"\" because \""+name+"\" already exists. Remove it to automirror") return True except: return False def lowerCaseFirstLetter(s): return s[:1].lower()+s[1:] def splitAndMirrorShapeKey(meshClone,shapeKey,rightMirrorGroup,leftMirrorGroup): global info bpy.ops.object.mode_set(mode = 'OBJECT') clearAllShapeKeyValues(meshClone) rightName = shapeKey.name+"_r" leftName = shapeKey.name +"_l" error = addErrorIfShapeKeyExists(meshClone,shapeKey.name,rightName) error = error or addErrorIfShapeKeyExists(meshClone,shapeKey.name,leftName) if error: return shapeKey.value = 1.0 shapeKey.vertex_group = "temp_MIRROR_BLEND_r" incomingName = "Key "+str(len(meshClone.data.shape_keys.key_blocks)) bpy.ops.object.shape_key_add(from_mix=True) meshClone.data.shape_keys.key_blocks[incomingName].name = rightName shapeKey.vertex_group = "temp_MIRROR_BLEND_l" incomingName = "Key "+str(len(meshClone.data.shape_keys.key_blocks)) bpy.ops.object.shape_key_add(from_mix=True) meshClone.data.shape_keys.key_blocks[incomingName].name = leftName info.append("[SHAPE KEY]\t Automirrored \""+shapeKey.name+"\"") def ratio(v,lo,hi): r = max(v-lo,lo)/(hi-lo) return min(1,r) def createMirrorVertexGroup(meshClone,rightSide): name = "temp_MIRROR_BLEND" if rightSide: name += "_r" else: name += "_l" group = meshClone.vertex_groups.new(name=name) meshClone.vertex_groups.active_index = group.index if not rightSide: for vertex in meshClone.data.vertices: group.add( [vertex.index], vertex.co.x*8+.5, 'ADD' ) else: for vertex in meshClone.data.vertices: group.add( [vertex.index], -vertex.co.x*8+.5, 'ADD' ) return group def writeShapeKeys(meshClone): global bytes global error global info #create x-axis smooth blend for mirroring blendShapes rightMirrorGroup = createMirrorVertexGroup(meshClone,True) leftMirrorGroup = createMirrorVertexGroup(meshClone,False) #find supported shape keys if any validShapeKeys = [] if meshClone.data.shape_keys != None: unmirroredShapeKeys = [ x.replace("_r","").replace("_l","") for x in allowedShapeKeys ] removeShapeKeys = [] #attempt to automirror compatible shapeKeys for shape_key in meshClone.data.shape_keys.key_blocks: if shape_key.name not in allowedShapeKeys: #mirror if not marked with a _side suffix but is supported if shape_key.name in unmirroredShapeKeys: splitAndMirrorShapeKey(meshClone,shape_key,rightMirrorGroup,leftMirrorGroup) meshClone.active_shape_key_index = meshClone.data.shape_keys.key_blocks.find(shape_key.name) bpy.ops.object.shape_key_remove() for shape_key in meshClone.data.shape_keys.key_blocks: if shape_key.name not in allowedShapeKeys: info.append("[SHAPE KEY]\t \""+shape_key.name+"\" not supported in-game") continue #check if being used inUse = False for i in range(0,len(shape_key.data)): shape_key_vertex = shape_key.data[i] ref_vertex = meshClone.data.vertices[i] delta = shape_key_vertex.co-ref_vertex.co if delta.x*delta.x+delta.y*delta.y+delta.z*delta.z != 0.0: inUse = True break if inUse: validShapeKeys.append(shape_key) else: info.append("[SHAPE KEY]\t Ignored unused \""+shape_key.name+"\"") clearAllShapeKeyValues(meshClone) #accumulate shape keys bytes += unsignedIntTo1Byte(len(validShapeKeys)) support = (float(len(validShapeKeys))/len(allowedShapeKeys))*100 info.append("[SHAPE KEY]\t Total: "+str(len(validShapeKeys))) for shape_key in validShapeKeys: nameInBytes = shape_key.name.encode() #default is utf-8 bytes += unsignedIntTo1Byte( len(nameInBytes) ) bytes += nameInBytes validDeltas = [] for i in range(0,len(shape_key.data)): shape_key_vertex = shape_key.data[i] ref_vertex = meshClone.data.vertices[i] delta = shape_key_vertex.co-ref_vertex.co if delta.x*delta.x+delta.y*delta.y+delta.z*delta.z != 0.0: validDeltas.append([ \ ref_vertex.index, \ delta.x, \ delta.y, \ delta.z \ ]) #accumulate shape key deltas bytes += unsignedIntTo2Bytes(len(validDeltas)) for deltaPos in validDeltas: bytes += unsignedIntTo2Bytes( deltaPos[0] ) bytes += floatTo4Bytes( deltaPos[1] ) bytes += floatTo4Bytes( deltaPos[2] ) bytes += floatTo4Bytes( deltaPos[3] ) #clearAllShapeKeyValues(meshClone) info.append("[SHAPE KEY]\t In-Game animation compatibility: "+str(int(support))+"%") def removeUnusedVertexGroups(meshClone,newBones): global info #remove vertex groups that aren't used or not part of the newBones vertexGroupEntries = [[x,0] for x in meshClone.vertex_groups] for vertex in meshClone.data.vertices: for g in vertex.groups: if g.weight > 0.0: vertexGroupEntries[g.group][1] += 1 #increment the count index #ensure vertex group exists for all keepBones so its not removed for bone in newBones: if bone.name in keepBones: #test has vertexGroup groupEntryIndex = -1 for i in range(0,len(vertexGroupEntries)): if vertexGroupEntries[i][0].name == bone.name: groupEntryIndex = i break if groupEntryIndex == -1: #add a vertex group since it wasnt found meshClone.vertex_groups.new(name=bone.name) else: #force override used by vertex so its not removed in next phase vertexGroupEntries[groupEntryIndex][1] = 1 #check if it has a bone for groupEntry in vertexGroupEntries: hasBone = False for bone in newBones: if groupEntry[0].name == bone.name: hasBone = True break if groupEntry[1]==0 or not hasBone: #if not used at least once by a vertex info.append("[VERTEX GROUPS]\t Removed unused \""+groupEntry[0].name+"\"") meshClone.vertex_groups.remove(groupEntry[0]) info.append("[VERTEX GROUPS]\t Total: "+str(len(meshClone.vertex_groups))) def isBonePartOfSkeleton(bone): parent = bone.parent while parent!=None: if FindBaseSkeletonBone(parent.name) != None: return True parent = parent.parent return False def getValidBones(meshClone): #get bones attached to base skeleton and ignore strays armature = None for modifier in meshClone.modifiers: if modifier.type == 'ARMATURE': if armature == None: armature = modifier #unhide it if not armature.object.visible_get(): armature.object.hide_set(False) else: addError("[ARMATURE]\t Multiple Armature modifiers found. Mesh canot have more than 1 armature modifier") #return blank if no armature found if armature == None: return [] armatureClone = clone(armature.object) armature.object = armatureClone select(armatureClone) bpy.ops.object.mode_set(mode = 'EDIT') #make all bones lowercase for bone in armatureClone.data.edit_bones: bone.name = lowerCaseFirstLetter(bone.name) #accumulate bones not part of the base skeleton newBones = [] for bone in armature.object.data.edit_bones: if not bone.use_deform: #bone must use deform info.append("[BONES]\t\t Ignoring \""+bone.name+"\" because it's set to not deform") continue newBones.append(bone) #ensure baseSkeleton bones are not changing hierarchy for bone in newBones: baseBone = FindBaseSkeletonBone(bone.name) if baseBone == None: continue #baseBone must match baseSkeleton hierarchy if bone.parent != None: if bone.parent.name != baseBone[1]: addError("[ARMATURE]\t Base skeleton bone \""+bone.name+"\" must have no parent or be parented to \""+baseBone[1]+"\"") elif baseBone[1] != None: for searchedBone in newBones: if searchedBone.name == baseBone[1]: addError("[ARMATURE]\t Base skeleton bone \""+bone.name+"\" must be parented to \""+searchedBone.name+"\"") break #filter out vertex groups that don't have a bone removeUnusedVertexGroups(meshClone,newBones) #remove bones without a vertex group newBoneUseCount = [0 for x in newBones] for i in range(len(newBones)-1,-1,-1): groupUsed = None bone = newBones[i] for group in meshClone.vertex_groups: if group.name == bone.name: groupUsed = group.index break if groupUsed == None: info.append("[BONES]\t\t Ignoring bone with missing vertex group \""+bone.name+"\"") newBones.remove(bone) #reorder newBones based on vertex group order finalNewBones = [] for group in meshClone.vertex_groups: for bone in newBones: if group.name == bone.name: finalNewBones.append(bone) break #delete armature clone bpy.ops.object.mode_set(mode = 'OBJECT') deleteCopy(armatureClone) return finalNewBones def writeBones(meshClone): global bytes global info newBones = getValidBones(meshClone) #accumulate bone table info.append("[BONES]\t\t Total: "+str(len(newBones))) bytes += unsignedIntTo1Byte( len(newBones) ) for bone in newBones: #check if should use base skeleton info enc_name = None try: enc_name = str.encode(bone.name) except: addError("[BONES]\t\t FATAL ERROR Could not serialize bone name! "+bone.name) enc_name = None bytes += unsignedIntTo1Byte(len(enc_name)) bytes += enc_name baseBone = FindBaseSkeletonBone(bone.name) if baseBone != None: info.append("[BONES]\t\t Serialized base skeleton bone \""+bone.name+"\"") bytes += floatTo4Bytes(baseBone[2][0]) bytes += floatTo4Bytes(baseBone[2][1]) bytes += floatTo4Bytes(baseBone[2][2]) bytes += floatTo4Bytes(baseBone[3][0]) bytes += floatTo4Bytes(baseBone[3][1]) bytes += floatTo4Bytes(baseBone[3][2]) bytes += floatTo4Bytes(baseBone[4]) #radians else: #use custom bone info info.append("[BONES]\t\t Serialized custom bone \""+bone.name+"\"") bytes += floatTo4Bytes(bone.head[0]) bytes += floatTo4Bytes(bone.head[1]) bytes += floatTo4Bytes(bone.head[2]) bytes += floatTo4Bytes(bone.tail[0]) bytes += floatTo4Bytes(bone.tail[1]) bytes += floatTo4Bytes(bone.tail[2]) bytes += floatTo4Bytes(bone.roll) #radians #accumulate bone hierarchy only with other valid bones inside newBones or skeletonBase for bone in newBones: validChildren = [] for child in bone.children: for i in range(0,len(newBones)): if newBones[i].name == child.name: validChildren.append(i) break bytes += unsignedIntTo1Byte(len(validChildren)) for childIndex in validChildren: bytes += unsignedIntTo1Byte(childIndex) def writeMaterialTable(meshClone): global bytes bytes += unsignedIntTo1Byte(len(meshClone.data.materials)) for material in meshClone.data.materials: enc_name = str.encode( lowerCaseFirstLetter(material.name)) bytes += unsignedIntTo1Byte( len(enc_name) ) bytes += enc_name def writeVertices(meshClone): global bytes #accumulate vertex info bytes += unsignedIntTo2Bytes( len(meshClone.data.vertices) ) lowestIndex = 65535 #largest theoretical index highestIndex = 0 for vertex in meshClone.data.vertices: #vertex.index is linear at this point lowestIndex = min( vertex.index, lowestIndex ) highestIndex = max( vertex.index, highestIndex ) bytes += floatTo4Bytes( vertex.co.x ) bytes += floatTo4Bytes( vertex.co.y ) bytes += floatTo4Bytes( vertex.co.z ) bytes += normalFloatTo1Byte( vertex.normal.x ) bytes += normalFloatTo1Byte( vertex.normal.y ) bytes += normalFloatTo1Byte( vertex.normal.z ) valid = 0 for group in vertex.groups: if group.weight > 0.0: valid += 1 bytes += unsignedIntTo1Byte( valid ) for group in vertex.groups: if group.weight > 0.0: bytes += unsignedIntTo1Byte( group.group ) bytes += unsignedNormalFloatTo1Byte( group.weight ) #8 bit precision info.append("[VERTICES]\t Total: "+str(len(meshClone.data.vertices))) #ensure vertex indices are linear info.append("[INDICES]\t Lo:"+str(lowestIndex)+" Hi:"+str(highestIndex)) if lowestIndex != 0 or highestIndex != len(meshClone.data.vertices)-1: addError("[INDICES]\t Vertex index order not linear! Sort mesh elements and try again.") def writeUVs(meshClone): global bytes if len(meshClone.data.materials) == 0: addError("[MATERIALS]\t Model has zero materials!") return #ensure model only has 1 uv map uvMap = meshClone.data.uv_layers.active if uvMap == None: addError("[UV]\t\t Model must have 1 active UV map. Add one.") else: #order material faces by materialIndex for Unity submeshes submeshes = [ [] for x in meshClone.data.materials ] for poly in meshClone.data.polygons: submeshes[ poly.material_index ].append(poly) #accumulate submeshes #we dont write length of submeshes because its the same length as the material table for submesh in submeshes: info.append("[SUBMESH] triangles: "+str(len(submesh))) bytes += unsignedIntTo2Bytes( len(submesh) ) for poly in submesh: if len(poly.vertices) != 3: addError("[FACES]\t Mesh must be triangulated. Found face with "+str(len(poly.vertices))+" vertices. All faces must have 3 vertices.") break #poly.loop_total will always be 3 if triangles are enforced index = 0 for loop_index in range( poly.loop_start, poly.loop_start+3 ): bytes += unsignedIntTo2Bytes( poly.vertices[index] ) index += 1 uv = meshClone.data.uv_layers.active.data[loop_index].uv bytes += floatTo4Bytes( uv.x ) bytes += floatTo4Bytes( uv.y ) def exportRaw(t): global errors global bytes global info #must have 1 object selected bytes = [] errors = [] bytes += floatTo4Bytes(VERSION) info = [] if t == None: ShowMessageBox("Cannot export None") return #safe copy and begin mesh export bpy.ops.object.mode_set(mode = 'OBJECT') select(t) meshClone = clone(t) #super_apply_modifiers(meshClone) writeBones(meshClone) #limit weights to 4 after the vertex group cleanup #bpy.ops.object.mode_set(mode = 'OBJECT') select(meshClone) bpy.ops.object.vertex_group_limit_total(group_select_mode='ALL', limit=4) writeMaterialTable(meshClone) writeVertices(meshClone) writeUVs(meshClone) writeShapeKeys(meshClone) #FINISHED bpy.ops.object.mode_set(mode = 'OBJECT') deleteCopy(meshClone) def ensureSelectedObject( type ): if len(bpy.context.selected_objects) != 1: ShowMessageBox("Select 1 target "+type+" object before running script") return None targetObj = bpy.context.selected_objects[0] if targetObj.type != type: ShowMessageBox("Target object must be of type "+type) return None return targetObj def executeExportVivaProjectHead(): #ensure running with sudo/admin for file writing try: admin = os.getuid() == 0 #Linux except AttributeError: admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 #Windows if not admin: ShowMessageBox("Script requires Blender to run in Administrator mode to generate files!") return #call export currentMS = lambda: int(round(time.time() * 1000)) startTime = currentMS() targetObj = ensureSelectedObject("MESH") if targetObj == None: return exportRaw( targetObj ) print(targetObj.type) global bytes global info #write log and create folder sep = '\\' if os.name != 'nt': sep = "/" folder = os.getcwd()+sep+"Viva Project Pre-Exports" filename = folder+sep+targetObj.name+" [log].txt" if not os.path.isdir(folder): os.mkdir(folder) print("Logging at "+folder) #write packed result file = open(filename, 'w') file.write("[Compiled \""+targetObj.name+"\" in "+str(currentMS()-startTime)+" ms]") #write errors if len(errors) > 0: file.write("\n\n***FOUND "+str(len(errors))+" ERROR(S)***") #write error file file.write("\nPlease fix the errors listed below and run the script again:\n") for i in range(0,len(errors)): file.write("#"+str(i+1)+" "+errors[i]+"\n") else: file.write("\n***SUCCEEDED!***") file.write("\n\nExported file size: "+str(float(len(bytes))/1000)+" KB") file.write("\nDrag and drop the generated .viva3d file into the game or have the game opened in model creation mode and it will automatically scan this file") file.write("\n\n\n[COMPILATION LOG]") for line in info: file.write("\n"+line) file.write("\n") file.close() if len(errors) > 0: try: subprocess.Popen(["notepad.exe",filename]) #windows only print("Opening "+filename) except: print("Couldn't open notepad") ShowMessageBox("Failed. See "+filename) else: try: subprocess.Popen(r'explorer '+folder,shell=True) except: print("Couldn't open explorer") #final bytes rawfilename = folder+sep+targetObj.name+".viva3d" file = open(rawfilename, "wb") file.write( bytearray( bytes ) ) file.close() ShowMessageBox("Success! See "+filename) def matchVivaProjectSkeleton(): targetArmature = ensureSelectedObject("ARMATURE") if targetArmature == None: return for bone in targetArmature.data.edit_bones: lowerName = lowerCaseFirstLetter(bone.name) if FindBaseSkeletonBone(lowerName) != None: bone.name = lowerName matched = 0 bpy.ops.object.mode_set(mode = 'EDIT') for bone in targetArmature.data.edit_bones: baseBone = FindBaseSkeletonBone( bone.name ) if baseBone == None: return bone.head = baseBone[2] bone.tail = baseBone[3] bone.roll = baseBone[4] matched += 1 #test if base bone is supposed to not have a parent for intendedParent in targetArmature.data.edit_bones: if intendedParent.name == baseBone[1]: bone.parent = intendedParent if baseBone[1] == None: bone.parent = None ShowMessageBox("Matched "+str(matched)+" base skeleton bones") def serializeBone(bone): subSkeleton = [] for child in bone.children: subSkeleton += [serializeBone(child)] head = [bone.head[0],bone.head[1],bone.head[2]] tail = [bone.tail[0],bone.tail[1],bone.tail[2]] #return [ bone.name, subSkeleton ] parent = None if bone.parent != None: parent = bone.parent.name return [ bone.name, parent, head, tail, bone.roll, subSkeleton ] #tooling def buildSkeletonInfo(): targetArmature = bpy.context.selected_objects[0] bpy.ops.object.mode_set(mode = 'EDIT') #find root spine1 = None for bone in targetArmature.data.edit_bones: if bone.name=="spine1": spine1 = bone break skeleton = [] skeleton += serializeBone(spine1) print(skeleton) #buildSkeletonInfo()