Skip to content

Instantly share code, notes, and snippets.

@DanielKeep
Created November 7, 2012 04:27
Show Gist options
  • Save DanielKeep/4029569 to your computer and use it in GitHub Desktop.
Save DanielKeep/4029569 to your computer and use it in GitHub Desktop.
JGLS (juggles) exporter for Blender 2.63
# ##### 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()
# ##### 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