Fast geometry processing.

This commit is contained in:
Lubos Lenco 2016-06-22 10:32:19 +02:00
parent fc92e5a556
commit 2d24606374

View file

@ -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