| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- # Usage:
- #
- # - You should make a copy of your .blend file eg. 'my-scene-export.blend'!
- #
- # - The armature's pivot must be the same as the meshe's it is attached to.
- #
- # - The animation must have at least 2 keyframes and start at position 1.
- #
- # - UVs are stored per face in Blender. The GPmesh format expects to have _one_ UV per vertex.
- # Blender can easily have multiple UV coordinates assigned to the same vertex-index. Thus,
- # in order to get UVs exported correctly, vertices must be dublicated. This can be done
- # in the following way:
- # 1.) Go into edit mode and select all edges.
- # 2.) From the menu choose Edge -> Mark Sharp.
- # 3.) Switch into object mode and assign the 'Edge Split' modifier. Apply that. Done.
- #
- # - The engine of the book expects all polygons to be a triangle. So if your mesh has
- # n-gons, with n > 3, you have to apply the 'Triangulate' modifier or split your polygons manually.
- bl_info = {
- "name": "gpmesh Exporter",
- "blender": (3,00,0),
- "category": "Export",
- "author": "Michael Eggers",
- "description": "GPmesh exporter for the book Game Programming in C++"
- }
- import bpy
- import json
- def generate_gpmesh_json():
- mesh = bpy.context.active_object.data
- uv_layer = mesh.uv_layers.active.data
- gpmesh = {
- "version": 1,
- "vertexformat": "PosNormSkinTex",
- "shader": "Skinned",
- "textures": [],
- "specularPower": 100.0,
- "vertices": [],
- "indices": []
- }
- for vert in mesh.vertices:
- pos = vert.co
- normal = vert.normal
- gp_vert = []
- gp_vert.extend([pos.y, pos.x, pos.z])
- gp_vert.extend([normal.y, normal.x, normal.z])
- # get bone indices and their weights that affect this vertex and sort them by weight from high to low
- boneToWeightTuples = []
- for group in vert.groups:
- u8_weight = int(group.weight * 255)
- boneToWeightTuples.append((group.group, u8_weight))
- boneToWeightTuples.sort(key=lambda boneToWeight : boneToWeight[1], reverse=True)
-
- # Only keep first 4 bones with their weights. As we sorted them by their bone weight (from high to low)
- # before, we only keep the once with highest influence.
- boneToWeightTuples = boneToWeightTuples[:4]
-
- # The file format expects always 4 bones.
- while len(boneToWeightTuples) < 4:
- boneToWeightTuples.append((0, 0))
-
- boneIndices = []
- weights = []
- for boneIdx, weight in boneToWeightTuples:
- boneIndices.append(boneIdx)
- weights.append(weight)
- gp_vert.extend(boneIndices)
- gp_vert.extend(weights)
-
- gpmesh["vertices"].append(gp_vert)
-
- # UVs are stored separately, because even if multiple vertices share the same pos/normal/..
- # they can have easily have completely differen UVs!
- for l in mesh.loops:
- uv = uv_layer[l.index].uv
- if len(gpmesh["vertices"][l.vertex_index]) <= 14:
- gpmesh["vertices"][l.vertex_index].extend([uv.x, -uv.y])
- # print(vert_idx, loop_idx, uv)
-
- for poly in mesh.polygons:
- tri = []
- for loop_index in poly.loop_indices:
- vertIndex = mesh.loops[loop_index].vertex_index
- tri.append(vertIndex)
-
- gpmesh["indices"].append(tri)
-
- textures = []
- materialSlots = bpy.context.active_object.material_slots
- for matSlot in materialSlots:
- if matSlot.material:
- if matSlot.material.node_tree:
- for node in matSlot.material.node_tree.nodes:
- if node.type == 'TEX_IMAGE':
- textures.append("Assets/" + node.image.name)
- gpmesh["textures"] = textures
-
- return gpmesh
- def find_armature(active_object):
- armature = active_object
- while armature.parent and armature.type != 'ARMATURE':
- armature = armature.parent
-
- if armature.type == 'ARMATURE':
- return armature
- return None
- def generate_gpskel_json():
- gpskel = {
- "version": 1,
- "bonecount": 0,
- "bones": []
- }
- boneInfos = []
- armature = find_armature(bpy.context.active_object)
- if armature:
- armature = armature.data
- for i, bone in enumerate(armature.bones):
- parentBone = bone.parent
- parentIndex = -1
- if parentBone:
- parentIndex = armature.bones.find(parentBone.name)
-
- # local_matrix = (bone.matrix_local if bone.parent is None else bone.parent.matrix_local.inverted() * bone.matrix_local)
- local_matrix = bone.matrix_local
- if bone.parent:
- local_matrix = bone.parent.matrix_local.inverted() @ bone.matrix_local
- rot = local_matrix.to_quaternion().inverted()
- trans = local_matrix.to_translation()
-
- boneInfo = {
- "name": bone.name,
- "index": i,
- "parent": parentIndex,
- "bindpose": {
- "rot": [rot.y, rot.x, rot.z, rot.w],
- "trans": [trans.y, trans.x, trans.z]
- }
- }
- boneInfos.append(boneInfo)
-
- gpskel["bonecount"] = len(armature.bones)
- gpskel["bones"] = boneInfos
- return gpskel
- def local_matrices_for_frame(root, rootMat):
- local_matrices = []
- for child in root.children_recursive:
- if child.parent.name == root.name:
- localTransform = rootMat.inverted() @ child.matrix_basis
- local_matrices.append(localTransform)
- local_matrices.extend(local_matrices_for_frame(child, localTransform))
-
- return local_matrices
- def generate_gpanim_json(action):
- gpanim = {
- "version": 1,
- "sequence": {
- "frames": 0,
- "length": 1.0, # TODO: Calculate from framerate
- "bonecount": 0,
- "tracks": []
- }
- }
- active_object = bpy.context.active_object
- # active_object.animation_data.action = action
- armature = find_armature(active_object)
- armature.animation_data.action = action
- frame_start, frame_end = int(action.frame_range.x), int(action.frame_range.y)
- gpanim["sequence"]["frames"] = frame_end - 1 # TODO: Hacky (engine expects duplicate of first keyframe at the end but should not count as one)
- gpanim["sequence"]["bonecount"] = len(armature.data.bones)
- for i, bones in enumerate(armature.data.bones):
- gpanim["sequence"]["tracks"].append({ "bone": i, "transforms": [] })
- for frame in range(frame_start, frame_end):
- bpy.context.scene.frame_set(frame)
- localMat = armature.pose.bones[i].matrix
- if armature.pose.bones[i].parent:
- localMat = armature.pose.bones[i].parent.matrix.inverted() @ armature.pose.bones[i].matrix
- rot = localMat.to_quaternion().inverted()
- trans = localMat.to_translation()
- gpanim["sequence"]["tracks"][i]["transforms"].append({ "rot": [rot.y, rot.x, rot.z, rot.w], "trans": [trans.y, trans.x, trans.z] })
- # for frame in range(frame_start, frame_end):
- # bpy.context.scene.frame_set(frame)
- # rootBone = armature.pose.bones["Root"]
- # rootTransform = rootBone.matrix
- # pose = [ rootTransform ] # a pose contains all the bones transforms for this particular frame
- # # now add children to the pose
- # pose.extend(local_matrices_for_frame(rootBone, rootBone.matrix))
- # print(len(pose))
- # print(pose)
- # # add them to the tracks
- # for i, localMat in enumerate(pose):
- # rot = localMat.to_quaternion()
- # trans = localMat.to_translation()
- # gpanim["sequence"]["tracks"][i]["transforms"].append({ "rot": [rot.y, rot.x, rot.z, rot.w], "trans": [trans.y, trans.x, trans.z] })
-
- return gpanim
- def write_to_disk(context, filepath, export_gpmesh, export_gpskel, export_gpanim):
- print("exporting to gpmesh...")
- if export_gpmesh:
- gpmesh = generate_gpmesh_json()
- f = open(filepath, 'w', encoding='utf-8')
- # f.write("Hello World %s" % use_some_setting)
- f.write(json.dumps(gpmesh, sort_keys=False, indent=2))
- f.close()
-
- if export_gpskel:
- gpskel = generate_gpskel_json()
- gpskel_filepath = filepath.split(".")[0] + '.gpskel'
- f = open(gpskel_filepath, "w", encoding="utf-8")
- f.write(json.dumps(gpskel, sort_keys=False, indent=2))
- f.close()
-
- if export_gpanim:
- print("EXPORT GPANIM")
- actions = bpy.data.actions
- print(actions)
- for action in actions:
- gpanim = generate_gpanim_json(action)
- gpanim_filepath = filepath.split(".")[0] + str(action.name) + '.gpanim'
- f = open(gpanim_filepath, "w", encoding="utf-8")
- f.write(json.dumps(gpanim, sort_keys=False, indent=2))
- f.close()
-
- print("Done!")
- return {'FINISHED'}
- # ExportHelper is a helper class, defines filename and
- # invoke() function which calls the file selector.
- from bpy_extras.io_utils import ExportHelper
- from bpy.props import StringProperty, BoolProperty, EnumProperty
- from bpy.types import Operator
- class ExportGPMESH(Operator, ExportHelper):
- """Export mesh in gpmesh format."""
- bl_idname = "export.gpmesh" # important since its how bpy.ops.import_test.some_data is constructed
- bl_label = "Export as gpmesh"
- # ExportHelper mixin class uses this
- filename_ext = ".gpmesh"
- filter_glob: StringProperty(
- default="*.gpmesh",
- options={'HIDDEN'},
- maxlen=255, # Max internal buffer length, longer would be clamped.
- )
- # List of operator properties, the attributes will be assigned
- # to the class instance from the operator settings before calling.
- export_gpmesh: BoolProperty(
- name="Export gpmesh",
- description="Writes the mesh as .gpmesh to disk",
- default=True
- )
- export_gpskel: BoolProperty(
- name="Export gpskel",
- description="Writes .gpskel file to disk",
- default=True
- )
- export_gpanim: BoolProperty(
- name="Export gpanim",
- description="Writes .gpanim file to disk",
- default=True
- )
- def execute(self, context):
- return write_to_disk(context, self.filepath, self.export_gpmesh, self.export_gpskel, self.export_gpanim)
- # Only needed if you want to add into a dynamic menu
- def menu_func_export(self, context):
- self.layout.operator(ExportGPMESH.bl_idname, text="gpmesh")
- # Register and add to the "file selector" menu (required to use F3 search "Text Export Operator" for quick access)
- def register():
- bpy.utils.register_class(ExportGPMESH)
- bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
- def unregister():
- bpy.utils.unregister_class(ExportGPMESH)
- bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
- if __name__ == "__main__":
- register()
- # test call
- bpy.ops.export.gpmesh('INVOKE_DEFAULT')
|