metaforce/hecl/blender/addon/hmdl/hmdl_mesh.py

532 lines
21 KiB
Python

'''
HMDL Export Blender Addon
By Jack Andersen <jackoalan@gmail.com>
'''
import struct
import bpy
class loop_vert:
def __init__(self, mesh, loop):
self.mesh = mesh
self.loop = loop
def __hash__(self):
return (self.mesh, self.loop.index).__hash__()
def __eq__(self, other):
return self.mesh == other.mesh and self.loop.index == other.loop.index
def __ne__(self, other):
return self.mesh != other.mesh or self.loop.index != other.loop.index
def __str__(self):
return (self.mesh, self.loop.index).__str__()
# Round up to nearest 32 multiple
def ROUND_UP_32(num):
return (num + 31) & ~31
# Round up to nearest 4 multiple
def ROUND_UP_4(num):
return (num + 3) & ~3
# This routine conditionally inserts a loop into a multi-tiered
# array/set collection; simultaneously relating verts to loops and
# eliminating redundant loops (containing identical UV coordinates)
def _augment_loop_vert_array(lv_array, mesh, loop, uv_count):
# Create loop_vert object for comparitive testing
lv = loop_vert(mesh, loop)
# First perform quick check to see if loop is already in a set
for existing_loop_set in lv_array:
if lv in existing_loop_set:
return
# Now perform extended check to see if any UV coordinate values already match
for existing_loop_set in lv_array:
for existing_loop in existing_loop_set:
matches = True
for uv_layer_idx in range(uv_count):
uv_layer = mesh.uv_layers[uv_layer_idx]
existing_uv_coords = uv_layer.data[existing_loop.loop.index].uv
check_uv_coords = uv_layer.data[loop.index].uv
if (existing_uv_coords[0] != check_uv_coords[0] or
existing_uv_coords[1] != check_uv_coords[1]):
matches = False
break
if matches:
existing_loop_set.append(lv)
return
# If we get here, no match found; add new set to `lv_array`
lv_array.append([lv])
# Get loop set from collection generated with above method;
# containing a specified loop
def _get_loop_set(lv_array, mesh, loop):
# Create loop_vert object for comparitive testing
lv = loop_vert(mesh, loop)
for existing_loop_set in lv_array:
if lv in existing_loop_set:
return existing_loop_set
return None
# Method to find triangle opposite another triangle over two vert-indices
def _find_polygon_opposite_idxs(mesh, original_triangle, a_idx, b_idx):
for triangle in mesh.polygons:
if triangle == original_triangle:
continue
if (a_idx in triangle.vertices and b_idx in triangle.vertices):
return triangle
return None
# Method to find triangle opposite another triangle over two loop-vert sets
def _find_polygon_opposite_lvs(mesh, original_triangle, lv_a, lv_b):
a_idx = lv_a[0].loop.vertex_index
b_idx = lv_b[0].loop.vertex_index
return _find_polygon_opposite_idxs(mesh, original_triangle, a_idx, b_idx)
class hmdl_mesh:
def __init__(self):
# 4-byte ID string used in generated HMDL file
self.file_identifier = '_GEN'
# Array that holds collections. A collection is a 16-bit index
# worth of vertices, elements referencing them, and a
# primitive array to draw them
self.collections = []
# If vertex index space is exceeded for a single additional vertex,
# a new collection is created and returned by this routine
def _check_collection_overflow(self, mesh, collection, rigger, uv_count):
max_bone_count = 0;
if rigger:
max_bone_count = rigger.max_bone_count
if not collection or len(collection['vertices']) >= 65535:
new_collection = {'uv_count':uv_count, 'max_bone_count':max_bone_count, 'vertices':[], 'vert_weights':[], 'tri_strips':[]}
self.collections.append(new_collection)
return new_collection, True
else:
return collection, False
# Augments draw generator with a single blender MESH data object
def add_mesh(self, mesh, rigger, uv_count):
max_bone_count = 0;
if rigger:
max_bone_count = rigger.max_bone_count
print("Optimizing mesh:", mesh.name)
opt_gpu_vert_count = 0
# First, generate compressed loop-vertex array-array-set collection
loop_vert_array = []
for vert in mesh.vertices:
loop_verts = []
for loop in mesh.loops:
if loop.vertex_index == vert.index:
_augment_loop_vert_array(loop_verts, mesh, loop, uv_count)
loop_vert_array.append(loop_verts)
# Find best collection to add mesh data into
best_collection = None
for collection in self.collections:
if (collection['uv_count'] == uv_count and
collection['max_bone_count'] == max_bone_count and
len(collection['vertices']) < 65000):
best_collection = collection
break
if not best_collection:
# Create a new one if no good one found
best_collection, is_new_collection = self._check_collection_overflow(mesh, None, rigger, uv_count)
# If rigging, start an array of bone names to be bound to contiguous tri-strips
tri_strip_bones = []
tri_strip_bones_overflow = False
# Now begin generating draw primitives
visited_polys = set()
for poly in mesh.polygons:
# Skip if already visited
if poly in visited_polys:
continue
# Allows restart if initial polygon was not added
good = False
while not good:
# Begin a tri-strip primitive (array of vert indices)
tri_strip = []
# Temporary references to trace out strips of triangles
temp_poly = poly
# Rolling references of last two emitted loop-vert sets (b is older)
last_loop_vert_a = None
last_loop_vert_b = None
# In the event of vertex-buffer overflow, this will be made true;
# resulting in the immediate splitting of a tri-strip
is_new_collection = False
# As long as there is a connected polygon to visit
while temp_poly:
if 0 == len(tri_strip): # First triangle in strip
# Order the loops so the last two connect to a next polygon
idx0 = mesh.loops[temp_poly.loop_indices[0]].vertex_index
idx1 = mesh.loops[temp_poly.loop_indices[1]].vertex_index
idx2 = mesh.loops[temp_poly.loop_indices[2]].vertex_index
if not _find_polygon_opposite_idxs(mesh, temp_poly, idx1, idx2):
loop_idxs = [temp_poly.loop_indices[2], temp_poly.loop_indices[0], temp_poly.loop_indices[1]]
if not _find_polygon_opposite_idxs(mesh, temp_poly, idx0, idx1):
loop_idxs = [temp_poly.loop_indices[1], temp_poly.loop_indices[2], temp_poly.loop_indices[0]]
else:
loop_idxs = temp_poly.loop_indices
# Add three loop-vert vertices to tri-strip
for poly_loop_idx in loop_idxs:
poly_loop = mesh.loops[poly_loop_idx]
loop_vert = _get_loop_set(loop_vert_array[poly_loop.vertex_index], mesh, poly_loop)
# If rigging, ensure that necessary bones are available and get weights
weights = None
if rigger:
weights = rigger.augment_bone_array_with_lv(mesh, tri_strip_bones, loop_vert)
if weights is None:
tri_strip_bones_overflow = True
break
if loop_vert not in best_collection['vertices']:
best_collection, is_new_collection = self._check_collection_overflow(mesh, best_collection, rigger, uv_count)
if is_new_collection:
break
best_collection['vertices'].append(loop_vert)
best_collection['vert_weights'].append(weights)
tri_strip.append(best_collection['vertices'].index(loop_vert))
last_loop_vert_b = last_loop_vert_a
last_loop_vert_a = loop_vert
opt_gpu_vert_count += 1
#print('appended initial loop', loop_vert[0].loop.index)
if is_new_collection or tri_strip_bones_overflow:
break
else: # Not the first triangle in strip; look up all three loop-verts,
# ensure it matches last-2 rolling reference, emit remaining loop-vert
# Iterate loop verts
odd_loop_vert_out = None
loop_vert_match_count = 0
for poly_loop_idx in temp_poly.loop_indices:
poly_loop = mesh.loops[poly_loop_idx]
loop_vert = _get_loop_set(loop_vert_array[poly_loop.vertex_index], mesh, poly_loop)
if (loop_vert == last_loop_vert_a or loop_vert == last_loop_vert_b):
loop_vert_match_count += 1
continue
odd_loop_vert_out = loop_vert
# Ensure there are two existing matches to continue tri-strip
if loop_vert_match_count != 2 or not odd_loop_vert_out:
break
# If rigging, ensure that necessary bones are available and get weights
weights = None
if rigger:
weights = rigger.augment_bone_array_with_lv(mesh, tri_strip_bones, odd_loop_vert_out)
if weights is None:
tri_strip_bones_overflow = True
break
# Add to tri-strip
if odd_loop_vert_out not in best_collection['vertices']:
best_collection, is_new_collection = self._check_collection_overflow(mesh, best_collection, rigger, uv_count)
if is_new_collection:
break
best_collection['vertices'].append(odd_loop_vert_out)
best_collection['vert_weights'].append(weights)
tri_strip.append(best_collection['vertices'].index(odd_loop_vert_out))
last_loop_vert_b = last_loop_vert_a
last_loop_vert_a = odd_loop_vert_out
opt_gpu_vert_count += 1
# This polygon is good
visited_polys.add(temp_poly)
# Find a polygon directly connected to this one to continue strip
temp_poly = _find_polygon_opposite_lvs(mesh, temp_poly, last_loop_vert_a, last_loop_vert_b)
if temp_poly in visited_polys:
temp_poly = None
# Add tri-strip to element array
if len(tri_strip):
best_collection['tri_strips'].append({'mesh':mesh, 'strip':tri_strip, 'strip_bones':tri_strip_bones})
good = True
if tri_strip_bones_overflow:
tri_strip_bones = []
tri_strip_bones_overflow = False
print("Mesh contains", len(mesh.polygons), "triangles")
print("Vert count: (%d -> %d)\n" % (len(mesh.loops), opt_gpu_vert_count))
# Generate binary vertex buffer of collection index
def generate_vertex_buffer(self, index, endian_char):
collection = self.collections[index]
if not collection:
return None
# Positions output
pos_out = []
# Generate vert buffer struct
vstruct = struct.Struct(endian_char + 'f')
# If rigging, determine maximum number of bones in this collection
max_bones = 0
for i in range(len(collection['vertices'])):
weight_count = 0
if collection['vert_weights'][i]:
weight_count = len(collection['vert_weights'][i])
if weight_count > max_bones:
max_bones = weight_count
max_bones = ROUND_UP_4(max_bones)
# Build byte array
vert_bytes = bytearray()
for i in range(len(collection['vertices'])):
loop_vert = collection['vertices'][i]
bloop = loop_vert[0]
mesh = bloop.mesh
bvert = mesh.vertices[bloop.loop.vertex_index]
#print(bvert.co)
# Position
pos_out.append((bvert.co, bvert.normal))
for comp in range(4):
if comp in range(len(bvert.co)):
vert_bytes += vstruct.pack(bvert.co[comp])
else:
vert_bytes += vstruct.pack(0.0)
# Normal
for comp in range(4):
if comp in range(len(bvert.normal)):
vert_bytes += vstruct.pack(bvert.normal[comp])
else:
vert_bytes += vstruct.pack(0.0)
# Weights
weights = collection['vert_weights'][i]
for j in range(max_bones):
if j < len(weights):
vert_bytes += vstruct.pack(weights[j])
else:
vert_bytes += vstruct.pack(0.0)
# UVs
added_uvs = 0
for uv_idx in range(collection['uv_count']):
coords = mesh.uv_layers[uv_idx].data[bloop.loop.index].uv
vert_bytes += vstruct.pack(coords[0])
vert_bytes += vstruct.pack(-coords[1])
added_uvs += 1
# Pad to 16-byte alignment
if added_uvs & 1:
vert_bytes += vstruct.pack(0.0)
vert_bytes += vstruct.pack(0.0)
return collection['uv_count'], max_bones, vert_bytes, pos_out
# Generate binary element buffer of collection index
def generate_element_buffer(self, index, endian_char):
collection = self.collections[index]
if not collection:
return None
# Numeric array out
arr_out = []
# Generate element buffer struct
estruct = struct.Struct(endian_char + 'H')
# Build mesh-primitive hierarchy
last_mesh = collection['tri_strips'][0]['mesh']
mesh_primitives = {'mesh':last_mesh, 'primitives':[]}
collection_primitives = [mesh_primitives]
# Collection element byte-array
cur_offset = 0
element_bytes = bytearray()
# Last element index entry and strip length for forming degenerate strip
last_elem = None
strip_len = 0
# Last strip bone array (for rigging)
last_strip_bones = collection['tri_strips'][0]['strip_bones']
# Build single degenerate tri-strip
for strip in collection['tri_strips']:
#print('new strip', collection['tri_strips'].index(strip))
if last_mesh != strip['mesh'] or last_strip_bones != strip['strip_bones']:
#print('splitting primitive')
# New mesh; force new strip
mesh_primitives['primitives'].append({'offset':cur_offset, 'length':strip_len, 'bones':last_strip_bones})
cur_offset += strip_len
last_elem = None
strip_len = 0
last_mesh = strip['mesh']
mesh_primitives = {'mesh':last_mesh, 'primitives':[]}
collection_primitives.append(mesh_primitives)
elif last_elem is not None:
#print('extending primitive')
# Existing mesh being extended as degenerate strip
strip_len += 2
element_bytes += estruct.pack(last_elem)
element_bytes += estruct.pack(strip['strip'][0])
arr_out.append(last_elem)
arr_out.append(strip['strip'][0])
# If current element count is odd, add additional degenerate strip to make it even
# This ensures that the sub-strip has proper winding-order for backface culling
if (strip_len & 1):
strip_len += 1
element_bytes += estruct.pack(strip['strip'][0])
arr_out.append(strip['strip'][0])
# Primitive tri-strip byte array
for idx in strip['strip']:
#print(idx)
strip_len += 1
element_bytes += estruct.pack(idx)
arr_out.append(idx)
last_elem = idx
# Final mesh entry
mesh_primitives['primitives'].append({'offset':cur_offset, 'length':strip_len, 'bones':last_strip_bones})
cur_offset += strip_len
return collection_primitives, element_bytes, arr_out
# Generate binary draw-index buffer of collection index
def generate_index_buffer(self, collection_primitives, endian_char, rigger):
# Bytearray to fill
index_bytes = bytearray()
# Submesh count
index_bytes += struct.pack(endian_char + 'I', len(collection_primitives))
# And array
for mesh in collection_primitives:
# Primitive count
index_bytes += struct.pack(endian_char + 'I', len(mesh['primitives']))
# Primitive array
for prim in mesh['primitives']:
# If rigging, append skin index
if rigger:
skin_index = rigger.augment_skin(prim['bones'])
index_bytes += struct.pack(endian_char + 'I', skin_index)
index_bytes += struct.pack(endian_char + 'I', 2)
index_bytes += struct.pack(endian_char + 'I', prim['offset'])
index_bytes += struct.pack(endian_char + 'I', prim['length'])
return index_bytes
# C-generation operator
import bmesh
class hmdl_mesh_operator(bpy.types.Operator):
bl_idname = "scene.hmdl_mesh"
bl_label = "HMDL C mesh maker"
bl_description = "HMDL Mesh source generation utility"
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'MESH'
def execute(self, context):
copy_mesh = context.object.data.copy()
copy_obj = context.object.copy()
copy_obj.data = copy_mesh
bm = bmesh.new()
bm.from_mesh(copy_mesh)
bmesh.ops.triangulate(bm, faces=bm.faces)
#to_remove = []
#for face in bm.faces:
# if face.material_index != 7:
# to_remove.append(face)
#bmesh.ops.delete(bm, geom=to_remove, context=5)
bm.to_mesh(copy_mesh)
bm.free()
context.scene.objects.link(copy_obj)
rmesh = hmdl_mesh()
rmesh.add_mesh(copy_mesh, None, 0)
str_out = '/* Vertex Buffer */\nstatic const float VERT_BUF[] = {\n'
vert_arr = rmesh.generate_vertex_buffer(0, '<')[3]
for v in vert_arr:
str_out += ' %f, %f, %f, 0.0, %f, %f, %f, 0.0,\n' % (v[0][0], v[0][1], v[0][2], v[1][0], v[1][1], v[1][2])
ebuf_arr = rmesh.generate_element_buffer(0, '<')[2]
str_out += '};\n\n/* Element Buffer */\n#define ELEM_BUF_COUNT %d\nstatic const u16 ELEM_BUF[] = {\n' % len(ebuf_arr)
for e in ebuf_arr:
str_out += ' %d,\n' % e
str_out += '};\n'
context.scene.objects.unlink(copy_obj)
bpy.data.objects.remove(copy_obj)
bpy.data.meshes.remove(copy_mesh)
context.window_manager.clipboard = str_out
self.report({'INFO'}, "Wrote mesh C to clipboard")
return {'FINISHED'}