From: Adam Bialogonski Date: Thu, 23 Nov 2023 17:03:04 +0000 (+0000) Subject: [Tizen] Added NavigationMeshExporter Blender script. X-Git-Tag: accepted/tizen/8.0/unified/20231130.180820~5 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=4f58d4c68772c65bd9828daca447820b4a445377;p=platform%2Fcore%2Fuifw%2Fdali-toolkit.git [Tizen] Added NavigationMeshExporter Blender script. Change-Id: I939e301e04198abc4ab310cd5b20752b0452b984 --- diff --git a/tools/navigation-mesh-exporter/NavigationMeshExport.py b/tools/navigation-mesh-exporter/NavigationMeshExport.py new file mode 100644 index 0000000..16e90ef --- /dev/null +++ b/tools/navigation-mesh-exporter/NavigationMeshExport.py @@ -0,0 +1,411 @@ +import bpy +import struct +import ctypes +import tempfile +import os +import mathutils +from mathutils import Matrix +from mathutils import Vector + +def MakeVersion( maj, min ): + return ctypes.c_uint32((ctypes.c_uint32(maj).value << 16) | ctypes.c_uint32(min).value).value + +EXPORT_VERSION = MakeVersion( 1, 0 ) + +# If BAKE_TRANSFORM is True the object transform is baked into vertices +BAKE_TRANSFORM = False + +CHECKSUM = "NAVM" # 4 bytes of checksum + +# From GLTF addon +def to_yup(): + """Transform to Yup.""" + return Matrix( + ((1.0, 0.0, 0.0, 0.0), + (0.0, 0.0, 1.0, 0.0), + (0.0, -1.0, 0.0, 0.0), + (0.0, 0.0, 0.0, 1.0)) + ) + +def to_yup3(): + """Transform to Yup.""" + return Matrix( + ((1.0, 0.0, 0.0), + (0.0, 0.0, 1.0), + (0.0, -1.0, 0.0)) + ) + +# struct NavMeshHeader +# version 10 +# The script writes multiple binary buffers which are concatenated at the end once +# the offsets are resolved +class NavMeshHeader10: + + # fields of NavMeshHeader + + def __init__(self, outputPath): + self.outputPath = outputPath + self.meshObject = bpy.context.selected_objects[0] + + self.checksum = CHECKSUM + self.version = EXPORT_VERSION + + # fields of version + self.dataOffset = 0 + + self.vertexCount = 0 + self.vertexDataOffset = 0 + + self.edgeCount = 0 + self.edgeDataOffset = 0 + + self.polyCount = 0 + self.polyDataOffset = 0 + + self.mesh = self.meshObject.data + + # helper fields + self.edgeBufferSize = 0 + self.vertexBufferSize = 0 + self.polyBufferSize = 0 + self.bufferHeaderSize = 0 + self.endianness = 0 # TODO + + with tempfile.TemporaryDirectory() as temp_dir: + self.tempDir = temp_dir + + print(temp_dir) + os.makedirs(temp_dir, exist_ok=True) + return + + def Process(self): + + # the data offset will be filled at later stage + self.polyCount = len(self.mesh.polygons) + self.vertexCount = len(self.mesh.vertices) + self.edgeCount = len(self.mesh.edges) + return + + # vertices format + # N x ( f, f, f ), indexed from 0 + def WriteVertices(self, bakeTransform=True): + out = open(os.path.join(self.tempDir, "myvertex-vertices-nt.bin"), "wb") + i = 0 + for vert in self.mesh.vertices: + fmt = '@fff' + co = vert.co.copy() + # for now bake transform into the mesh (it has to be aligned properly) + if bakeTransform: + print('baking') + co = self.meshObject.matrix_world @ Vector(co) + + out.write(struct.pack(fmt, co[0], co[1], co[2])) + print(f'i: {i:d}: {co[0]:f}, {co[1]:f}, {co[2]:f}') + i += 1 + self.vertexBufferSize = out.tell() + out.close() + return + + # This function builds vertex map after finding duplicate vertices + # it is essential to do it as two or more vertices may appear as one + # in the editor, yet they may be defined as multiple vertices due to carrying + # different UVs or other attributes. NavMesh needs only coordinates and + # two vertices with the same coordinates are considered as one. + def CreateEdgeMap(self): + mesh = self.mesh + vertexBuffer = {} + i = 0 + for v in mesh.vertices: + # using string of coordinates as a key + key = f'{v.co}' + + if key in vertexBuffer: + vertexBuffer[key].append(i) + else: + vertexBuffer[key] = [i] + i += 1 + + # build substitute array + # value at each index will be either own (no substitute) + # or of another vertex so the keys of edges can be substituted + + vertexSubstitute = [] + for v in mesh.vertices: + key = f'{v.co}' + verts = vertexBuffer[key] + + # store always only first index of vertex + vertexSubstitute.append(verts[0]) + + #for i in range(0, len(vertexSubstitute)): + # print(f'{i} = {vertexSubstitute[i]}') + + # build edge map + # we need to remap edge keys so we can point out to a single edge + # end remove duplicates + # Blender edge key is made of vertex indices sorted + origKeys = [] + for e in mesh.edges: + v0 = e.vertices[0] + v1 = e.vertices[1] + key = [v0, v1] + key.sort() + origKeys.append(key) + #print(key) + + # create new keys by using vertex substitutes + keyMap = {} + edgeMap = {} + edgeIndex = 0 + + for key in origKeys: + newKey = [vertexSubstitute[key[0]], vertexSubstitute[key[1]]] + newKey.sort() + + # turn newkey into hashable string + newKeyStr = f'{newKey}' + + #print(f'{key} -> {newKey}') + + if newKeyStr in keyMap: + keyMap[newKeyStr].append(edgeIndex) + else: + keyMap[newKeyStr] = [edgeIndex] + + # turn original key into string + edgeMap[f'{key}'] = keyMap[newKeyStr][0] + edgeIndex += 1 + + return edgeMap + + + """ + Edge data stores: + Pair of vertices (by index) + Pair of polygons (by index) + + The polygon index is stored as uint16_t. + It is possible that the edge belongs to a single polygon. In such case, + the maximum of 0xFFFFF is stored as an index. + The maximum index of polygon is 0xFFFE (65534). + + The pair of polygons are stored in order to speed up lookup of adjacent + floor faces. + """ + # N x ( f, f, f ), indexed from 0 + def WriteEdges(self): + out = open(os.path.join(self.tempDir, "myvertex-edges.bin"), "wb") + + # add vertices to the edge + edgeFaceList = [] + for edge in self.mesh.edges: + edgeFaceList.append([]) + edgeFaceList[-1].append(edge.vertices[0]) + edgeFaceList[-1].append(edge.vertices[1]) + + edgeMap = self.CreateEdgeMap() + + # for each polygon update the edges + for poly in self.mesh.polygons: + for ek in poly.edge_keys: + ekArray = [ ek[0], ek[1] ] + edgeIndexKey = f'{ekArray}' + edgeIndex = edgeMap[edgeIndexKey] + edgeFaceList[edgeIndex].append(poly.index) + + # make sure each edge contains 4 items + for edge in edgeFaceList: + if len(edge) == 2: + edge.append( 0xffff ) + if len(edge) == 3: + edge.append( 0xffff ) + count = 0 + for edge in edgeFaceList: + if len(edge) == 4: + count += 1 + if count == len(edgeFaceList): + print("all correct!") + # save the data into file + fmt = '@HHHH' + i = 0 + for edge in edgeFaceList: + out.write(struct.pack(fmt, edge[0], edge[1], edge[2], edge[3])) + print(f'i: {i:d}: {edge[0]:d}, {edge[1]:d}, {edge[2]:d}, {edge[3]:d}') + i += 1 + + self.edgeBufferSize = out.tell() + + out.close() + return + + ################################################################### + """ + Writes buffer of polygons. Polys must be triangles. + The data contains: + - vertex indices 3*u16 + - edge indices 3*u16 + - normal vector of poly 3*f32 + - center 3*f32 (we may need center point for navigation) + """ + def WritePolys(self): + out = open(os.path.join(self.tempDir, "myvertex-polys.bin"), "wb") + + # for each polygon update the edges + + polyIndex = 0 + + edgeMap = self.CreateEdgeMap() + + for poly in self.mesh.polygons: + edges = [] + for ek in poly.edge_keys: + ekArray = [ ek[0], ek[1] ] + edgeIndexKey = f'{ekArray}' + edges.append(edgeMap[edgeIndexKey]) + + verts = poly.vertices + norm = poly.normal.copy() + + # transform + print(f'{norm.x:f}, {norm.y:f}, {norm.z:f}') + + center = poly.center + + # calculate normal + + fmt='@HHHHHHffffff' + + out.write(struct.pack(fmt, + verts[0], verts[1], verts[2], + edges[0], edges[1], edges[2], + norm.x, norm.y, norm.z, + center.x, center.y, center.z)) + + print('{edges[0]}, {edges[1]}, {edges[2]}') + self.polyBufferSize = out.tell() + out.close() + return + + def WriteBinary(self): + + out = open(self.outputPath, "wb") + + # write common header fields + c = bytes(CHECKSUM, "ascii") + csum = ctypes.c_uint32( (c[3] << 24) | (c[2] << 16) | (c[1] << 8) | c[0] ).value + fmt = "@II" + out.write(struct.pack(fmt, csum, self.version)) + + # write remaining fields + fmt = '@IIIIIIIfff' + gravity = bpy.context.scene.gravity.copy().normalized() + out.write(struct.pack(fmt, + self.dataOffset, + self.vertexCount, + self.vertexDataOffset, + self.edgeCount, + self.edgeDataOffset, + self.polyCount, + self.polyDataOffset, + gravity.x, gravity.y, gravity.z + )) + + self.bufferHeaderSize = out.tell() + + out.close() + + return + + def Finalize(self): + + self.dataOffset = self.bufferHeaderSize + self.vertexDataOffset = 0 # relative to data offset + self.edgeDataOffset = self.vertexBufferSize + self.polyDataOffset = self.edgeDataOffset + self.edgeBufferSize + + # write header again + self.WriteBinary() + + # concatenate files + + with open(self.outputPath, "ab") as out, open(os.path.join(self.tempDir, "myvertex-vertices-nt.bin"), "rb") as file2: + out.write(file2.read()) + + with open(self.outputPath, "ab") as out, open(os.path.join(self.tempDir, "myvertex-edges.bin"), "rb") as file2: + out.write(file2.read()) + + with open(self.outputPath, "ab") as out, open(os.path.join(self.tempDir, "myvertex-polys.bin"), "rb") as file2: + out.write(file2.read()) + + return + + +################################################################ +# UI + +# Navigation Mesh Exporter +class OBJECT_OT_CustomOperator(bpy.types.Operator): + bl_idname = "object.custom_operator" + bl_label = "Export Navigation Mesh" + + def execute(self, context): + bpy.ops.export.some_data('INVOKE_DEFAULT') + return {'FINISHED'} + +def submenu_func(self, context): + layout = self.layout + layout.operator("object.custom_operator") + +def menu_func(self, context): + layout = self.layout + + # Add a submenu + layout.menu("VIEW3D_MT_custom_submenu", text="DALi") + +class CustomSubmenu(bpy.types.Menu): + bl_idname = "VIEW3D_MT_custom_submenu" + bl_label = "DALi" + + def draw(self, context): + layout = self.layout + layout.operator("object.custom_operator") + +def register(): + bpy.utils.register_class(OBJECT_OT_CustomOperator) + bpy.types.VIEW3D_MT_object_context_menu.append(menu_func) + bpy.utils.register_class(CustomSubmenu) + bpy.utils.register_class(ExportSomeData) +def unregister(): + bpy.utils.unregister_class(OBJECT_OT_CustomOperator) + bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func) + bpy.utils.unregister_class(CustomSubmenu) + bpy.utils.unregister_class(ExportSomeData) + +# exporter +class ExportSomeData(bpy.types.Operator): + bl_idname = "export.some_data" + bl_label = "Export DALi Navigation Mesh" + + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + @classmethod + def poll(cls, context): + return context.object is not None + + def execute(self, context): + print(self.filepath) + navmesh = NavMeshHeader10( self.filepath ) + navmesh.Process() + navmesh.WriteBinary() + navmesh.WriteVertices( False ) + navmesh.WriteEdges() + navmesh.WritePolys() + navmesh.Finalize() + return {'FINISHED'} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + +if __name__ == "__main__": + register() diff --git a/tools/navigation-mesh-exporter/README.md b/tools/navigation-mesh-exporter/README.md new file mode 100644 index 0000000..fa8be14 --- /dev/null +++ b/tools/navigation-mesh-exporter/README.md @@ -0,0 +1,18 @@ +NavigationMeshExporter +====================== + +Script for Blender which allows exporting navigation (collision) mesh data. + +Register new operator: +1. Open script in Blender (or copy paste as internal) +2. Execute +3. Nothing will happen but new operator will register + +Usage: + +1. Press RMB on the object to export +2. Go to "DALi" group +3. Select "Export Navigation Mesh" + +![Popup menu](screen.png) + diff --git a/tools/navigation-mesh-exporter/screen.png b/tools/navigation-mesh-exporter/screen.png new file mode 100644 index 0000000..fda7c01 Binary files /dev/null and b/tools/navigation-mesh-exporter/screen.png differ