import bpy
from bpy.props import *
from mathutils import *
from pprint import pprint
from copy import copy
from itertools import islice
import math, struct, csv, sys
import os.path
""" FORMAT
BUFFER: (min size = 4)
- uint32 size
- uint8 data[size]
SMALLBUF: (min size = 1)
- uint8 size
- uint8 data[size]
PART: (min size = 20)
- uint8 flags
- uint8 blendmode
- uint32 voff
- uint32 vcount
- uint32 ioff
- uint32 icount
- uint8 texcount
- smallbuf shader
- smallbuf textures[texcount]
MESH:
- magic "SS3DMESH"
- uint32 flags
- float boundsmin[3]
- float boundsmax[3]
- buffer vdata
- buffer idata
- smallbuf format
- uint8 numparts
- part parts[numparts]
minimum size = 12+24+10 = 46
"""
bl_info = {
"name": "SS3DMESH Mesh Format (.ssm)",
"author": "Arvīds Kokins",
"version": (0, 5, 2),
"blender": (2, 6, 9),
"api": 38019,
"location": "File > Export > SS3DMESH (.ssm)",
"description": "SS3DMESH Mesh Export (.ssm)",
"warning": "",
"wiki_url": "http://cragegames.com",
"tracker_url": "http://cragegames.com",
"category": "Import-Export"
}
def write_smallbuf( f, bytebuf ):
if len( bytebuf ) > 255:
raise Exception( "smallbuf too big" )
if type( bytebuf ) == str:
bytebuf = bytes( bytebuf, "UTF-8" )
f.write( struct.pack( "B", len( bytebuf ) ) )
f.write( bytebuf )
#
def write_buffer( f, bytebuf ):
if len( bytebuf ) > 0xffffffff:
raise Exception( "buffer too big" )
if type( bytebuf ) == str:
bytebuf = bytes( bytebuf, "UTF-8" )
f.write( struct.pack( "L", len( bytebuf ) ) )
f.write( bytebuf )
#
def serialize_matrix( m ):
return struct.pack( "16f",
m[0][0], m[1][0], m[2][0], m[3][0],
m[0][1], m[1][1], m[2][1], m[3][1],
m[0][2], m[1][2], m[2][2], m[3][2],
m[0][3], m[1][3], m[2][3], m[3][3] )
def write_part( f, part ):
if len( part["textures"] ) > 8:
raise Exception( "too many textures (max. 8 allowed)" )
f.write( struct.pack( "=BB", part["flags"], part["blendmode"] ) )
write_smallbuf( f, part["name"] )
f.write( serialize_matrix( part[ "mtx" ] ) )
f.write( struct.pack( "=LLLLB", part["voff"], part["vcount"],
part["ioff"], part["icount"], len( part["textures"] ) ) )
write_smallbuf( f, bytes( part["shader"], "UTF-8" ) )
for tex in part["textures"]:
write_smallbuf( f, tex.replace("\\", "/") )
#
def write_mesh( f, meshdata, armdata, boneorder ):
is_skinned = armdata != None
bbmin = meshdata["bbmin"]
bbmax = meshdata["bbmax"]
vertices = meshdata["vertices"]
indices = meshdata["indices"]
format = meshdata["format"]
parts = meshdata["parts"]
if len( parts ) > 255:
raise Exception( "too many parts (max. 255 allowed)" )
is_i32 = len( vertices ) > 65535
print( "--- MESH STATS ---" )
print( "Vertex count: %d" % ( len(vertices) ) )
print( "Index count: %d" % ( len(indices) ) )
print( "Format string: " + format )
print( "Part count: %d" % ( len(parts) ) )
for part_id, part in enumerate( parts ):
print( "- part %d: name=%s voff=%d vcount=%d ioff=%d icount=%d flags=%d blendmode=%d texcount=%d shader='%s'" % (
part_id, part["name"], part["voff"], part["vcount"], part["ioff"], part["icount"],
part["flags"], part["blendmode"], len( part["textures"] ), part["shader"] ) )
f.write( bytes( "SS3DMESH", "UTF-8" ) )
# mesh data flags, 0x100 = extended mtl data, 0x200 = part name data
f.write( struct.pack( "L", 0x100 + 0x200 +
(1 if is_i32 else 0) * 0x01 + \
(1 if is_skinned else 0) * 0x80 ) )
f.write( struct.pack( "6f", bbmin.x, bbmin.y, bbmin.z, bbmax.x, bbmax.y, bbmax.z ) )
vdata = bytes()
for vertex in vertices:
vdata += vertex
write_buffer( f, vdata )
idata = bytes()
if( is_i32 ):
for index in indices:
idata += struct.pack( "L", index )
else:
for index in indices:
idata += struct.pack( "H", index )
write_buffer( f, idata )
write_smallbuf( f, format )
f.write( struct.pack( "B", len(parts) ) )
for part in parts:
write_part( f, part )
if is_skinned:
f.write( struct.pack( "B", len(boneorder) ) )
for bonename in boneorder:
bone = armdata.bones[ bonename ]
print( "Bone found: " + bone.name )
write_smallbuf( f, bone.name )
pid = 255
m = bone.matrix_local
if bone.parent is not None:
m = bone.parent.matrix_local.inverted() * m
for bpid, pbone in enumerate(boneorder):
if bone.parent.name == pbone:
pid = bpid
break
#
# print(m)
f.write( struct.pack( "B", pid ) )
f.write( serialize_matrix( m ) )
#
#
return
#
def write_anims( f, anims ):
print( "--- ANIMATIONS' STATS ---" )
print( "Count: %d" % len( anims ) )
f.write( bytes( "SS3DANIM", "UTF-8" ) )
f.write( struct.pack( "L", len( anims ) ) )
i = 0
for anim in anims:
i += 1
print( "Animation #%d: %s" % ( i, anim["name"] ) )
a_name = anim["name"]
a_frames = anim["frames"]
a_tracks = anim["tracks"]
a_markers = anim["markers"]
a_speed = anim["speed"]
a_name_bytes = bytes( a_name, "UTF-8" )
animbuf = struct.pack( "B", len(a_name_bytes) ) + a_name_bytes
animbuf += struct.pack( "=LfBB", a_frames, a_speed, len( a_tracks ), len( a_markers ) )
for track_name, track_matrices in a_tracks.items():
track_name_bytes = bytes( track_name, "UTF-8" )
trackbuf = struct.pack( "B", len(track_name_bytes) ) + track_name_bytes
for fid in range(a_frames):
mtx = track_matrices[ fid ]
# decompose
pos = mtx.to_translation()
rot = mtx.to_quaternion()
scl = mtx.to_scale()
# write
trackbuf += struct.pack( "10f",
pos.x, pos.y, pos.z,
rot.x, rot.y, rot.z, rot.w,
scl.x, scl.y, scl.z
)
#
animbuf += struct.pack( "L", len(trackbuf) ) + trackbuf
#
for marker in a_markers:
animbuf += struct.pack( "=16sL", bytes( marker["name"], "UTF-8" ), marker["frame"] )
write_buffer( f, animbuf )
#
#
def serialize_vec3_array( arr ):
out = bytes()
for item in arr:
out += struct.pack( "fff", item.x, item.y, item.z )
return out
def serialize_quat_array( arr ):
out = bytes()
for item in arr:
out += struct.pack( "ffff", item.x, item.y, item.z, item.w )
return out
def wrap_small_string( strdata ):
data = bytes( strdata, "UTF-8" )
return struct.pack( "B", len(data) ) + data
def wrap_chunk( name, data ):
return bytes( name, "UTF-8" ) + struct.pack( "L", len( data ) ) + data
def reduce_array( arr ):
if len(arr) > 1:
diff = False
for item in arr:
if item != arr[0]:
diff = True
break
if diff == False:
arr = arr[:1]
return arr
def write_anims_anbd( f, anims ):
print( "--- ANIMATIONS' STATS ---" )
print( "Count: %d" % len( anims ) )
anbd_chunk = bytes()
i = 0
for anim in anims:
i += 1
print( "Animation #%d: %s" % ( i, anim["name"] ) )
a_name = anim["name"]
a_frames = anim["frames"]
a_tracks = anim["tracks"]
a_markers = anim["markers"]
a_speed = anim["speed"]
anim_chunk = struct.pack( "fH", a_speed, a_frames )
# parse animation
float_data = bytes()
track_data = bytes()
track_count = 0
for track_name, track_matrices in a_tracks.items():
track_offset = len( float_data ) // 4
pos_arr = []
rot_arr = []
scl_arr = []
# decompose animation matrices into position/rotation/scale
for fid in range(a_frames):
mtx = track_matrices[ fid ]
pos = mtx.to_translation()
rot = mtx.to_quaternion()
scl = mtx.to_scale()
pos_arr.append( pos )
rot_arr.append( rot )
scl_arr.append( scl )
#
# see if each list is constant and reduce it if so
pos_arr = reduce_array( pos_arr )
rot_arr = reduce_array( rot_arr )
scl_arr = reduce_array( scl_arr )
track_data += wrap_small_string( track_name )
track_data += struct.pack( "LHHHH", track_offset, len(pos_arr), len(rot_arr), len(scl_arr), 0 )
track_count += 1
float_data += serialize_vec3_array( pos_arr )
float_data += serialize_quat_array( rot_arr )
float_data += serialize_vec3_array( scl_arr )
#
# floats
anim_chunk += struct.pack( "L", len( float_data ) // 4 ) + float_data
# tracks
anim_chunk += struct.pack( "L", track_count ) + track_data
# markers
anim_chunk += struct.pack( "L", len( a_markers ) )
for marker in a_markers:
anim_chunk += struct.pack( "16sH", bytes( marker["name"], "UTF-8" ), marker["frame"] )
anbd_chunk += wrap_chunk( "ANIM", anim_chunk ) + wrap_small_string( a_name )
#
anbd_chunk = wrap_chunk( "SGRXANBD", anbd_chunk )
f.write( anbd_chunk )
#
def addCached( olist, o, minpos = 0 ):
try:
return olist[ minpos: ].index( o )
except:
olist.append( o )
return len( olist ) - 1 - minpos
#
def find_in_userdata( obj, key, default = None ):
for prop in obj.items():
if type( prop[ 1 ] ) in ( int, str, float, bool ):
if prop[ 0 ] == key:
return prop[ 1 ]
return default
#
def parse_materials( geom_node, textures ):
materials = []
print( "Parsing materials... ", end="" )
for mtl in geom_node.data.materials:
outmtl = { "textures": [], "shader": "default", "flags": 0, "blendmode": 0 }
for tex in mtl.texture_slots:
outmtl["textures"].append( textures[ tex.name ] if tex != None else "" )
while len(outmtl["textures"]) and outmtl["textures"][-1] == "":
outmtl["textures"].pop()
shdr = find_in_userdata( mtl, "shader" )
if type( shdr ) == str:
outmtl["shader"] = shdr
bmode = find_in_userdata( mtl, "blendmode" )
if type( bmode ) == str:
if bmode == "none":
outmtl["blendmode"] = 0
if bmode == "basic":
outmtl["blendmode"] = 1
if bmode == "additive":
outmtl["blendmode"] = 2
if bmode == "multiply":
outmtl["blendmode"] = 3
if find_in_userdata( mtl, "unlit", False ):
outmtl["flags"] |= 1
if find_in_userdata( mtl, "nocull", False ):
outmtl["flags"] |= 2
materials.append( outmtl )
print( "OK!" )
return materials
def parse_geometry( geom_node, textures, opt_boneorder, props ):
if props.apply_modifiers == "NONE":
MESH = geom_node.to_mesh( bpy.context.scene, False, "PREVIEW" )
else:
preview_settings = []
if props.apply_modifiers == "SKIPARM":
for mod in geom_node.modifiers:
preview_settings.append( mod.show_viewport )
if mod.type == "ARMATURE":
mod.show_viewport = False
#
MESH = geom_node.to_mesh( bpy.context.scene, True, "PREVIEW" )
if props.apply_modifiers == "SKIPARM":
for i, mod in enumerate( geom_node.modifiers ):
mod.show_viewport = preview_settings[ i ]
#
#
opt_vgroups = geom_node.vertex_groups if len(geom_node.vertex_groups) else None
materials = parse_materials( geom_node, textures )
print( "Generating geometry for %s... " % geom_node.name, end="" )
MESH.calc_normals_split()
# SORT BY MATERIAL
MID2FACES = {} # material index -> faces
FACE2MID = {} # face -> material index
for face in MESH.polygons:
if face.material_index not in MID2FACES:
MID2FACES[ face.material_index ] = []
MID2FACES[ face.material_index ].append( face )
# GENERATE COMPACT DATA
Plist = []
Nlist = []
Tlists = [ [] for tl in MESH.uv_layers ]
Clists = [ [] for cl in MESH.vertex_colors ]
genParts = [] # array of Part ( array of Face ( array of Vertex ( position index, normal index, texcoord indices, color indices ) ) )
foundMIDs = []
if len( Tlists ) > 2:
print( "Too many UV layers" )
Tlists = Tlists[:1]
if len( Clists ) > 1:
print( "Too many color layers" )
Clists = Clists[:1]
for flist_id in MID2FACES:
genPart = []
flist = MID2FACES[ flist_id ]
m_id = 0
for face in flist:
m_id = face.material_index
if flist_id != m_id:
continue
genFace = []
for vid in range( len( face.vertices ) ):
v_id = face.vertices[ vid ]
l_id = face.loop_start + vid
VTX = MESH.vertices[ v_id ]
pos_id = addCached( Plist, VTX.co )
if face.use_smooth != False:
nrm_id = addCached( Nlist, MESH.loops[ l_id ].normal )
else:
nrm_id = addCached( Nlist, face.normal )
genVertex = [ pos_id, nrm_id ]
for si in range( len( Tlists ) ):
txc_id = addCached( Tlists[ si ], MESH.uv_layers[ si ].data[ l_id ].uv )
genVertex.append( txc_id )
for si in range( len( Clists ) ):
col_id = addCached( Clists[ si ], MESH.vertex_colors[ si ].data[ l_id ].color )
genVertex.append( col_id )
if opt_vgroups != None and opt_boneorder != None:
groupweights = []
for vg in VTX.groups:
for grp in opt_vgroups:
if vg.group == grp.index:
groupweights.append([ opt_boneorder.index( grp.name ), vg.weight ])
break
#
#
#
# sort by importance
groupweights.sort( key=lambda x: x[1], reverse = True )
# trim useless
for gwoff, gw in enumerate(groupweights):
if gw[1] < 1.0/256.0:
del groupweights[ gwoff: ]
break
# check if more than 4, warn / trim if so
if len(groupweights) > 4:
print( "Too many weights (%d > 4) for vertex %d at %s" % ( len(groupweights), v_id, VTX.co ) )
del groupweights[ 4: ]
if len(groupweights) == 0:
groupweights.append([ 0, 1.0 ])
# renormalize
wsum = 0.0
for gw in groupweights:
wsum += gw[1]
wsum /= 255.0
for gw in groupweights:
gw[1] = round( gw[1] / wsum )
# finish quantization to bytes
wsum = 0.0
for gw in groupweights:
wsum += gw[1]
gwoff = 0
while wsum != 255:
sgnadd = -1 if wsum > 255 else 1
groupweights[ gwoff ][1] += sgnadd
wsum += sgnadd
# compress
while len(groupweights) < 4:
groupweights.append([ 0, 0 ])
gw_groups = struct.pack( "4B", groupweights[0][0], groupweights[1][0], groupweights[2][0], groupweights[3][0] )
gw_weights = struct.pack( "4B", groupweights[0][1], groupweights[1][1], groupweights[2][1], groupweights[3][1] )
genVertex.append( gw_groups )
genVertex.append( gw_weights )
#
genFace.append( genVertex )
genPart.append( genFace )
genParts.append( genPart )
foundMIDs.append( m_id )
#
# VALIDATION
if len( Plist ) <= 0:
raise Exception( "Mesh has no vertices!" )
#
# CONVERT TO VERTEX BUFFER FORMAT
vertices = []
indices = []
parts = []
defmtl = { "textures": [], "shader": "default", "flags": 0, "blendmode": 0 }
mtl_num = -1
for part in genParts:
mtl_num += 1
mtl_id = foundMIDs[ mtl_num ]
vroot = len(vertices)
outpart = {
"name": geom_node.name + "#" + str(mtl_num),
"mtx": geom_node.matrix_world,
"voff": len(vertices),
"vcount": 0,
"ioff": len(indices),
"icount": 0,
"flags": materials[ mtl_id ]["flags"] if mtl_id in materials else defmtl["flags"],
"blendmode": materials[ mtl_id ]["blendmode"] if mtl_id in materials else defmtl["blendmode"],
"shader": materials[ mtl_id ]["shader"] if mtl_id in materials else defmtl["shader"],
"textures": materials[ mtl_id ]["textures"] if mtl_id in materials else defmtl["textures"],
}
for face in part:
tmpidcs = []
for vertex in face:
P = Plist[ vertex[0] ]
N = Nlist[ vertex[1] ]
vertexdata = struct.pack( "3f3f", P.x, P.y, P.z, N.x, N.y, N.z )
vip = 2
for si in range( len( Tlists ) ):
T = Tlists[ si ][ vertex[ vip ] ]
vip += 1
vertexdata += struct.pack( "2f", T.x, 1 - T.y )
for si in range( len( Clists ) ):
C = Clists[ si ][ vertex[ vip ] ]
vip += 1
vertexdata += struct.pack( "4B", int(C.r * 255), int(C.g * 255), int(C.b * 255), 255 )
if opt_vgroups != None and opt_boneorder != None:
vertexdata += vertex[ vip ] # indices (groups)
vip += 1
vertexdata += vertex[ vip ] # weights
vip += 1
tmpidcs.append( addCached( vertices, vertexdata, vroot ) )
#
for i in range( 2, len( tmpidcs ) ):
indices.append( tmpidcs[ 0 ] )
indices.append( tmpidcs[ i ] )
indices.append( tmpidcs[ i - 1 ] )
#
#
outpart["vcount"] = len(vertices) - outpart["voff"]
outpart["icount"] = len(indices) - outpart["ioff"]
parts.append( outpart )
#
# TANGENT SPACE CALC
if len( Tlists ) > 0:
tan1list = [ Vector([0,0,0]) for i in range(len(vertices)) ]
tan2list = [ Vector([0,0,0]) for i in range(len(vertices)) ]
hitlist = [ 0 for i in range(len(vertices)) ]
for part in parts:
voff = part["voff"]
ioff = part["ioff"]
for i in range( 0, part["icount"], 3 ):
i1 = indices[ ioff + i + 0 ] + voff
i2 = indices[ ioff + i + 1 ] + voff
i3 = indices[ ioff + i + 2 ] + voff
Pdc1 = struct.unpack( "3f", vertices[ i1 ][ :12 ] )
Pdc2 = struct.unpack( "3f", vertices[ i2 ][ :12 ] )
Pdc3 = struct.unpack( "3f", vertices[ i3 ][ :12 ] )
v1 = Vector([ Pdc1[0], Pdc1[1], Pdc1[2] ])
v2 = Vector([ Pdc2[0], Pdc2[1], Pdc2[2] ])
v3 = Vector([ Pdc3[0], Pdc3[1], Pdc3[2] ])
Tdc1 = struct.unpack( "2f", vertices[ i1 ][ 24:32 ] )
Tdc2 = struct.unpack( "2f", vertices[ i2 ][ 24:32 ] )
Tdc3 = struct.unpack( "2f", vertices[ i3 ][ 24:32 ] )
w1 = Vector([ Tdc1[0], Tdc1[1], 0 ])
w2 = Vector([ Tdc2[0], Tdc2[1], 0 ])
w3 = Vector([ Tdc3[0], Tdc3[1], 0 ])
x1 = v2.x - v1.x;
x2 = v3.x - v1.x;
y1 = v2.y - v1.y;
y2 = v3.y - v1.y;
z1 = v2.z - v1.z;
z2 = v3.z - v1.z;
s1 = w2.x - w1.x;
s2 = w3.x - w1.x;
t1 = w2.y - w1.y;
t2 = w3.y - w1.y;
ir = s1 * t2 - s2 * t1
if True: # abs( ir ) > 0.000001:
r = abs( ir ) # 1.0 / ir
sdir = Vector([(t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r])
tdir = Vector([(s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r])
tan1list[ i1 ] += sdir
tan1list[ i2 ] += sdir
tan1list[ i3 ] += sdir
tan2list[ i1 ] += tdir
tan2list[ i2 ] += tdir
tan2list[ i3 ] += tdir
hitlist[ i1 ] += 1
hitlist[ i2 ] += 1
hitlist[ i3 ] += 1
else:
print( "Bad triangle UV map!!! invR: %f | UV0: %f;%f | UV1: %f;%f | UV2: %f;%f" % \
(ir, Tdc1[0],Tdc1[1],Tdc2[0],Tdc2[1],Tdc3[0],Tdc3[1]))
#
#
#
for v_id, vertex in enumerate( vertices ):
Ndc = struct.unpack( "3f", vertex[ 12:24 ] )
n = Vector([ Ndc[0], Ndc[1], Ndc[2] ])
t = tan1list[ v_id ]
t2 = tan2list[ v_id ]
outtan = ( t - n * n.dot( t ) ).normalized()
if outtan == Vector([0.0,0.0,0.0]):
outtan = Vector([0.0,0.0,1.0])
print( "Tangent was detected to be 0,0,0 on vertex %d - changed to 0,0,1" % v_id )
Tdc = struct.unpack( "2f", vertex[ 24:32 ] )
print( "HC: %d, Nrm: %s, Tx1: %s, Tg2: %s" % ( hitlist[ v_id ], n, Tdc, t2 ) )
sign = -1.0 if n.cross( t ).dot( t2 ) < 0.0 else 1.0
vertices[ v_id ] = vertex[ :24 ] + struct.pack( "4f", outtan.x, outtan.y, outtan.z, sign ) + vertex[ 24: ]
#
#
# AABB
bbmin = Plist[0].copy()
bbmax = Plist[0].copy()
for pos in Plist:
if bbmin.x > pos.x:
bbmin.x = pos.x
if bbmin.y > pos.y:
bbmin.y = pos.y
if bbmin.z > pos.z:
bbmin.z = pos.z
if bbmax.x < pos.x:
bbmax.x = pos.x
if bbmax.y < pos.y:
bbmax.y = pos.y
if bbmax.z < pos.z:
bbmax.z = pos.z
#
# FORMAT STRING
format = "pf3nf3"
if len( Tlists ) > 0:
format += "tf4"
for si in range( len( Tlists ) ):
format += "%df2" % ( si )
for si in range( len( Clists ) ): # only one expected
format += "cb4"
if opt_vgroups != None and opt_boneorder != None:
format += "ib4wb4"
#
print( "OK!" )
if MESH is not geom_node.data:
bpy.data.meshes.remove( MESH )
return {
"bbmin": bbmin, "bbmax": bbmax,
"vertices": vertices, "indices": indices,
"format": format, "parts": parts,
}
#
def gen_empty_mesh_data():
return {
"bbmin": Vector([ sys.float_info.max, sys.float_info.max, sys.float_info.max ]),
"bbmax": Vector([ -sys.float_info.max, -sys.float_info.max, -sys.float_info.max ]),
"vertices": [], "indices": [], "format": "pf3nf3", "parts": [],
}
#
def mesh_data_add( meshdata, ndata ):
# AABB
if meshdata["bbmin"].x > ndata["bbmin"].x:
meshdata["bbmin"].x = ndata["bbmin"].x
if meshdata["bbmin"].y > ndata["bbmin"].y:
meshdata["bbmin"].y = ndata["bbmin"].y
if meshdata["bbmin"].z > ndata["bbmin"].z:
meshdata["bbmin"].z = ndata["bbmin"].z
if meshdata["bbmax"].x < ndata["bbmax"].x:
meshdata["bbmax"].x = ndata["bbmax"].x
if meshdata["bbmax"].y < ndata["bbmax"].y:
meshdata["bbmax"].y = ndata["bbmax"].y
if meshdata["bbmax"].z < ndata["bbmax"].z:
meshdata["bbmax"].z = ndata["bbmax"].z
# parts
for part in ndata["parts"]:
part = copy(part)
part["voff"] += len(meshdata["vertices"])
part["ioff"] += len(meshdata["indices"])
meshdata["parts"].append( part )
# format diff
format_part_order = [ "pf3", "nf3", "tf4", "0f2", "1f2",
"3f2", "4f2", "cb4", "ib4", "wb4", "DUMMY",
]
# - split into chunks
oldfmt_A = meshdata["format"]
oldfmt_B = ndata["format"]
fmt_A = [oldfmt_A[i:i+3] for i in range(0, len(oldfmt_A), 3)] + [ "DUMMY" ]
fmt_B = [oldfmt_B[i:i+3] for i in range(0, len(oldfmt_B), 3)] + [ "DUMMY" ]
# - iterate through
i = 0
while i < len(fmt_A) or i < len(fmt_B):
if fmt_A[ i ] == fmt_B[ i ]:
i += 1
continue
# - formats not equal, need to pad one side
# - pad the side whose index is higher
if format_part_order.index( fmt_A[ i ] ) > format_part_order.index( fmt_B[ i ] ):
fmt_tgt = fmt_A
fmt_src = fmt_B
data_tgt = meshdata["vertices"]
else:
fmt_src = fmt_A
fmt_tgt = fmt_B
data_tgt = ndata["vertices"]
# - pad the format
curfmt = fmt_src[ i ]
fmt_tgt.insert( i, curfmt )
# - calculate padding offset
pad_offset = 0
for f in islice( fmt_src, i ):
nominal = 4 if f[1] == "f" else 1
multiplier = int(f[2])
pad_offset += nominal * multiplier
# - calculate padding data
if curfmt == "pf3" or curfmt == "nf3":
raise Exception( "UNEXPECTED PADDING FORMAT" )
pad_core = struct.pack( "f", 0.0 ) if curfmt[1] == "f" else struct.pack( "B", 0 )
pad_mult = int(curfmt[2])
pad_data = pad_core * pad_mult
# print( "pad with %d bytes of data by format %s" % ( len(pad_data), curfmt ) )
# - perform padding on each vertex
for v in range( len( data_tgt ) ):
data_tgt[ v ] = data_tgt[ v ][ : pad_offset ] + pad_data + data_tgt[ v ][ pad_offset : ]
# - move on because formats have been made equal here
i += 1
#
# - set new format
meshdata["format"] = "".join( fmt_A[:-1] )
# validate vertex size
if len(meshdata["vertices"]) > 0 and len(ndata["vertices"]) > 0:
if len(meshdata["vertices"][0]) != len(ndata["vertices"][0]):
raise Exception(
"Vertex sizes not equal: old=%d new=%d fmtA=%s fmtB=%s newfmtA=%s newfmtB=%s" % (
len(meshdata["vertices"][0]),
len(ndata["vertices"][0]),
oldfmt_A,
oldfmt_B,
"".join( fmt_A[:-1] ),
"".join( fmt_B[:-1] ),
)
)
#
# combine vertex/index data
meshdata["vertices"] += ndata["vertices"]
meshdata["indices"] += ndata["indices"]
#
def parse_armature( node ):
for mod in node.modifiers:
if mod.type == "ARMATURE":
return mod.object
return None
#
def generate_bone_order( armdata ):
if armdata == None:
return None
bonelist = []
bonequeue = []
for bone in armdata.bones:
if bone.parent == None:
bonequeue.append( bone )
#
while len(bonequeue) != 0:
bone = bonequeue.pop(0)
bonelist.append( bone.name )
for bone in bone.children:
bonequeue.append( bone )
#
return bonelist
#
def parse_animations( armobj, boneorder, filepath ):
animations = []
if armobj is not None and armobj.animation_data is not None:
print( "Generating animations... " )
oldact = armobj.animation_data.action
for action in bpy.data.actions:
if action.use_fake_user is False:
continue # do not export animations that are not pinned (likely to be deleted)
armobj.animation_data.action = action
anim_tracks = {}
for bonename in boneorder:
anim_tracks[ bonename ] = []
frame_begin, frame_end = [ int(x) for x in action.frame_range ]
for frame in range( frame_begin, frame_end + 1 ):
bpy.context.scene.frame_set( frame )
for bonename in boneorder:
bone = armobj.pose.bones[ bonename ]
track = anim_tracks[ bonename ]
# input: object space pose/anim matrices (CP, CA, PP, PA)
# output: pose space anim matrix (out)
# out = inv( inv(PP) * CP ) * ( inv(PA) * CA )
mtx_anim = bone.matrix
mtx_base = bone.bone.matrix_local
if bone.parent is not None:
mtx_anim = bone.parent.matrix.inverted() * mtx_anim
mtx_base = bone.parent.bone.matrix_local.inverted() * mtx_base
mtx = mtx_base.inverted() * mtx_anim
track.append( mtx )
#
#
anim_markers = []
for pmrk in action.pose_markers:
anim_markers.append({ "name": pmrk.name, "frame": pmrk.frame })
#
animspeed = bpy.context.scene.render.fps / bpy.context.scene.render.fps_base
animlistname = os.path.dirname( filepath ) + "/" + action.name + ".animlist.csv"
print( "Looking for animation descriptor - " + animlistname )
if os.path.isfile( animlistname ):
print( "Found it, decoding action..." )
try:
animlist = csv.reader( open( animlistname, "r" ), delimiter = ",", quotechar = '"' )
for anim in animlist:
tstart = int(anim[0], 10)
tend = int(anim[1], 10)
tname = anim[2].strip()
anim_sliced_tracks = {}
for track_name, track_matrices in anim_tracks.items():
anim_sliced_tracks[ track_name ] = track_matrices[ tstart : tend ]
animations.append({ "name": tname, "frames": tend - tstart, "tracks": anim_sliced_tracks, "speed": animspeed })
#
except IOError as e:
print( "I/O error({0}): {1}".format(e.errno, e.strerror) )
except ValueError:
print( "Could not convert data to an integer." )
#
else:
print( "Did not find it, will append the whole action." )
animations.append({ "name": action.name, "frames": frame_end - frame_begin + 1, "tracks": anim_tracks, "markers" : anim_markers, "speed": animspeed })
#
#
armobj.animation_data.action = oldact
print( "\tOK!" )
#
return animations
#
def parse_textures():
textures = {}
print( "Parsing textures... ", end="" )
for tex in bpy.data.textures:
texpath = ""
if hasattr( tex, "image" ) and tex.image != None:
texpath = tex.image.filepath[ 2: ]
textures[ tex.name ] = texpath
print( "OK!" )
return textures
def write_ss3dmesh( ctx, props ):
filepath = props.filepath
print( "\n\\\\\n>>> SS3DMESH Exporter v0.5!\n//\n\n" )
print( "Exporting..." )
textures = parse_textures()
meshdata = gen_empty_mesh_data()
armobj = None
armdata = None
boneorder = []
for node in ctx.scene.objects:
if node.type != "MESH":
continue
if props.export_selected and not node.select:
continue
cur_armobj = parse_armature( node )
# do not allow multiple armatures
if armobj is not None and cur_armobj is not None and armobj.name != cur_armobj.name:
props.report( {"ERROR"},
"multiple armatures are not supported (curr=%s, new=%s)" % (
armobj.name, cur_armobj.name ) )
return {'CANCELLED'}
# do this only for the first time
if armobj is None and cur_armobj is not None:
armobj = cur_armobj
armdata = None if armobj is None else armobj.data
boneorder = generate_bone_order( armdata )
cur_meshdata = parse_geometry( node, textures, boneorder, props )
mesh_data_add( meshdata, cur_meshdata )
if props.export_anim:
print( "Parsing animations..." )
animations = parse_animations( armobj, boneorder, filepath )
print( "OK!" )
print( "Writing mesh... " )
with open( filepath, 'wb' ) as f:
write_mesh( f, meshdata, armdata, boneorder )
if props.export_anim:
if len(animations) == 0:
props.report( {"WARNING"}, "No animations found!" )
else:
print( "Writing animations... " )
with open( filepath + ".anm", 'wb' ) as f:
write_anims( f, animations )
with open( filepath + ".anb", 'wb' ) as f:
write_anims_anbd( f, animations )
#
print( "\n\\\\\n>>> Done!\n//\n\n" )
return {'CANCELLED'}
#
def write_sgrxanbd( ctx, props ):
print( "\n\\\\\n>>> SGRXANBD Exporter v0.5!\n//\n\n" )
print( "Exporting..." )
filepath = props.filepath
print( "Parsing nodes... ", end="" )
armobj = None
geom_node = bpy.context.active_object
if bpy.context.active_object.type == "ARMATURE":
armobj = bpy.context.active_object
for node in ctx.scene.objects:
if node.type == "ARMATURE":
armobj = node
break
#
if armobj is None:
props.report( {"ERROR"}, "No armature found!" )
return {'CANCELLED'}
else:
armdata = armobj.data
print( "OK!" )
boneorder = generate_bone_order( armdata )
print( "Parsing animations..." )
animations = parse_animations( armobj, boneorder, filepath )
print( "OK!" )
if len(animations) == 0:
props.report( {"WARNING"}, "No animations found!" )
else:
print( "Writing animations... " )
with open( filepath, 'wb' ) as f:
write_anims_anbd( f, animations )
#
print( "\n\\\\\n>>> Done!\n//\n\n" )
return {'CANCELLED'}
# 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
apply_mod_ui_items = [
( "NONE", "None", "Don't apply any modifiers" ),
( "SKIPARM", "All except armatures", "Apply non-armature modifiers" ),
( "ALL", "All", "Apply all modifiers" ),
]
class ExportSS3DMESH( bpy.types.Operator, ExportHelper ):
'''SS3DMESH Exporter'''
bl_idname = "export.ss3dmesh"
bl_label = "[SGRX] Export .ssm"
bl_options = {'REGISTER', 'UNDO'}
# ExportHelper mixin class uses this
filename_ext = ".ssm"
filter_glob = StringProperty(
default = "*.ssm",
options = {'HIDDEN'},
)
export_anim = BoolProperty(name="Export animation", default=False)
export_selected = BoolProperty(name="Export selected mesh only", default=True)
apply_modifiers = EnumProperty(items=apply_mod_ui_items,
name="Apply modifiers", default="SKIPARM")
def execute( self, ctx ):
return write_ss3dmesh( ctx, self )
class ExportSGRXANBD( bpy.types.Operator, ExportHelper ):
'''SGRXANBD (anim. bundle) Exporter'''
bl_idname = "export.sgrxanbd"
bl_label = "[SGRX] Export .anb"
bl_options = {'REGISTER', 'UNDO'}
filename_ext = ".anb"
filter_glob = StringProperty(
default = "*.anb",
options = {'HIDDEN'},
)
def execute( self, ctx ):
return write_sgrxanbd( ctx, self )
# Only needed if you want to add into a dynamic menu
def menu_func_export( self, ctx ):
self.layout.operator( ExportSS3DMESH.bl_idname, text="SS3DMESH Exporter" )
self.layout.operator( ExportSGRXANBD.bl_idname, text="SGRXANBD (anim. bundle) Exporter" )
def makeMaterial(name, uvlayer):
tex = bpy.data.textures.new(name, type = 'IMAGE')
mat = bpy.data.materials.new(name)
mat.diffuse_color = (1,1,1)
mat.diffuse_shader = 'LAMBERT'
mat.diffuse_intensity = 1.0
mat.specular_color = (1,1,1)
mat.specular_shader = 'COOKTORR'
mat.specular_intensity = 0.2
mat.alpha = 1
mat.ambient = 1
mtex = mat.texture_slots.add()
mtex.texture = tex
mtex.texture_coords = 'UV'
mtex.use_map_color_diffuse = True
mtex.mapping = 'FLAT'
mtex.uv_layer = uvlayer
return mat
class DialogOperator(bpy.types.Operator):
bl_idname = "object.generate_material"
bl_label = "Generate Material"
prop_mtlname = StringProperty(name="Material name", default="newmtl")
def execute(self, context):
active = bpy.context.active_object.data
if hasattr(active, "materials"):
mtlname = self.prop_mtlname
active.materials.append( makeMaterial( mtlname, active.uv_layers.keys()[0] ) )
self.report({'INFO'}, "Material created: %s!" % mtlname)
else:
self.report({'INFO'}, "Object cannot have materials!")
#
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
class DialogPanel(bpy.types.Panel):
bl_label = "Texturing Tools"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
def draw(self, context):
self.layout.operator("object.generate_material")
def register():
bpy.utils.register_class( ExportSS3DMESH )
bpy.utils.register_class( ExportSGRXANBD )
bpy.utils.register_class( DialogOperator )
bpy.utils.register_class( DialogPanel )
bpy.types.INFO_MT_file_export.append( menu_func_export )
# bpy.types.VIEW3D_PT_tools_object.append( generate_material_func )
def unregister():
bpy.utils.unregister_class( ExportSS3DMESH )
bpy.utils.unregister_class( ExportSGRXANBD )
bpy.utils.unregister_class( DialogOperator )
bpy.utils.unregister_class( DialogPanel )
bpy.types.INFO_MT_file_export.remove( menu_func_export )
# bpy.types.VIEW3D_PT_tools_object.remove( generate_material_func )
if __name__ == "__main__":
register()
#
Comments