Update for changes in Blender's API
[blender-addons.git] / light_field_tools / light_field_tools.py
blobb4693d52e24eddad331c75e8291611a9f8115f3d
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 import bpy
20 from bpy.types import (
21 Operator,
22 Panel,
24 import os
25 from math import (
26 degrees, tan,
27 radians,
29 from mathutils import Vector
31 __bpydoc__ = """
32 Light Field Tools
34 This script helps setting up rendering of lightfields. It
35 also supports the projection of lightfields with textured
36 spotlights.
38 Usage:
39 A simple interface can be accessed in the tool shelf panel
40 in 3D View ([T] Key).
42 A base mesh has to be provided, which will normaly be a
43 subdivided plane. The script will then create a camera rig
44 and a light rig with adjustable properties. A sample camera
45 and a spotlight will be created on each vertex of the
46 basemesh object axis (maybe vertex normal in future
47 versions).
49 Vertex order:
50 The user has to provide the number of cameras or
51 lights in one row in an unevenly spaced grid, the
52 basemesh. Then the right vertex order can be
53 computed as shown here.
54 6-7-8
55 | | |
56 ^ 3-4-5
57 | | | |
58 y 0-1-2
59 x->
61 There is also a tool to create a basemesh, which is an
62 evenly spaced grid. The row length parameter is taken to
63 construct such a NxN grid. Someone would start out by adding
64 a rectengular plane as the slice plane of the frustrum of
65 the most middle camera of the light field rig. The spacing
66 parameter then places the other cameras in a way, so they
67 have an offset of n pixels from the other camera on this
68 plane.
71 Version history:
72 v0.3.0 - Make compatible with 2.64
73 v0.2.1 - Empty handler, multiple camera grid, r34843
74 v0.2.0 - To be included in contrib, r34456
75 v0.1.4 - To work with r34261
76 v0.1.3 - Fixed base mesh creation for r29998
77 v0.1.2 - Minor fixes, working with r29994
78 v0.1.1 - Basemesh from focal plane.
79 v0.1.0 - API updates, draft done.
80 v0.0.4 - Texturing.
81 v0.0.3 - Creates an array of non textured spotlights.
82 v0.0.2 - Renders lightfields.
83 v0.0.1 - Initial version.
85 TODO:
86 * Restore view after primary camera is changed.
87 * Apply object matrix to normals.
88 * Allign to normals, somehow,....
89 * StringProperties with PATH tag, for proper ui.
90 """
93 class OBJECT_OT_create_lightfield_rig(Operator):
94 bl_idname = "object.create_lightfield_rig"
95 bl_label = "Create a light field rig"
96 bl_description = "Create a lightfield rig based on the active object/mesh"
97 bl_options = {'REGISTER'}
99 layer0 = [True] + [False] * 19
101 numSamples = 0
102 baseObject = None
103 handler = None
104 verts = []
105 imagePaths = []
107 def arrangeVerts(self):
108 """Sorts the vertices as described in the usage part of the doc."""
109 # FIXME get mesh with applied modifer stack
110 scene = bpy.context.scene
111 mesh = self.baseObject.data
112 verts = []
113 row_length = scene.lightfield.row_length
114 matrix = self.baseObject.matrix_local.copy()
115 for vert in mesh.vertices:
116 # world/parent origin
117 # ???, normal and co are in different spaces, sure you want this?
118 co = matrix * vert.co
119 normal = vert.normal
120 verts.append([co, normal])
122 def key_x(v):
123 return v[0][0]
125 def key_y(v):
126 return v[0][1]
128 verts.sort(key=key_y)
129 sorted_verts = []
130 for i in range(0, len(verts), row_length):
131 row = verts[i: i + row_length]
132 row.sort(key=key_x)
133 sorted_verts.extend(row)
135 return sorted_verts
137 def createCameraAnimated(self):
138 scene = bpy.context.scene
140 bpy.ops.object.camera_add(view_align=False)
141 cam = bpy.context.active_object
142 cam.name = "light_field_camera"
144 # set props
145 cam.data.angle = scene.lightfield.angle
147 # display options of the camera
148 cam.data.lens_unit = 'FOV'
150 # handler parent
151 if scene.lightfield.create_handler:
152 cam.parent = self.handler
154 # set as primary camera
155 scene.camera = cam
157 # animate
158 scene.frame_current = 0
160 for frame, vert in enumerate(self.verts):
161 scene.frame_current = frame
162 # translate
163 cam.location = vert[0]
164 # rotation
165 cam.rotation_euler = self.baseObject.rotation_euler
166 # insert LocRot keyframes
167 cam.keyframe_insert('location')
169 # set anim render props
170 scene.frame_current = 0
171 scene.frame_start = 0
172 scene.frame_end = self.numSamples - 1
174 def createCameraMultiple(self):
175 scene = bpy.context.scene
177 for cam_idx, vert in enumerate(self.verts):
178 # add and name camera
179 bpy.ops.object.camera_add(view_align=False)
180 cam = bpy.context.active_object
181 cam.name = "light_field_cam_" + str(cam_idx)
183 # translate
184 cam.location = vert[0]
185 # rotation
186 cam.rotation_euler = self.baseObject.rotation_euler
188 # set camera props
189 cam.data.angle = scene.lightfield.angle
191 # display options of the camera
192 cam.data.draw_size = 0.15
193 cam.data.lens_unit = 'FOV'
195 # handler parent
196 if scene.lightfield.create_handler:
197 cam.parent = self.handler
199 def createCamera(self):
200 if bpy.context.scene.lightfield.animate_camera:
201 self.createCameraAnimated()
202 else:
203 self.createCameraMultiple()
205 def getImagePaths(self):
206 path = bpy.context.scene.lightfield.texture_path
207 if not os.path.isdir(path):
208 return False
209 files = os.listdir(path)
210 if not len(files) == self.numSamples:
211 return False
212 files.sort()
213 self.imagePaths = list(map(lambda f: os.path.join(path, f), files))
214 return True
216 def createTexture(self, index):
217 name = "light_field_spot_tex_" + str(index)
218 tex = bpy.data.textures.new(name, type='IMAGE')
220 # load and set the image
221 # FIXME width, height. not necessary to set in the past.
222 img = bpy.data.images.new("lfe_str_" + str(index), width=5, height=5)
223 img.filepath = self.imagePaths[index]
224 img.source = 'FILE'
225 tex.image = img
227 return tex
229 def createSpot(self, index, textured=False):
230 scene = bpy.context.scene
231 bpy.ops.object.lamp_add(
232 type='SPOT')
233 spot = bpy.context.active_object
235 # set object props
236 spot.name = "light_field_spot_" + str(index)
238 # set constants
239 spot.data.use_square = True
240 spot.data.shadow_method = "RAY_SHADOW"
241 # FIXME
242 spot.data.distance = 10
244 # set spot props
245 spot.data.energy = scene.lightfield.light_intensity / self.numSamples
246 spot.data.spot_size = scene.lightfield.angle
247 spot.data.spot_blend = scene.lightfield.spot_blend
249 # add texture
250 if textured:
251 spot.data.active_texture = self.createTexture(index)
252 # texture mapping
253 spot.data.texture_slots[0].texture_coords = 'VIEW'
255 # handler parent
256 if scene.lightfield.create_handler:
257 spot.parent = self.handler
259 return spot
261 def createLightfieldEmitter(self, textured=False):
262 for i, vert in enumerate(self.verts):
263 spot = self.createSpot(i, textured)
264 spot.location = vert[0]
265 spot.rotation_euler = self.baseObject.rotation_euler
267 def execute(self, context):
268 scene = context.scene
270 obj = self.baseObject = context.active_object
271 if not obj or obj.type != 'MESH':
272 self.report({'ERROR'}, "No selected mesh object!")
273 return {'CANCELLED'}
275 self.verts = self.arrangeVerts()
276 self.numSamples = len(self.verts)
278 if scene.lightfield.create_handler:
279 # create an empty
280 bpy.ops.object.add(type='EMPTY')
281 empty = bpy.context.active_object
282 empty.location = self.baseObject.location
283 empty.name = "light_field_handler"
284 empty.rotation_euler = self.baseObject.rotation_euler
285 self.handler = empty
287 if scene.lightfield.do_camera:
288 self.createCamera()
290 if scene.lightfield.do_projection:
291 if self.getImagePaths():
292 self.createLightfieldEmitter(textured=True)
293 else:
294 self.createLightfieldEmitter(textured=False)
296 return {'FINISHED'}
299 class OBJECT_OT_create_lightfield_basemesh(Operator):
300 bl_idname = "object.create_lightfield_basemesh"
301 bl_label = "Create a basemesh from the selected focal plane"
302 bl_description = "Creates a basemesh from the selected focal plane"
303 bl_options = {'REGISTER'}
305 objName = "lf_basemesh"
307 def getWidth(self, obj):
308 mat = obj.matrix_local
309 mesh = obj.data
310 v0 = mat * mesh.vertices[mesh.edges[0].vertices[0]].co
311 v1 = mat * mesh.vertices[mesh.edges[0].vertices[1]].co
312 return (v0 - v1).length
314 def getCamVec(self, obj, angle):
315 width = self.getWidth(obj)
316 itmat = obj.matrix_local.inverted().transposed()
317 normal = itmat * obj.data.polygons[0].normal.normalized()
318 vl = (width / 2) * (1 / tan(radians(angle / 2)))
319 return normal * vl
321 def addMeshObj(self, mesh):
322 scene = bpy.context.scene
324 for o in scene.objects:
325 o.select = False
327 mesh.update()
328 nobj = bpy.data.objects.new(self.objName, mesh)
329 scene.objects.link(nobj)
330 nobj.select = True
332 if scene.objects.active is None or scene.objects.active.mode == 'OBJECT':
333 scene.objects.active = nobj
335 def execute(self, context):
336 scene = context.scene
337 obj = context.active_object
338 # check if active object is a mesh object
339 if not obj or obj.type != 'MESH':
340 self.report({'ERROR'}, "No selected mesh object!")
341 return {'CANCELLED'}
343 # check if it has one single face
344 if len(obj.data.polygons) != 1:
345 self.report({'ERROR'}, "The selected mesh object has to have exactly one quad!")
346 return {'CANCELLED'}
348 rl = scene.lightfield.row_length
349 # use a degree angle here
350 angle = degrees(scene.lightfield.angle)
351 spacing = scene.lightfield.spacing
352 # resolution of final renderings
353 res = round(scene.render.resolution_x * (scene.render.resolution_percentage / 100.))
354 width = self.getWidth(obj)
356 # the offset between n pixels on the focal plane
357 fplane_offset = (width / res) * spacing
359 # vertices for the basemesh
360 verts = []
361 # the offset vector
362 vec = self.getCamVec(obj, angle)
363 # lower left coordinates of the grid
364 sx = obj.location[0] - fplane_offset * int(rl / 2)
365 sy = obj.location[1] - fplane_offset * int(rl / 2)
366 z = obj.location[2]
367 # position on the focal plane
368 fplane_pos = Vector()
369 for x in [sx + fplane_offset * i for i in range(rl)]:
370 for y in [sy + fplane_offset * i for i in range(rl)]:
371 fplane_pos.x = x
372 fplane_pos.y = y
373 fplane_pos.z = z
374 # position of a vertex in a basemesh
375 pos = fplane_pos + vec
376 # pack coordinates flat into the vert list
377 verts.append((pos.x, pos.y, pos.z))
379 # setup the basemesh and add verts
380 mesh = bpy.data.meshes.new(self.objName)
381 mesh.from_pydata(verts, [], [])
382 self.addMeshObj(mesh)
384 return {'FINISHED'}
387 class VIEW3D_PT_lightfield_tools(Panel):
388 bl_space_type = "VIEW_3D"
389 bl_region_type = "TOOLS"
390 bl_context = "objectmode"
391 bl_label = "Light Field Tools"
392 bl_category = "Tools"
394 def draw(self, context):
395 layout = self.layout
396 scene = context.scene
398 col = layout.column()
399 col.prop(scene.lightfield, "row_length")
400 col.prop(scene.lightfield, "angle")
402 col.prop(scene.lightfield, "create_handler")
404 col.prop(scene.lightfield, "do_camera")
405 col.prop(scene.lightfield, "animate_camera")
406 col.prop(scene.lightfield, "do_projection")
408 col = layout.column(align=True)
409 col.enabled = scene.lightfield.do_projection
410 col.prop(scene.lightfield, "texture_path")
411 col.prop(scene.lightfield, "light_intensity")
412 col.prop(scene.lightfield, "spot_blend")
414 # create a basemesh
415 col = layout.column(align=True)
416 col.operator("object.create_lightfield_basemesh", text="Create Base Grid")
417 col.prop(scene.lightfield, "spacing")
419 layout.operator("object.create_lightfield_rig", text="Create Rig")