|
@@ -0,0 +1,2409 @@
|
|
|
|
+# ##### BEGIN GPL LICENSE BLOCK #####
|
|
|
|
+#
|
|
|
|
+# This program is free software; you can redistribute it and/or
|
|
|
|
+# modify it under the terms of the GNU General Public License
|
|
|
|
+# as published by the Free Software Foundation; either version 2
|
|
|
|
+# of the License, or (at your option) any later version.
|
|
|
|
+#
|
|
|
|
+# This program is distributed in the hope that it will be useful,
|
|
|
|
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
+# GNU General Public License for more details.
|
|
|
|
+#
|
|
|
|
+# You should have received a copy of the GNU General Public License
|
|
|
|
+# along with this program; if not, write to the Free Software Foundation,
|
|
|
|
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
+#
|
|
|
|
+# ##### END GPL LICENSE BLOCK #####
|
|
|
|
+
|
|
|
|
+"""
|
|
|
|
+Blender exporter for Three.js (ASCII JSON format).
|
|
|
|
+
|
|
|
|
+TODO
|
|
|
|
+ - binary format
|
|
|
|
+"""
|
|
|
|
+
|
|
|
|
+import bpy
|
|
|
|
+import mathutils
|
|
|
|
+
|
|
|
|
+import shutil
|
|
|
|
+import os
|
|
|
|
+import os.path
|
|
|
|
+import math
|
|
|
|
+import operator
|
|
|
|
+import random
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Configuration
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+DEFAULTS = {
|
|
|
|
+"bgcolor" : [0, 0, 0],
|
|
|
|
+"bgalpha" : 1.0,
|
|
|
|
+
|
|
|
|
+"position" : [0, 0, 0],
|
|
|
|
+"rotation" : [-math.pi/2, 0, 0],
|
|
|
|
+"scale" : [1, 1, 1],
|
|
|
|
+
|
|
|
|
+"camera" :
|
|
|
|
+ {
|
|
|
|
+ "name" : "default_camera",
|
|
|
|
+ "type" : "PerspectiveCamera",
|
|
|
|
+ "near" : 1,
|
|
|
|
+ "far" : 10000,
|
|
|
|
+ "fov" : 60,
|
|
|
|
+ "aspect": 1.333,
|
|
|
|
+ "position" : [0, 0, 10],
|
|
|
|
+ "target" : [0, 0, 0]
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+"light" :
|
|
|
|
+ {
|
|
|
|
+ "name" : "default_light",
|
|
|
|
+ "type" : "DirectionalLight",
|
|
|
|
+ "direction" : [0, 1, 1],
|
|
|
|
+ "color" : [1, 1, 1],
|
|
|
|
+ "intensity" : 0.8
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+# default colors for debugging (each material gets one distinct color):
|
|
|
|
+# white, red, green, blue, yellow, cyan, magenta
|
|
|
|
+COLORS = [0xeeeeee, 0xee0000, 0x00ee00, 0x0000ee, 0xeeee00, 0x00eeee, 0xee00ee]
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+# skinning
|
|
|
|
+MAX_INFLUENCES = 2
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Templates - scene
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+TEMPLATE_SCENE_ASCII = """\
|
|
|
|
+{
|
|
|
|
+
|
|
|
|
+"metadata" :
|
|
|
|
+{
|
|
|
|
+ "formatVersion" : 3.2,
|
|
|
|
+ "type" : "scene",
|
|
|
|
+ "sourceFile" : "%(fname)s",
|
|
|
|
+ "generatedBy" : "Blender 2.65 Exporter",
|
|
|
|
+ "objects" : %(nobjects)s,
|
|
|
|
+ "geometries" : %(ngeometries)s,
|
|
|
|
+ "materials" : %(nmaterials)s,
|
|
|
|
+ "textures" : %(ntextures)s
|
|
|
|
+},
|
|
|
|
+
|
|
|
|
+"urlBaseType" : %(basetype)s,
|
|
|
|
+
|
|
|
|
+%(sections)s
|
|
|
|
+
|
|
|
|
+"transform" :
|
|
|
|
+{
|
|
|
|
+ "position" : %(position)s,
|
|
|
|
+ "rotation" : %(rotation)s,
|
|
|
|
+ "scale" : %(scale)s
|
|
|
|
+},
|
|
|
|
+
|
|
|
|
+"defaults" :
|
|
|
|
+{
|
|
|
|
+ "bgcolor" : %(bgcolor)s,
|
|
|
|
+ "bgalpha" : %(bgalpha)f,
|
|
|
|
+ "camera" : %(defcamera)s
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+}
|
|
|
|
+"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_SECTION = """
|
|
|
|
+"%s" :
|
|
|
|
+{
|
|
|
|
+%s
|
|
|
|
+},
|
|
|
|
+"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_OBJECT = """\
|
|
|
|
+ %(object_id)s : {
|
|
|
|
+ "geometry" : %(geometry_id)s,
|
|
|
|
+ "groups" : [ %(group_id)s ],
|
|
|
|
+ "material" : %(material_id)s,
|
|
|
|
+ "position" : %(position)s,
|
|
|
|
+ "rotation" : %(rotation)s,
|
|
|
|
+ "quaternion": %(quaternion)s,
|
|
|
|
+ "scale" : %(scale)s,
|
|
|
|
+ "visible" : %(visible)s,
|
|
|
|
+ "castShadow" : %(castShadow)s,
|
|
|
|
+ "receiveShadow" : %(receiveShadow)s,
|
|
|
|
+ "doubleSided" : %(doubleSided)s
|
|
|
|
+ }"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_EMPTY = """\
|
|
|
|
+ %(object_id)s : {
|
|
|
|
+ "groups" : [ %(group_id)s ],
|
|
|
|
+ "position" : %(position)s,
|
|
|
|
+ "rotation" : %(rotation)s,
|
|
|
|
+ "quaternion": %(quaternion)s,
|
|
|
|
+ "scale" : %(scale)s
|
|
|
|
+ }"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_GEOMETRY_LINK = """\
|
|
|
|
+ %(geometry_id)s : {
|
|
|
|
+ "type" : "ascii",
|
|
|
|
+ "url" : %(model_file)s
|
|
|
|
+ }"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_GEOMETRY_EMBED = """\
|
|
|
|
+ %(geometry_id)s : {
|
|
|
|
+ "type" : "embedded",
|
|
|
|
+ "id" : %(embed_id)s
|
|
|
|
+ }"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_TEXTURE = """\
|
|
|
|
+ %(texture_id)s : {
|
|
|
|
+ "url": %(texture_file)s%(extras)s
|
|
|
|
+ }"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_MATERIAL_SCENE = """\
|
|
|
|
+ %(material_id)s : {
|
|
|
|
+ "type": %(type)s,
|
|
|
|
+ "parameters": { %(parameters)s }
|
|
|
|
+ }"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_CAMERA_PERSPECTIVE = """\
|
|
|
|
+ %(camera_id)s : {
|
|
|
|
+ "type" : "PerspectiveCamera",
|
|
|
|
+ "fov" : %(fov)f,
|
|
|
|
+ "aspect": %(aspect)f,
|
|
|
|
+ "near" : %(near)f,
|
|
|
|
+ "far" : %(far)f,
|
|
|
|
+ "position": %(position)s,
|
|
|
|
+ "target" : %(target)s
|
|
|
|
+ }"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_CAMERA_ORTHO = """\
|
|
|
|
+ %(camera_id)s : {
|
|
|
|
+ "type" : "OrthographicCamera",
|
|
|
|
+ "left" : %(left)f,
|
|
|
|
+ "right" : %(right)f,
|
|
|
|
+ "top" : %(top)f,
|
|
|
|
+ "bottom": %(bottom)f,
|
|
|
|
+ "near" : %(near)f,
|
|
|
|
+ "far" : %(far)f,
|
|
|
|
+ "position": %(position)s,
|
|
|
|
+ "target" : %(target)s
|
|
|
|
+ }"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_LIGHT_DIRECTIONAL = """\
|
|
|
|
+ %(light_id)s : {
|
|
|
|
+ "type" : "DirectionalLight",
|
|
|
|
+ "direction" : %(direction)s,
|
|
|
|
+ "color" : %(color)d,
|
|
|
|
+ "intensity" : %(intensity).2f
|
|
|
|
+ }"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_LIGHT_POINT = """\
|
|
|
|
+ %(light_id)s : {
|
|
|
|
+ "type" : "PointLight",
|
|
|
|
+ "position" : %(position)s,
|
|
|
|
+ "color" : %(color)d,
|
|
|
|
+ "intensity" : %(intensity).3f
|
|
|
|
+ }"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_VEC4 = '[ %g, %g, %g, %g ]'
|
|
|
|
+TEMPLATE_VEC3 = '[ %g, %g, %g ]'
|
|
|
|
+TEMPLATE_VEC2 = '[ %g, %g ]'
|
|
|
|
+TEMPLATE_STRING = '"%s"'
|
|
|
|
+TEMPLATE_HEX = "0x%06x"
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Templates - model
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+TEMPLATE_FILE_ASCII = """\
|
|
|
|
+{
|
|
|
|
+
|
|
|
|
+ "metadata" :
|
|
|
|
+ {
|
|
|
|
+ "formatVersion" : 3.1,
|
|
|
|
+ "generatedBy" : "Blender 2.65 Exporter",
|
|
|
|
+ "vertices" : %(nvertex)d,
|
|
|
|
+ "faces" : %(nface)d,
|
|
|
|
+ "normals" : %(nnormal)d,
|
|
|
|
+ "colors" : %(ncolor)d,
|
|
|
|
+ "uvs" : [%(nuvs)s],
|
|
|
|
+ "materials" : %(nmaterial)d,
|
|
|
|
+ "morphTargets" : %(nmorphTarget)d,
|
|
|
|
+ "bones" : %(nbone)d
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+%(model)s
|
|
|
|
+
|
|
|
|
+}
|
|
|
|
+"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_MODEL_ASCII = """\
|
|
|
|
+ "scale" : %(scale)f,
|
|
|
|
+
|
|
|
|
+ "materials" : [%(materials)s],
|
|
|
|
+
|
|
|
|
+ "vertices" : [%(vertices)s],
|
|
|
|
+
|
|
|
|
+ "morphTargets" : [%(morphTargets)s],
|
|
|
|
+
|
|
|
|
+ "normals" : [%(normals)s],
|
|
|
|
+
|
|
|
|
+ "colors" : [%(colors)s],
|
|
|
|
+
|
|
|
|
+ "uvs" : [%(uvs)s],
|
|
|
|
+
|
|
|
|
+ "faces" : [%(faces)s],
|
|
|
|
+
|
|
|
|
+ "bones" : [%(bones)s],
|
|
|
|
+
|
|
|
|
+ "skinIndices" : [%(indices)s],
|
|
|
|
+
|
|
|
|
+ "skinWeights" : [%(weights)s],
|
|
|
|
+
|
|
|
|
+ "animation" : {%(animation)s}
|
|
|
|
+"""
|
|
|
|
+
|
|
|
|
+TEMPLATE_VERTEX = "%g,%g,%g"
|
|
|
|
+TEMPLATE_VERTEX_TRUNCATE = "%d,%d,%d"
|
|
|
|
+
|
|
|
|
+TEMPLATE_N = "%g,%g,%g"
|
|
|
|
+TEMPLATE_UV = "%g,%g"
|
|
|
|
+TEMPLATE_C = "%d"
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Utils
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def veckey3(x,y,z):
|
|
|
|
+ return round(x, 6), round(y, 6), round(z, 6)
|
|
|
|
+
|
|
|
|
+def veckey3d(v):
|
|
|
|
+ return veckey3(v.x, v.y, v.z)
|
|
|
|
+
|
|
|
|
+def veckey2d(v):
|
|
|
|
+ return round(v[0], 6), round(v[1], 6)
|
|
|
|
+
|
|
|
|
+def get_faces(obj):
|
|
|
|
+ if hasattr(obj, "tessfaces"):
|
|
|
|
+ return obj.tessfaces
|
|
|
|
+ else:
|
|
|
|
+ return obj.faces
|
|
|
|
+
|
|
|
|
+def get_normal_indices(v, normals, mesh):
|
|
|
|
+ n = []
|
|
|
|
+ mv = mesh.vertices
|
|
|
|
+
|
|
|
|
+ for i in v:
|
|
|
|
+ normal = mv[i].normal
|
|
|
|
+ key = veckey3d(normal)
|
|
|
|
+
|
|
|
|
+ n.append( normals[key] )
|
|
|
|
+
|
|
|
|
+ return n
|
|
|
|
+
|
|
|
|
+def get_uv_indices(face_index, uvs, mesh, layer_index):
|
|
|
|
+ uv = []
|
|
|
|
+ uv_layer = mesh.tessface_uv_textures[layer_index].data
|
|
|
|
+ for i in uv_layer[face_index].uv:
|
|
|
|
+ uv.append( uvs[veckey2d(i)] )
|
|
|
|
+ return uv
|
|
|
|
+
|
|
|
|
+def get_color_indices(face_index, colors, mesh):
|
|
|
|
+ c = []
|
|
|
|
+ color_layer = mesh.tessface_vertex_colors.active.data
|
|
|
|
+ face_colors = color_layer[face_index]
|
|
|
|
+ face_colors = face_colors.color1, face_colors.color2, face_colors.color3, face_colors.color4
|
|
|
|
+ for i in face_colors:
|
|
|
|
+ c.append( colors[hexcolor(i)] )
|
|
|
|
+ return c
|
|
|
|
+
|
|
|
|
+def rgb2int(rgb):
|
|
|
|
+ color = (int(rgb[0]*255) << 16) + (int(rgb[1]*255) << 8) + int(rgb[2]*255);
|
|
|
|
+ return color
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Utils - files
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def write_file(fname, content):
|
|
|
|
+ out = open(fname, "w")
|
|
|
|
+ out.write(content)
|
|
|
|
+ out.close()
|
|
|
|
+
|
|
|
|
+def ensure_folder_exist(foldername):
|
|
|
|
+ """Create folder (with whole path) if it doesn't exist yet."""
|
|
|
|
+
|
|
|
|
+ if not os.access(foldername, os.R_OK|os.W_OK|os.X_OK):
|
|
|
|
+ os.makedirs(foldername)
|
|
|
|
+
|
|
|
|
+def ensure_extension(filepath, extension):
|
|
|
|
+ if not filepath.lower().endswith(extension):
|
|
|
|
+ filepath += extension
|
|
|
|
+ return filepath
|
|
|
|
+
|
|
|
|
+def generate_mesh_filename(meshname, filepath):
|
|
|
|
+ normpath = os.path.normpath(filepath)
|
|
|
|
+ path, ext = os.path.splitext(normpath)
|
|
|
|
+ return "%s.%s%s" % (path, meshname, ext)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Utils - alignment
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def bbox(vertices):
|
|
|
|
+ """Compute bounding box of vertex array.
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ if len(vertices)>0:
|
|
|
|
+ minx = maxx = vertices[0].co.x
|
|
|
|
+ miny = maxy = vertices[0].co.y
|
|
|
|
+ minz = maxz = vertices[0].co.z
|
|
|
|
+
|
|
|
|
+ for v in vertices[1:]:
|
|
|
|
+ if v.co.x < minx:
|
|
|
|
+ minx = v.co.x
|
|
|
|
+ elif v.co.x > maxx:
|
|
|
|
+ maxx = v.co.x
|
|
|
|
+
|
|
|
|
+ if v.co.y < miny:
|
|
|
|
+ miny = v.co.y
|
|
|
|
+ elif v.co.y > maxy:
|
|
|
|
+ maxy = v.co.y
|
|
|
|
+
|
|
|
|
+ if v.co.z < minz:
|
|
|
|
+ minz = v.co.z
|
|
|
|
+ elif v.co.z > maxz:
|
|
|
|
+ maxz = v.co.z
|
|
|
|
+
|
|
|
|
+ return { 'x':[minx,maxx], 'y':[miny,maxy], 'z':[minz,maxz] }
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+ return { 'x':[0,0], 'y':[0,0], 'z':[0,0] }
|
|
|
|
+
|
|
|
|
+def translate(vertices, t):
|
|
|
|
+ """Translate array of vertices by vector t.
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ for i in range(len(vertices)):
|
|
|
|
+ vertices[i].co.x += t[0]
|
|
|
|
+ vertices[i].co.y += t[1]
|
|
|
|
+ vertices[i].co.z += t[2]
|
|
|
|
+
|
|
|
|
+def center(vertices):
|
|
|
|
+ """Center model (middle of bounding box).
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ bb = bbox(vertices)
|
|
|
|
+
|
|
|
|
+ cx = bb['x'][0] + (bb['x'][1] - bb['x'][0])/2.0
|
|
|
|
+ cy = bb['y'][0] + (bb['y'][1] - bb['y'][0])/2.0
|
|
|
|
+ cz = bb['z'][0] + (bb['z'][1] - bb['z'][0])/2.0
|
|
|
|
+
|
|
|
|
+ translate(vertices, [-cx,-cy,-cz])
|
|
|
|
+
|
|
|
|
+ return [-cx,-cy,-cz]
|
|
|
|
+
|
|
|
|
+def top(vertices):
|
|
|
|
+ """Align top of the model with the floor (Y-axis) and center it around X and Z.
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ bb = bbox(vertices)
|
|
|
|
+
|
|
|
|
+ cx = bb['x'][0] + (bb['x'][1] - bb['x'][0])/2.0
|
|
|
|
+ cy = bb['y'][1]
|
|
|
|
+ cz = bb['z'][0] + (bb['z'][1] - bb['z'][0])/2.0
|
|
|
|
+
|
|
|
|
+ translate(vertices, [-cx,-cy,-cz])
|
|
|
|
+
|
|
|
|
+ return [-cx,-cy,-cz]
|
|
|
|
+
|
|
|
|
+def bottom(vertices):
|
|
|
|
+ """Align bottom of the model with the floor (Y-axis) and center it around X and Z.
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ bb = bbox(vertices)
|
|
|
|
+
|
|
|
|
+ cx = bb['x'][0] + (bb['x'][1] - bb['x'][0])/2.0
|
|
|
|
+ cy = bb['y'][0]
|
|
|
|
+ cz = bb['z'][0] + (bb['z'][1] - bb['z'][0])/2.0
|
|
|
|
+
|
|
|
|
+ translate(vertices, [-cx,-cy,-cz])
|
|
|
|
+
|
|
|
|
+ return [-cx,-cy,-cz]
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Elements rendering
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def hexcolor(c):
|
|
|
|
+ return ( int(c[0] * 255) << 16 ) + ( int(c[1] * 255) << 8 ) + int(c[2] * 255)
|
|
|
|
+
|
|
|
|
+def generate_vertices(vertices, option_vertices_truncate, option_vertices):
|
|
|
|
+ if not option_vertices:
|
|
|
|
+ return ""
|
|
|
|
+
|
|
|
|
+ return ",".join(generate_vertex(v, option_vertices_truncate) for v in vertices)
|
|
|
|
+
|
|
|
|
+def generate_vertex(v, option_vertices_truncate):
|
|
|
|
+ if not option_vertices_truncate:
|
|
|
|
+ return TEMPLATE_VERTEX % (v.co.x, v.co.y, v.co.z)
|
|
|
|
+ else:
|
|
|
|
+ return TEMPLATE_VERTEX_TRUNCATE % (v.co.x, v.co.y, v.co.z)
|
|
|
|
+
|
|
|
|
+def generate_normal(n):
|
|
|
|
+ return TEMPLATE_N % (n[0], n[1], n[2])
|
|
|
|
+
|
|
|
|
+def generate_vertex_color(c):
|
|
|
|
+ return TEMPLATE_C % c
|
|
|
|
+
|
|
|
|
+def generate_uv(uv):
|
|
|
|
+ return TEMPLATE_UV % (uv[0], uv[1])
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Model exporter - faces
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def setBit(value, position, on):
|
|
|
|
+ if on:
|
|
|
|
+ mask = 1 << position
|
|
|
|
+ return (value | mask)
|
|
|
|
+ else:
|
|
|
|
+ mask = ~(1 << position)
|
|
|
|
+ return (value & mask)
|
|
|
|
+
|
|
|
|
+def generate_faces(normals, uv_layers, colors, meshes, option_normals, option_colors, option_uv_coords, option_materials, option_faces):
|
|
|
|
+
|
|
|
|
+ if not option_faces:
|
|
|
|
+ return "", 0
|
|
|
|
+
|
|
|
|
+ vertex_offset = 0
|
|
|
|
+ material_offset = 0
|
|
|
|
+
|
|
|
|
+ chunks = []
|
|
|
|
+ for mesh, object in meshes:
|
|
|
|
+
|
|
|
|
+ vertexUV = len(mesh.uv_textures) > 0
|
|
|
|
+ vertexColors = len(mesh.vertex_colors) > 0
|
|
|
|
+
|
|
|
|
+ mesh_colors = option_colors and vertexColors
|
|
|
|
+ mesh_uvs = option_uv_coords and vertexUV
|
|
|
|
+
|
|
|
|
+ if vertexUV:
|
|
|
|
+ active_uv_layer = mesh.uv_textures.active
|
|
|
|
+ if not active_uv_layer:
|
|
|
|
+ mesh_extract_uvs = False
|
|
|
|
+
|
|
|
|
+ if vertexColors:
|
|
|
|
+ active_col_layer = mesh.vertex_colors.active
|
|
|
|
+ if not active_col_layer:
|
|
|
|
+ mesh_extract_colors = False
|
|
|
|
+
|
|
|
|
+ for i, f in enumerate(get_faces(mesh)):
|
|
|
|
+ face = generate_face(f, i, normals, uv_layers, colors, mesh, option_normals, mesh_colors, mesh_uvs, option_materials, vertex_offset, material_offset)
|
|
|
|
+ chunks.append(face)
|
|
|
|
+
|
|
|
|
+ vertex_offset += len(mesh.vertices)
|
|
|
|
+
|
|
|
|
+ material_count = len(mesh.materials)
|
|
|
|
+ if material_count == 0:
|
|
|
|
+ material_count = 1
|
|
|
|
+
|
|
|
|
+ material_offset += material_count
|
|
|
|
+
|
|
|
|
+ return ",".join(chunks), len(chunks)
|
|
|
|
+
|
|
|
|
+def generate_face(f, faceIndex, normals, uv_layers, colors, mesh, option_normals, option_colors, option_uv_coords, option_materials, vertex_offset, material_offset):
|
|
|
|
+ isTriangle = ( len(f.vertices) == 3 )
|
|
|
|
+
|
|
|
|
+ if isTriangle:
|
|
|
|
+ nVertices = 3
|
|
|
|
+ else:
|
|
|
|
+ nVertices = 4
|
|
|
|
+
|
|
|
|
+ hasMaterial = option_materials
|
|
|
|
+
|
|
|
|
+ hasFaceUvs = False # not supported in Blender
|
|
|
|
+ hasFaceVertexUvs = option_uv_coords
|
|
|
|
+
|
|
|
|
+ hasFaceNormals = False # don't export any face normals (as they are computed in engine)
|
|
|
|
+ hasFaceVertexNormals = option_normals
|
|
|
|
+
|
|
|
|
+ hasFaceColors = False # not supported in Blender
|
|
|
|
+ hasFaceVertexColors = option_colors
|
|
|
|
+
|
|
|
|
+ faceType = 0
|
|
|
|
+ faceType = setBit(faceType, 0, not isTriangle)
|
|
|
|
+ faceType = setBit(faceType, 1, hasMaterial)
|
|
|
|
+ faceType = setBit(faceType, 2, hasFaceUvs)
|
|
|
|
+ faceType = setBit(faceType, 3, hasFaceVertexUvs)
|
|
|
|
+ faceType = setBit(faceType, 4, hasFaceNormals)
|
|
|
|
+ faceType = setBit(faceType, 5, hasFaceVertexNormals)
|
|
|
|
+ faceType = setBit(faceType, 6, hasFaceColors)
|
|
|
|
+ faceType = setBit(faceType, 7, hasFaceVertexColors)
|
|
|
|
+
|
|
|
|
+ faceData = []
|
|
|
|
+
|
|
|
|
+ # order is important, must match order in JSONLoader
|
|
|
|
+
|
|
|
|
+ # face type
|
|
|
|
+ # vertex indices
|
|
|
|
+ # material index
|
|
|
|
+ # face uvs index
|
|
|
|
+ # face vertex uvs indices
|
|
|
|
+ # face color index
|
|
|
|
+ # face vertex colors indices
|
|
|
|
+
|
|
|
|
+ faceData.append(faceType)
|
|
|
|
+
|
|
|
|
+ # must clamp in case on polygons bigger than quads
|
|
|
|
+
|
|
|
|
+ for i in range(nVertices):
|
|
|
|
+ index = f.vertices[i] + vertex_offset
|
|
|
|
+ faceData.append(index)
|
|
|
|
+
|
|
|
|
+ if hasMaterial:
|
|
|
|
+ index = f.material_index + material_offset
|
|
|
|
+ faceData.append( index )
|
|
|
|
+
|
|
|
|
+ if hasFaceVertexUvs:
|
|
|
|
+ for layer_index, uvs in enumerate(uv_layers):
|
|
|
|
+ uv = get_uv_indices(faceIndex, uvs, mesh, layer_index)
|
|
|
|
+ for i in range(nVertices):
|
|
|
|
+ index = uv[i]
|
|
|
|
+ faceData.append(index)
|
|
|
|
+
|
|
|
|
+ if hasFaceVertexNormals:
|
|
|
|
+ n = get_normal_indices(f.vertices, normals, mesh)
|
|
|
|
+ for i in range(nVertices):
|
|
|
|
+ index = n[i]
|
|
|
|
+ faceData.append(index)
|
|
|
|
+
|
|
|
|
+ if hasFaceVertexColors:
|
|
|
|
+ c = get_color_indices(faceIndex, colors, mesh)
|
|
|
|
+ for i in range(nVertices):
|
|
|
|
+ index = c[i]
|
|
|
|
+ faceData.append(index)
|
|
|
|
+
|
|
|
|
+ return ",".join( map(str, faceData) )
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Model exporter - normals
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def extract_vertex_normals(mesh, normals, count):
|
|
|
|
+ for f in get_faces(mesh):
|
|
|
|
+ for v in f.vertices:
|
|
|
|
+
|
|
|
|
+ normal = mesh.vertices[v].normal
|
|
|
|
+ key = veckey3d(normal)
|
|
|
|
+
|
|
|
|
+ if key not in normals:
|
|
|
|
+ normals[key] = count
|
|
|
|
+ count += 1
|
|
|
|
+
|
|
|
|
+ return count
|
|
|
|
+
|
|
|
|
+def generate_normals(normals, option_normals):
|
|
|
|
+ if not option_normals:
|
|
|
|
+ return ""
|
|
|
|
+
|
|
|
|
+ chunks = []
|
|
|
|
+ for key, index in sorted(normals.items(), key = operator.itemgetter(1)):
|
|
|
|
+ chunks.append(key)
|
|
|
|
+
|
|
|
|
+ return ",".join(generate_normal(n) for n in chunks)
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Model exporter - vertex colors
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def extract_vertex_colors(mesh, colors, count):
|
|
|
|
+ color_layer = mesh.tessface_vertex_colors.active.data
|
|
|
|
+
|
|
|
|
+ for face_index, face in enumerate(get_faces(mesh)):
|
|
|
|
+
|
|
|
|
+ face_colors = color_layer[face_index]
|
|
|
|
+ face_colors = face_colors.color1, face_colors.color2, face_colors.color3, face_colors.color4
|
|
|
|
+
|
|
|
|
+ for c in face_colors:
|
|
|
|
+ key = hexcolor(c)
|
|
|
|
+ if key not in colors:
|
|
|
|
+ colors[key] = count
|
|
|
|
+ count += 1
|
|
|
|
+
|
|
|
|
+ return count
|
|
|
|
+
|
|
|
|
+def generate_vertex_colors(colors, option_colors):
|
|
|
|
+ if not option_colors:
|
|
|
|
+ return ""
|
|
|
|
+
|
|
|
|
+ chunks = []
|
|
|
|
+ for key, index in sorted(colors.items(), key=operator.itemgetter(1)):
|
|
|
|
+ chunks.append(key)
|
|
|
|
+
|
|
|
|
+ return ",".join(generate_vertex_color(c) for c in chunks)
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Model exporter - UVs
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def extract_uvs(mesh, uv_layers, counts):
|
|
|
|
+ for index, layer in enumerate(mesh.tessface_uv_textures):
|
|
|
|
+
|
|
|
|
+ if len(uv_layers) <= index:
|
|
|
|
+ uvs = {}
|
|
|
|
+ count = 0
|
|
|
|
+ uv_layers.append(uvs)
|
|
|
|
+ counts.append(count)
|
|
|
|
+ else:
|
|
|
|
+ uvs = uv_layers[index]
|
|
|
|
+ count = counts[index]
|
|
|
|
+
|
|
|
|
+ uv_layer = layer.data
|
|
|
|
+
|
|
|
|
+ for face_index, face in enumerate(get_faces(mesh)):
|
|
|
|
+
|
|
|
|
+ for uv_index, uv in enumerate(uv_layer[face_index].uv):
|
|
|
|
+
|
|
|
|
+ key = veckey2d(uv)
|
|
|
|
+ if key not in uvs:
|
|
|
|
+ uvs[key] = count
|
|
|
|
+ count += 1
|
|
|
|
+
|
|
|
|
+ counts[index] = count
|
|
|
|
+
|
|
|
|
+ return counts
|
|
|
|
+
|
|
|
|
+def generate_uvs(uv_layers, option_uv_coords):
|
|
|
|
+ if not option_uv_coords:
|
|
|
|
+ return "[]"
|
|
|
|
+
|
|
|
|
+ layers = []
|
|
|
|
+ for uvs in uv_layers:
|
|
|
|
+ chunks = []
|
|
|
|
+ for key, index in sorted(uvs.items(), key=operator.itemgetter(1)):
|
|
|
|
+ chunks.append(key)
|
|
|
|
+ layer = ",".join(generate_uv(n) for n in chunks)
|
|
|
|
+ layers.append(layer)
|
|
|
|
+
|
|
|
|
+ return ",".join("[%s]" % n for n in layers)
|
|
|
|
+
|
|
|
|
+# ##############################################################################
|
|
|
|
+# Model exporter - bones
|
|
|
|
+# (only the first armature will exported)
|
|
|
|
+# ##############################################################################
|
|
|
|
+
|
|
|
|
+def generate_bones(option_bones, flipyz):
|
|
|
|
+
|
|
|
|
+ if not option_bones or len(bpy.data.armatures) == 0:
|
|
|
|
+ return "", 0
|
|
|
|
+
|
|
|
|
+ hierarchy = []
|
|
|
|
+
|
|
|
|
+ armature = bpy.data.armatures[0]
|
|
|
|
+
|
|
|
|
+ TEMPLATE_BONE = '{"parent":%d,"name":"%s","pos":[%g,%g,%g],"rotq":[0,0,0,1]}'
|
|
|
|
+
|
|
|
|
+ for bone in armature.bones:
|
|
|
|
+ if bone.parent == None:
|
|
|
|
+ if flipyz:
|
|
|
|
+ joint = TEMPLATE_BONE % (-1, bone.name, bone.head.x, bone.head.z, -bone.head.y)
|
|
|
|
+ hierarchy.append(joint)
|
|
|
|
+ else:
|
|
|
|
+ joint = TEMPLATE_BONE % (-1, bone.name, bone.head.x, bone.head.y, bone.head.z)
|
|
|
|
+ hierarchy.append(joint)
|
|
|
|
+ else:
|
|
|
|
+ index = i = 0
|
|
|
|
+ for parent in armature.bones:
|
|
|
|
+ if parent.name == bone.parent.name:
|
|
|
|
+ index = i
|
|
|
|
+ i += 1
|
|
|
|
+
|
|
|
|
+ position = bone.head_local - bone.parent.head_local
|
|
|
|
+
|
|
|
|
+ if flipyz:
|
|
|
|
+ joint = TEMPLATE_BONE % (index, bone.name, position.x, position.z, -position.y)
|
|
|
|
+ hierarchy.append(joint)
|
|
|
|
+ else:
|
|
|
|
+ joint = TEMPLATE_BONE % (index, bone.name, position.x, position.y, position.z)
|
|
|
|
+ hierarchy.append(joint)
|
|
|
|
+
|
|
|
|
+ bones_string = ",".join(hierarchy)
|
|
|
|
+
|
|
|
|
+ return bones_string, len(armature.bones)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+# ##############################################################################
|
|
|
|
+# Model exporter - skin indices and weights
|
|
|
|
+# ##############################################################################
|
|
|
|
+
|
|
|
|
+def generate_indices_and_weights(meshes, option_skinning):
|
|
|
|
+
|
|
|
|
+ if not option_skinning or len(bpy.data.armatures) == 0:
|
|
|
|
+ return "", ""
|
|
|
|
+
|
|
|
|
+ indices = []
|
|
|
|
+ weights = []
|
|
|
|
+
|
|
|
|
+ armature = bpy.data.armatures[0]
|
|
|
|
+
|
|
|
|
+ for mesh, object in meshes:
|
|
|
|
+
|
|
|
|
+ i = 0
|
|
|
|
+ mesh_index = -1
|
|
|
|
+
|
|
|
|
+ # find the original object
|
|
|
|
+
|
|
|
|
+ for obj in bpy.data.objects:
|
|
|
|
+ if obj.name == mesh.name or obj == object:
|
|
|
|
+ mesh_index = i
|
|
|
|
+ i += 1
|
|
|
|
+
|
|
|
|
+ if mesh_index == -1:
|
|
|
|
+ print("generate_indices: couldn't find object for mesh", mesh.name)
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ object = bpy.data.objects[mesh_index]
|
|
|
|
+
|
|
|
|
+ for vertex in mesh.vertices:
|
|
|
|
+
|
|
|
|
+ # sort bones by influence
|
|
|
|
+
|
|
|
|
+ bone_array = []
|
|
|
|
+
|
|
|
|
+ for group in vertex.groups:
|
|
|
|
+ index = group.group
|
|
|
|
+ weight = group.weight
|
|
|
|
+
|
|
|
|
+ bone_array.append( (index, weight) )
|
|
|
|
+
|
|
|
|
+ bone_array.sort(key = operator.itemgetter(1), reverse=True)
|
|
|
|
+
|
|
|
|
+ # select first N bones
|
|
|
|
+
|
|
|
|
+ for i in range(MAX_INFLUENCES):
|
|
|
|
+
|
|
|
|
+ if i < len(bone_array):
|
|
|
|
+ bone_proxy = bone_array[i]
|
|
|
|
+
|
|
|
|
+ index = bone_proxy[0]
|
|
|
|
+ weight = bone_proxy[1]
|
|
|
|
+
|
|
|
|
+ for j, bone in enumerate(armature.bones):
|
|
|
|
+ if object.vertex_groups[index].name == bone.name:
|
|
|
|
+ indices.append('%d' % j)
|
|
|
|
+ weights.append('%g' % weight)
|
|
|
|
+ break
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+ indices.append('0')
|
|
|
|
+ weights.append('0')
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ indices_string = ",".join(indices)
|
|
|
|
+ weights_string = ",".join(weights)
|
|
|
|
+
|
|
|
|
+ return indices_string, weights_string
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+# ##############################################################################
|
|
|
|
+# Model exporter - skeletal animation
|
|
|
|
+# (only the first action will exported)
|
|
|
|
+# ##############################################################################
|
|
|
|
+
|
|
|
|
+def generate_animation(option_animation_skeletal, option_frame_step, flipyz):
|
|
|
|
+
|
|
|
|
+ if not option_animation_skeletal or len(bpy.data.actions) == 0 or len(bpy.data.armatures) == 0:
|
|
|
|
+ return ""
|
|
|
|
+
|
|
|
|
+ # TODO: Add scaling influences
|
|
|
|
+
|
|
|
|
+ action = bpy.data.actions[0]
|
|
|
|
+ armature = bpy.data.armatures[0]
|
|
|
|
+
|
|
|
|
+ parents = []
|
|
|
|
+ parent_index = -1
|
|
|
|
+
|
|
|
|
+ fps = bpy.data.scenes[0].render.fps
|
|
|
|
+
|
|
|
|
+ end_frame = action.frame_range[1]
|
|
|
|
+ start_frame = action.frame_range[0]
|
|
|
|
+
|
|
|
|
+ frame_length = end_frame - start_frame
|
|
|
|
+
|
|
|
|
+ TEMPLATE_KEYFRAME_FULL = '{"time":%g,"pos":[%g,%g,%g],"rot":[%g,%g,%g,%g],"scl":[1,1,1]}'
|
|
|
|
+ TEMPLATE_KEYFRAME = '{"time":%g,"pos":[%g,%g,%g],"rot":[%g,%g,%g,%g]}'
|
|
|
|
+ TEMPLATE_KEYFRAME_POS = '{"time":%g,"pos":[%g,%g,%g]}'
|
|
|
|
+ TEMPLATE_KEYFRAME_ROT = '{"time":%g,"rot":[%g,%g,%g,%g]}'
|
|
|
|
+
|
|
|
|
+ for hierarchy in armature.bones:
|
|
|
|
+
|
|
|
|
+ keys = []
|
|
|
|
+
|
|
|
|
+ for frame in range(int(start_frame), int(end_frame / option_frame_step) + 1):
|
|
|
|
+
|
|
|
|
+ pos, pchange = position(hierarchy, frame * option_frame_step)
|
|
|
|
+ rot, rchange = rotation(hierarchy, frame * option_frame_step)
|
|
|
|
+
|
|
|
|
+ if flipyz:
|
|
|
|
+ px, py, pz = pos.x, pos.z, -pos.y
|
|
|
|
+ rx, ry, rz, rw = rot.x, rot.z, -rot.y, rot.w
|
|
|
|
+ else:
|
|
|
|
+ px, py, pz = pos.x, pos.y, pos.z
|
|
|
|
+ rx, ry, rz, rw = rot.x, rot.y, rot.z, rot.w
|
|
|
|
+
|
|
|
|
+ # START-FRAME: needs pos, rot and scl attributes (required frame)
|
|
|
|
+
|
|
|
|
+ if frame == int(start_frame):
|
|
|
|
+
|
|
|
|
+ time = (frame * option_frame_step - start_frame) / fps
|
|
|
|
+ keyframe = TEMPLATE_KEYFRAME_FULL % (time, px, py, pz, rx, ry, rz, rw)
|
|
|
|
+ keys.append(keyframe)
|
|
|
|
+
|
|
|
|
+ # END-FRAME: needs pos, rot and scl attributes with animation length (required frame)
|
|
|
|
+
|
|
|
|
+ elif frame == int(end_frame / option_frame_step):
|
|
|
|
+
|
|
|
|
+ time = frame_length / fps
|
|
|
|
+ keyframe = TEMPLATE_KEYFRAME_FULL % (time, px, py, pz, rx, ry, rz, rw)
|
|
|
|
+ keys.append(keyframe)
|
|
|
|
+
|
|
|
|
+ # MIDDLE-FRAME: needs only one of the attributes, can be an empty frame (optional frame)
|
|
|
|
+
|
|
|
|
+ elif pchange == True or rchange == True:
|
|
|
|
+
|
|
|
|
+ time = (frame * option_frame_step - start_frame) / fps
|
|
|
|
+
|
|
|
|
+ if pchange == True and rchange == True:
|
|
|
|
+ keyframe = TEMPLATE_KEYFRAME % (time, px, py, pz, rx, ry, rz, rw)
|
|
|
|
+ elif pchange == True:
|
|
|
|
+ keyframe = TEMPLATE_KEYFRAME_POS % (time, px, py, pz)
|
|
|
|
+ elif rchange == True:
|
|
|
|
+ keyframe = TEMPLATE_KEYFRAME_ROT % (time, rx, ry, rz, rw)
|
|
|
|
+
|
|
|
|
+ keys.append(keyframe)
|
|
|
|
+
|
|
|
|
+ keys_string = ",".join(keys)
|
|
|
|
+ parent = '{"parent":%d,"keys":[%s]}' % (parent_index, keys_string)
|
|
|
|
+ parent_index += 1
|
|
|
|
+ parents.append(parent)
|
|
|
|
+
|
|
|
|
+ hierarchy_string = ",".join(parents)
|
|
|
|
+ animation_string = '"name":"%s","fps":%d,"length":%g,"hierarchy":[%s]' % (action.name, fps, (frame_length / fps), hierarchy_string)
|
|
|
|
+
|
|
|
|
+ return animation_string
|
|
|
|
+
|
|
|
|
+def handle_position_channel(channel, frame, position):
|
|
|
|
+
|
|
|
|
+ change = False
|
|
|
|
+
|
|
|
|
+ if channel.array_index in [0, 1, 2]:
|
|
|
|
+ for keyframe in channel.keyframe_points:
|
|
|
|
+ if keyframe.co[0] == frame:
|
|
|
|
+ change = True
|
|
|
|
+
|
|
|
|
+ value = channel.evaluate(frame)
|
|
|
|
+
|
|
|
|
+ if channel.array_index == 0:
|
|
|
|
+ position.x = value
|
|
|
|
+
|
|
|
|
+ if channel.array_index == 1:
|
|
|
|
+ position.y = value
|
|
|
|
+
|
|
|
|
+ if channel.array_index == 2:
|
|
|
|
+ position.z = value
|
|
|
|
+
|
|
|
|
+ return change
|
|
|
|
+
|
|
|
|
+def position(bone, frame):
|
|
|
|
+
|
|
|
|
+ position = mathutils.Vector((0,0,0))
|
|
|
|
+ change = False
|
|
|
|
+
|
|
|
|
+ action = bpy.data.actions[0]
|
|
|
|
+ ngroups = len(action.groups)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ if ngroups > 0:
|
|
|
|
+
|
|
|
|
+ index = 0
|
|
|
|
+
|
|
|
|
+ for i in range(ngroups):
|
|
|
|
+ if action.groups[i].name == bone.name:
|
|
|
|
+ index = i
|
|
|
|
+
|
|
|
|
+ for channel in action.groups[index].channels:
|
|
|
|
+ if "location" in channel.data_path:
|
|
|
|
+ hasChanged = handle_position_channel(channel, frame, position)
|
|
|
|
+ change = change or hasChanged
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+
|
|
|
|
+ bone_label = '"%s"' % bone.name
|
|
|
|
+
|
|
|
|
+ for channel in action.fcurves:
|
|
|
|
+ data_path = channel.data_path
|
|
|
|
+ if bone_label in data_path and "location" in data_path:
|
|
|
|
+ hasChanged = handle_position_channel(channel, frame, position)
|
|
|
|
+ change = change or hasChanged
|
|
|
|
+
|
|
|
|
+ position = position * bone.matrix_local.inverted()
|
|
|
|
+
|
|
|
|
+ if bone.parent == None:
|
|
|
|
+
|
|
|
|
+ position.x += bone.head.x
|
|
|
|
+ position.y += bone.head.y
|
|
|
|
+ position.z += bone.head.z
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+
|
|
|
|
+ parent = bone.parent
|
|
|
|
+
|
|
|
|
+ parentInvertedLocalMatrix = parent.matrix_local.inverted()
|
|
|
|
+ parentHeadTailDiff = parent.tail_local - parent.head_local
|
|
|
|
+
|
|
|
|
+ position.x += (bone.head * parentInvertedLocalMatrix).x + parentHeadTailDiff.x
|
|
|
|
+ position.y += (bone.head * parentInvertedLocalMatrix).y + parentHeadTailDiff.y
|
|
|
|
+ position.z += (bone.head * parentInvertedLocalMatrix).z + parentHeadTailDiff.z
|
|
|
|
+
|
|
|
|
+ return position, change
|
|
|
|
+
|
|
|
|
+def handle_rotation_channel(channel, frame, rotation):
|
|
|
|
+
|
|
|
|
+ change = False
|
|
|
|
+
|
|
|
|
+ if channel.array_index in [0, 1, 2, 3]:
|
|
|
|
+
|
|
|
|
+ for keyframe in channel.keyframe_points:
|
|
|
|
+ if keyframe.co[0] == frame:
|
|
|
|
+ change = True
|
|
|
|
+
|
|
|
|
+ value = channel.evaluate(frame)
|
|
|
|
+
|
|
|
|
+ if channel.array_index == 1:
|
|
|
|
+ rotation.x = value
|
|
|
|
+
|
|
|
|
+ elif channel.array_index == 2:
|
|
|
|
+ rotation.y = value
|
|
|
|
+
|
|
|
|
+ elif channel.array_index == 3:
|
|
|
|
+ rotation.z = value
|
|
|
|
+
|
|
|
|
+ elif channel.array_index == 0:
|
|
|
|
+ rotation.w = value
|
|
|
|
+
|
|
|
|
+ return change
|
|
|
|
+
|
|
|
|
+def rotation(bone, frame):
|
|
|
|
+
|
|
|
|
+ # TODO: calculate rotation also from rotation_euler channels
|
|
|
|
+
|
|
|
|
+ rotation = mathutils.Vector((0,0,0,1))
|
|
|
|
+
|
|
|
|
+ change = False
|
|
|
|
+
|
|
|
|
+ action = bpy.data.actions[0]
|
|
|
|
+ ngroups = len(action.groups)
|
|
|
|
+
|
|
|
|
+ # animation grouped by bones
|
|
|
|
+
|
|
|
|
+ if ngroups > 0:
|
|
|
|
+
|
|
|
|
+ index = 0
|
|
|
|
+
|
|
|
|
+ for i in range(ngroups):
|
|
|
|
+ if action.groups[i].name == bone.name:
|
|
|
|
+ index = i
|
|
|
|
+
|
|
|
|
+ for channel in action.groups[index].channels:
|
|
|
|
+ if "quaternion" in channel.data_path:
|
|
|
|
+ hasChanged = handle_rotation_channel(channel, frame, rotation)
|
|
|
|
+ change = change or hasChanged
|
|
|
|
+
|
|
|
|
+ # animation in raw fcurves
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+
|
|
|
|
+ bone_label = '"%s"' % bone.name
|
|
|
|
+
|
|
|
|
+ for channel in action.fcurves:
|
|
|
|
+ data_path = channel.data_path
|
|
|
|
+ if bone_label in data_path and "quaternion" in data_path:
|
|
|
|
+ hasChanged = handle_rotation_channel(channel, frame, rotation)
|
|
|
|
+ change = change or hasChanged
|
|
|
|
+
|
|
|
|
+ rot3 = rotation.to_3d()
|
|
|
|
+ rotation.xyz = rot3 * bone.matrix_local.inverted()
|
|
|
|
+
|
|
|
|
+ return rotation, change
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Model exporter - materials
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def generate_color(i):
|
|
|
|
+ """Generate hex color corresponding to integer.
|
|
|
|
+
|
|
|
|
+ Colors should have well defined ordering.
|
|
|
|
+ First N colors are hardcoded, then colors are random
|
|
|
|
+ (must seed random number generator with deterministic value
|
|
|
|
+ before getting colors).
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ if i < len(COLORS):
|
|
|
|
+ #return "0x%06x" % COLORS[i]
|
|
|
|
+ return COLORS[i]
|
|
|
|
+ else:
|
|
|
|
+ #return "0x%06x" % int(0xffffff * random.random())
|
|
|
|
+ return int(0xffffff * random.random())
|
|
|
|
+
|
|
|
|
+def generate_mtl(materials):
|
|
|
|
+ """Generate dummy materials.
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ mtl = {}
|
|
|
|
+ for m in materials:
|
|
|
|
+ index = materials[m]
|
|
|
|
+ mtl[m] = {
|
|
|
|
+ "DbgName": m,
|
|
|
|
+ "DbgIndex": index,
|
|
|
|
+ "DbgColor": generate_color(index),
|
|
|
|
+ "vertexColors" : False
|
|
|
|
+ }
|
|
|
|
+ return mtl
|
|
|
|
+
|
|
|
|
+def value2string(v):
|
|
|
|
+ if type(v) == str and v[0:2] != "0x":
|
|
|
|
+ return '"%s"' % v
|
|
|
|
+ elif type(v) == bool:
|
|
|
|
+ return str(v).lower()
|
|
|
|
+ elif type(v) == list:
|
|
|
|
+ return "[%s]" % (", ".join(value2string(x) for x in v))
|
|
|
|
+ return str(v)
|
|
|
|
+
|
|
|
|
+def generate_materials(mtl, materials, draw_type):
|
|
|
|
+ """Generate JS array of materials objects
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ mtl_array = []
|
|
|
|
+ for m in mtl:
|
|
|
|
+ index = materials[m]
|
|
|
|
+
|
|
|
|
+ # add debug information
|
|
|
|
+ # materials should be sorted according to how
|
|
|
|
+ # they appeared in OBJ file (for the first time)
|
|
|
|
+ # this index is identifier used in face definitions
|
|
|
|
+ mtl[m]['DbgName'] = m
|
|
|
|
+ mtl[m]['DbgIndex'] = index
|
|
|
|
+ mtl[m]['DbgColor'] = generate_color(index)
|
|
|
|
+
|
|
|
|
+ if draw_type in [ "BOUNDS", "WIRE" ]:
|
|
|
|
+ mtl[m]['wireframe'] = True
|
|
|
|
+ mtl[m]['DbgColor'] = 0xff0000
|
|
|
|
+
|
|
|
|
+ mtl_raw = ",\n".join(['\t\t"%s" : %s' % (n, value2string(v)) for n,v in sorted(mtl[m].items())])
|
|
|
|
+ mtl_string = "\t{\n%s\n\t}" % mtl_raw
|
|
|
|
+ mtl_array.append([index, mtl_string])
|
|
|
|
+
|
|
|
|
+ return ",\n\n".join([m for i,m in sorted(mtl_array)]), len(mtl_array)
|
|
|
|
+
|
|
|
|
+def extract_materials(mesh, scene, option_colors, option_copy_textures, filepath):
|
|
|
|
+ world = scene.world
|
|
|
|
+
|
|
|
|
+ materials = {}
|
|
|
|
+ for m in mesh.materials:
|
|
|
|
+ if m:
|
|
|
|
+ materials[m.name] = {}
|
|
|
|
+ material = materials[m.name]
|
|
|
|
+
|
|
|
|
+ material['colorDiffuse'] = [m.diffuse_intensity * m.diffuse_color[0],
|
|
|
|
+ m.diffuse_intensity * m.diffuse_color[1],
|
|
|
|
+ m.diffuse_intensity * m.diffuse_color[2]]
|
|
|
|
+
|
|
|
|
+ material['colorSpecular'] = [m.specular_intensity * m.specular_color[0],
|
|
|
|
+ m.specular_intensity * m.specular_color[1],
|
|
|
|
+ m.specular_intensity * m.specular_color[2]]
|
|
|
|
+
|
|
|
|
+ material['colorAmbient'] = [m.ambient * material['colorDiffuse'][0],
|
|
|
|
+ m.ambient * material['colorDiffuse'][1],
|
|
|
|
+ m.ambient * material['colorDiffuse'][2]]
|
|
|
|
+
|
|
|
|
+ material['transparency'] = m.alpha
|
|
|
|
+
|
|
|
|
+ # not sure about mapping values to Blinn-Phong shader
|
|
|
|
+ # Blender uses INT from [1, 511] with default 0
|
|
|
|
+ # http://www.blender.org/documentation/blender_python_api_2_54_0/bpy.types.Material.html#bpy.types.Material.specular_hardness
|
|
|
|
+
|
|
|
|
+ material["specularCoef"] = m.specular_hardness
|
|
|
|
+
|
|
|
|
+ textures = guess_material_textures(m)
|
|
|
|
+
|
|
|
|
+ handle_texture('diffuse', textures, material, filepath, option_copy_textures)
|
|
|
|
+ handle_texture('light', textures, material, filepath, option_copy_textures)
|
|
|
|
+ handle_texture('normal', textures, material, filepath, option_copy_textures)
|
|
|
|
+ handle_texture('specular', textures, material, filepath, option_copy_textures)
|
|
|
|
+ handle_texture('bump', textures, material, filepath, option_copy_textures)
|
|
|
|
+
|
|
|
|
+ material["vertexColors"] = m.THREE_useVertexColors and option_colors
|
|
|
|
+
|
|
|
|
+ # can't really use this reliably to tell apart Phong from Lambert
|
|
|
|
+ # as Blender defaults to non-zero specular color
|
|
|
|
+ #if m.specular_intensity > 0.0 and (m.specular_color[0] > 0 or m.specular_color[1] > 0 or m.specular_color[2] > 0):
|
|
|
|
+ # material['shading'] = "Phong"
|
|
|
|
+ #else:
|
|
|
|
+ # material['shading'] = "Lambert"
|
|
|
|
+
|
|
|
|
+ if textures['normal']:
|
|
|
|
+ material['shading'] = "Phong"
|
|
|
|
+ else:
|
|
|
|
+ material['shading'] = m.THREE_materialType
|
|
|
|
+
|
|
|
|
+ material['blending'] = m.THREE_blendingType
|
|
|
|
+ material['depthWrite'] = m.THREE_depthWrite
|
|
|
|
+ material['depthTest'] = m.THREE_depthTest
|
|
|
|
+ material['transparent'] = m.use_transparency
|
|
|
|
+
|
|
|
|
+ return materials
|
|
|
|
+
|
|
|
|
+def generate_materials_string(mesh, scene, option_colors, draw_type, option_copy_textures, filepath, offset):
|
|
|
|
+
|
|
|
|
+ random.seed(42) # to get well defined color order for debug materials
|
|
|
|
+
|
|
|
|
+ materials = {}
|
|
|
|
+ if mesh.materials:
|
|
|
|
+ for i, m in enumerate(mesh.materials):
|
|
|
|
+ mat_id = i + offset
|
|
|
|
+ if m:
|
|
|
|
+ materials[m.name] = mat_id
|
|
|
|
+ else:
|
|
|
|
+ materials["undefined_dummy_%0d" % mat_id] = mat_id
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ if not materials:
|
|
|
|
+ materials = { 'default': 0 }
|
|
|
|
+
|
|
|
|
+ # default dummy materials
|
|
|
|
+
|
|
|
|
+ mtl = generate_mtl(materials)
|
|
|
|
+
|
|
|
|
+ # extract real materials from the mesh
|
|
|
|
+
|
|
|
|
+ mtl.update(extract_materials(mesh, scene, option_colors, option_copy_textures, filepath))
|
|
|
|
+
|
|
|
|
+ return generate_materials(mtl, materials, draw_type)
|
|
|
|
+
|
|
|
|
+def handle_texture(id, textures, material, filepath, option_copy_textures):
|
|
|
|
+
|
|
|
|
+ if textures[id]:
|
|
|
|
+ texName = 'map%s' % id.capitalize()
|
|
|
|
+ repeatName = 'map%sRepeat' % id.capitalize()
|
|
|
|
+ wrapName = 'map%sWrap' % id.capitalize()
|
|
|
|
+
|
|
|
|
+ slot = textures[id]['slot']
|
|
|
|
+ texture = textures[id]['texture']
|
|
|
|
+ image = texture.image
|
|
|
|
+ fname = extract_texture_filename(image)
|
|
|
|
+ material[texName] = fname
|
|
|
|
+
|
|
|
|
+ if option_copy_textures:
|
|
|
|
+ save_image(image, fname, filepath)
|
|
|
|
+
|
|
|
|
+ if texture.repeat_x != 1 or texture.repeat_y != 1:
|
|
|
|
+ material[repeatName] = [texture.repeat_x, texture.repeat_y]
|
|
|
|
+
|
|
|
|
+ if texture.extension == "REPEAT":
|
|
|
|
+ wrap_x = "repeat"
|
|
|
|
+ wrap_y = "repeat"
|
|
|
|
+
|
|
|
|
+ if texture.use_mirror_x:
|
|
|
|
+ wrap_x = "mirror"
|
|
|
|
+ if texture.use_mirror_y:
|
|
|
|
+ wrap_y = "mirror"
|
|
|
|
+
|
|
|
|
+ material[wrapName] = [wrap_x, wrap_y]
|
|
|
|
+
|
|
|
|
+ if slot.use_map_normal:
|
|
|
|
+ if slot.normal_factor != 1.0:
|
|
|
|
+ if id == "bump":
|
|
|
|
+ material['mapBumpScale'] = slot.normal_factor
|
|
|
|
+ else:
|
|
|
|
+ material['mapNormalFactor'] = slot.normal_factor
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# ASCII model generator
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def generate_ascii_model(meshes, morphs,
|
|
|
|
+ scene,
|
|
|
|
+ option_vertices,
|
|
|
|
+ option_vertices_truncate,
|
|
|
|
+ option_faces,
|
|
|
|
+ option_normals,
|
|
|
|
+ option_uv_coords,
|
|
|
|
+ option_materials,
|
|
|
|
+ option_colors,
|
|
|
|
+ option_bones,
|
|
|
|
+ option_skinning,
|
|
|
|
+ align_model,
|
|
|
|
+ flipyz,
|
|
|
|
+ option_scale,
|
|
|
|
+ option_copy_textures,
|
|
|
|
+ filepath,
|
|
|
|
+ option_animation_morph,
|
|
|
|
+ option_animation_skeletal,
|
|
|
|
+ option_frame_step):
|
|
|
|
+
|
|
|
|
+ vertices = []
|
|
|
|
+
|
|
|
|
+ vertex_offset = 0
|
|
|
|
+ vertex_offsets = []
|
|
|
|
+
|
|
|
|
+ nnormal = 0
|
|
|
|
+ normals = {}
|
|
|
|
+
|
|
|
|
+ ncolor = 0
|
|
|
|
+ colors = {}
|
|
|
|
+
|
|
|
|
+ nuvs = []
|
|
|
|
+ uv_layers = []
|
|
|
|
+
|
|
|
|
+ nmaterial = 0
|
|
|
|
+ materials = []
|
|
|
|
+
|
|
|
|
+ for mesh, object in meshes:
|
|
|
|
+
|
|
|
|
+ vertexUV = len(mesh.uv_textures) > 0
|
|
|
|
+ vertexColors = len(mesh.vertex_colors) > 0
|
|
|
|
+
|
|
|
|
+ mesh_extract_colors = option_colors and vertexColors
|
|
|
|
+ mesh_extract_uvs = option_uv_coords and vertexUV
|
|
|
|
+
|
|
|
|
+ if vertexUV:
|
|
|
|
+ active_uv_layer = mesh.uv_textures.active
|
|
|
|
+ if not active_uv_layer:
|
|
|
|
+ mesh_extract_uvs = False
|
|
|
|
+
|
|
|
|
+ if vertexColors:
|
|
|
|
+ active_col_layer = mesh.vertex_colors.active
|
|
|
|
+ if not active_col_layer:
|
|
|
|
+ mesh_extract_colors = False
|
|
|
|
+
|
|
|
|
+ vertex_offsets.append(vertex_offset)
|
|
|
|
+ vertex_offset += len(vertices)
|
|
|
|
+
|
|
|
|
+ vertices.extend(mesh.vertices[:])
|
|
|
|
+
|
|
|
|
+ if option_normals:
|
|
|
|
+ nnormal = extract_vertex_normals(mesh, normals, nnormal)
|
|
|
|
+
|
|
|
|
+ if mesh_extract_colors:
|
|
|
|
+ ncolor = extract_vertex_colors(mesh, colors, ncolor)
|
|
|
|
+
|
|
|
|
+ if mesh_extract_uvs:
|
|
|
|
+ nuvs = extract_uvs(mesh, uv_layers, nuvs)
|
|
|
|
+
|
|
|
|
+ if option_materials:
|
|
|
|
+ mesh_materials, nmaterial = generate_materials_string(mesh, scene, mesh_extract_colors, object.draw_type, option_copy_textures, filepath, nmaterial)
|
|
|
|
+ materials.append(mesh_materials)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ morphTargets_string = ""
|
|
|
|
+ nmorphTarget = 0
|
|
|
|
+
|
|
|
|
+ if option_animation_morph:
|
|
|
|
+ chunks = []
|
|
|
|
+ for i, morphVertices in enumerate(morphs):
|
|
|
|
+ morphTarget = '{ "name": "%s_%06d", "vertices": [%s] }' % ("animation", i, morphVertices)
|
|
|
|
+ chunks.append(morphTarget)
|
|
|
|
+
|
|
|
|
+ morphTargets_string = ",\n\t".join(chunks)
|
|
|
|
+ nmorphTarget = len(morphs)
|
|
|
|
+
|
|
|
|
+ if align_model == 1:
|
|
|
|
+ center(vertices)
|
|
|
|
+ elif align_model == 2:
|
|
|
|
+ bottom(vertices)
|
|
|
|
+ elif align_model == 3:
|
|
|
|
+ top(vertices)
|
|
|
|
+
|
|
|
|
+ faces_string, nfaces = generate_faces(normals, uv_layers, colors, meshes, option_normals, option_colors, option_uv_coords, option_materials, option_faces)
|
|
|
|
+
|
|
|
|
+ bones_string, nbone = generate_bones(option_bones, flipyz)
|
|
|
|
+ indices_string, weights_string = generate_indices_and_weights(meshes, option_skinning)
|
|
|
|
+
|
|
|
|
+ materials_string = ",\n\n".join(materials)
|
|
|
|
+
|
|
|
|
+ model_string = TEMPLATE_MODEL_ASCII % {
|
|
|
|
+ "scale" : option_scale,
|
|
|
|
+
|
|
|
|
+ "uvs" : generate_uvs(uv_layers, option_uv_coords),
|
|
|
|
+ "normals" : generate_normals(normals, option_normals),
|
|
|
|
+ "colors" : generate_vertex_colors(colors, option_colors),
|
|
|
|
+
|
|
|
|
+ "materials" : materials_string,
|
|
|
|
+
|
|
|
|
+ "vertices" : generate_vertices(vertices, option_vertices_truncate, option_vertices),
|
|
|
|
+
|
|
|
|
+ "faces" : faces_string,
|
|
|
|
+
|
|
|
|
+ "morphTargets" : morphTargets_string,
|
|
|
|
+
|
|
|
|
+ "bones" : bones_string,
|
|
|
|
+ "indices" : indices_string,
|
|
|
|
+ "weights" : weights_string,
|
|
|
|
+ "animation" : generate_animation(option_animation_skeletal, option_frame_step, flipyz)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ text = TEMPLATE_FILE_ASCII % {
|
|
|
|
+ "nvertex" : len(vertices),
|
|
|
|
+ "nface" : nfaces,
|
|
|
|
+ "nuvs" : ",".join("%d" % n for n in nuvs),
|
|
|
|
+ "nnormal" : nnormal,
|
|
|
|
+ "ncolor" : ncolor,
|
|
|
|
+ "nmaterial" : nmaterial,
|
|
|
|
+ "nmorphTarget": nmorphTarget,
|
|
|
|
+ "nbone" : nbone,
|
|
|
|
+
|
|
|
|
+ "model" : model_string
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ return text, model_string
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Model exporter - export single mesh
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def extract_meshes(objects, scene, export_single_model, option_scale, flipyz):
|
|
|
|
+
|
|
|
|
+ meshes = []
|
|
|
|
+
|
|
|
|
+ for object in objects:
|
|
|
|
+
|
|
|
|
+ if object.type == "MESH" and object.THREE_exportGeometry:
|
|
|
|
+
|
|
|
|
+ # collapse modifiers into mesh
|
|
|
|
+
|
|
|
|
+ mesh = object.to_mesh(scene, True, 'RENDER')
|
|
|
|
+
|
|
|
|
+ if not mesh:
|
|
|
|
+ raise Exception("Error, could not get mesh data from object [%s]" % object.name)
|
|
|
|
+
|
|
|
|
+ # preserve original name
|
|
|
|
+
|
|
|
|
+ mesh.name = object.name
|
|
|
|
+
|
|
|
|
+ if export_single_model:
|
|
|
|
+
|
|
|
|
+ if flipyz:
|
|
|
|
+
|
|
|
|
+ # that's what Blender's native export_obj.py does to flip YZ
|
|
|
|
+
|
|
|
|
+ X_ROT = mathutils.Matrix.Rotation(-math.pi/2, 4, 'X')
|
|
|
|
+ mesh.transform(X_ROT * object.matrix_world)
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+ mesh.transform(object.matrix_world)
|
|
|
|
+
|
|
|
|
+ mesh.calc_normals()
|
|
|
|
+ mesh.calc_tessface()
|
|
|
|
+ mesh.transform(mathutils.Matrix.Scale(option_scale, 4))
|
|
|
|
+ meshes.append([mesh, object])
|
|
|
|
+
|
|
|
|
+ return meshes
|
|
|
|
+
|
|
|
|
+def generate_mesh_string(objects, scene,
|
|
|
|
+ option_vertices,
|
|
|
|
+ option_vertices_truncate,
|
|
|
|
+ option_faces,
|
|
|
|
+ option_normals,
|
|
|
|
+ option_uv_coords,
|
|
|
|
+ option_materials,
|
|
|
|
+ option_colors,
|
|
|
|
+ option_bones,
|
|
|
|
+ option_skinning,
|
|
|
|
+ align_model,
|
|
|
|
+ flipyz,
|
|
|
|
+ option_scale,
|
|
|
|
+ export_single_model,
|
|
|
|
+ option_copy_textures,
|
|
|
|
+ filepath,
|
|
|
|
+ option_animation_morph,
|
|
|
|
+ option_animation_skeletal,
|
|
|
|
+ option_frame_step):
|
|
|
|
+
|
|
|
|
+ meshes = extract_meshes(objects, scene, export_single_model, option_scale, flipyz)
|
|
|
|
+
|
|
|
|
+ morphs = []
|
|
|
|
+
|
|
|
|
+ if option_animation_morph:
|
|
|
|
+
|
|
|
|
+ original_frame = scene.frame_current # save animation state
|
|
|
|
+
|
|
|
|
+ scene_frames = range(scene.frame_start, scene.frame_end + 1, option_frame_step)
|
|
|
|
+
|
|
|
|
+ for index, frame in enumerate(scene_frames):
|
|
|
|
+ scene.frame_set(frame, 0.0)
|
|
|
|
+
|
|
|
|
+ anim_meshes = extract_meshes(objects, scene, export_single_model, option_scale, flipyz)
|
|
|
|
+
|
|
|
|
+ frame_vertices = []
|
|
|
|
+
|
|
|
|
+ for mesh, object in anim_meshes:
|
|
|
|
+ frame_vertices.extend(mesh.vertices[:])
|
|
|
|
+
|
|
|
|
+ if index == 0:
|
|
|
|
+ if align_model == 1:
|
|
|
|
+ offset = center(frame_vertices)
|
|
|
|
+ elif align_model == 2:
|
|
|
|
+ offset = bottom(frame_vertices)
|
|
|
|
+ elif align_model == 3:
|
|
|
|
+ offset = top(frame_vertices)
|
|
|
|
+ else:
|
|
|
|
+ offset = False
|
|
|
|
+ else:
|
|
|
|
+ if offset:
|
|
|
|
+ translate(frame_vertices, offset)
|
|
|
|
+
|
|
|
|
+ morphVertices = generate_vertices(frame_vertices, option_vertices_truncate, option_vertices)
|
|
|
|
+ morphs.append(morphVertices)
|
|
|
|
+
|
|
|
|
+ # remove temp meshes
|
|
|
|
+
|
|
|
|
+ for mesh, object in anim_meshes:
|
|
|
|
+ bpy.data.meshes.remove(mesh)
|
|
|
|
+
|
|
|
|
+ scene.frame_set(original_frame, 0.0) # restore animation state
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ text, model_string = generate_ascii_model(meshes, morphs,
|
|
|
|
+ scene,
|
|
|
|
+ option_vertices,
|
|
|
|
+ option_vertices_truncate,
|
|
|
|
+ option_faces,
|
|
|
|
+ option_normals,
|
|
|
|
+ option_uv_coords,
|
|
|
|
+ option_materials,
|
|
|
|
+ option_colors,
|
|
|
|
+ option_bones,
|
|
|
|
+ option_skinning,
|
|
|
|
+ align_model,
|
|
|
|
+ flipyz,
|
|
|
|
+ option_scale,
|
|
|
|
+ option_copy_textures,
|
|
|
|
+ filepath,
|
|
|
|
+ option_animation_morph,
|
|
|
|
+ option_animation_skeletal,
|
|
|
|
+ option_frame_step)
|
|
|
|
+
|
|
|
|
+ # remove temp meshes
|
|
|
|
+
|
|
|
|
+ for mesh, object in meshes:
|
|
|
|
+ bpy.data.meshes.remove(mesh)
|
|
|
|
+
|
|
|
|
+ return text, model_string
|
|
|
|
+
|
|
|
|
+def export_mesh(objects,
|
|
|
|
+ scene, filepath,
|
|
|
|
+ option_vertices,
|
|
|
|
+ option_vertices_truncate,
|
|
|
|
+ option_faces,
|
|
|
|
+ option_normals,
|
|
|
|
+ option_uv_coords,
|
|
|
|
+ option_materials,
|
|
|
|
+ option_colors,
|
|
|
|
+ option_bones,
|
|
|
|
+ option_skinning,
|
|
|
|
+ align_model,
|
|
|
|
+ flipyz,
|
|
|
|
+ option_scale,
|
|
|
|
+ export_single_model,
|
|
|
|
+ option_copy_textures,
|
|
|
|
+ option_animation_morph,
|
|
|
|
+ option_animation_skeletal,
|
|
|
|
+ option_frame_step):
|
|
|
|
+
|
|
|
|
+ """Export single mesh"""
|
|
|
|
+
|
|
|
|
+ text, model_string = generate_mesh_string(objects,
|
|
|
|
+ scene,
|
|
|
|
+ option_vertices,
|
|
|
|
+ option_vertices_truncate,
|
|
|
|
+ option_faces,
|
|
|
|
+ option_normals,
|
|
|
|
+ option_uv_coords,
|
|
|
|
+ option_materials,
|
|
|
|
+ option_colors,
|
|
|
|
+ option_bones,
|
|
|
|
+ option_skinning,
|
|
|
|
+ align_model,
|
|
|
|
+ flipyz,
|
|
|
|
+ option_scale,
|
|
|
|
+ export_single_model,
|
|
|
|
+ option_copy_textures,
|
|
|
|
+ filepath,
|
|
|
|
+ option_animation_morph,
|
|
|
|
+ option_animation_skeletal,
|
|
|
|
+ option_frame_step)
|
|
|
|
+
|
|
|
|
+ write_file(filepath, text)
|
|
|
|
+
|
|
|
|
+ print("writing", filepath, "done")
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Scene exporter - render elements
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def generate_vec4(vec):
|
|
|
|
+ return TEMPLATE_VEC4 % (vec[0], vec[1], vec[2], vec[3])
|
|
|
|
+
|
|
|
|
+def generate_vec3(vec):
|
|
|
|
+ return TEMPLATE_VEC3 % (vec[0], vec[1], vec[2])
|
|
|
|
+
|
|
|
|
+def generate_vec2(vec):
|
|
|
|
+ return TEMPLATE_VEC2 % (vec[0], vec[1])
|
|
|
|
+
|
|
|
|
+def generate_hex(number):
|
|
|
|
+ return TEMPLATE_HEX % number
|
|
|
|
+
|
|
|
|
+def generate_string(s):
|
|
|
|
+ return TEMPLATE_STRING % s
|
|
|
|
+
|
|
|
|
+def generate_string_list(src_list):
|
|
|
|
+ return ", ".join(generate_string(item) for item in src_list)
|
|
|
|
+
|
|
|
|
+def generate_section(label, content):
|
|
|
|
+ return TEMPLATE_SECTION % (label, content)
|
|
|
|
+
|
|
|
|
+def get_mesh_filename(mesh):
|
|
|
|
+ object_id = mesh["data"]["name"]
|
|
|
|
+ filename = "%s.js" % sanitize(object_id)
|
|
|
|
+ return filename
|
|
|
|
+
|
|
|
|
+def generate_material_id_list(materials):
|
|
|
|
+ chunks = []
|
|
|
|
+ for material in materials:
|
|
|
|
+ chunks.append(material.name)
|
|
|
|
+
|
|
|
|
+ return chunks
|
|
|
|
+
|
|
|
|
+def generate_group_id_list(obj):
|
|
|
|
+ chunks = []
|
|
|
|
+
|
|
|
|
+ for group in bpy.data.groups:
|
|
|
|
+ if obj.name in group.objects:
|
|
|
|
+ chunks.append(group.name)
|
|
|
|
+
|
|
|
|
+ return chunks
|
|
|
|
+
|
|
|
|
+def generate_bool_property(property):
|
|
|
|
+ if property:
|
|
|
|
+ return "true"
|
|
|
|
+ return "false"
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Scene exporter - objects
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def generate_objects(data):
|
|
|
|
+ chunks = []
|
|
|
|
+
|
|
|
|
+ for obj in data["objects"]:
|
|
|
|
+
|
|
|
|
+ if obj.type == "MESH" and obj.THREE_exportGeometry:
|
|
|
|
+ object_id = obj.name
|
|
|
|
+
|
|
|
|
+ if len(obj.modifiers) > 0:
|
|
|
|
+ geo_name = obj.name
|
|
|
|
+ else:
|
|
|
|
+ geo_name = obj.data.name
|
|
|
|
+
|
|
|
|
+ geometry_id = "geo_%s" % geo_name
|
|
|
|
+
|
|
|
|
+ material_ids = generate_material_id_list(obj.material_slots)
|
|
|
|
+ group_ids = generate_group_id_list(obj)
|
|
|
|
+
|
|
|
|
+ position, quaternion, scale = obj.matrix_world.decompose()
|
|
|
|
+ rotation = quaternion.to_euler("XYZ")
|
|
|
|
+
|
|
|
|
+ # use empty material string for multi-material objects
|
|
|
|
+ # this will trigger use of MeshFaceMaterial in SceneLoader
|
|
|
|
+
|
|
|
|
+ material_string = ""
|
|
|
|
+ if len(material_ids) == 1:
|
|
|
|
+ material_string = generate_string_list(material_ids)
|
|
|
|
+
|
|
|
|
+ group_string = ""
|
|
|
|
+ if len(group_ids) > 0:
|
|
|
|
+ group_string = generate_string_list(group_ids)
|
|
|
|
+
|
|
|
|
+ castShadow = obj.THREE_castShadow
|
|
|
|
+ receiveShadow = obj.THREE_receiveShadow
|
|
|
|
+ doubleSided = obj.THREE_doubleSided
|
|
|
|
+
|
|
|
|
+ visible = True
|
|
|
|
+
|
|
|
|
+ geometry_string = generate_string(geometry_id)
|
|
|
|
+
|
|
|
|
+ object_string = TEMPLATE_OBJECT % {
|
|
|
|
+ "object_id" : generate_string(object_id),
|
|
|
|
+ "geometry_id" : geometry_string,
|
|
|
|
+ "group_id" : group_string,
|
|
|
|
+ "material_id" : material_string,
|
|
|
|
+
|
|
|
|
+ "position" : generate_vec3(position),
|
|
|
|
+ "rotation" : generate_vec3(rotation),
|
|
|
|
+ "quaternion" : generate_vec4(quaternion),
|
|
|
|
+ "scale" : generate_vec3(scale),
|
|
|
|
+
|
|
|
|
+ "castShadow" : generate_bool_property(castShadow),
|
|
|
|
+ "receiveShadow" : generate_bool_property(receiveShadow),
|
|
|
|
+ "doubleSided" : generate_bool_property(doubleSided),
|
|
|
|
+ "visible" : generate_bool_property(visible)
|
|
|
|
+ }
|
|
|
|
+ chunks.append(object_string)
|
|
|
|
+
|
|
|
|
+ elif obj.type == "EMPTY" or (obj.type == "MESH" and not obj.THREE_exportGeometry):
|
|
|
|
+
|
|
|
|
+ object_id = obj.name
|
|
|
|
+ group_ids = generate_group_id_list(obj)
|
|
|
|
+
|
|
|
|
+ position, quaternion, scale = obj.matrix_world.decompose()
|
|
|
|
+ rotation = quaternion.to_euler("XYZ")
|
|
|
|
+
|
|
|
|
+ group_string = ""
|
|
|
|
+ if len(group_ids) > 0:
|
|
|
|
+ group_string = generate_string_list(group_ids)
|
|
|
|
+
|
|
|
|
+ object_string = TEMPLATE_EMPTY % {
|
|
|
|
+ "object_id" : generate_string(object_id),
|
|
|
|
+ "group_id" : group_string,
|
|
|
|
+
|
|
|
|
+ "position" : generate_vec3(position),
|
|
|
|
+ "rotation" : generate_vec3(rotation),
|
|
|
|
+ "quaternion" : generate_vec4(quaternion),
|
|
|
|
+ "scale" : generate_vec3(scale)
|
|
|
|
+ }
|
|
|
|
+ chunks.append(object_string)
|
|
|
|
+
|
|
|
|
+ return ",\n\n".join(chunks), len(chunks)
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Scene exporter - geometries
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def generate_geometries(data):
|
|
|
|
+ chunks = []
|
|
|
|
+
|
|
|
|
+ geo_set = set()
|
|
|
|
+
|
|
|
|
+ for obj in data["objects"]:
|
|
|
|
+ if obj.type == "MESH" and obj.THREE_exportGeometry:
|
|
|
|
+
|
|
|
|
+ if len(obj.modifiers) > 0:
|
|
|
|
+ name = obj.name
|
|
|
|
+ else:
|
|
|
|
+ name = obj.data.name
|
|
|
|
+
|
|
|
|
+ if name not in geo_set:
|
|
|
|
+
|
|
|
|
+ geometry_id = "geo_%s" % name
|
|
|
|
+
|
|
|
|
+ if data["embed_meshes"]:
|
|
|
|
+
|
|
|
|
+ embed_id = "emb_%s" % name
|
|
|
|
+
|
|
|
|
+ geometry_string = TEMPLATE_GEOMETRY_EMBED % {
|
|
|
|
+ "geometry_id" : generate_string(geometry_id),
|
|
|
|
+ "embed_id" : generate_string(embed_id)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+
|
|
|
|
+ model_filename = os.path.basename(generate_mesh_filename(name, data["filepath"]))
|
|
|
|
+
|
|
|
|
+ geometry_string = TEMPLATE_GEOMETRY_LINK % {
|
|
|
|
+ "geometry_id" : generate_string(geometry_id),
|
|
|
|
+ "model_file" : generate_string(model_filename)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ chunks.append(geometry_string)
|
|
|
|
+
|
|
|
|
+ geo_set.add(name)
|
|
|
|
+
|
|
|
|
+ return ",\n\n".join(chunks), len(chunks)
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Scene exporter - textures
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def generate_textures_scene(data):
|
|
|
|
+ chunks = []
|
|
|
|
+
|
|
|
|
+ # TODO: extract just textures actually used by some objects in the scene
|
|
|
|
+
|
|
|
|
+ for texture in bpy.data.textures:
|
|
|
|
+
|
|
|
|
+ if texture.type == 'IMAGE' and texture.image:
|
|
|
|
+
|
|
|
|
+ img = texture.image
|
|
|
|
+
|
|
|
|
+ texture_id = img.name
|
|
|
|
+ texture_file = extract_texture_filename(img)
|
|
|
|
+
|
|
|
|
+ if data["copy_textures"]:
|
|
|
|
+ save_image(img, texture_file, data["filepath"])
|
|
|
|
+
|
|
|
|
+ extras = ""
|
|
|
|
+
|
|
|
|
+ if texture.repeat_x != 1 or texture.repeat_y != 1:
|
|
|
|
+ extras += ',\n "repeat": [%g, %g]' % (texture.repeat_x, texture.repeat_y)
|
|
|
|
+
|
|
|
|
+ if texture.extension == "REPEAT":
|
|
|
|
+ wrap_x = "repeat"
|
|
|
|
+ wrap_y = "repeat"
|
|
|
|
+
|
|
|
|
+ if texture.use_mirror_x:
|
|
|
|
+ wrap_x = "mirror"
|
|
|
|
+ if texture.use_mirror_y:
|
|
|
|
+ wrap_y = "mirror"
|
|
|
|
+
|
|
|
|
+ extras += ',\n "wrap": ["%s", "%s"]' % (wrap_x, wrap_y)
|
|
|
|
+
|
|
|
|
+ texture_string = TEMPLATE_TEXTURE % {
|
|
|
|
+ "texture_id" : generate_string(texture_id),
|
|
|
|
+ "texture_file" : generate_string(texture_file),
|
|
|
|
+ "extras" : extras
|
|
|
|
+ }
|
|
|
|
+ chunks.append(texture_string)
|
|
|
|
+
|
|
|
|
+ return ",\n\n".join(chunks), len(chunks)
|
|
|
|
+
|
|
|
|
+def extract_texture_filename(image):
|
|
|
|
+ fn = bpy.path.abspath(image.filepath)
|
|
|
|
+ fn = os.path.normpath(fn)
|
|
|
|
+ fn_strip = os.path.basename(fn)
|
|
|
|
+ return fn_strip
|
|
|
|
+
|
|
|
|
+def save_image(img, name, fpath):
|
|
|
|
+ dst_dir = os.path.dirname(fpath)
|
|
|
|
+ dst_path = os.path.join(dst_dir, name)
|
|
|
|
+
|
|
|
|
+ ensure_folder_exist(dst_dir)
|
|
|
|
+
|
|
|
|
+ if img.packed_file:
|
|
|
|
+ img.save_render(dst_path)
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+ src_path = bpy.path.abspath(img.filepath)
|
|
|
|
+ shutil.copy(src_path, dst_dir)
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Scene exporter - materials
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def extract_material_data(m, option_colors):
|
|
|
|
+ world = bpy.context.scene.world
|
|
|
|
+
|
|
|
|
+ material = { 'name': m.name }
|
|
|
|
+
|
|
|
|
+ material['colorDiffuse'] = [m.diffuse_intensity * m.diffuse_color[0],
|
|
|
|
+ m.diffuse_intensity * m.diffuse_color[1],
|
|
|
|
+ m.diffuse_intensity * m.diffuse_color[2]]
|
|
|
|
+
|
|
|
|
+ material['colorSpecular'] = [m.specular_intensity * m.specular_color[0],
|
|
|
|
+ m.specular_intensity * m.specular_color[1],
|
|
|
|
+ m.specular_intensity * m.specular_color[2]]
|
|
|
|
+
|
|
|
|
+ material['colorAmbient'] = [m.ambient * material['colorDiffuse'][0],
|
|
|
|
+ m.ambient * material['colorDiffuse'][1],
|
|
|
|
+ m.ambient * material['colorDiffuse'][2]]
|
|
|
|
+
|
|
|
|
+ material['transparency'] = m.alpha
|
|
|
|
+
|
|
|
|
+ # not sure about mapping values to Blinn-Phong shader
|
|
|
|
+ # Blender uses INT from [1,511] with default 0
|
|
|
|
+ # http://www.blender.org/documentation/blender_python_api_2_54_0/bpy.types.Material.html#bpy.types.Material.specular_hardness
|
|
|
|
+
|
|
|
|
+ material["specularCoef"] = m.specular_hardness
|
|
|
|
+
|
|
|
|
+ material["vertexColors"] = m.THREE_useVertexColors and option_colors
|
|
|
|
+
|
|
|
|
+ material['mapDiffuse'] = ""
|
|
|
|
+ material['mapLight'] = ""
|
|
|
|
+ material['mapSpecular'] = ""
|
|
|
|
+ material['mapNormal'] = ""
|
|
|
|
+ material['mapBump'] = ""
|
|
|
|
+
|
|
|
|
+ material['mapNormalFactor'] = 1.0
|
|
|
|
+ material['mapBumpScale'] = 1.0
|
|
|
|
+
|
|
|
|
+ textures = guess_material_textures(m)
|
|
|
|
+
|
|
|
|
+ if textures['diffuse']:
|
|
|
|
+ material['mapDiffuse'] = textures['diffuse']['texture'].image.name
|
|
|
|
+
|
|
|
|
+ if textures['light']:
|
|
|
|
+ material['mapLight'] = textures['light']['texture'].image.name
|
|
|
|
+
|
|
|
|
+ if textures['specular']:
|
|
|
|
+ material['mapSpecular'] = textures['specular']['texture'].image.name
|
|
|
|
+
|
|
|
|
+ if textures['normal']:
|
|
|
|
+ material['mapNormal'] = textures['normal']['texture'].image.name
|
|
|
|
+ if textures['normal']['slot'].use_map_normal:
|
|
|
|
+ material['mapNormalFactor'] = textures['normal']['slot'].normal_factor
|
|
|
|
+
|
|
|
|
+ if textures['bump']:
|
|
|
|
+ material['mapBump'] = textures['bump']['texture'].image.name
|
|
|
|
+ if textures['normal']['slot'].use_map_normal:
|
|
|
|
+ material['mapBumpScale'] = textures['normal']['slot'].normal_factor
|
|
|
|
+
|
|
|
|
+ material['shading'] = m.THREE_materialType
|
|
|
|
+ material['blending'] = m.THREE_blendingType
|
|
|
|
+ material['depthWrite'] = m.THREE_depthWrite
|
|
|
|
+ material['depthTest'] = m.THREE_depthTest
|
|
|
|
+ material['transparent'] = m.use_transparency
|
|
|
|
+
|
|
|
|
+ return material
|
|
|
|
+
|
|
|
|
+def guess_material_textures(material):
|
|
|
|
+ textures = {
|
|
|
|
+ 'diffuse' : None,
|
|
|
|
+ 'light' : None,
|
|
|
|
+ 'normal' : None,
|
|
|
|
+ 'specular': None,
|
|
|
|
+ 'bump' : None
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ # just take first textures of each, for the moment three.js materials can't handle more
|
|
|
|
+ # assume diffuse comes before lightmap, normalmap has checked flag
|
|
|
|
+
|
|
|
|
+ for i in range(len(material.texture_slots)):
|
|
|
|
+ slot = material.texture_slots[i]
|
|
|
|
+ if slot:
|
|
|
|
+ texture = slot.texture
|
|
|
|
+ if slot.use and texture and texture.type == 'IMAGE':
|
|
|
|
+
|
|
|
|
+ # normal map in Blender UI: textures => image sampling => normal map
|
|
|
|
+
|
|
|
|
+ if texture.use_normal_map:
|
|
|
|
+ textures['normal'] = { "texture": texture, "slot": slot }
|
|
|
|
+
|
|
|
|
+ # bump map in Blender UI: textures => influence => geometry => normal
|
|
|
|
+
|
|
|
|
+ elif slot.use_map_normal:
|
|
|
|
+ textures['bump'] = { "texture": texture, "slot": slot }
|
|
|
|
+
|
|
|
|
+ elif slot.use_map_specular or slot.use_map_hardness:
|
|
|
|
+ textures['specular'] = { "texture": texture, "slot": slot }
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+ if not textures['diffuse'] and not slot.blend_type == 'MULTIPLY':
|
|
|
|
+ textures['diffuse'] = { "texture": texture, "slot": slot }
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+ textures['light'] = { "texture": texture, "slot": slot }
|
|
|
|
+
|
|
|
|
+ if textures['diffuse'] and textures['normal'] and textures['light'] and textures['specular'] and textures['bump']:
|
|
|
|
+ break
|
|
|
|
+
|
|
|
|
+ return textures
|
|
|
|
+
|
|
|
|
+def generate_material_string(material):
|
|
|
|
+
|
|
|
|
+ material_id = material["name"]
|
|
|
|
+
|
|
|
|
+ # default to Lambert
|
|
|
|
+
|
|
|
|
+ shading = material.get("shading", "Lambert")
|
|
|
|
+
|
|
|
|
+ # normal and bump mapped materials must use Phong
|
|
|
|
+ # to get all required parameters for normal shader
|
|
|
|
+
|
|
|
|
+ if material['mapNormal'] or material['mapBump']:
|
|
|
|
+ shading = "Phong"
|
|
|
|
+
|
|
|
|
+ type_map = {
|
|
|
|
+ "Lambert" : "MeshLambertMaterial",
|
|
|
|
+ "Phong" : "MeshPhongMaterial"
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ material_type = type_map.get(shading, "MeshBasicMaterial")
|
|
|
|
+
|
|
|
|
+ parameters = '"color": %d' % rgb2int(material["colorDiffuse"])
|
|
|
|
+ parameters += ', "opacity": %.2g' % material["transparency"]
|
|
|
|
+
|
|
|
|
+ if shading == "Phong":
|
|
|
|
+ parameters += ', "ambient": %d' % rgb2int(material["colorAmbient"])
|
|
|
|
+ parameters += ', "specular": %d' % rgb2int(material["colorSpecular"])
|
|
|
|
+ parameters += ', "shininess": %.1g' % material["specularCoef"]
|
|
|
|
+
|
|
|
|
+ colorMap = material['mapDiffuse']
|
|
|
|
+ lightMap = material['mapLight']
|
|
|
|
+ specularMap = material['mapSpecular']
|
|
|
|
+ normalMap = material['mapNormal']
|
|
|
|
+ bumpMap = material['mapBump']
|
|
|
|
+ normalMapFactor = material['mapNormalFactor']
|
|
|
|
+ bumpMapScale = material['mapBumpScale']
|
|
|
|
+
|
|
|
|
+ if colorMap:
|
|
|
|
+ parameters += ', "map": %s' % generate_string(colorMap)
|
|
|
|
+ if lightMap:
|
|
|
|
+ parameters += ', "lightMap": %s' % generate_string(lightMap)
|
|
|
|
+ if specularMap:
|
|
|
|
+ parameters += ', "specularMap": %s' % generate_string(specularMap)
|
|
|
|
+ if normalMap:
|
|
|
|
+ parameters += ', "normalMap": %s' % generate_string(normalMap)
|
|
|
|
+ if bumpMap:
|
|
|
|
+ parameters += ', "bumpMap": %s' % generate_string(bumpMap)
|
|
|
|
+
|
|
|
|
+ if normalMapFactor != 1.0:
|
|
|
|
+ parameters += ', "normalMapFactor": %g' % normalMapFactor
|
|
|
|
+
|
|
|
|
+ if bumpMapScale != 1.0:
|
|
|
|
+ parameters += ', "bumpMapScale": %g' % bumpMapScale
|
|
|
|
+
|
|
|
|
+ if material['vertexColors']:
|
|
|
|
+ parameters += ', "vertexColors": "vertex"'
|
|
|
|
+
|
|
|
|
+ if material['transparent']:
|
|
|
|
+ parameters += ', "transparent": true'
|
|
|
|
+
|
|
|
|
+ parameters += ', "blending": "%s"' % material['blending']
|
|
|
|
+
|
|
|
|
+ if not material['depthWrite']:
|
|
|
|
+ parameters += ', "depthWrite": false'
|
|
|
|
+
|
|
|
|
+ if not material['depthTest']:
|
|
|
|
+ parameters += ', "depthTest": false'
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ material_string = TEMPLATE_MATERIAL_SCENE % {
|
|
|
|
+ "material_id" : generate_string(material_id),
|
|
|
|
+ "type" : generate_string(material_type),
|
|
|
|
+ "parameters" : parameters
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return material_string
|
|
|
|
+
|
|
|
|
+def generate_materials_scene(data):
|
|
|
|
+ chunks = []
|
|
|
|
+
|
|
|
|
+ # TODO: extract just materials actually used by some objects in the scene
|
|
|
|
+
|
|
|
|
+ for m in bpy.data.materials:
|
|
|
|
+ material = extract_material_data(m, data["use_colors"])
|
|
|
|
+ material_string = generate_material_string(material)
|
|
|
|
+ chunks.append(material_string)
|
|
|
|
+
|
|
|
|
+ return ",\n\n".join(chunks), len(chunks)
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Scene exporter - cameras
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def generate_cameras(data):
|
|
|
|
+ chunks = []
|
|
|
|
+
|
|
|
|
+ if data["use_cameras"]:
|
|
|
|
+
|
|
|
|
+ cams = bpy.data.objects
|
|
|
|
+ cams = [ob for ob in cams if (ob.type == 'CAMERA' and ob.select)]
|
|
|
|
+
|
|
|
|
+ if not cams:
|
|
|
|
+ camera = DEFAULTS["camera"]
|
|
|
|
+
|
|
|
|
+ if camera["type"] == "PerspectiveCamera":
|
|
|
|
+
|
|
|
|
+ camera_string = TEMPLATE_CAMERA_PERSPECTIVE % {
|
|
|
|
+ "camera_id" : generate_string(camera["name"]),
|
|
|
|
+ "fov" : camera["fov"],
|
|
|
|
+ "aspect" : camera["aspect"],
|
|
|
|
+ "near" : camera["near"],
|
|
|
|
+ "far" : camera["far"],
|
|
|
|
+ "position" : generate_vec3(camera["position"]),
|
|
|
|
+ "target" : generate_vec3(camera["target"])
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ elif camera["type"] == "OrthographicCamera":
|
|
|
|
+
|
|
|
|
+ camera_string = TEMPLATE_CAMERA_ORTHO % {
|
|
|
|
+ "camera_id" : generate_string(camera["name"]),
|
|
|
|
+ "left" : camera["left"],
|
|
|
|
+ "right" : camera["right"],
|
|
|
|
+ "top" : camera["top"],
|
|
|
|
+ "bottom" : camera["bottom"],
|
|
|
|
+ "near" : camera["near"],
|
|
|
|
+ "far" : camera["far"],
|
|
|
|
+ "position" : generate_vec3(camera["position"]),
|
|
|
|
+ "target" : generate_vec3(camera["target"])
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ chunks.append(camera_string)
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+
|
|
|
|
+ for cameraobj in cams:
|
|
|
|
+ camera = bpy.data.cameras[cameraobj.name]
|
|
|
|
+
|
|
|
|
+ # TODO:
|
|
|
|
+ # Support more than perspective camera
|
|
|
|
+ # Calculate a target/lookat
|
|
|
|
+ # Get correct aspect ratio
|
|
|
|
+ if camera.id_data.type == "PERSP":
|
|
|
|
+
|
|
|
|
+ camera_string = TEMPLATE_CAMERA_PERSPECTIVE % {
|
|
|
|
+ "camera_id" : generate_string(camera.name),
|
|
|
|
+ "fov" : (camera.angle / 3.14) * 180.0,
|
|
|
|
+ "aspect" : 1.333,
|
|
|
|
+ "near" : camera.clip_start,
|
|
|
|
+ "far" : camera.clip_end,
|
|
|
|
+ "position" : generate_vec3([cameraobj.location[0], -cameraobj.location[1], cameraobj.location[2]]),
|
|
|
|
+ "target" : generate_vec3([0, 0, 0])
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ chunks.append(camera_string)
|
|
|
|
+
|
|
|
|
+ return ",\n\n".join(chunks), len(chunks)
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Scene exporter - lights
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def generate_lights(data):
|
|
|
|
+ chunks = []
|
|
|
|
+
|
|
|
|
+ if data["use_lights"]:
|
|
|
|
+
|
|
|
|
+ lights = data.get("lights", [])
|
|
|
|
+ if not lights:
|
|
|
|
+ lights.append(DEFAULTS["light"])
|
|
|
|
+
|
|
|
|
+ for light in lights:
|
|
|
|
+
|
|
|
|
+ if light["type"] == "DirectionalLight":
|
|
|
|
+ light_string = TEMPLATE_LIGHT_DIRECTIONAL % {
|
|
|
|
+ "light_id" : generate_string(light["name"]),
|
|
|
|
+ "direction" : generate_vec3(light["direction"]),
|
|
|
|
+ "color" : rgb2int(light["color"]),
|
|
|
|
+ "intensity" : light["intensity"]
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ elif light["type"] == "PointLight":
|
|
|
|
+ light_string = TEMPLATE_LIGHT_POINT % {
|
|
|
|
+ "light_id" : generate_string(light["name"]),
|
|
|
|
+ "position" : generate_vec3(light["position"]),
|
|
|
|
+ "color" : rgb2int(light["color"]),
|
|
|
|
+ "intensity" : light["intensity"]
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ chunks.append(light_string)
|
|
|
|
+
|
|
|
|
+ return ",\n\n".join(chunks), len(chunks)
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Scene exporter - embedded meshes
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def generate_embeds(data):
|
|
|
|
+
|
|
|
|
+ if data["embed_meshes"]:
|
|
|
|
+
|
|
|
|
+ chunks = []
|
|
|
|
+
|
|
|
|
+ for e in data["embeds"]:
|
|
|
|
+
|
|
|
|
+ embed = '"emb_%s": {%s}' % (e, data["embeds"][e])
|
|
|
|
+ chunks.append(embed)
|
|
|
|
+
|
|
|
|
+ return ",\n\n".join(chunks)
|
|
|
|
+
|
|
|
|
+ return ""
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Scene exporter - generate ASCII scene
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def generate_ascii_scene(data):
|
|
|
|
+
|
|
|
|
+ objects, nobjects = generate_objects(data)
|
|
|
|
+ geometries, ngeometries = generate_geometries(data)
|
|
|
|
+ textures, ntextures = generate_textures_scene(data)
|
|
|
|
+ materials, nmaterials = generate_materials_scene(data)
|
|
|
|
+ lights, nlights = generate_lights(data)
|
|
|
|
+ cameras, ncameras = generate_cameras(data)
|
|
|
|
+
|
|
|
|
+ embeds = generate_embeds(data)
|
|
|
|
+
|
|
|
|
+ if nlights > 0:
|
|
|
|
+ if nobjects > 0:
|
|
|
|
+ objects = objects + ",\n\n" + lights
|
|
|
|
+ else:
|
|
|
|
+ objects = lights
|
|
|
|
+ nobjects += nlights
|
|
|
|
+
|
|
|
|
+ if ncameras > 0:
|
|
|
|
+ if nobjects > 0:
|
|
|
|
+ objects = objects + ",\n\n" + cameras
|
|
|
|
+ else:
|
|
|
|
+ objects = cameras
|
|
|
|
+ nobjects += ncameras
|
|
|
|
+
|
|
|
|
+ basetype = "relativeTo"
|
|
|
|
+
|
|
|
|
+ if data["base_html"]:
|
|
|
|
+ basetype += "HTML"
|
|
|
|
+ else:
|
|
|
|
+ basetype += "Scene"
|
|
|
|
+
|
|
|
|
+ sections = [
|
|
|
|
+ ["objects", objects],
|
|
|
|
+ ["geometries", geometries],
|
|
|
|
+ ["textures", textures],
|
|
|
|
+ ["materials", materials],
|
|
|
|
+ ["embeds", embeds]
|
|
|
|
+ ]
|
|
|
|
+
|
|
|
|
+ chunks = []
|
|
|
|
+ for label, content in sections:
|
|
|
|
+ if content:
|
|
|
|
+ chunks.append(generate_section(label, content))
|
|
|
|
+
|
|
|
|
+ sections_string = "\n".join(chunks)
|
|
|
|
+
|
|
|
|
+ default_camera = ""
|
|
|
|
+ if data["use_cameras"]:
|
|
|
|
+ cams = [ob for ob in bpy.data.objects if (ob.type == 'CAMERA' and ob.select)]
|
|
|
|
+ if not cams:
|
|
|
|
+ default_camera = "default_camera"
|
|
|
|
+ else:
|
|
|
|
+ default_camera = cams[0].name
|
|
|
|
+
|
|
|
|
+ parameters = {
|
|
|
|
+ "fname" : data["source_file"],
|
|
|
|
+
|
|
|
|
+ "sections" : sections_string,
|
|
|
|
+
|
|
|
|
+ "bgcolor" : generate_vec3(DEFAULTS["bgcolor"]),
|
|
|
|
+ "bgalpha" : DEFAULTS["bgalpha"],
|
|
|
|
+ "defcamera" : generate_string(default_camera),
|
|
|
|
+
|
|
|
|
+ "nobjects" : nobjects,
|
|
|
|
+ "ngeometries" : ngeometries,
|
|
|
|
+ "ntextures" : ntextures,
|
|
|
|
+ "basetype" : generate_string(basetype),
|
|
|
|
+ "nmaterials" : nmaterials,
|
|
|
|
+
|
|
|
|
+ "position" : generate_vec3(DEFAULTS["position"]),
|
|
|
|
+ "rotation" : generate_vec3(DEFAULTS["rotation"]),
|
|
|
|
+ "scale" : generate_vec3(DEFAULTS["scale"])
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ text = TEMPLATE_SCENE_ASCII % parameters
|
|
|
|
+
|
|
|
|
+ return text
|
|
|
|
+
|
|
|
|
+def export_scene(scene, filepath, flipyz, option_colors, option_lights, option_cameras, option_embed_meshes, embeds, option_url_base_html, option_copy_textures):
|
|
|
|
+
|
|
|
|
+ source_file = os.path.basename(bpy.data.filepath)
|
|
|
|
+
|
|
|
|
+ # objects are contained in scene and linked groups
|
|
|
|
+ objects = []
|
|
|
|
+
|
|
|
|
+ # get scene objects
|
|
|
|
+ sceneobjects = scene.objects
|
|
|
|
+ for obj in sceneobjects:
|
|
|
|
+ objects.append(obj)
|
|
|
|
+
|
|
|
|
+ # get linked group objcts
|
|
|
|
+ for group in bpy.data.groups:
|
|
|
|
+ for object in group.objects:
|
|
|
|
+ objects.append(object)
|
|
|
|
+
|
|
|
|
+ scene_text = ""
|
|
|
|
+ data = {
|
|
|
|
+ "scene" : scene,
|
|
|
|
+ "objects" : objects,
|
|
|
|
+ "embeds" : embeds,
|
|
|
|
+ "source_file" : source_file,
|
|
|
|
+ "filepath" : filepath,
|
|
|
|
+ "flipyz" : flipyz,
|
|
|
|
+ "use_colors" : option_colors,
|
|
|
|
+ "use_lights" : option_lights,
|
|
|
|
+ "use_cameras" : option_cameras,
|
|
|
|
+ "embed_meshes" : option_embed_meshes,
|
|
|
|
+ "base_html" : option_url_base_html,
|
|
|
|
+ "copy_textures": option_copy_textures
|
|
|
|
+ }
|
|
|
|
+ scene_text += generate_ascii_scene(data)
|
|
|
|
+
|
|
|
|
+ write_file(filepath, scene_text)
|
|
|
|
+
|
|
|
|
+# #####################################################
|
|
|
|
+# Main
|
|
|
|
+# #####################################################
|
|
|
|
+
|
|
|
|
+def save(operator, context, filepath = "",
|
|
|
|
+ option_flip_yz = True,
|
|
|
|
+ option_vertices = True,
|
|
|
|
+ option_vertices_truncate = False,
|
|
|
|
+ option_faces = True,
|
|
|
|
+ option_normals = True,
|
|
|
|
+ option_uv_coords = True,
|
|
|
|
+ option_materials = True,
|
|
|
|
+ option_colors = True,
|
|
|
|
+ option_bones = True,
|
|
|
|
+ option_skinning = True,
|
|
|
|
+ align_model = 0,
|
|
|
|
+ option_export_scene = False,
|
|
|
|
+ option_lights = False,
|
|
|
|
+ option_cameras = False,
|
|
|
|
+ option_scale = 1.0,
|
|
|
|
+ option_embed_meshes = True,
|
|
|
|
+ option_url_base_html = False,
|
|
|
|
+ option_copy_textures = False,
|
|
|
|
+ option_animation_morph = False,
|
|
|
|
+ option_animation_skeletal = False,
|
|
|
|
+ option_frame_step = 1,
|
|
|
|
+ option_all_meshes = True):
|
|
|
|
+
|
|
|
|
+ #print("URL TYPE", option_url_base_html)
|
|
|
|
+
|
|
|
|
+ filepath = ensure_extension(filepath, '.js')
|
|
|
|
+
|
|
|
|
+ scene = context.scene
|
|
|
|
+
|
|
|
|
+ if scene.objects.active:
|
|
|
|
+ bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
+
|
|
|
|
+ if option_all_meshes:
|
|
|
|
+ sceneobjects = scene.objects
|
|
|
|
+ else:
|
|
|
|
+ sceneobjects = context.selected_objects
|
|
|
|
+
|
|
|
|
+ # objects are contained in scene and linked groups
|
|
|
|
+ objects = []
|
|
|
|
+
|
|
|
|
+ # get scene objects
|
|
|
|
+ for obj in sceneobjects:
|
|
|
|
+ objects.append(obj)
|
|
|
|
+
|
|
|
|
+ # get objects in linked groups
|
|
|
|
+ for group in bpy.data.groups:
|
|
|
|
+ for object in group.objects:
|
|
|
|
+ objects.append(object)
|
|
|
|
+
|
|
|
|
+ if option_export_scene:
|
|
|
|
+
|
|
|
|
+ geo_set = set()
|
|
|
|
+ embeds = {}
|
|
|
|
+
|
|
|
|
+ for object in objects:
|
|
|
|
+ if object.type == "MESH" and object.THREE_exportGeometry:
|
|
|
|
+
|
|
|
|
+ # create extra copy of geometry with applied modifiers
|
|
|
|
+ # (if they exist)
|
|
|
|
+
|
|
|
|
+ if len(object.modifiers) > 0:
|
|
|
|
+ name = object.name
|
|
|
|
+
|
|
|
|
+ # otherwise can share geometry
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+ name = object.data.name
|
|
|
|
+
|
|
|
|
+ if name not in geo_set:
|
|
|
|
+
|
|
|
|
+ if option_embed_meshes:
|
|
|
|
+
|
|
|
|
+ text, model_string = generate_mesh_string([object], scene,
|
|
|
|
+ option_vertices,
|
|
|
|
+ option_vertices_truncate,
|
|
|
|
+ option_faces,
|
|
|
|
+ option_normals,
|
|
|
|
+ option_uv_coords,
|
|
|
|
+ option_materials,
|
|
|
|
+ option_colors,
|
|
|
|
+ option_bones,
|
|
|
|
+ option_skinning,
|
|
|
|
+ False, # align_model
|
|
|
|
+ option_flip_yz,
|
|
|
|
+ option_scale,
|
|
|
|
+ False, # export_single_model
|
|
|
|
+ False, # option_copy_textures
|
|
|
|
+ filepath,
|
|
|
|
+ option_animation_morph,
|
|
|
|
+ option_animation_skeletal,
|
|
|
|
+ option_frame_step)
|
|
|
|
+
|
|
|
|
+ embeds[name] = model_string
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+
|
|
|
|
+ fname = generate_mesh_filename(name, filepath)
|
|
|
|
+ export_mesh([object], scene,
|
|
|
|
+ fname,
|
|
|
|
+ option_vertices,
|
|
|
|
+ option_vertices_truncate,
|
|
|
|
+ option_faces,
|
|
|
|
+ option_normals,
|
|
|
|
+ option_uv_coords,
|
|
|
|
+ option_materials,
|
|
|
|
+ option_colors,
|
|
|
|
+ option_bones,
|
|
|
|
+ option_skinning,
|
|
|
|
+ False, # align_model
|
|
|
|
+ option_flip_yz,
|
|
|
|
+ option_scale,
|
|
|
|
+ False, # export_single_model
|
|
|
|
+ option_copy_textures,
|
|
|
|
+ option_animation_morph,
|
|
|
|
+ option_animation_skeletal,
|
|
|
|
+ option_frame_step)
|
|
|
|
+
|
|
|
|
+ geo_set.add(name)
|
|
|
|
+
|
|
|
|
+ export_scene(scene, filepath,
|
|
|
|
+ option_flip_yz,
|
|
|
|
+ option_colors,
|
|
|
|
+ option_lights,
|
|
|
|
+ option_cameras,
|
|
|
|
+ option_embed_meshes,
|
|
|
|
+ embeds,
|
|
|
|
+ option_url_base_html,
|
|
|
|
+ option_copy_textures)
|
|
|
|
+
|
|
|
|
+ else:
|
|
|
|
+
|
|
|
|
+ export_mesh(objects, scene, filepath,
|
|
|
|
+ option_vertices,
|
|
|
|
+ option_vertices_truncate,
|
|
|
|
+ option_faces,
|
|
|
|
+ option_normals,
|
|
|
|
+ option_uv_coords,
|
|
|
|
+ option_materials,
|
|
|
|
+ option_colors,
|
|
|
|
+ option_bones,
|
|
|
|
+ option_skinning,
|
|
|
|
+ align_model,
|
|
|
|
+ option_flip_yz,
|
|
|
|
+ option_scale,
|
|
|
|
+ True, # export_single_model
|
|
|
|
+ option_copy_textures,
|
|
|
|
+ option_animation_morph,
|
|
|
|
+ option_animation_skeletal,
|
|
|
|
+ option_frame_step)
|
|
|
|
+
|
|
|
|
+ return {'FINISHED'}
|