Created
November 2, 2019 18:38
-
-
Save dwilliamson/048bcdd4fb755b85299dd0042c93d51b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# <pep8-80 compliant> | |
bl_info = { | |
"name": "Star mesh format (.starmesh)", | |
"author": "Don Williamson", | |
"version": (0, 1), | |
"blender": (2, 6, 3), | |
"location": "File > Import-Export > Star Mesh (.starmesh) ", | |
"description": "Import-Export Star Mesh", | |
"warning": "", | |
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/" | |
"Scripts/Import-Export/Raw_Mesh_IO", | |
"tracker_url": "https://projects.blender.org/tracker/index.php?" | |
"func=detail&aid=25692", | |
"category": "Import-Export"} | |
import bpy | |
import os | |
import struct | |
import math | |
from mathutils import Vector, Matrix, Euler, Quaternion | |
def ChangeMatrixCoordSys(m): | |
# Swap Y/Z | |
s = Matrix.Scale(-1, 4, Vector((0, 0, 1))) | |
r = Matrix.Rotation(math.radians(-90), 4, "X") | |
m = r * m | |
return m | |
def ExportTransform(f, matrix): | |
# Decompose matrix | |
rot = matrix.to_quaternion() | |
loc = matrix.to_translation() | |
# Convert rotation amounts to right-hand | |
rot.x = -rot.x; | |
rot.y = -rot.y; | |
rot.z = -rot.z; | |
# Mirror rotation on z-axis | |
rot.z = -rot.z | |
rot.w = -rot.w | |
# Mirror position on z-axis | |
loc.z = -loc.z | |
f.write(struct.pack("4f", rot.x, rot.y, rot.z, rot.w)) | |
f.write(struct.pack("3f", loc.x, loc.y, loc.z)) | |
def GetObjectProxy(obj): | |
# Nasty backward mapping | |
for proxy in bpy.data.objects: | |
if proxy.proxy == obj: | |
return proxy | |
# No proxy, return the object itself | |
return obj | |
def GetArmature(obj): | |
# Search the modifier list | |
for m in obj.modifiers: | |
if m.type == "ARMATURE": | |
return m.object | |
return None | |
def GetBonesSorted(bones, bone, parent, matrix_local): | |
# Want transform from object space to bone space | |
m = ChangeMatrixCoordSys(matrix_local * bone.matrix_local) | |
# Add this bone description to the list | |
bone_desc = ( bone.name, bone.length, parent, m ) | |
index = len(bones) | |
bones += [ bone_desc ] | |
# Collate children | |
for c in bone.children: | |
GetBonesSorted(bones, c, index, matrix_local) | |
def GetBones(obj): | |
# An armature must be attached to the object | |
armature = GetArmature(obj) | |
if armature == None: | |
return [ ] | |
matrix_local = armature.matrix_local | |
armature = armature.data | |
# Collect in hierarchy order | |
bones = [ ] | |
root = armature.bones[0] | |
GetBonesSorted(bones, root, -1, matrix_local) | |
return bones | |
def GatherBoneWeights(obj, mesh, v, bones): | |
# Gather weights mapped to group index | |
weights = [ ] | |
for group in v.groups: | |
weights += [ (group.weight, group.group) ] | |
# Sort by weight and keep the weight limit to 4 per vertex | |
weights.sort(key = lambda weight: weight[0], reverse = True) | |
if len(weights) > 4: | |
weights = weights[0:4] | |
# Normalise and quantise the weighting to 8-bits | |
total = 0.0 | |
for weight in weights: | |
total += weight[0] | |
weights = [ (round(w[0] * 255 / total), w[1]) for w in weights ] | |
# Ensure weights sum to 255 by reducing weakest weight when above 255 and | |
# increasing strongest weight when below | |
while sum(w[0] for w in weights) > 255: | |
for i, w in reversed(list(enumerate(weights))): | |
if w[0] > 1: | |
weights[i] = (w[0] - 1, w[1]) | |
break | |
while sum(w[0] for w in weights) < 255: | |
weights[0] = (weights[0][0] + 1, weights[0][1]) | |
# Map from vertex group index to bone index | |
for i, w in list(enumerate(weights)): | |
group = obj.vertex_groups[w[1]] | |
bone_indices = [index for index, val in enumerate(bones) if val[0] == group.name] | |
weights[i] = (w[0], bone_indices[0]) | |
# Propagate the last bone with zero weith to ensure there are always 4 | |
# weights per vertex | |
last_bone_index = weights[len(weights) - 1][1] | |
while len(weights) < 4: | |
weights += [ (0, last_bone_index) ] | |
return weights | |
def ExportMesh(filename, obj, mesh): | |
with open(filename, "wb") as f: | |
bones = GetBones(obj) | |
# Pre-calculate the triangle count | |
nb_triangles = 0 | |
for p in mesh.polygons: | |
if len(p.vertices) == 4: | |
nb_triangles += 2 | |
else: | |
nb_triangles += 1 | |
# Write the header | |
f.write(struct.pack("B", len(bones))) | |
f.write(struct.pack("I", len(mesh.vertices))) | |
f.write(struct.pack("I", nb_triangles)) | |
# Write the skeleton used to weight the vertices | |
for bone in bones: | |
f.write(struct.pack("B", len(bone[0]))) | |
f.write(bytes(bone[0].encode("ascii"))) | |
f.write(struct.pack("f", bone[1])) | |
f.write(struct.pack("i", bone[2])) | |
ExportTransform(f, bone[3]) | |
# Write vertex positions and normals, swapping z/y axes | |
for v in mesh.vertices: | |
f.write(struct.pack("fff", v.co.x, v.co.z, v.co.y)) | |
for v in mesh.vertices: | |
f.write(struct.pack("fff", v.normal.x, v.normal.z, v.normal.y)) | |
# Write vertex bone weights | |
if len(bones): | |
for v in mesh.vertices: | |
weights = GatherBoneWeights(obj, mesh, v, bones) | |
f.write(struct.pack("BBBB", weights[0][0], weights[1][0], weights[2][0], weights[3][0])) | |
f.write(struct.pack("BBBB", weights[0][1], weights[1][1], weights[2][1], weights[3][1])) | |
# Write the triangulated polygons | |
for p in mesh.polygons: | |
if len(p.vertices) == 4: | |
t0 = (p.vertices[0], p.vertices[2], p.vertices[1]) | |
t1 = (p.vertices[0], p.vertices[3], p.vertices[2]) | |
f.write(struct.pack("III", *t0)) | |
f.write(struct.pack("III", *t1)) | |
else: | |
f.write(struct.pack("III", p.vertices[0], p.vertices[2], p.vertices[1])) | |
def ExportAnim(filename, obj): | |
# Calculate the frame step for export | |
target_fps = 30 | |
fps = bpy.context.scene.render.fps | |
frame_step = int(fps / target_fps) | |
print("STAR: Sourcefps/Targetfps/Step: " + str(fps) + "/" + str(target_fps) + "/" + str(frame_step)) | |
with open(filename, "wb") as f: | |
# Write the header | |
frame_start = bpy.context.scene.frame_start | |
frame_end = bpy.context.scene.frame_end | |
f.write(struct.pack("B", len(obj.pose.bones))) | |
f.write(struct.pack("I", int((frame_end - frame_start) / frame_step))) | |
# Loop over and activate all frames | |
for i in range(frame_start, frame_end, frame_step): | |
bpy.context.scene.frame_set(i) | |
# Export bone transforms in armature space | |
for bone in obj.pose.bones: | |
m = ChangeMatrixCoordSys(obj.matrix_local * bone.matrix) | |
ExportTransform(f, m) | |
def GetMeshExportPath(obj): | |
# Ensure there is a filename for the mesh | |
mesh = obj.data | |
filename = mesh.star_mesh_filename | |
if filename == None or filename == "": | |
filename = mesh.name | |
# Ensure it ends with the required extension | |
if not filename.endswith(".starmesh"): | |
filename += ".starmesh" | |
# Join the filename with the parent blender file | |
filepath = os.path.dirname(bpy.data.filepath) | |
if filepath == "": | |
print("ERROR: Couldn't export mesh has no blend file is loaded") | |
return None | |
filename = os.path.join(filepath, filename) | |
# Ensure the output directory exists | |
dirname = os.path.dirname(filename) | |
if not os.path.exists(dirname): | |
os.makedirs(dirname) | |
return filename | |
class StarMeshExporter(bpy.types.Operator): | |
bl_idname = "star_export.mesh" | |
bl_label = "Export Star Mesh" | |
def invoke(self, context, event): | |
scene = bpy.context.scene | |
# Walk over all mesh objects in the scene | |
for obj in scene.objects: | |
if obj.type != "MESH": | |
continue | |
# Generate an export mesh with modifiers applied if necessary and export | |
mesh = obj.to_mesh(scene, obj.data.star_mesh_apply_modifiers, "PREVIEW") | |
filename = GetMeshExportPath(obj) | |
ExportMesh(filename, obj, obj.data) | |
print("STAR: Finished export of " + filename) | |
return {'FINISHED'} | |
class StarAnimExporter(bpy.types.Operator): | |
bl_idname = "star_export.anim" | |
bl_label = "Export Star Animation" | |
def invoke(self, context, event): | |
# Is a mesh selected? | |
obj = bpy.context.active_object | |
if obj == None or obj.type != "MESH": | |
print("ERROR: Mesh not selected") | |
return {'FINISHED'} | |
# Get the armature and check for a proxy object | |
armature = GetArmature(obj) | |
if armature == None: | |
print("ERROR: No armature on the mesh") | |
return {'FINISHED'} | |
armature = GetObjectProxy(armature) | |
# Ensure there is an export path for the mesh | |
filename = GetMeshExportPath(obj) | |
if filename == None: | |
return {'FINISHED'} | |
# Append the name of the animation to the mesh for export | |
filename = filename.replace(".starmesh", "") | |
blenderfile = os.path.basename(bpy.data.filepath) | |
blenderfile = blenderfile.replace(".blend", "") | |
filename += "_" + blenderfile + ".staranim" | |
ExportAnim(filename, armature) | |
print("STAR: Finished export of " + filename) | |
return {'FINISHED'} | |
def menu_export(self, context): | |
self.layout.operator(StarMeshExporter.bl_idname, text="Star Mesh (.starmesh)") | |
class StarMeshExportPanel(bpy.types.Panel): | |
bl_label = "Star Mesh Export" | |
bl_space_type = "PROPERTIES" | |
bl_region_type = "WINDOW" | |
bl_context = "object" | |
def draw(self, context): | |
# Cast to mesh if possible | |
if not bpy.context.active_object: | |
return | |
obj = bpy.context.active_object | |
if obj.type != "MESH": | |
return | |
mesh = obj.data | |
# Layout the panel | |
layout = self.layout | |
row = layout.row() | |
col = row.column() | |
col.prop(mesh, "star_mesh_export") | |
col = row.column() | |
col.prop(mesh, "star_mesh_apply_modifiers") | |
row = layout.row() | |
row.prop(mesh, "star_mesh_filename") | |
row = layout.row() | |
row.operator("star_export.mesh") | |
row.operator("star_export.anim") | |
def register(): | |
# Add the start export properties to the Mesh type | |
bpy.types.Mesh.star_mesh_export = bpy.props.BoolProperty( | |
name="Export as Star Mesh", | |
description="Check to export mesh", | |
default=False) | |
bpy.types.Mesh.star_mesh_filename = bpy.props.StringProperty( | |
name="Export Filename", | |
description="Relative filename of the exported mesh", | |
subtype="FILE_PATH") | |
bpy.types.Mesh.star_mesh_apply_modifiers = bpy.props.BoolProperty( | |
name="Apply Modifiers", | |
description="Use transformed mesh data from each object", | |
default=True) | |
bpy.utils.register_module(__name__) | |
bpy.types.INFO_MT_file_export.append(menu_export) | |
def unregister(): | |
bpy.utils.unregister_module(__name__) | |
bpy.types.INFO_MT_file_export.remove(menu_export) | |
if __name__ == "__main__": | |
register() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment