From 2d24606374d4ac69a2dfb933101b6692c6168d6d Mon Sep 17 00:00:00 2001 From: Lubos Lenco Date: Wed, 22 Jun 2016 10:32:19 +0200 Subject: [PATCH] Fast geometry processing. --- blender/armory.py | 391 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 275 insertions(+), 116 deletions(-) diff --git a/blender/armory.py b/blender/armory.py index d6145b4f..cb9f086f 100644 --- a/blender/armory.py +++ b/blender/armory.py @@ -1,37 +1,31 @@ -# ============================================================= -# Armory Scene Exporter -# http://lue3d.org/ -# by Lubos Lenco +# Armory Scene Exporter by Lubos Lenco +# http://armory3d.org/ # -# Based on -# Open Game Engine Exchange +# Based on Open Game Engine Exchange # http://opengex.org/ -# -# Export plugin for Blender -# by Eric Lengyel -# Version 1.1.2.2 +# Export plugin for Blender by Eric Lengyel # Copyright 2015, Terathon Software LLC # # This software is licensed under the Creative Commons # Attribution-ShareAlike 3.0 Unported License: # http://creativecommons.org/licenses/by-sa/3.0/deed.en_US -# -# ============================================================= bl_info = { "name": "Armory format (.json)", "description": "Armory Exporter", - "author": "Eric Lengyel, Armory by Lubos Lenco", + "author": "Eric Lengyel, Lubos Lenco", "version": (1, 0, 0), "location": "File > Import-Export", - "wiki_url": "http://lue3d.org/", + "wiki_url": "http://armory3d.org/", "category": "Import-Export"} import os import bpy import math from mathutils import * +import time import json +import numpy import ast import write_probes from bpy_extras.io_utils import ExportHelper @@ -59,6 +53,57 @@ deltaSubrotationName = ["dxrot", "dyrot", "dzrot"] deltaSubscaleName = ["dxscl", "dyscl", "dzscl"] axisName = ["x", "y", "z"] +class Object: + def to_JSON(self): + if ArmoryExporter.option_minimize: + return json.dumps(self, default=lambda o: o.__dict__, separators=(',',':')) + else: + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + +class Vertex: + __slots__ = ("co", "normal", "uvs", "col", "loop_indices", "index", "weights", "joint_indexes") + def __init__(self, mesh, loop): + vi = loop.vertex_index + i = loop.index + self.co = mesh.vertices[vi].co.freeze() + self.normal = loop.normal.freeze() + self.uvs = tuple(layer.data[i].uv.freeze() for layer in mesh.uv_layers) + self.col = [0, 0, 0] + if len(mesh.vertex_colors) > 0: + self.col = mesh.vertex_colors[0].data[i].color.freeze() + self.loop_indices = [i] + + # Take the four most influential groups + groups = sorted(mesh.vertices[vi].groups, key=lambda group: group.weight, reverse=True) + if len(groups) > 4: + groups = groups[:4] + + self.weights = [group.weight for group in groups] + self.joint_indexes = [group.group for group in groups] + + if len(self.weights) < 4: + for i in range(len(self.weights), 4): + self.weights.append(0.0) + self.joint_indexes.append(0) + + self.index = 0 + + def __hash__(self): + return hash((self.co, self.normal, self.uvs)) + + def __eq__(self, other): + eq = ( + (self.co == other.co) and + (self.normal == other.normal) and + (self.uvs == other.uvs) + ) + + if eq: + indices = self.loop_indices + other.loop_indices + self.loop_indices = indices + other.loop_indices = indices + return eq + class ExportVertex: __slots__ = ("hash", "vertexIndex", "faceIndex", "position", "normal", "color", "texcoord0", "texcoord1") @@ -74,12 +119,12 @@ class ExportVertex: return (False) if (self.normal != v.normal): return (False) - if (self.color != v.color): - return (False) if (self.texcoord0 != v.texcoord0): return (False) - if (self.texcoord1 != v.texcoord1): + if (self.color != v.color): return (False) + if (self.texcoord1 != v.texcoord1): + return (False) return (True) def Hash(self): @@ -98,13 +143,6 @@ class ExportVertex: h = h * 21737 + hash(self.texcoord1[1]) self.hash = h -class Object: - def to_JSON(self): - if ArmoryExporter.option_minimize: - return json.dumps(self, default=lambda o: o.__dict__, separators=(',',':')) - else: - return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) - class ArmoryExporter(bpy.types.Operator, ExportHelper): """Export to Armory format""" bl_idname = "export_scene.armory" @@ -117,9 +155,6 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): option_geometry_per_file = bpy.props.BoolProperty(name = "Export Geometry Per File", description = "Export each geometry to individual JSON files", default = False) option_minimize = bpy.props.BoolProperty(name = "Export Minimized", description = "Export minimized JSON data", default = True) - def WriteColor(self, color): - return [color[0], color[1], color[2]] - def WriteMatrix(self, matrix): return [matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3], matrix[1][0], matrix[1][1], matrix[1][2], matrix[1][3], @@ -181,12 +216,6 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): return va - def WriteInt(self, i): - return i - - def WriteFloat(self, f): - return f - def WriteTriangle(self, triangleIndex, indexTable): i = triangleIndex * 3 return [indexTable[i], indexTable[i + 1], indexTable[i + 2]] @@ -507,6 +536,12 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): @staticmethod def UnifyVertices(exportVertexArray, indexTable): + + # Non-indexed + # for i in range(len(exportVertexArray)): + # indexTable.append(i) + # return exportVertexArray + # This function looks for identical vertices having exactly the same position, normal, # color, and texcoords. Duplicate vertices are unified, and a new index table is returned. bucketCount = len(exportVertexArray) >> 3 @@ -526,15 +561,21 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): for i in range(len(exportVertexArray)): ev = exportVertexArray[i] bucket = ev.hash & (bucketCount - 1) - index = ArmoryExporter.FindExportVertex(hashTable[bucket], exportVertexArray, ev) - if (index < 0): + + index = -1 + for b in hashTable[bucket]: + if exportVertexArray[b] == ev: + index = b + break + + if index < 0: indexTable.append(len(unifiedVertexArray)) unifiedVertexArray.append(ev) hashTable[bucket].append(i) else: indexTable.append(indexTable[index]) - return (unifiedVertexArray) + return unifiedVertexArray def ExportBone(self, armature, bone, scene, o): nodeRef = self.nodeArray.get(bone) @@ -590,9 +631,9 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): o.animation.track.time.values = [] for i in range(self.beginFrame, self.endFrame): - o.animation.track.time.values.append(self.WriteFloat((i - self.beginFrame) * self.frameTime)) + o.animation.track.time.values.append(((i - self.beginFrame) * self.frameTime)) - o.animation.track.time.values.append(self.WriteFloat(self.endFrame * self.frameTime)) + o.animation.track.time.values.append((self.endFrame * self.frameTime)) o.animation.track.value = Object() o.animation.track.value.values = [] @@ -629,9 +670,9 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): o.animation.track.time.values = [] for i in range(self.beginFrame, self.endFrame): - o.animation.track.time.values.append(self.WriteFloat((i - self.beginFrame) * self.frameTime)) + o.animation.track.time.values.append(((i - self.beginFrame) * self.frameTime)) - o.animation.track.time.values.append(self.WriteFloat(self.endFrame * self.frameTime)) + o.animation.track.time.values.append((self.endFrame * self.frameTime)) o.animation.track.value = Object() o.animation.track.value.values = [] @@ -1326,6 +1367,182 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): # Write the bone weight array. The number of entries is the sum of the bone counts for all vertices. om.skin.bone_weight_array = boneWeightArray + def calc_tangents(self, posa, nora, uva, ia): + triangle_count = int(len(ia) / 3) + vertex_count = int(len(posa) / 3) + tangents = [0] * vertex_count * 3 + # bitangents = [0] * vertex_count * 3 + for i in range(0, triangle_count): + i0 = ia[i * 3 + 0] + i1 = ia[i * 3 + 1] + i2 = ia[i * 3 + 2] + # TODO: Slow + v0 = Vector((posa[i0 * 3 + 0], posa[i0 * 3 + 1], posa[i0 * 3 + 2])) + v1 = Vector((posa[i1 * 3 + 0], posa[i1 * 3 + 1], posa[i1 * 3 + 2])) + v2 = Vector((posa[i2 * 3 + 0], posa[i2 * 3 + 1], posa[i2 * 3 + 2])) + uv0 = Vector((uva[i0 * 2 + 0], uva[i0 * 2 + 1])) + uv1 = Vector((uva[i1 * 2 + 0], uva[i1 * 2 + 1])) + uv2 = Vector((uva[i2 * 2 + 0], uva[i2 * 2 + 1])) + + tangent = ArmoryExporter.calc_tangent(v0, v1, v2, uv0, uv1, uv2) + + tangents[i0 * 3 + 0] += tangent.x + tangents[i0 * 3 + 1] += tangent.y + tangents[i0 * 3 + 2] += tangent.z + tangents[i1 * 3 + 0] += tangent.x + tangents[i1 * 3 + 1] += tangent.y + tangents[i1 * 3 + 2] += tangent.z + tangents[i2 * 3 + 0] += tangent.x + tangents[i2 * 3 + 1] += tangent.y + tangents[i2 * 3 + 2] += tangent.z + # bitangents[i0 * 3 + 0] += bitangent.x + # bitangents[i0 * 3 + 1] += bitangent.y + # bitangents[i0 * 3 + 2] += bitangent.z + # bitangents[i1 * 3 + 0] += bitangent.x + # bitangents[i1 * 3 + 1] += bitangent.y + # bitangents[i1 * 3 + 2] += bitangent.z + # bitangents[i2 * 3 + 0] += bitangent.x + # bitangents[i2 * 3 + 1] += bitangent.y + # bitangents[i2 * 3 + 2] += bitangent.z + + # Orthogonalize + for i in range(0, vertex_count): + # Slow + t = Vector((tangents[i * 3], tangents[i * 3 + 1], tangents[i * 3 + 2])) + # b = Vector((bitangents[i * 3], bitangents[i * 3 + 1], bitangents[i * 3 + 2])) + n = Vector((nora[i * 3], nora[i * 3 + 1], nora[i * 3 + 2])) + v = t - n * n.dot(t) + v.normalize() + # Calculate handedness + # cnv = n.cross(v) + # if cnv.dot(b) < 0.0: + # v = v * -1.0 + tangents[i * 3] = v.x + tangents[i * 3 + 1] = v.y + tangents[i * 3 + 2] = v.z + return tangents + + def write_geometry(self, node, fp, o): + # One geometry data per file + if ArmoryExporter.option_geometry_per_file: + geom_obj = Object() + geom_obj.geometry_resources = [o] + with open(fp, 'w') as f: + f.write(geom_obj.to_JSON()) + self.node_set_geometry_cached(node, True) + else: + self.output.geometry_resources.append(o) + + def export_geometry_fast(self, exportMesh, node, fp, o, om): + # Much faster export but produces slightly less efficient data + exportMesh.calc_normals_split() + exportMesh.calc_tessface() + vert_list = { Vertex(exportMesh, loop) : 0 for loop in exportMesh.loops}.keys() + num_verts = len(vert_list) + num_uv_layers = len(exportMesh.uv_layers) + num_colors = len(exportMesh.vertex_colors) + vdata = [0] * num_verts * 3 + ndata = [0] * num_verts * 3 + if num_uv_layers > 0: + t0data = [0] * num_verts * 2 + if num_uv_layers > 1: + t1data = [0] * num_verts * 2 + if num_colors > 0: + cdata = [0] * num_verts * 3 + # Make arrays + for i, vtx in enumerate(vert_list): + vtx.index = i + co = vtx.co + normal = vtx.normal + for j in range(3): + vdata[(i * 3) + j] = co[j] + ndata[(i * 3) + j] = normal[j] + if num_uv_layers > 0: + t0data[i * 2] = vtx.uvs[0].x + t0data[i * 2 + 1] = vtx.uvs[0].y + if num_uv_layers > 1: + t1data[i * 2] = vtx.uvs[1].x + t1data[i * 2 + 1] = vtx.uvs[1].y + if num_colors > 0: + cdata[i * 3] = vtx.cols[0] + cdata[i * 3 + 1] = vtx.cols[1] + cdata[i * 3 + 2] = vtx.cols[2] + # Output + om.vertex_arrays = [] + pa = Object() + pa.attrib = "position" + pa.size = 3 + pa.values = vdata + om.vertex_arrays.append(pa) + na = Object() + na.attrib = "normal" + na.size = 3 + na.values = ndata + om.vertex_arrays.append(na) + if num_uv_layers > 0: + ta = Object() + ta.attrib = "texcoord" + ta.size = 2 + ta.values = t0data + om.vertex_arrays.append(ta) + if num_uv_layers > 1: + ta2 = Object() + ta2.attrib = "texcoord1" + ta2.size = 2 + ta2.values = t1data + om.vertex_arrays.append(ta2) + if num_colors > 0: + ca = Object() + ca.attrib = "color" + ca.size = 3 + ca.values = cdata + om.vertex_arrays.append(ca) + + # Indices + prims = {ma.name if ma else '': [] for ma in exportMesh.materials} + if not prims: + prims = {'': []} + + vert_dict = {i : v for v in vert_list for i in v.loop_indices} + for poly in exportMesh.polygons: + first = poly.loop_start + if len(exportMesh.materials) == 0: + prim = prims[''] + else: + mat = exportMesh.materials[poly.material_index] + prim = prims[mat.name if mat else ''] + indices = [vert_dict[i].index for i in range(first, first+poly.loop_total)] + + if poly.loop_total == 3: + prim += indices + elif poly.loop_total > 3: + for i in range(poly.loop_total-1): + prim += (indices[-1], indices[i], indices[i + 1]) + + # Write indices + om.index_arrays = [] + for mat, prim in prims.items(): + idata = [0] * len(prim) + for i, v in enumerate(prim): + idata[i] = v + ia = Object() + ia.size = 3 + ia.values = idata + ia.material = len(om.index_arrays) + om.index_arrays.append(ia) + + # Make tangents + if (self.get_export_tangents(exportMesh) == True and num_uv_layers > 0): + tana = Object() + tana.attrib = "tangent" + tana.size = 3 + tana.values = self.calc_tangents(pa.values, na.values, ta.values, om.index_arrays[0].values) + om.vertex_arrays.append(tana) + + # Write + o.mesh = om + self.write_geometry(node, fp, o) + def ExportGeometry(self, objectRef, scene): # This function exports a single geometry object. node = objectRef[1]["nodeTable"][0] @@ -1342,8 +1559,6 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): o = Object() o.id = oid - #self.WriteNodeTable(objectRef) #// # TODO - mesh = objectRef[0] structFlag = False; @@ -1409,6 +1624,12 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): # arbitrary stage in the modifier stack. exportMesh = node.to_mesh(scene, applyModifiers, "RENDER", True, False) + + # Default to fast for now + self.export_geometry_fast(exportMesh, node, fp, o, om) + # self.export_geometry_quality(exportMesh, node, fp, o, om) + + def export_geometry_quality(self, exportMesh, node, fp, o, om): # Triangulate mesh and remap vertices to eliminate duplicates. materialTable = [] exportVertexArray = ArmoryExporter.DeindexMesh(exportMesh, materialTable) @@ -1455,7 +1676,7 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): if (texcoordCount > 1): ta2 = Object() - ta2.attrib = "texcoord[1]" + ta2.attrib = "texcoord1" ta2.size = 2 ta2.values = self.WriteVertexArray2D(unifiedVertexArray, "texcoord1") om.vertex_arrays.append(ta2) @@ -1520,9 +1741,8 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): ia = Object() ia.size = 3 ia.values = self.WriteTriangleArray(triangleCount, indexTable) - ia.material = self.WriteInt(0) + ia.material = 0 om.index_arrays.append(ia) - else: # If there are multiple material indexes, then write a separate index array for each one. materialTriangleCount = [0 for i in range(maxMaterialIndex + 1)] @@ -1542,72 +1762,15 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): ia = Object() ia.size = 3 ia.values = self.WriteTriangleArray(materialTriangleCount[m], materialIndexTable) - ia.material = self.WriteInt(m) + ia.material = m om.index_arrays.append(ia) # Export tangents - if (self.get_export_tangents(exportMesh) == True and len(exportMesh.uv_textures) > 0): - ia = om.index_arrays[0].values - posa = pa.values - uva = ta.values - triangle_count = int(len(ia) / 3) - vertex_count = int(len(posa) / 3) - tangents = [0] * vertex_count * 3 - # bitangents = [0] * vertex_count * 3 - for i in range(0, triangle_count): - i0 = ia[i * 3 + 0] - i1 = ia[i * 3 + 1] - i2 = ia[i * 3 + 2] - # TODO: Slow - v0 = Vector((posa[i0 * 3 + 0], posa[i0 * 3 + 1], posa[i0 * 3 + 2])) - v1 = Vector((posa[i1 * 3 + 0], posa[i1 * 3 + 1], posa[i1 * 3 + 2])) - v2 = Vector((posa[i2 * 3 + 0], posa[i2 * 3 + 1], posa[i2 * 3 + 2])) - uv0 = Vector((uva[i0 * 2 + 0], uva[i0 * 2 + 1])) - uv1 = Vector((uva[i1 * 2 + 0], uva[i1 * 2 + 1])) - uv2 = Vector((uva[i2 * 2 + 0], uva[i2 * 2 + 1])) - - tangent = ArmoryExporter.calc_tangent(v0, v1, v2, uv0, uv1, uv2) - - tangents[i0 * 3 + 0] += tangent.x - tangents[i0 * 3 + 1] += tangent.y - tangents[i0 * 3 + 2] += tangent.z - tangents[i1 * 3 + 0] += tangent.x - tangents[i1 * 3 + 1] += tangent.y - tangents[i1 * 3 + 2] += tangent.z - tangents[i2 * 3 + 0] += tangent.x - tangents[i2 * 3 + 1] += tangent.y - tangents[i2 * 3 + 2] += tangent.z - # bitangents[i0 * 3 + 0] += bitangent.x - # bitangents[i0 * 3 + 1] += bitangent.y - # bitangents[i0 * 3 + 2] += bitangent.z - # bitangents[i1 * 3 + 0] += bitangent.x - # bitangents[i1 * 3 + 1] += bitangent.y - # bitangents[i1 * 3 + 2] += bitangent.z - # bitangents[i2 * 3 + 0] += bitangent.x - # bitangents[i2 * 3 + 1] += bitangent.y - # bitangents[i2 * 3 + 2] += bitangent.z - - # Orthogonalize - nora = na.values - for i in range(0, vertex_count): - # TODO: Slow - t = Vector((tangents[i * 3], tangents[i * 3 + 1], tangents[i * 3 + 2])) - # b = Vector((bitangents[i * 3], bitangents[i * 3 + 1], bitangents[i * 3 + 2])) - n = Vector((nora[i * 3], nora[i * 3 + 1], nora[i * 3 + 2])) - v = t - n * n.dot(t) - v.normalize() - # Calculate handedness - # cnv = n.cross(v) - # if cnv.dot(b) < 0.0: - # v = v * -1.0 - tangents[i * 3] = v.x - tangents[i * 3 + 1] = v.y - tangents[i * 3 + 2] = v.z - + if (self.get_export_tangents(exportMesh) == True and len(exportMesh.uv_textures) > 0): tana = Object() tana.attrib = "tangent" tana.size = 3 - tana.values = tangents + tana.values = self.calc_tangents(pa.values, na.values, ta.values, om.index_arrays[0].values) om.vertex_arrays.append(tana) # If the mesh is skinned, export the skinning data here. @@ -1635,16 +1798,7 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): bpy.data.meshes.remove(exportMesh) o.mesh = om - - # One geometry data per file - if ArmoryExporter.option_geometry_per_file: - geom_obj = Object() - geom_obj.geometry_resources = [o] - with open(fp, 'w') as f: - f.write(geom_obj.to_JSON()) - self.node_set_geometry_cached(node, True) - else: - self.output.geometry_resources.append(o) + self.write_geometry(node, fp, o) def ExportLight(self, objectRef): # This function exports a single light object. @@ -1672,7 +1826,8 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): # Parse nodes, only emission for now for n in object.node_tree.nodes: if n.type == 'EMISSION': - o.color = self.WriteColor(n.inputs[0].default_value) + col = n.inputs[0].default_value + o.color = [col[0], col[1], col[2]] o.strength = n.inputs[1].default_value / 1000.0 break @@ -1768,6 +1923,8 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): self.ExportGeometry(objectRef, scene) def execute(self, context): + profile_time = time.time() + self.output = Object() scene = context.scene @@ -1837,6 +1994,8 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): with open(self.filepath, 'w') as f: f.write(self.output.to_JSON()) + print('Scene built in ' + str(time.time() - profile_time)) + return {'FINISHED'} # Callbacks