Skip to content

Instantly share code, notes, and snippets.

@rgajrawala
Created February 22, 2022 04:31
Show Gist options
  • Save rgajrawala/ed027e9f2afc203366e43a203c673db8 to your computer and use it in GitHub Desktop.
Save rgajrawala/ed027e9f2afc203366e43a203c673db8 to your computer and use it in GitHub Desktop.
ThreeJS Blender Converter Plugin
# ##### 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 #####
# ################################################################
# Init
# ################################################################
bl_info = {
"name": "three.js format",
"author": "mrdoob, kikko, alteredq, remoe, pxf, n3tfr34k, crobi",
"version": (1, 5, 0),
"blender": (2, 7, 0),
"location": "File > Import-Export",
"description": "Import-Export three.js meshes",
"warning": "",
"wiki_url": "https://github.com/mrdoob/three.js/tree/master/utils/exporters/blender",
"tracker_url": "https://github.com/mrdoob/three.js/issues",
"category": "Import-Export"}
# To support reload properly, try to access a package var,
# if it's there, reload everything
import bpy
if "bpy" in locals():
import imp
if "export_threejs" in locals():
imp.reload(export_threejs)
if "import_threejs" in locals():
imp.reload(import_threejs)
from bpy.props import *
from bpy_extras.io_utils import ExportHelper, ImportHelper
# ################################################################
# Custom properties
# ################################################################
bpy.types.Object.THREE_castShadow = bpy.props.BoolProperty()
bpy.types.Object.THREE_receiveShadow = bpy.props.BoolProperty()
bpy.types.Object.THREE_doubleSided = bpy.props.BoolProperty()
bpy.types.Object.THREE_exportGeometry = bpy.props.BoolProperty(default = True)
bpy.types.Material.THREE_useVertexColors = bpy.props.BoolProperty()
bpy.types.Material.THREE_depthWrite = bpy.props.BoolProperty(default = True)
bpy.types.Material.THREE_depthTest = bpy.props.BoolProperty(default = True)
THREE_material_types = [("Basic", "Basic", "Basic"), ("Phong", "Phong", "Phong"), ("Lambert", "Lambert", "Lambert")]
bpy.types.Material.THREE_materialType = EnumProperty(name = "Material type", description = "Material type", items = THREE_material_types, default = "Lambert")
THREE_blending_types = [("NoBlending", "NoBlending", "NoBlending"), ("NormalBlending", "NormalBlending", "NormalBlending"),
("AdditiveBlending", "AdditiveBlending", "AdditiveBlending"), ("SubtractiveBlending", "SubtractiveBlending", "SubtractiveBlending"),
("MultiplyBlending", "MultiplyBlending", "MultiplyBlending"), ("AdditiveAlphaBlending", "AdditiveAlphaBlending", "AdditiveAlphaBlending")]
bpy.types.Material.THREE_blendingType = EnumProperty(name = "Blending type", description = "Blending type", items = THREE_blending_types, default = "NormalBlending")
class OBJECT_PT_hello( bpy.types.Panel ):
bl_label = "THREE"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
def draw(self, context):
layout = self.layout
obj = context.object
row = layout.row()
row.label(text="Selected object: " + obj.name )
row = layout.row()
row.prop( obj, "THREE_exportGeometry", text="Export geometry" )
row = layout.row()
row.prop( obj, "THREE_castShadow", text="Casts shadow" )
row = layout.row()
row.prop( obj, "THREE_receiveShadow", text="Receives shadow" )
row = layout.row()
row.prop( obj, "THREE_doubleSided", text="Double sided" )
class MATERIAL_PT_hello( bpy.types.Panel ):
bl_label = "THREE"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
def draw(self, context):
layout = self.layout
mat = context.material
row = layout.row()
row.label(text="Selected material: " + mat.name )
row = layout.row()
row.prop( mat, "THREE_materialType", text="Material type" )
row = layout.row()
row.prop( mat, "THREE_blendingType", text="Blending type" )
row = layout.row()
row.prop( mat, "THREE_useVertexColors", text="Use vertex colors" )
row = layout.row()
row.prop( mat, "THREE_depthWrite", text="Enable depth writing" )
row = layout.row()
row.prop( mat, "THREE_depthTest", text="Enable depth testing" )
# ################################################################
# Importer
# ################################################################
class ImportTHREEJS(bpy.types.Operator, ImportHelper):
'''Load a Three.js ASCII JSON model'''
bl_idname = "import.threejs"
bl_label = "Import Three.js"
filename_ext = ".js"
filter_glob = StringProperty(default="*.js", options={'HIDDEN'})
option_flip_yz = BoolProperty(name="Flip YZ", description="Flip YZ", default=True)
recalculate_normals = BoolProperty(name="Recalculate normals", description="Recalculate vertex normals", default=True)
option_worker = BoolProperty(name="Worker", description="Old format using workers", default=False)
def execute(self, context):
import io_mesh_threejs.import_threejs
return io_mesh_threejs.import_threejs.load(self, context, **self.properties)
def draw(self, context):
layout = self.layout
row = layout.row()
row.prop(self.properties, "option_flip_yz")
row = layout.row()
row.prop(self.properties, "recalculate_normals")
row = layout.row()
row.prop(self.properties, "option_worker")
# ################################################################
# Exporter - settings
# ################################################################
SETTINGS_FILE_EXPORT = "threejs_settings_export.js"
import os
import json
def file_exists(filename):
"""Return true if file exists and accessible for reading.
Should be safer than just testing for existence due to links and
permissions magic on Unix filesystems.
@rtype: boolean
"""
try:
f = open(filename, 'r')
f.close()
return True
except IOError:
return False
def get_settings_fullpath():
return os.path.join(bpy.app.tempdir, SETTINGS_FILE_EXPORT)
def save_settings_export(properties):
settings = {
"option_export_scene" : properties.option_export_scene,
"option_embed_meshes" : properties.option_embed_meshes,
"option_url_base_html" : properties.option_url_base_html,
"option_copy_textures" : properties.option_copy_textures,
"option_lights" : properties.option_lights,
"option_cameras" : properties.option_cameras,
"option_animation_morph" : properties.option_animation_morph,
"option_animation_skeletal" : properties.option_animation_skeletal,
"option_frame_index_as_time" : properties.option_frame_index_as_time,
"option_frame_step" : properties.option_frame_step,
"option_all_meshes" : properties.option_all_meshes,
"option_flip_yz" : properties.option_flip_yz,
"option_materials" : properties.option_materials,
"option_normals" : properties.option_normals,
"option_colors" : properties.option_colors,
"option_uv_coords" : properties.option_uv_coords,
"option_faces" : properties.option_faces,
"option_vertices" : properties.option_vertices,
"option_skinning" : properties.option_skinning,
"option_bones" : properties.option_bones,
"option_vertices_truncate" : properties.option_vertices_truncate,
"option_scale" : properties.option_scale,
"align_model" : properties.align_model
}
fname = get_settings_fullpath()
f = open(fname, "w")
json.dump(settings, f)
def restore_settings_export(properties):
settings = {}
fname = get_settings_fullpath()
if file_exists(fname):
f = open(fname, "r")
settings = json.load(f)
properties.option_vertices = settings.get("option_vertices", True)
properties.option_vertices_truncate = settings.get("option_vertices_truncate", False)
properties.option_faces = settings.get("option_faces", True)
properties.option_normals = settings.get("option_normals", True)
properties.option_colors = settings.get("option_colors", True)
properties.option_uv_coords = settings.get("option_uv_coords", True)
properties.option_materials = settings.get("option_materials", True)
properties.option_skinning = settings.get("option_skinning", True)
properties.option_bones = settings.get("option_bones", True)
properties.align_model = settings.get("align_model", "None")
properties.option_scale = settings.get("option_scale", 1.0)
properties.option_flip_yz = settings.get("option_flip_yz", True)
properties.option_export_scene = settings.get("option_export_scene", False)
properties.option_embed_meshes = settings.get("option_embed_meshes", True)
properties.option_url_base_html = settings.get("option_url_base_html", False)
properties.option_copy_textures = settings.get("option_copy_textures", False)
properties.option_lights = settings.get("option_lights", False)
properties.option_cameras = settings.get("option_cameras", False)
properties.option_animation_morph = settings.get("option_animation_morph", False)
properties.option_animation_skeletal = settings.get("option_animation_skeletal", False)
properties.option_frame_index_as_time = settings.get("option_frame_index_as_time", False)
properties.option_frame_step = settings.get("option_frame_step", 1)
properties.option_all_meshes = settings.get("option_all_meshes", True)
# ################################################################
# Exporter
# ################################################################
class ExportTHREEJS(bpy.types.Operator, ExportHelper):
'''Export selected object / scene for Three.js (ASCII JSON format).'''
bl_idname = "export.threejs"
bl_label = "Export Three.js"
filename_ext = ".js"
option_vertices = BoolProperty(name = "Vertices", description = "Export vertices", default = True)
option_vertices_deltas = BoolProperty(name = "Deltas", description = "Delta vertices", default = False)
option_vertices_truncate = BoolProperty(name = "Truncate", description = "Truncate vertices", default = False)
option_faces = BoolProperty(name = "Faces", description = "Export faces", default = True)
option_faces_deltas = BoolProperty(name = "Deltas", description = "Delta faces", default = False)
option_normals = BoolProperty(name = "Normals", description = "Export normals", default = True)
option_colors = BoolProperty(name = "Colors", description = "Export vertex colors", default = True)
option_uv_coords = BoolProperty(name = "UVs", description = "Export texture coordinates", default = True)
option_materials = BoolProperty(name = "Materials", description = "Export materials", default = True)
option_skinning = BoolProperty(name = "Skinning", description = "Export skin data", default = True)
option_bones = BoolProperty(name = "Bones", description = "Export bones", default = True)
align_types = [("None","None","None"), ("Center","Center","Center"), ("Bottom","Bottom","Bottom"), ("Top","Top","Top")]
align_model = EnumProperty(name = "Align model", description = "Align model", items = align_types, default = "None")
option_scale = FloatProperty(name = "Scale", description = "Scale vertices", min = 0.01, max = 1000.0, soft_min = 0.01, soft_max = 1000.0, default = 1.0)
option_flip_yz = BoolProperty(name = "Flip YZ", description = "Flip YZ", default = True)
option_export_scene = BoolProperty(name = "Scene", description = "Export scene", default = False)
option_embed_meshes = BoolProperty(name = "Embed meshes", description = "Embed meshes", default = True)
option_copy_textures = BoolProperty(name = "Copy textures", description = "Copy textures", default = False)
option_url_base_html = BoolProperty(name = "HTML as url base", description = "Use HTML as url base ", default = False)
option_lights = BoolProperty(name = "Lights", description = "Export default scene lights", default = False)
option_cameras = BoolProperty(name = "Cameras", description = "Export default scene cameras", default = False)
option_animation_morph = BoolProperty(name = "Morph animation", description = "Export animation (morphs)", default = False)
option_animation_skeletal = BoolProperty(name = "Skeletal animation", description = "Export animation (skeletal)", default = False)
option_frame_index_as_time = BoolProperty(name = "Frame index as time", description = "Use (original) frame index as frame time", default = False)
option_frame_step = IntProperty(name = "Frame step", description = "Animation frame step", min = 1, max = 1000, soft_min = 1, soft_max = 1000, default = 1)
option_all_meshes = BoolProperty(name = "All meshes", description = "All meshes (merged)", default = True)
def invoke(self, context, event):
restore_settings_export(self.properties)
return ExportHelper.invoke(self, context, event)
@classmethod
def poll(cls, context):
return context.active_object != None
def execute(self, context):
print("Selected: " + context.active_object.name)
if not self.properties.filepath:
raise Exception("filename not set")
save_settings_export(self.properties)
filepath = self.filepath
import io_mesh_threejs.export_threejs
return io_mesh_threejs.export_threejs.save(self, context, **self.properties)
def draw(self, context):
layout = self.layout
row = layout.row()
row.label(text="Geometry:")
row = layout.row()
row.prop(self.properties, "option_vertices")
# row = layout.row()
# row.enabled = self.properties.option_vertices
# row.prop(self.properties, "option_vertices_deltas")
row.prop(self.properties, "option_vertices_truncate")
layout.separator()
row = layout.row()
row.prop(self.properties, "option_faces")
row = layout.row()
row.enabled = self.properties.option_faces
# row.prop(self.properties, "option_faces_deltas")
layout.separator()
row = layout.row()
row.prop(self.properties, "option_normals")
layout.separator()
row = layout.row()
row.prop(self.properties, "option_bones")
row.prop(self.properties, "option_skinning")
layout.separator()
row = layout.row()
row.label(text="Materials:")
row = layout.row()
row.prop(self.properties, "option_uv_coords")
row.prop(self.properties, "option_colors")
row = layout.row()
row.prop(self.properties, "option_materials")
layout.separator()
row = layout.row()
row.label(text="Settings:")
row = layout.row()
row.prop(self.properties, "align_model")
row = layout.row()
row.prop(self.properties, "option_flip_yz")
row.prop(self.properties, "option_scale")
layout.separator()
row = layout.row()
row.label(text="--------- Experimental ---------")
layout.separator()
row = layout.row()
row.label(text="Scene:")
row = layout.row()
row.prop(self.properties, "option_export_scene")
row.prop(self.properties, "option_embed_meshes")
row = layout.row()
row.prop(self.properties, "option_lights")
row.prop(self.properties, "option_cameras")
layout.separator()
row = layout.row()
row.label(text="Animation:")
row = layout.row()
row.prop(self.properties, "option_animation_morph")
row = layout.row()
row.prop(self.properties, "option_animation_skeletal")
row = layout.row()
row.prop(self.properties, "option_frame_index_as_time")
row = layout.row()
row.prop(self.properties, "option_frame_step")
layout.separator()
row = layout.row()
row.label(text="Settings:")
row = layout.row()
row.prop(self.properties, "option_all_meshes")
row = layout.row()
row.prop(self.properties, "option_copy_textures")
row = layout.row()
row.prop(self.properties, "option_url_base_html")
layout.separator()
# ################################################################
# Common
# ################################################################
def menu_func_export(self, context):
default_path = bpy.data.filepath.replace(".blend", ".js")
self.layout.operator(ExportTHREEJS.bl_idname, text="Three.js (.js)").filepath = default_path
def menu_func_import(self, context):
self.layout.operator(ImportTHREEJS.bl_idname, text="Three.js (.js)")
def register():
bpy.utils.register_module(__name__)
bpy.types.INFO_MT_file_export.append(menu_func_export)
bpy.types.INFO_MT_file_import.append(menu_func_import)
def unregister():
bpy.utils.unregister_module(__name__)
bpy.types.INFO_MT_file_export.remove(menu_func_export)
bpy.types.INFO_MT_file_import.remove(menu_func_import)
if __name__ == "__main__":
register()
# ##### 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" : [0, 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
}
}
ROTATE_X_PI2 = mathutils.Quaternion((1.0, 0.0, 0.0), math.radians(-90.0)).to_matrix().to_4x4()
# 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.7 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_POINT = """\
%(light_id)s : {
"type" : "PointLight",
"position" : %(position)s,
"rotation" : %(rotation)s,
"color" : %(color)d,
"distance" : %(distance).3f,
"intensity" : %(intensity).3f
}"""
TEMPLATE_LIGHT_SUN = """\
%(light_id)s : {
"type" : "AmbientLight",
"position" : %(position)s,
"rotation" : %(rotation)s,
"color" : %(color)d,
"distance" : %(distance).3f,
"intensity" : %(intensity).3f
}"""
TEMPLATE_LIGHT_SPOT = """\
%(light_id)s : {
"type" : "SpotLight",
"position" : %(position)s,
"rotation" : %(rotation)s,
"color" : %(color)d,
"distance" : %(distance).3f,
"intensity" : %(intensity).3f,
"use_shadow" : %(use_shadow)d,
"angle" : %(angle).3f
}"""
TEMPLATE_LIGHT_HEMI = """\
%(light_id)s : {
"type" : "HemisphereLight",
"position" : %(position)s,
"rotation" : %(rotation)s,
"color" : %(color)d,
"distance" : %(distance).3f,
"intensity" : %(intensity).3f
}"""
TEMPLATE_LIGHT_AREA = """\
%(light_id)s : {
"type" : "AreaLight",
"position" : %(position)s,
"rotation" : %(rotation)s,
"color" : %(color)d,
"distance" : %(distance).3f,
"intensity" : %(intensity).3f,
"gamma" : %(gamma).3f,
"shape" : "%(shape)s",
"size" : %(size).3f,
"size_y" : %(size_y).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.7 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],
"animations" : [%(animations)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", encoding="utf-8")
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 - armature
# (only the first armature will exported)
# ##############################################################################
def get_armature():
if len(bpy.data.armatures) == 0:
print("Warning: no armatures in the scene")
return None, None
armature = bpy.data.armatures[0]
# Someone please figure out a proper way to get the armature node
for object in bpy.data.objects:
if object.type == 'ARMATURE':
return armature, object
print("Warning: no node of type 'ARMATURE' in the scene")
return None, None
# ##############################################################################
# Model exporter - bones
# (only the first armature will exported)
# ##############################################################################
def generate_bones(meshes, option_bones, flipyz):
if not option_bones:
return "", 0
armature, armature_object = get_armature()
if armature_object is None:
return "", 0
hierarchy = []
armature_matrix = armature_object.matrix_world
pose_bones = armature_object.pose.bones
#pose_bones = armature.bones
TEMPLATE_BONE = '{"parent":%d,"name":"%s","pos":[%g,%g,%g],"rotq":[%g,%g,%g,%g],"scl":[%g,%g,%g]}'
for pose_bone in pose_bones:
armature_bone = pose_bone.bone
#armature_bone = pose_bone
bonePos = armature_matrix * armature_bone.head_local
boneIndex = None
if armature_bone.parent is None:
bone_matrix = armature_matrix * armature_bone.matrix_local
bone_index = -1
else:
parent_matrix = armature_matrix * armature_bone.parent.matrix_local
bone_matrix = armature_matrix * armature_bone.matrix_local
bone_matrix = parent_matrix.inverted() * bone_matrix
bone_index = i = 0
for pose_parent in pose_bones:
armature_parent = pose_parent.bone
#armature_parent = pose_parent
if armature_parent.name == armature_bone.parent.name:
bone_index = i
i += 1
pos, rot, scl = bone_matrix.decompose()
if flipyz:
joint = TEMPLATE_BONE % (bone_index, armature_bone.name, pos.x, pos.z, -pos.y, rot.x, rot.z, -rot.y, rot.w, scl.x, scl.z, scl.y)
hierarchy.append(joint)
else:
joint = TEMPLATE_BONE % (bone_index, armature_bone.name, pos.x, pos.y, pos.z, rot.x, rot.y, rot.z, rot.w, scl.x, scl.y, scl.z)
hierarchy.append(joint)
bones_string = ",".join(hierarchy)
return bones_string, len(pose_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, armature_object = get_armature()
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]
found = 0
index = bone_proxy[0]
weight = bone_proxy[1]
for j, bone in enumerate(armature_object.pose.bones):
if object.vertex_groups[index].name == bone.name:
indices.append('%d' % j)
weights.append('%g' % weight)
found = 1
break
if found != 1:
indices.append('0')
weights.append('0')
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, option_frame_index_as_time, index):
if not option_animation_skeletal or len(bpy.data.actions) == 0:
return ""
# TODO: Add scaling influences
action = bpy.data.actions[index]
# get current context and then switch to dopesheet temporarily
current_context = bpy.context.area.type
bpy.context.area.type = "DOPESHEET_EDITOR"
bpy.context.space_data.mode = "ACTION"
# set active action
bpy.context.area.spaces.active.action = action
armature, armature_object = get_armature()
if armature_object is None or armature is None:
return "", 0
#armature_object = bpy.data.objects['marine_rig']
armature_matrix = armature_object.matrix_world
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
used_frames = int(frame_length / option_frame_step) + 1
TEMPLATE_KEYFRAME_FULL = '{"time":%g,"pos":[%g,%g,%g],"rot":[%g,%g,%g,%g],"scl":[%g,%g,%g]}'
TEMPLATE_KEYFRAME_BEGIN = '{"time":%g'
TEMPLATE_KEYFRAME_END = '}'
TEMPLATE_KEYFRAME_POS = ',"pos":[%g,%g,%g]'
TEMPLATE_KEYFRAME_ROT = ',"rot":[%g,%g,%g,%g]'
TEMPLATE_KEYFRAME_SCL = ',"scl":[%g,%g,%g]'
keys = []
channels_location = []
channels_rotation = []
channels_scale = []
# Precompute per-bone data
for pose_bone in armature_object.pose.bones:
armature_bone = pose_bone.bone
keys.append([])
channels_location.append( find_channels(action, armature_bone, "location"))
channels_rotation.append( find_channels(action, armature_bone, "rotation_quaternion"))
channels_rotation.append( find_channels(action, armature_bone, "rotation_euler"))
channels_scale.append( find_channels(action, armature_bone, "scale"))
# Process all frames
for frame_i in range(0, used_frames):
#print("Processing frame %d/%d" % (frame_i, used_frames))
# Compute the index of the current frame (snap the last index to the end)
frame = start_frame + frame_i * option_frame_step
if frame_i == used_frames-1:
frame = end_frame
# Compute the time of the frame
if option_frame_index_as_time:
time = frame - start_frame
else:
time = (frame - start_frame) / fps
# Let blender compute the pose bone transformations
bpy.data.scenes[0].frame_set(frame)
# Process all bones for the current frame
bone_index = 0
for pose_bone in armature_object.pose.bones:
# Extract the bone transformations
if pose_bone.parent is None:
bone_matrix = armature_matrix * pose_bone.matrix
else:
parent_matrix = armature_matrix * pose_bone.parent.matrix
bone_matrix = armature_matrix * pose_bone.matrix
bone_matrix = parent_matrix.inverted() * bone_matrix
pos, rot, scl = bone_matrix.decompose()
pchange = True or has_keyframe_at(channels_location[bone_index], frame)
rchange = True or has_keyframe_at(channels_rotation[bone_index], frame)
schange = True or has_keyframe_at(channels_scale[bone_index], frame)
if flipyz:
px, py, pz = pos.x, pos.z, -pos.y
rx, ry, rz, rw = rot.x, rot.z, -rot.y, rot.w
sx, sy, sz = scl.x, scl.z, scl.y
else:
px, py, pz = pos.x, pos.y, pos.z
rx, ry, rz, rw = rot.x, rot.y, rot.z, rot.w
sx, sy, sz = scl.x, scl.y, scl.z
# START-FRAME: needs pos, rot and scl attributes (required frame)
if frame == start_frame:
keyframe = TEMPLATE_KEYFRAME_FULL % (time, px, py, pz, rx, ry, rz, rw, sx, sy, sz)
keys[bone_index].append(keyframe)
# END-FRAME: needs pos, rot and scl attributes with animation length (required frame)
elif frame == end_frame:
keyframe = TEMPLATE_KEYFRAME_FULL % (time, px, py, pz, rx, ry, rz, rw, sx, sy, sz)
keys[bone_index].append(keyframe)
# MIDDLE-FRAME: needs only one of the attributes, can be an empty frame (optional frame)
elif pchange == True or rchange == True:
keyframe = TEMPLATE_KEYFRAME_BEGIN % time
if pchange == True:
keyframe = keyframe + TEMPLATE_KEYFRAME_POS % (px, py, pz)
if rchange == True:
keyframe = keyframe + TEMPLATE_KEYFRAME_ROT % (rx, ry, rz, rw)
if schange == True:
keyframe = keyframe + TEMPLATE_KEYFRAME_SCL % (sx, sy, sz)
keyframe = keyframe + TEMPLATE_KEYFRAME_END
keys[bone_index].append(keyframe)
bone_index += 1
# Gather data
parents = []
bone_index = 0
for pose_bone in armature_object.pose.bones:
keys_string = ",".join(keys[bone_index])
parent_index = bone_index - 1 # WTF? Also, this property is not used by three.js
parent = '{"parent":%d,"keys":[%s]}' % (parent_index, keys_string)
bone_index += 1
parents.append(parent)
hierarchy_string = ",".join(parents)
if option_frame_index_as_time:
length = frame_length
else:
length = frame_length / fps
animation_string = '"name":"%s","fps":%d,"length":%g,"hierarchy":[%s]' % (action.name, fps, length, hierarchy_string)
bpy.data.scenes[0].frame_set(start_frame)
# reset context
bpy.context.area.type = current_context
return animation_string
def find_channels(action, bone, channel_type):
bone_name = bone.name
ngroups = len(action.groups)
result = []
# Variant 1: channels grouped by bone names
if ngroups > 0:
# Find the channel group for the given bone
group_index = -1
for i in range(ngroups):
if action.groups[i].name == bone_name:
group_index = i
# Get all desired channels in that group
if group_index > -1:
for channel in action.groups[group_index].channels:
if channel_type in channel.data_path:
result.append(channel)
# Variant 2: no channel groups, bone names included in channel names
else:
bone_label = '"%s"' % bone_name
for channel in action.fcurves:
data_path = channel.data_path
if bone_label in data_path and channel_type in data_path:
result.append(channel)
return result
def find_keyframe_at(channel, frame):
for keyframe in channel.keyframe_points:
if keyframe.co[0] == frame:
return keyframe
return None
def has_keyframe_at(channels, frame):
for channel in channels:
if not find_keyframe_at(channel, frame) is None:
return True
return False
def generate_all_animations(option_animation_skeletal, option_frame_step, flipyz, option_frame_index_as_time):
all_animations_string = ""
if option_animation_skeletal:
for index in range(0, len(bpy.data.actions)):
if index != 0 :
all_animations_string += ", \n"
all_animations_string += "{" + generate_animation(option_animation_skeletal, option_frame_step, flipyz, option_frame_index_as_time,index) + "}"
return all_animations_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, action, armatureMatrix):
position = mathutils.Vector((0,0,0))
change = False
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 armatureMatrix*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, action, armatureMatrix):
# TODO: calculate rotation also from rotation_euler channels
rotation = mathutils.Vector((0,0,0,1))
change = False
ngroups = len(action.groups)
# animation grouped by bones
if ngroups > 0:
index = -1
for i in range(ngroups):
if action.groups[i].name == bone.name:
index = i
if index > -1:
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()
rotation.xyz = armatureMatrix * rotation.xyz
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['colorEmissive'] = [m.emit * material['colorDiffuse'][0],
m.emit * material['colorDiffuse'][1],
m.emit * 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] and textures[id]['texture'].users > 0 and len(textures[id]['texture'].users_material) > 0:
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_index_as_time,
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(meshes, 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,
"animations" : generate_all_animations(option_animation_skeletal, option_frame_step, flipyz, option_frame_index_as_time)
}
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.update(calc_tessface=True)
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_index_as_time,
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_index_as_time,
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,
option_frame_index_as_time):
"""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_index_as_time,
option_frame_step)
write_file(filepath, text)
print("writing", filepath, "done")
# #####################################################
# Scene exporter - render elements
# #####################################################
def generate_quat(quat):
return TEMPLATE_VEC4 % (quat.x, quat.y, quat.z, quat.w)
def generate_vec4(vec):
return TEMPLATE_VEC4 % (vec[0], vec[1], vec[2], vec[3])
def generate_vec3(vec, flipyz = False):
if flipyz:
return TEMPLATE_VEC3 % (vec[0], vec[2], vec[1])
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)
if data["flipyz"]:
matrix_world = ROTATE_X_PI2 * obj.matrix_world
else:
matrix_world = obj.matrix_world
position, quaternion, scale = matrix_world.decompose()
rotation = quaternion.to_euler("ZYX")
# 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_quat(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)
if data["flipyz"]:
matrix_world = ROTATE_X_PI2 * obj.matrix_world
else:
matrix_world = obj.matrix_world
position, quaternion, scale = matrix_world.decompose()
rotation = quaternion.to_euler("ZYX")
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_quat(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 and texture.users > 0 and len(texture.users_material) > 0:
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['colorEmissive'] = [m.emit * material['colorDiffuse'][0],
m.emit * material['colorDiffuse'][1],
m.emit * 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['bump']['slot'].use_map_normal:
material['mapBumpScale'] = textures['bump']['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 += ', "ambient": %d' % rgb2int(material["colorDiffuse"])
parameters += ', "emissive": %d' % rgb2int(material["colorEmissive"])
parameters += ', "opacity": %.2g' % material["transparency"]
if shading == "Phong":
parameters += ', "ambient": %d' % rgb2int(material["colorAmbient"])
parameters += ', "emissive": %d' % rgb2int(material["colorEmissive"])
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 = []
def material_is_used(mat):
minimum_users = 1
if mat.use_fake_user:
minimum_users = 2 #we must ignore the "fake user" in this case
return mat.users >= minimum_users
used_materials = [m for m in bpy.data.materials if material_is_used(m)]
for m in used_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')]
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.data.name]
if camera.id_data.type == "PERSP":
camera_string = TEMPLATE_CAMERA_PERSPECTIVE % {
"camera_id" : generate_string(cameraobj.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]], data["flipyz"]),
"target" : generate_vec3([0, 0, 0])
}
elif camera.id_data.type == "ORTHO":
camera_string = TEMPLATE_CAMERA_ORTHO % {
"camera_id" : generate_string(camera.name),
"left" : -(camera.angle_x * camera.ortho_scale),
"right" : (camera.angle_x * camera.ortho_scale),
"top" : (camera.angle_y * camera.ortho_scale),
"bottom" : -(camera.angle_y * camera.ortho_scale),
"near" : camera.clip_start,
"far" : camera.clip_end,
"position" : generate_vec3([cameraobj.location[0], -cameraobj.location[1], cameraobj.location[2]], data["flipyz"]),
"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"]:
lamps = data["objects"]
lamps = [ob for ob in lamps if (ob.type == 'LAMP')]
for lamp in lamps:
light_string = ""
concrete_lamp = lamp.data
if concrete_lamp.type == "POINT":
light_string = TEMPLATE_LIGHT_POINT % {
"light_id" : generate_string(concrete_lamp.name),
"position" : generate_vec3(lamp.location, data["flipyz"]),
"rotation" : generate_vec3(lamp.rotation_euler, data["flipyz"]),
"color" : rgb2int(concrete_lamp.color),
"distance" : concrete_lamp.distance,
"intensity" : concrete_lamp.energy
}
elif concrete_lamp.type == "SUN":
light_string = TEMPLATE_LIGHT_SUN % {
"light_id" : generate_string(concrete_lamp.name),
"position" : generate_vec3(lamp.location, data["flipyz"]),
"rotation" : generate_vec3(lamp.rotation_euler, data["flipyz"]),
"color" : rgb2int(concrete_lamp.color),
"distance" : concrete_lamp.distance,
"intensity" : concrete_lamp.energy
}
elif concrete_lamp.type == "SPOT":
light_string = TEMPLATE_LIGHT_SPOT % {
"light_id" : generate_string(concrete_lamp.name),
"position" : generate_vec3(lamp.location, data["flipyz"]),
"rotation" : generate_vec3(lamp.rotation_euler, data["flipyz"]),
"color" : rgb2int(concrete_lamp.color),
"distance" : concrete_lamp.distance,
"intensity" : concrete_lamp.energy,
"use_shadow" : concrete_lamp.use_shadow,
"angle" : concrete_lamp.spot_size
}
elif concrete_lamp.type == "HEMI":
light_string = TEMPLATE_LIGHT_HEMI % {
"light_id" : generate_string(concrete_lamp.name),
"position" : generate_vec3(lamp.location, data["flipyz"]),
"rotation" : generate_vec3(lamp.rotation_euler, data["flipyz"]),
"color" : rgb2int(concrete_lamp.color),
"distance" : concrete_lamp.distance,
"intensity" : concrete_lamp.energy
}
elif concrete_lamp.type == "AREA":
light_string = TEMPLATE_LIGHT_AREA % {
"light_id" : generate_string(concrete_lamp.name),
"position" : generate_vec3(lamp.location, data["flipyz"]),
"rotation" : generate_vec3(lamp.rotation_euler, data["flipyz"]),
"color" : rgb2int(concrete_lamp.color),
"distance" : concrete_lamp.distance,
"intensity" : concrete_lamp.energy,
"gamma" : concrete_lamp.gamma,
"shape" : concrete_lamp.shape,
"size" : concrete_lamp.size,
"size_y" : concrete_lamp.size_y
}
chunks.append(light_string)
if not lamps:
lamps.append(DEFAULTS["light"])
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)
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,
option_frame_index_as_time = False):
#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)
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_index_as_time,
option_frame_step)
embeds[object.data.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,
option_frame_index_as_time)
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,
option_frame_index_as_time)
return {'FINISHED'}
# ##### 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 importer for Three.js (ASCII JSON format).
"""
import os
import time
import json
import bpy
import mathutils
from mathutils.geometry import tessellate_polygon
from bpy_extras.image_utils import load_image
# #####################################################
# Generators
# #####################################################
def setColor(c, t):
c.r = t[0]
c.g = t[1]
c.b = t[2]
def create_texture(filename, modelpath):
name = filename
texture = bpy.data.textures.new(name, type='IMAGE')
image = load_image(filename, modelpath)
has_data = False
if image:
texture.image = image
has_data = image.has_data
return texture
def create_materials(data, modelpath):
materials = []
materials_data = data.get("materials", [])
for i, m in enumerate(materials_data):
name = m.get("DbgName", "material_%d" % i)
colorAmbient = m.get("colorAmbient", None)
colorDiffuse = m.get("colorDiffuse", None)
colorSpecular = m.get("colorSpecular", None)
alpha = m.get("transparency", 1.0)
specular_hardness = m.get("specularCoef", 0)
mapDiffuse = m.get("mapDiffuse", None)
mapLightmap = m.get("mapLightmap", None)
vertexColorsType = m.get("vertexColors", False)
useVertexColors = False
if vertexColorsType:
useVertexColors = True
material = bpy.data.materials.new(name)
material.THREE_useVertexColors = useVertexColors
if colorDiffuse:
setColor(material.diffuse_color, colorDiffuse)
material.diffuse_intensity = 1.0
if colorSpecular:
setColor(material.specular_color, colorSpecular)
material.specular_intensity = 1.0
if alpha < 1.0:
material.alpha = alpha
material.use_transparency = True
if specular_hardness:
material.specular_hardness = specular_hardness
if mapDiffuse:
texture = create_texture(mapDiffuse, modelpath)
mtex = material.texture_slots.add()
mtex.texture = texture
mtex.texture_coords = 'UV'
mtex.use = True
mtex.use_map_color_diffuse = True
material.active_texture = texture
materials.append(material)
return materials
def create_mesh_object(name, vertices, materials, face_data, flipYZ, recalculate_normals):
faces = face_data["faces"]
vertexNormals = face_data["vertexNormals"]
vertexColors = face_data["vertexColors"]
vertexUVs = face_data["vertexUVs"]
faceMaterials = face_data["materials"]
faceColors = face_data["faceColors"]
edges = []
# Create a new mesh
me = bpy.data.meshes.new(name)
me.from_pydata(vertices, edges, faces)
# Handle normals
if not recalculate_normals:
me.update(calc_edges = True)
if face_data["hasVertexNormals"]:
print("setting vertex normals")
for fi in range(len(faces)):
if vertexNormals[fi]:
#print("setting face %i with %i vertices" % (fi, len(normals[fi])))
# if me.update() is called after setting vertex normals
# setting face.use_smooth overrides these normals
# - this fixes weird shading artefacts (seems to come from sharing
# of vertices between faces, didn't find a way how to set vertex normals
# per face use of vertex as opposed to per vertex),
# - probably this just overrides all custom vertex normals
# - to preserve vertex normals from the original data
# call me.update() before setting them
me.tessfaces[fi].use_smooth = True
if not recalculate_normals:
for j in range(len(vertexNormals[fi])):
vertexNormal = vertexNormals[fi][j]
x = vertexNormal[0]
y = vertexNormal[1]
z = vertexNormal[2]
if flipYZ:
tmp = y
y = -z
z = tmp
# flip normals (this make them look consistent with the original before export)
#x = -x
#y = -y
#z = -z
vi = me.tessfaces[fi].vertices[j]
me.vertices[vi].normal.x = x
me.vertices[vi].normal.y = y
me.vertices[vi].normal.z = z
if recalculate_normals:
me.update(calc_edges = True)
# Handle colors
if face_data["hasVertexColors"]:
print("setting vertex colors")
me.vertex_colors.new("vertex_color_layer_0")
for fi in range(len(faces)):
if vertexColors[fi]:
face_colors = me.vertex_colors[0].data[fi]
face_colors = face_colors.color1, face_colors.color2, face_colors.color3, face_colors.color4
for vi in range(len(vertexColors[fi])):
r = vertexColors[fi][vi][0]
g = vertexColors[fi][vi][1]
b = vertexColors[fi][vi][2]
face_colors[vi].r = r
face_colors[vi].g = g
face_colors[vi].b = b
elif face_data["hasFaceColors"]:
print("setting vertex colors from face colors")
me.vertex_colors.new("vertex_color_layer_0")
for fi in range(len(faces)):
if faceColors[fi]:
r = faceColors[fi][0]
g = faceColors[fi][1]
b = faceColors[fi][2]
face_colors = me.vertex_colors[0].data[fi]
face_colors = face_colors.color1, face_colors.color2, face_colors.color3, face_colors.color4
for vi in range(len(faces[fi])):
face_colors[vi].r = r
face_colors[vi].g = g
face_colors[vi].b = b
# Handle uvs
if face_data["hasVertexUVs"]:
print("setting vertex uvs")
for li, layer in enumerate(vertexUVs):
me.uv_textures.new("uv_layer_%d" % li)
for fi in range(len(faces)):
if layer[fi]:
uv_face = me.uv_textures[li].data[fi]
face_uvs = uv_face.uv1, uv_face.uv2, uv_face.uv3, uv_face.uv4
for vi in range(len(layer[fi])):
u = layer[fi][vi][0]
v = layer[fi][vi][1]
face_uvs[vi].x = u
face_uvs[vi].y = v
active_texture = materials[faceMaterials[fi]].active_texture
if active_texture:
uv_face.image = active_texture.image
# Handle materials # 1
if face_data["hasMaterials"]:
print("setting materials (mesh)")
for m in materials:
me.materials.append(m)
print("setting materials (faces)")
for fi in range(len(faces)):
if faceMaterials[fi] >= 0:
me.tessfaces[fi].material_index = faceMaterials[fi]
# Create a new object
ob = bpy.data.objects.new(name, me)
ob.data = me # link the mesh data to the object
scene = bpy.context.scene # get the current scene
scene.objects.link(ob) # link the object into the scene
ob.location = scene.cursor_location # position object at 3d-cursor
# #####################################################
# Faces
# #####################################################
def extract_faces(data):
result = {
"faces" : [],
"materials" : [],
"faceUVs" : [],
"vertexUVs" : [],
"faceNormals" : [],
"vertexNormals" : [],
"faceColors" : [],
"vertexColors" : [],
"hasVertexNormals" : False,
"hasVertexUVs" : False,
"hasVertexColors" : False,
"hasFaceColors" : False,
"hasMaterials" : False
}
faces = data.get("faces", [])
normals = data.get("normals", [])
colors = data.get("colors", [])
offset = 0
zLength = len(faces)
# disregard empty arrays
nUvLayers = 0
for layer in data["uvs"]:
if len(layer) > 0:
nUvLayers += 1
result["faceUVs"].append([])
result["vertexUVs"].append([])
while ( offset < zLength ):
type = faces[ offset ]
offset += 1
isQuad = isBitSet( type, 0 )
hasMaterial = isBitSet( type, 1 )
hasFaceUv = isBitSet( type, 2 )
hasFaceVertexUv = isBitSet( type, 3 )
hasFaceNormal = isBitSet( type, 4 )
hasFaceVertexNormal = isBitSet( type, 5 )
hasFaceColor = isBitSet( type, 6 )
hasFaceVertexColor = isBitSet( type, 7 )
#print("type", type, "bits", isQuad, hasMaterial, hasFaceUv, hasFaceVertexUv, hasFaceNormal, hasFaceVertexNormal, hasFaceColor, hasFaceVertexColor)
result["hasVertexUVs"] = result["hasVertexUVs"] or hasFaceVertexUv
result["hasVertexNormals"] = result["hasVertexNormals"] or hasFaceVertexNormal
result["hasVertexColors"] = result["hasVertexColors"] or hasFaceVertexColor
result["hasFaceColors"] = result["hasFaceColors"] or hasFaceColor
result["hasMaterials"] = result["hasMaterials"] or hasMaterial
# vertices
if isQuad:
a = faces[ offset ]
offset += 1
b = faces[ offset ]
offset += 1
c = faces[ offset ]
offset += 1
d = faces[ offset ]
offset += 1
face = [a, b, c, d]
nVertices = 4
else:
a = faces[ offset ]
offset += 1
b = faces[ offset ]
offset += 1
c = faces[ offset ]
offset += 1
face = [a, b, c]
nVertices = 3
result["faces"].append(face)
# material
if hasMaterial:
materialIndex = faces[ offset ]
offset += 1
else:
materialIndex = -1
result["materials"].append(materialIndex)
# uvs
for i in range(nUvLayers):
faceUv = None
if hasFaceUv:
uvLayer = data["uvs"][ i ]
uvIndex = faces[ offset ]
offset += 1
u = uvLayer[ uvIndex * 2 ]
v = uvLayer[ uvIndex * 2 + 1 ]
faceUv = [u, v]
result["faceUVs"][i].append(faceUv)
if hasFaceVertexUv:
uvLayer = data["uvs"][ i ]
vertexUvs = []
for j in range(nVertices):
uvIndex = faces[ offset ]
offset += 1
u = uvLayer[ uvIndex * 2 ]
v = uvLayer[ uvIndex * 2 + 1 ]
vertexUvs.append([u, v])
result["vertexUVs"][i].append(vertexUvs)
if hasFaceNormal:
normalIndex = faces[ offset ] * 3
offset += 1
x = normals[ normalIndex ]
y = normals[ normalIndex + 1 ]
z = normals[ normalIndex + 2 ]
faceNormal = [x, y, z]
else:
faceNormal = None
result["faceNormals"].append(faceNormal)
if hasFaceVertexNormal:
vertexNormals = []
for j in range(nVertices):
normalIndex = faces[ offset ] * 3
offset += 1
x = normals[ normalIndex ]
y = normals[ normalIndex + 1 ]
z = normals[ normalIndex + 2 ]
vertexNormals.append( [x, y, z] )
else:
vertexNormals = None
result["vertexNormals"].append(vertexNormals)
if hasFaceColor:
colorIndex = faces[ offset ]
offset += 1
faceColor = hexToTuple( colors[ colorIndex ] )
else:
faceColor = None
result["faceColors"].append(faceColor)
if hasFaceVertexColor:
vertexColors = []
for j in range(nVertices):
colorIndex = faces[ offset ]
offset += 1
color = hexToTuple( colors[ colorIndex ] )
vertexColors.append( color )
else:
vertexColors = None
result["vertexColors"].append(vertexColors)
return result
# #####################################################
# Utils
# #####################################################
def hexToTuple( hexColor ):
r = (( hexColor >> 16 ) & 0xff) / 255.0
g = (( hexColor >> 8 ) & 0xff) / 255.0
b = ( hexColor & 0xff) / 255.0
return (r, g, b)
def isBitSet(value, position):
return value & ( 1 << position )
def splitArray(data, chunkSize):
result = []
chunk = []
for i in range(len(data)):
if i > 0 and i % chunkSize == 0:
result.append(chunk)
chunk = []
chunk.append(data[i])
result.append(chunk)
return result
def extract_json_string(text):
marker_begin = "var model ="
marker_end = "postMessage"
start = text.find(marker_begin) + len(marker_begin)
end = text.find(marker_end)
end = text.rfind("}", start, end)
return text[start:end+1].strip()
def get_name(filepath):
return os.path.splitext(os.path.basename(filepath))[0]
def get_path(filepath):
return os.path.dirname(filepath)
# #####################################################
# Parser
# #####################################################
def load(operator, context, filepath, option_flip_yz = True, recalculate_normals = True, option_worker = False):
print('\nimporting %r' % filepath)
time_main = time.time()
print("\tparsing JSON file...")
time_sub = time.time()
file = open(filepath, 'rU')
rawcontent = file.read()
file.close()
if option_worker:
json_string = extract_json_string(rawcontent)
else:
json_string = rawcontent
data = json.loads( json_string )
time_new = time.time()
print('parsing %.4f sec' % (time_new - time_sub))
time_sub = time_new
# flip YZ
vertices = splitArray(data["vertices"], 3)
if option_flip_yz:
vertices[:] = [(v[0], -v[2], v[1]) for v in vertices]
# extract faces
face_data = extract_faces(data)
# deselect all
bpy.ops.object.select_all(action='DESELECT')
nfaces = len(face_data["faces"])
nvertices = len(vertices)
nnormals = len(data.get("normals", [])) / 3
ncolors = len(data.get("colors", [])) / 3
nuvs = len(data.get("uvs", [])) / 2
nmaterials = len(data.get("materials", []))
print('\tbuilding geometry...\n\tfaces:%i, vertices:%i, vertex normals: %i, vertex uvs: %i, vertex colors: %i, materials: %i ...' % (
nfaces, nvertices, nnormals, nuvs, ncolors, nmaterials ))
# Create materials
materials = create_materials(data, get_path(filepath))
# Create new obj
create_mesh_object(get_name(filepath), vertices, materials, face_data, option_flip_yz, recalculate_normals)
scene = bpy.context.scene
scene.update()
time_new = time.time()
print('finished importing: %r in %.4f sec.' % (filepath, (time_new - time_main)))
return {'FINISHED'}
if __name__ == "__main__":
register()
@rgajrawala
Copy link
Author

Add files to $BLENDER_HOME/scripts/addons/io_mesh_threejs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment