7 from mathutils import Matrix
8 from mathutils import Vector
10 def MakeVersion( maj, min ):
11 return ctypes.c_uint32((ctypes.c_uint32(maj).value << 16) | ctypes.c_uint32(min).value).value
13 EXPORT_VERSION = MakeVersion( 1, 0 )
15 # If BAKE_TRANSFORM is True the object transform is baked into vertices
16 BAKE_TRANSFORM = False
18 CHECKSUM = "NAVM" # 4 bytes of checksum
22 """Transform to Yup."""
24 ((1.0, 0.0, 0.0, 0.0),
26 (0.0, -1.0, 0.0, 0.0),
31 """Transform to Yup."""
38 # struct NavMeshHeader
40 # The script writes multiple binary buffers which are concatenated at the end once
41 # the offsets are resolved
42 class NavMeshHeader10:
44 # fields of NavMeshHeader
46 def __init__(self, outputPath):
47 self.outputPath = outputPath
48 self.meshObject = bpy.context.selected_objects[0]
50 self.checksum = CHECKSUM
51 self.version = EXPORT_VERSION
57 self.vertexDataOffset = 0
60 self.edgeDataOffset = 0
63 self.polyDataOffset = 0
65 self.mesh = self.meshObject.data
68 self.edgeBufferSize = 0
69 self.vertexBufferSize = 0
70 self.polyBufferSize = 0
71 self.bufferHeaderSize = 0
72 self.endianness = 0 # TODO
74 with tempfile.TemporaryDirectory() as temp_dir:
75 self.tempDir = temp_dir
78 os.makedirs(temp_dir, exist_ok=True)
83 # the data offset will be filled at later stage
84 self.polyCount = len(self.mesh.polygons)
85 self.vertexCount = len(self.mesh.vertices)
86 self.edgeCount = len(self.mesh.edges)
90 # N x ( f, f, f ), indexed from 0
91 def WriteVertices(self, bakeTransform=True):
92 out = open(os.path.join(self.tempDir, "myvertex-vertices-nt.bin"), "wb")
94 for vert in self.mesh.vertices:
97 # for now bake transform into the mesh (it has to be aligned properly)
100 co = self.meshObject.matrix_world @ Vector(co)
102 out.write(struct.pack(fmt, co[0], co[1], co[2]))
103 print(f'i: {i:d}: {co[0]:f}, {co[1]:f}, {co[2]:f}')
105 self.vertexBufferSize = out.tell()
109 # This function builds vertex map after finding duplicate vertices
110 # it is essential to do it as two or more vertices may appear as one
111 # in the editor, yet they may be defined as multiple vertices due to carrying
112 # different UVs or other attributes. NavMesh needs only coordinates and
113 # two vertices with the same coordinates are considered as one.
114 def CreateEdgeMap(self):
118 for v in mesh.vertices:
119 # using string of coordinates as a key
122 if key in vertexBuffer:
123 vertexBuffer[key].append(i)
125 vertexBuffer[key] = [i]
128 # build substitute array
129 # value at each index will be either own (no substitute)
130 # or of another vertex so the keys of edges can be substituted
132 vertexSubstitute = []
133 for v in mesh.vertices:
135 verts = vertexBuffer[key]
137 # store always only first index of vertex
138 vertexSubstitute.append(verts[0])
140 #for i in range(0, len(vertexSubstitute)):
141 # print(f'{i} = {vertexSubstitute[i]}')
144 # we need to remap edge keys so we can point out to a single edge
145 # end remove duplicates
146 # Blender edge key is made of vertex indices sorted
156 # create new keys by using vertex substitutes
162 newKey = [vertexSubstitute[key[0]], vertexSubstitute[key[1]]]
165 # turn newkey into hashable string
166 newKeyStr = f'{newKey}'
168 #print(f'{key} -> {newKey}')
170 if newKeyStr in keyMap:
171 keyMap[newKeyStr].append(edgeIndex)
173 keyMap[newKeyStr] = [edgeIndex]
175 # turn original key into string
176 edgeMap[f'{key}'] = keyMap[newKeyStr][0]
184 Pair of vertices (by index)
185 Pair of polygons (by index)
187 The polygon index is stored as uint16_t.
188 It is possible that the edge belongs to a single polygon. In such case,
189 the maximum of 0xFFFFF is stored as an index.
190 The maximum index of polygon is 0xFFFE (65534).
192 The pair of polygons are stored in order to speed up lookup of adjacent
195 # N x ( f, f, f ), indexed from 0
196 def WriteEdges(self):
197 out = open(os.path.join(self.tempDir, "myvertex-edges.bin"), "wb")
199 # add vertices to the edge
201 for edge in self.mesh.edges:
202 edgeFaceList.append([])
203 edgeFaceList[-1].append(edge.vertices[0])
204 edgeFaceList[-1].append(edge.vertices[1])
206 edgeMap = self.CreateEdgeMap()
208 # for each polygon update the edges
209 for poly in self.mesh.polygons:
210 for ek in poly.edge_keys:
211 ekArray = [ ek[0], ek[1] ]
212 edgeIndexKey = f'{ekArray}'
213 edgeIndex = edgeMap[edgeIndexKey]
214 edgeFaceList[edgeIndex].append(poly.index)
216 # make sure each edge contains 4 items
217 for edge in edgeFaceList:
219 edge.append( 0xffff )
221 edge.append( 0xffff )
223 for edge in edgeFaceList:
226 if count == len(edgeFaceList):
227 print("all correct!")
228 # save the data into file
231 for edge in edgeFaceList:
232 out.write(struct.pack(fmt, edge[0], edge[1], edge[2], edge[3]))
233 print(f'i: {i:d}: {edge[0]:d}, {edge[1]:d}, {edge[2]:d}, {edge[3]:d}')
236 self.edgeBufferSize = out.tell()
241 ###################################################################
243 Writes buffer of polygons. Polys must be triangles.
245 - vertex indices 3*u16
247 - normal vector of poly 3*f32
248 - center 3*f32 (we may need center point for navigation)
250 def WritePolys(self):
251 out = open(os.path.join(self.tempDir, "myvertex-polys.bin"), "wb")
253 # for each polygon update the edges
257 edgeMap = self.CreateEdgeMap()
259 for poly in self.mesh.polygons:
261 for ek in poly.edge_keys:
262 ekArray = [ ek[0], ek[1] ]
263 edgeIndexKey = f'{ekArray}'
264 edges.append(edgeMap[edgeIndexKey])
266 verts = poly.vertices
267 norm = poly.normal.copy()
270 print(f'{norm.x:f}, {norm.y:f}, {norm.z:f}')
278 out.write(struct.pack(fmt,
279 verts[0], verts[1], verts[2],
280 edges[0], edges[1], edges[2],
281 norm.x, norm.y, norm.z,
282 center.x, center.y, center.z))
284 print('{edges[0]}, {edges[1]}, {edges[2]}')
285 self.polyBufferSize = out.tell()
289 def WriteBinary(self):
291 out = open(self.outputPath, "wb")
293 # write common header fields
294 c = bytes(CHECKSUM, "ascii")
295 csum = ctypes.c_uint32( (c[3] << 24) | (c[2] << 16) | (c[1] << 8) | c[0] ).value
297 out.write(struct.pack(fmt, csum, self.version))
299 # write remaining fields
301 gravity = bpy.context.scene.gravity.copy().normalized()
302 out.write(struct.pack(fmt,
305 self.vertexDataOffset,
310 gravity.x, gravity.y, gravity.z
313 self.bufferHeaderSize = out.tell()
321 self.dataOffset = self.bufferHeaderSize
322 self.vertexDataOffset = 0 # relative to data offset
323 self.edgeDataOffset = self.vertexBufferSize
324 self.polyDataOffset = self.edgeDataOffset + self.edgeBufferSize
331 with open(self.outputPath, "ab") as out, open(os.path.join(self.tempDir, "myvertex-vertices-nt.bin"), "rb") as file2:
332 out.write(file2.read())
334 with open(self.outputPath, "ab") as out, open(os.path.join(self.tempDir, "myvertex-edges.bin"), "rb") as file2:
335 out.write(file2.read())
337 with open(self.outputPath, "ab") as out, open(os.path.join(self.tempDir, "myvertex-polys.bin"), "rb") as file2:
338 out.write(file2.read())
343 ################################################################
346 # Navigation Mesh Exporter
347 class OBJECT_OT_CustomOperator(bpy.types.Operator):
348 bl_idname = "object.custom_operator"
349 bl_label = "Export Navigation Mesh"
351 def execute(self, context):
352 bpy.ops.export.some_data('INVOKE_DEFAULT')
355 def submenu_func(self, context):
357 layout.operator("object.custom_operator")
359 def menu_func(self, context):
363 layout.menu("VIEW3D_MT_custom_submenu", text="DALi")
365 class CustomSubmenu(bpy.types.Menu):
366 bl_idname = "VIEW3D_MT_custom_submenu"
369 def draw(self, context):
371 layout.operator("object.custom_operator")
374 bpy.utils.register_class(OBJECT_OT_CustomOperator)
375 bpy.types.VIEW3D_MT_object_context_menu.append(menu_func)
376 bpy.utils.register_class(CustomSubmenu)
377 bpy.utils.register_class(ExportSomeData)
379 bpy.utils.unregister_class(OBJECT_OT_CustomOperator)
380 bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func)
381 bpy.utils.unregister_class(CustomSubmenu)
382 bpy.utils.unregister_class(ExportSomeData)
385 class ExportSomeData(bpy.types.Operator):
386 bl_idname = "export.some_data"
387 bl_label = "Export DALi Navigation Mesh"
389 filepath: bpy.props.StringProperty(subtype="FILE_PATH")
392 def poll(cls, context):
393 return context.object is not None
395 def execute(self, context):
397 navmesh = NavMeshHeader10( self.filepath )
399 navmesh.WriteBinary()
400 navmesh.WriteVertices( False )
406 def invoke(self, context, event):
407 context.window_manager.fileselect_add(self)
408 return {'RUNNING_MODAL'}
410 if __name__ == "__main__":