Skip to content

Instantly share code, notes, and snippets.

@dwilliamson
Created November 2, 2019 18:38
Show Gist options
  • Save dwilliamson/048bcdd4fb755b85299dd0042c93d51b to your computer and use it in GitHub Desktop.
Save dwilliamson/048bcdd4fb755b85299dd0042c93d51b to your computer and use it in GitHub Desktop.
# <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