Created
November 7, 2012 04:27
-
-
Save DanielKeep/4029569 to your computer and use it in GitHub Desktop.
JGLS (juggles) exporter for Blender 2.63
This file contains 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 ##### | |
# <pep8-80 compliant> | |
bl_info = { | |
"name": "JSON GL Scene (JGLS /ˈdʒʌɡlz/) format", | |
"author": "Daniel Keep", | |
"blender": (2, 6, 0), | |
"location": "File > Import-Export", | |
"description": "Exports scenes to JGLS with meshes (normals, " | |
"vertex colors, transform), lights and cameras", | |
"warning": "", | |
"wiki_url": "", | |
"tracker_url": "", | |
"category": "Import-Export"} | |
# This is my first time writing a Blender script, so most of this has | |
# been copied from io_scene_fbx. | |
if "bpy" in locals(): | |
import imp | |
if "export_jgls" in locals(): | |
imp.reload(export_jgls) | |
import bpy | |
from bpy.props import (StringProperty, | |
BoolProperty, | |
FloatProperty, | |
EnumProperty, | |
) | |
from bpy_extras.io_utils import (ExportHelper, | |
path_reference_mode, | |
axis_conversion, | |
) | |
class ExportJGLS(bpy.types.Operator, ExportHelper): | |
'''Export scene to a JSON GL Scene''' | |
bl_idname = 'export_scene.jgls' | |
bl_label = 'Export JGLS' | |
bl_options = {'PRESET'} | |
filename_ext = '.json' | |
filter_glob = StringProperty(default='*.json', options={'HIDDEN'}) | |
def execute(self, context): | |
if not self.filepath: | |
raise Exception("filepath not set") | |
from . import export_jgls | |
export_jgls.write(context.scene, self.filepath) | |
return {'FINISHED'} | |
def menu_func(self, context): | |
self.layout.operator(ExportJGLS.bl_idname, text="JSON GL Scene (.json)") | |
def register(): | |
bpy.utils.register_module(__name__) | |
bpy.types.INFO_MT_file_export.append(menu_func) | |
def unregister(): | |
bpy.utils.unregister_module(__name__) | |
bpy.types.INFO_MT_file_export.remove(menu_func) | |
if __name__ == '__main__': | |
register() |
This file contains 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 ##### | |
# <pep8 compliant> | |
# Copyright 2012 Daniel Keep | |
# Notes: | |
# | |
# - Everything gets triangulated. The justification for this is that it | |
# simplifies rendering the result: instead of needing to do two batches | |
# with different primitives, you just do one big batch with everything. | |
# | |
# - Vertex streams are output separately instead of interleaved. This is | |
# mostly a format purity choice. Having them interleaved *would* make them | |
# easier to load... *unless* you're writing code that doesn't use, say, | |
# vertex colors. At which point, you then need to de-interleave them. | |
# Yuck. Easier to just have the loading program interleave the streams as | |
# necessary. That, or cop out and just concatenate them. | |
import bpy | |
from math import pi | |
from mathutils import Matrix, Vector | |
# Used in one spot to distinguish between an un-specified argument and | |
# a deliberately specified None value. | |
_EMPTY = object() | |
# Used to transform from Blender's coordinate space to OpenGL's. | |
ROTATE_P90_X = Matrix(((1, 0, 0, 0), | |
(0, 0,-1, 0), | |
(0, 1, 0, 0), | |
(0, 0, 0, 1))) | |
ROTATE_N90_X = Matrix(((1, 0, 0, 0), | |
(0, 0, 1, 0), | |
(0,-1, 0, 0), | |
(0, 0, 0, 1))) | |
def fix_vector(v): | |
return Vector((v.x, v.z, -v.y)) | |
def fix_matrix(m): | |
return ROTATE_N90_X * m * ROTATE_P90_X | |
class json_writer: | |
""" | |
Simple JSON pretty-printer. | |
""" | |
INDENT = ' ' | |
def __init__(self, file): | |
self.file = file | |
self.depth = 0 | |
self.state = 'START' | |
def do_indent(self): | |
self.file.write(json_writer.INDENT * self.depth) | |
return self | |
def begin_object(self): | |
if self.state == 'NEW_SCOPE': | |
self.file.write('\n') | |
self.do_indent() | |
elif self.state == 'AFTER_MEMBER': | |
self.file.write(',\n') | |
self.do_indent() | |
self.file.write('{') | |
self.depth += 1 | |
self.state = 'NEW_SCOPE' | |
return self | |
def end_object(self): | |
self.depth -= 1 | |
if self.state == 'AFTER_MEMBER': | |
self.file.write('\n') | |
self.do_indent() | |
self.file.write('}') | |
self.state = 'AFTER_MEMBER' | |
return self | |
def begin_array(self): | |
if self.state == 'NEW_SCOPE': | |
self.file.write('\n') | |
self.do_indent() | |
elif self.state == 'AFTER_MEMBER': | |
self.file.write(',\n') | |
self.do_indent() | |
self.file.write('[') | |
self.depth += 1 | |
self.state = 'NEW_SCOPE' | |
return self | |
def end_array(self): | |
self.depth -= 1 | |
if self.state == 'AFTER_MEMBER': | |
self.file.write('\n') | |
self.do_indent() | |
self.file.write(']') | |
self.state = 'AFTER_MEMBER' | |
return self | |
def key(self, key, value = _EMPTY): | |
if not isinstance(key, str): | |
raise Exception("key must a string, not a " + repr(type(key))) | |
self.put(key) | |
self.file.write(': ') | |
self.state = '' | |
if value is not _EMPTY: | |
self.put(value) | |
return self | |
def put(self, value): | |
if self.state == 'NEW_SCOPE': | |
self.file.write('\n') | |
self.do_indent() | |
elif self.state == 'AFTER_MEMBER': | |
self.file.write(',\n') | |
self.do_indent() | |
if isinstance(value, str): | |
self.file.write( | |
'"' | |
+ value.replace('\\', '\\\\').replace('"', '\\"') | |
+ '"') | |
elif isinstance(value, float) or isinstance(value, int): | |
self.file.write(repr(value)) | |
elif isinstance(value, bool): | |
self.file.write('true' if value else 'false') | |
elif value is None: | |
self.file.write('null') | |
elif isinstance(value, list): | |
self.begin_array() | |
for elem in value: | |
self.put(elem) | |
self.end_array() | |
elif isinstance(value, dict): | |
self.begin_object() | |
for (k,v) in value.items(): | |
self.key(k) | |
self.put(v) | |
self.end_object() | |
else: | |
raise Exception("cannot put values of type %s" % repr(type(value))) | |
self.state = 'AFTER_MEMBER' | |
return self | |
def write_matrix(out, matrix, is_camera=False): | |
out.begin_array() | |
m = matrix | |
if is_camera: | |
m = m * ROTATE_N90_X | |
m = fix_matrix(m) | |
# Transpose from row-major to column-major | |
for v in m.transposed(): | |
for e in v: | |
out.put(e) | |
out.end_array() | |
def write_inline_vector(out, vector): | |
for c in fix_vector(vector): | |
out.put(c) | |
def write_inline_normal(out, normal): | |
for c in fix_vector(normal): | |
out.put(c) | |
def write_color(out, color): | |
out.put(list(color)) | |
def write_inline_color(out, color): | |
for c in color: | |
out.put(c) | |
def write_lamp(out, lamp): | |
# Limitations and assumptions: | |
# - Only point lights. | |
# - 1/x^2 falloff. | |
# - No power independent from falloff. | |
out.begin_object() | |
out.key("name", lamp.name) | |
out.key("transform") | |
write_matrix(out, lamp.matrix_world) | |
out.key("color") | |
write_color(out, lamp.data.color) | |
out.key("falloff", lamp.data.distance) | |
out.end_object() | |
def write_camera(out, camera): | |
# Limitations and assumptions: | |
# - Only perspective | |
out.begin_object() | |
out.key("name", camera.name) | |
out.key("transform") | |
write_matrix(out, camera.matrix_world, True) | |
out.key("z_near", camera.data.clip_start) | |
out.key("z_far", camera.data.clip_end) | |
out.key("fov_x", camera.data.angle_x) | |
out.end_object() | |
def write_mesh(out, mesh): | |
# Limitations and assumptions: | |
# - tessfaces gives us either tris and quads. | |
# - I have no idea whether this will apply modifiers or what. Haven't | |
# needed to try yet. | |
# - No idea how extra layers like UVs will work, but I hope the | |
# "fat pixel" approach will scale. | |
# - Vertex colours are RGB, not RGBA. | |
use_face_normals = bool(mesh.get("use_face_normals", False)) | |
if use_face_normals: | |
def extra_face_vertex_key(f): | |
return f.index | |
else: | |
def extra_face_vertex_key(f): | |
return None | |
out.begin_object() | |
if len(mesh.data.tessfaces) == 0: | |
mesh.data.update(calc_tessface=True) | |
out.key("name", mesh.name) | |
out.key("transform") | |
write_matrix(out, mesh.matrix_world) | |
# This will hold our "fat vertex" list. The problem is that OpenGL | |
# expects vertex colours and UV coordinates to be unique per-vertex. | |
# Blender, on the other hand, makes them unique per-vertex per-face. | |
# We'll use a set for this and only worry about fixing the order just | |
# before we write it all out. | |
vertex_set = set() | |
face_map = {} | |
for f,vcf in enumerate(mesh.data.tessface_vertex_colors['Col'].data): | |
fc = (vcf.color1, vcf.color2, vcf.color3, vcf.color4) | |
extra_key = extra_face_vertex_key(mesh.data.tessfaces[f]) | |
for fvi,mvi in enumerate(mesh.data.tessfaces[f].vertices): | |
v = (mvi, tuple(fc[fvi]), extra_key) | |
vertex_set.add(v) | |
face_map[(f,fvi)] = v | |
# Fix order. | |
vertices = list(vertex_set) | |
# Change face map from full vertex to index | |
face_map = dict((k, vertices.index(v)) for (k,v) in face_map.items()) | |
# Output data. | |
out.key("vertices") | |
out.begin_object() | |
out.key("count", len(vertices)) | |
out.key("positions") | |
out.begin_array() | |
for mvi in (v[0] for v in vertices): | |
v = mesh.data.vertices[mvi].co | |
write_inline_vector(out, v) | |
out.end_array() | |
out.key("normals") | |
out.begin_array() | |
if use_face_normals: | |
for f in (v[2] for v in vertices): | |
v = mesh.data.tessfaces[f].normal | |
write_inline_normal(out, v) | |
else: | |
for mvi in (v[0] for v in vertices): | |
v = mesh.data.vertices[mvi].normal | |
write_inline_normal(out, v) | |
out.end_array() | |
out.key("colors") | |
out.begin_array() | |
for c in (v[1] for v in vertices): | |
write_inline_color(out, c) | |
out.end_array() | |
out.end_object() | |
out.key("triangles") | |
out.begin_array() | |
for (f,face) in enumerate(mesh.data.tessfaces): | |
vis = [face_map[(f,i)] for i in range(len(face.vertices))] | |
out.put(vis[2]).put(vis[1]).put(vis[0]) | |
if len(vis) == 4: | |
out.put(vis[3]).put(vis[2]).put(vis[0]) | |
elif len(vis) > 4: | |
print('Warning: %d-point polygon in %s!' % ( | |
len(vis), mesh.name)) | |
out.end_array() | |
out.end_object() | |
def write(scene, filepath): | |
out = json_writer(open(filepath, 'wt', encoding='utf_8')) | |
out.begin_object() | |
out.key("ambient") | |
write_color(out, scene.world.ambient_color) | |
out.key("horizon") | |
write_color(out, scene.world.horizon_color) | |
out.key("cameras") | |
out.begin_array() | |
for obj in (o for o in scene.objects if o.type == 'CAMERA'): | |
#out.key(obj.name) | |
write_camera(out, obj) | |
out.end_array() | |
out.key("lights") | |
out.begin_array() | |
for obj in (o for o in scene.objects if o.type == 'LAMP'): | |
#out.key(obj.name) | |
write_lamp(out, obj) | |
out.end_array() | |
out.key("meshes") | |
out.begin_array() | |
for obj in (o for o in scene.objects if o.type == 'MESH'): | |
#out.key(obj.name) | |
write_mesh(out, obj) | |
out.end_array() | |
out.end_object() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment