gpmesh_export_v2.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. # Usage:
  2. #
  3. # - You should make a copy of your .blend file eg. 'my-scene-export.blend'!
  4. #
  5. # - The armature's pivot must be the same as the meshe's it is attached to.
  6. #
  7. # - The animation must have at least 2 keyframes and start at position 1.
  8. #
  9. # - UVs are stored per face in Blender. The GPmesh format expects to have _one_ UV per vertex.
  10. # Blender can easily have multiple UV coordinates assigned to the same vertex-index. Thus,
  11. # in order to get UVs exported correctly, vertices must be dublicated. This can be done
  12. # in the following way:
  13. # 1.) Go into edit mode and select all edges.
  14. # 2.) From the menu choose Edge -> Mark Sharp.
  15. # 3.) Switch into object mode and assign the 'Edge Split' modifier. Apply that. Done.
  16. #
  17. # - The engine of the book expects all polygons to be a triangle. So if your mesh has
  18. # n-gons, with n > 3, you have to apply the 'Triangulate' modifier or split your polygons manually.
  19. bl_info = {
  20. "name": "gpmesh Exporter",
  21. "blender": (3,00,0),
  22. "category": "Export",
  23. "author": "Michael Eggers",
  24. "description": "GPmesh exporter for the book Game Programming in C++"
  25. }
  26. import bpy
  27. import json
  28. def generate_gpmesh_json():
  29. mesh = bpy.context.active_object.data
  30. uv_layer = mesh.uv_layers.active.data
  31. gpmesh = {
  32. "version": 1,
  33. "vertexformat": "PosNormSkinTex",
  34. "shader": "Skinned",
  35. "textures": [],
  36. "specularPower": 100.0,
  37. "vertices": [],
  38. "indices": []
  39. }
  40. for vert in mesh.vertices:
  41. pos = vert.co
  42. normal = vert.normal
  43. gp_vert = []
  44. gp_vert.extend([pos.y, pos.x, pos.z])
  45. gp_vert.extend([normal.y, normal.x, normal.z])
  46. # get bone indices and their weights that affect this vertex and sort them by weight from high to low
  47. boneToWeightTuples = []
  48. for group in vert.groups:
  49. u8_weight = int(group.weight * 255)
  50. boneToWeightTuples.append((group.group, u8_weight))
  51. boneToWeightTuples.sort(key=lambda boneToWeight : boneToWeight[1], reverse=True)
  52. # Only keep first 4 bones with their weights. As we sorted them by their bone weight (from high to low)
  53. # before, we only keep the once with highest influence.
  54. boneToWeightTuples = boneToWeightTuples[:4]
  55. # The file format expects always 4 bones.
  56. while len(boneToWeightTuples) < 4:
  57. boneToWeightTuples.append((0, 0))
  58. boneIndices = []
  59. weights = []
  60. for boneIdx, weight in boneToWeightTuples:
  61. boneIndices.append(boneIdx)
  62. weights.append(weight)
  63. gp_vert.extend(boneIndices)
  64. gp_vert.extend(weights)
  65. gpmesh["vertices"].append(gp_vert)
  66. # UVs are stored separately, because even if multiple vertices share the same pos/normal/..
  67. # they can have easily have completely differen UVs!
  68. for l in mesh.loops:
  69. uv = uv_layer[l.index].uv
  70. if len(gpmesh["vertices"][l.vertex_index]) <= 14:
  71. gpmesh["vertices"][l.vertex_index].extend([uv.x, -uv.y])
  72. # print(vert_idx, loop_idx, uv)
  73. for poly in mesh.polygons:
  74. tri = []
  75. for loop_index in poly.loop_indices:
  76. vertIndex = mesh.loops[loop_index].vertex_index
  77. tri.append(vertIndex)
  78. gpmesh["indices"].append(tri)
  79. textures = []
  80. materialSlots = bpy.context.active_object.material_slots
  81. for matSlot in materialSlots:
  82. if matSlot.material:
  83. if matSlot.material.node_tree:
  84. for node in matSlot.material.node_tree.nodes:
  85. if node.type == 'TEX_IMAGE':
  86. textures.append("Assets/" + node.image.name)
  87. gpmesh["textures"] = textures
  88. return gpmesh
  89. def find_armature(active_object):
  90. armature = active_object
  91. while armature.parent and armature.type != 'ARMATURE':
  92. armature = armature.parent
  93. if armature.type == 'ARMATURE':
  94. return armature
  95. return None
  96. def generate_gpskel_json():
  97. gpskel = {
  98. "version": 1,
  99. "bonecount": 0,
  100. "bones": []
  101. }
  102. boneInfos = []
  103. armature = find_armature(bpy.context.active_object)
  104. if armature:
  105. armature = armature.data
  106. for i, bone in enumerate(armature.bones):
  107. parentBone = bone.parent
  108. parentIndex = -1
  109. if parentBone:
  110. parentIndex = armature.bones.find(parentBone.name)
  111. # local_matrix = (bone.matrix_local if bone.parent is None else bone.parent.matrix_local.inverted() * bone.matrix_local)
  112. local_matrix = bone.matrix_local
  113. if bone.parent:
  114. local_matrix = bone.parent.matrix_local.inverted() @ bone.matrix_local
  115. rot = local_matrix.to_quaternion().inverted()
  116. trans = local_matrix.to_translation()
  117. boneInfo = {
  118. "name": bone.name,
  119. "index": i,
  120. "parent": parentIndex,
  121. "bindpose": {
  122. "rot": [rot.y, rot.x, rot.z, rot.w],
  123. "trans": [trans.y, trans.x, trans.z]
  124. }
  125. }
  126. boneInfos.append(boneInfo)
  127. gpskel["bonecount"] = len(armature.bones)
  128. gpskel["bones"] = boneInfos
  129. return gpskel
  130. def local_matrices_for_frame(root, rootMat):
  131. local_matrices = []
  132. for child in root.children_recursive:
  133. if child.parent.name == root.name:
  134. localTransform = rootMat.inverted() @ child.matrix_basis
  135. local_matrices.append(localTransform)
  136. local_matrices.extend(local_matrices_for_frame(child, localTransform))
  137. return local_matrices
  138. def generate_gpanim_json(action):
  139. gpanim = {
  140. "version": 1,
  141. "sequence": {
  142. "frames": 0,
  143. "length": 1.0, # TODO: Calculate from framerate
  144. "bonecount": 0,
  145. "tracks": []
  146. }
  147. }
  148. active_object = bpy.context.active_object
  149. # active_object.animation_data.action = action
  150. armature = find_armature(active_object)
  151. armature.animation_data.action = action
  152. frame_start, frame_end = int(action.frame_range.x), int(action.frame_range.y)
  153. gpanim["sequence"]["frames"] = frame_end - 1 # TODO: Hacky (engine expects duplicate of first keyframe at the end but should not count as one)
  154. gpanim["sequence"]["bonecount"] = len(armature.data.bones)
  155. for i, bones in enumerate(armature.data.bones):
  156. gpanim["sequence"]["tracks"].append({ "bone": i, "transforms": [] })
  157. for frame in range(frame_start, frame_end):
  158. bpy.context.scene.frame_set(frame)
  159. localMat = armature.pose.bones[i].matrix
  160. if armature.pose.bones[i].parent:
  161. localMat = armature.pose.bones[i].parent.matrix.inverted() @ armature.pose.bones[i].matrix
  162. rot = localMat.to_quaternion().inverted()
  163. trans = localMat.to_translation()
  164. gpanim["sequence"]["tracks"][i]["transforms"].append({ "rot": [rot.y, rot.x, rot.z, rot.w], "trans": [trans.y, trans.x, trans.z] })
  165. # for frame in range(frame_start, frame_end):
  166. # bpy.context.scene.frame_set(frame)
  167. # rootBone = armature.pose.bones["Root"]
  168. # rootTransform = rootBone.matrix
  169. # pose = [ rootTransform ] # a pose contains all the bones transforms for this particular frame
  170. # # now add children to the pose
  171. # pose.extend(local_matrices_for_frame(rootBone, rootBone.matrix))
  172. # print(len(pose))
  173. # print(pose)
  174. # # add them to the tracks
  175. # for i, localMat in enumerate(pose):
  176. # rot = localMat.to_quaternion()
  177. # trans = localMat.to_translation()
  178. # gpanim["sequence"]["tracks"][i]["transforms"].append({ "rot": [rot.y, rot.x, rot.z, rot.w], "trans": [trans.y, trans.x, trans.z] })
  179. return gpanim
  180. def write_to_disk(context, filepath, export_gpmesh, export_gpskel, export_gpanim):
  181. print("exporting to gpmesh...")
  182. if export_gpmesh:
  183. gpmesh = generate_gpmesh_json()
  184. f = open(filepath, 'w', encoding='utf-8')
  185. # f.write("Hello World %s" % use_some_setting)
  186. f.write(json.dumps(gpmesh, sort_keys=False, indent=2))
  187. f.close()
  188. if export_gpskel:
  189. gpskel = generate_gpskel_json()
  190. gpskel_filepath = filepath.split(".")[0] + '.gpskel'
  191. f = open(gpskel_filepath, "w", encoding="utf-8")
  192. f.write(json.dumps(gpskel, sort_keys=False, indent=2))
  193. f.close()
  194. if export_gpanim:
  195. print("EXPORT GPANIM")
  196. actions = bpy.data.actions
  197. print(actions)
  198. for action in actions:
  199. gpanim = generate_gpanim_json(action)
  200. gpanim_filepath = filepath.split(".")[0] + str(action.name) + '.gpanim'
  201. f = open(gpanim_filepath, "w", encoding="utf-8")
  202. f.write(json.dumps(gpanim, sort_keys=False, indent=2))
  203. f.close()
  204. print("Done!")
  205. return {'FINISHED'}
  206. # ExportHelper is a helper class, defines filename and
  207. # invoke() function which calls the file selector.
  208. from bpy_extras.io_utils import ExportHelper
  209. from bpy.props import StringProperty, BoolProperty, EnumProperty
  210. from bpy.types import Operator
  211. class ExportGPMESH(Operator, ExportHelper):
  212. """Export mesh in gpmesh format."""
  213. bl_idname = "export.gpmesh" # important since its how bpy.ops.import_test.some_data is constructed
  214. bl_label = "Export as gpmesh"
  215. # ExportHelper mixin class uses this
  216. filename_ext = ".gpmesh"
  217. filter_glob: StringProperty(
  218. default="*.gpmesh",
  219. options={'HIDDEN'},
  220. maxlen=255, # Max internal buffer length, longer would be clamped.
  221. )
  222. # List of operator properties, the attributes will be assigned
  223. # to the class instance from the operator settings before calling.
  224. export_gpmesh: BoolProperty(
  225. name="Export gpmesh",
  226. description="Writes the mesh as .gpmesh to disk",
  227. default=True
  228. )
  229. export_gpskel: BoolProperty(
  230. name="Export gpskel",
  231. description="Writes .gpskel file to disk",
  232. default=True
  233. )
  234. export_gpanim: BoolProperty(
  235. name="Export gpanim",
  236. description="Writes .gpanim file to disk",
  237. default=True
  238. )
  239. def execute(self, context):
  240. return write_to_disk(context, self.filepath, self.export_gpmesh, self.export_gpskel, self.export_gpanim)
  241. # Only needed if you want to add into a dynamic menu
  242. def menu_func_export(self, context):
  243. self.layout.operator(ExportGPMESH.bl_idname, text="gpmesh")
  244. # Register and add to the "file selector" menu (required to use F3 search "Text Export Operator" for quick access)
  245. def register():
  246. bpy.utils.register_class(ExportGPMESH)
  247. bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
  248. def unregister():
  249. bpy.utils.unregister_class(ExportGPMESH)
  250. bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
  251. if __name__ == "__main__":
  252. register()
  253. # test call
  254. bpy.ops.export.gpmesh('INVOKE_DEFAULT')