[dali_2.3.21] Merge branch 'devel/master'
[platform/core/uifw/dali-toolkit.git] / tools / navigation-mesh-exporter / NavigationMeshExport.py
1 import bpy
2 import struct
3 import ctypes
4 import tempfile
5 import os
6 import mathutils
7 from mathutils import Matrix
8 from mathutils import Vector
9
10 def MakeVersion( maj, min ):
11     return ctypes.c_uint32((ctypes.c_uint32(maj).value << 16) | ctypes.c_uint32(min).value).value
12
13 EXPORT_VERSION = MakeVersion( 1, 0 )
14
15 # If BAKE_TRANSFORM is True the object transform is baked into vertices
16 BAKE_TRANSFORM = False
17
18 CHECKSUM = "NAVM" # 4 bytes of checksum
19
20 # From GLTF addon
21 def to_yup():
22     """Transform to Yup."""
23     return Matrix(
24         ((1.0, 0.0, 0.0, 0.0),
25          (0.0, 0.0, 1.0, 0.0),
26          (0.0, -1.0, 0.0, 0.0),
27          (0.0, 0.0, 0.0, 1.0))
28     )
29
30 def to_yup3():
31     """Transform to Yup."""
32     return Matrix(
33         ((1.0, 0.0, 0.0),
34          (0.0, 0.0, 1.0),
35          (0.0, -1.0, 0.0))
36     )
37
38 # struct NavMeshHeader
39 # version 10
40 # The script writes multiple binary buffers which are concatenated at the end once
41 # the offsets are resolved
42 class NavMeshHeader10:
43
44     # fields of NavMeshHeader
45
46     def __init__(self, outputPath):
47         self.outputPath = outputPath
48         self.meshObject = bpy.context.selected_objects[0]
49
50         self.checksum = CHECKSUM
51         self.version = EXPORT_VERSION
52
53         # fields of version
54         self.dataOffset = 0
55
56         self.vertexCount = 0
57         self.vertexDataOffset = 0
58
59         self.edgeCount = 0
60         self.edgeDataOffset = 0
61
62         self.polyCount = 0
63         self.polyDataOffset = 0
64
65         self.mesh = self.meshObject.data
66
67         # helper fields
68         self.edgeBufferSize = 0
69         self.vertexBufferSize = 0
70         self.polyBufferSize = 0
71         self.bufferHeaderSize = 0
72         self.endianness = 0 # TODO
73
74         with tempfile.TemporaryDirectory() as temp_dir:
75             self.tempDir = temp_dir
76
77         print(temp_dir)
78         os.makedirs(temp_dir, exist_ok=True)
79         return
80
81     def Process(self):
82
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)
87         return
88
89     # vertices format
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")
93         i = 0
94         for vert in self.mesh.vertices:
95             fmt = '@fff'
96             co = vert.co.copy()
97             # for now bake transform into the mesh (it has to be aligned properly)
98             if bakeTransform:
99                 print('baking')
100                 co = self.meshObject.matrix_world @ Vector(co)
101
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}')
104             i += 1
105         self.vertexBufferSize = out.tell()
106         out.close()
107         return
108
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):
115         mesh = self.mesh
116         vertexBuffer = {}
117         i = 0
118         for v in mesh.vertices:
119             # using string of coordinates as a key
120             key = f'{v.co}'
121
122             if key in vertexBuffer:
123                 vertexBuffer[key].append(i)
124             else:
125                 vertexBuffer[key] = [i]
126             i += 1
127
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
131
132         vertexSubstitute = []
133         for v in mesh.vertices:
134             key = f'{v.co}'
135             verts = vertexBuffer[key]
136
137             # store always only first index of vertex
138             vertexSubstitute.append(verts[0])
139
140         #for i in range(0, len(vertexSubstitute)):
141         #    print(f'{i} = {vertexSubstitute[i]}')
142
143         # build edge map
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
147         origKeys = []
148         for e in mesh.edges:
149             v0 = e.vertices[0]
150             v1 = e.vertices[1]
151             key = [v0, v1]
152             key.sort()
153             origKeys.append(key)
154             #print(key)
155
156         # create new keys by using vertex substitutes
157         keyMap = {}
158         edgeMap = {}
159         edgeIndex = 0
160
161         for key in origKeys:
162             newKey = [vertexSubstitute[key[0]], vertexSubstitute[key[1]]]
163             newKey.sort()
164
165             # turn newkey into hashable string
166             newKeyStr = f'{newKey}'
167
168             #print(f'{key} -> {newKey}')
169
170             if newKeyStr in keyMap:
171                 keyMap[newKeyStr].append(edgeIndex)
172             else:
173                 keyMap[newKeyStr] = [edgeIndex]
174
175             # turn original key into string
176             edgeMap[f'{key}'] = keyMap[newKeyStr][0]
177             edgeIndex += 1
178
179         return edgeMap
180
181
182     """
183     Edge data stores:
184     Pair of vertices (by index)
185     Pair of polygons (by index)
186
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).
191
192     The pair of polygons are stored in order to speed up lookup of adjacent
193     floor faces.
194     """
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")
198
199         # add vertices to the edge
200         edgeFaceList = []
201         for edge in self.mesh.edges:
202             edgeFaceList.append([])
203             edgeFaceList[-1].append(edge.vertices[0])
204             edgeFaceList[-1].append(edge.vertices[1])
205
206         edgeMap = self.CreateEdgeMap()
207
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)
215
216         # make sure each edge contains 4 items
217         for edge in edgeFaceList:
218             if len(edge) == 2:
219                 edge.append( 0xffff )
220             if len(edge) == 3:
221                 edge.append( 0xffff )
222         count = 0
223         for edge in edgeFaceList:
224             if len(edge) == 4:
225                 count += 1
226         if count == len(edgeFaceList):
227             print("all correct!")
228         # save the data into file
229         fmt = '@HHHH'
230         i = 0
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}')
234             i += 1
235
236         self.edgeBufferSize = out.tell()
237
238         out.close()
239         return
240
241     ###################################################################
242     """
243     Writes buffer of polygons. Polys must be triangles.
244     The data contains:
245     - vertex indices 3*u16
246     - edge indices   3*u16
247     - normal vector of poly 3*f32
248     - center 3*f32 (we may need center point for navigation)
249     """
250     def WritePolys(self):
251         out = open(os.path.join(self.tempDir, "myvertex-polys.bin"), "wb")
252
253         # for each polygon update the edges
254
255         polyIndex = 0
256
257         edgeMap = self.CreateEdgeMap()
258
259         for poly in self.mesh.polygons:
260             edges = []
261             for ek in poly.edge_keys:
262                 ekArray = [ ek[0], ek[1] ]
263                 edgeIndexKey = f'{ekArray}'
264                 edges.append(edgeMap[edgeIndexKey])
265
266             verts = poly.vertices
267             norm = poly.normal.copy()
268
269             # transform
270             print(f'{norm.x:f}, {norm.y:f}, {norm.z:f}')
271
272             center = poly.center
273
274             # calculate normal
275
276             fmt='@HHHHHHffffff'
277
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))
283
284             print('{edges[0]}, {edges[1]}, {edges[2]}')
285         self.polyBufferSize = out.tell()
286         out.close()
287         return
288
289     def WriteBinary(self):
290
291         out = open(self.outputPath, "wb")
292
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
296         fmt = "@II"
297         out.write(struct.pack(fmt, csum, self.version))
298
299         # write remaining fields
300         fmt = '@IIIIIIIfff'
301         gravity = bpy.context.scene.gravity.copy().normalized()
302         out.write(struct.pack(fmt,
303                               self.dataOffset,
304                               self.vertexCount,
305                               self.vertexDataOffset,
306                               self.edgeCount,
307                               self.edgeDataOffset,
308                               self.polyCount,
309                               self.polyDataOffset,
310                               gravity.x, gravity.y, gravity.z
311                               ))
312
313         self.bufferHeaderSize = out.tell()
314
315         out.close()
316
317         return
318
319     def Finalize(self):
320
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
325
326         # write header again
327         self.WriteBinary()
328
329         # concatenate files
330
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())
333
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())
336
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())
339
340         return
341
342
343 ################################################################
344 # UI
345
346 # Navigation Mesh Exporter
347 class OBJECT_OT_CustomOperator(bpy.types.Operator):
348     bl_idname = "object.custom_operator"
349     bl_label = "Export Navigation Mesh"
350
351     def execute(self, context):
352         bpy.ops.export.some_data('INVOKE_DEFAULT')
353         return {'FINISHED'}
354
355 def submenu_func(self, context):
356     layout = self.layout
357     layout.operator("object.custom_operator")
358
359 def menu_func(self, context):
360     layout = self.layout
361
362     # Add a submenu
363     layout.menu("VIEW3D_MT_custom_submenu", text="DALi")
364
365 class CustomSubmenu(bpy.types.Menu):
366     bl_idname = "VIEW3D_MT_custom_submenu"
367     bl_label = "DALi"
368
369     def draw(self, context):
370         layout = self.layout
371         layout.operator("object.custom_operator")
372
373 def register():
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)
378 def unregister():
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)
383
384 # exporter
385 class ExportSomeData(bpy.types.Operator):
386     bl_idname = "export.some_data"
387     bl_label = "Export DALi Navigation Mesh"
388
389     filepath: bpy.props.StringProperty(subtype="FILE_PATH")
390
391     @classmethod
392     def poll(cls, context):
393         return context.object is not None
394
395     def execute(self, context):
396         print(self.filepath)
397         navmesh = NavMeshHeader10( self.filepath )
398         navmesh.Process()
399         navmesh.WriteBinary()
400         navmesh.WriteVertices( False )
401         navmesh.WriteEdges()
402         navmesh.WritePolys()
403         navmesh.Finalize()
404         return {'FINISHED'}
405
406     def invoke(self, context, event):
407         context.window_manager.fileselect_add(self)
408         return {'RUNNING_MODAL'}
409
410 if __name__ == "__main__":
411     register()