Created
February 22, 2022 04:31
-
-
Save rgajrawala/ed027e9f2afc203366e43a203c673db8 to your computer and use it in GitHub Desktop.
ThreeJS Blender Converter Plugin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ##### 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ##### 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'} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ##### 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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add files to
$BLENDER_HOME/scripts/addons/io_mesh_threejs