Preview generation module: explicitly exclude any lib item.
[blender.git] / release / scripts / modules / bl_previews_utils / bl_previews_render.py
1 # ***** BEGIN GPL LICENSE BLOCK *****
2 #
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.
7 #
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.
12 #
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.
16 #
17 # ***** END GPL LICENSE BLOCK *****
18
19 # <pep8 compliant>
20
21 # Populate a template file (POT format currently) from Blender RNA/py/C data.
22 # Note: This script is meant to be used from inside Blender!
23
24 import collections
25 import os
26 import sys
27
28 import bpy
29 from mathutils import Vector, Euler
30
31
32 INTERN_PREVIEW_TYPES = {'MATERIAL', 'LAMP', 'WORLD', 'TEXTURE', 'IMAGE'}
33 OBJECT_TYPES_RENDER = {'MESH', 'CURVE', 'SURFACE', 'META', 'FONT'}
34
35
36 def ids_nolib(bids):
37     return (bid for bid in bids if not bid.library)
38
39
40 def rna_backup_gen(data, include_props=None, exclude_props=None, root=()):
41     # only writable properties...
42     for p in data.bl_rna.properties:
43         pid = p.identifier
44         if pid in {'rna_type',}:
45             continue
46         path = root + (pid,)
47         if include_props is not None and path not in include_props:
48             continue
49         if exclude_props is not None and path in exclude_props:
50             continue
51         val = getattr(data, pid)
52         if val is not None and p.type == 'POINTER':
53             # recurse!
54             yield from rna_backup_gen(val, include_props, exclude_props, root=path)
55         elif data.is_property_readonly(pid):
56             continue
57         else:
58             yield path, val
59
60
61 def rna_backup_restore(data, backup):
62     for path, val in backup:
63         dt = data
64         for pid in path[:-1]:
65             dt = getattr(dt, pid)
66         setattr(dt, path[-1], val)
67
68
69 def do_previews(do_objects, do_groups, do_scenes, do_data_intern):
70     # Helpers.
71     RenderContext = collections.namedtuple("RenderContext", (
72         "scene", "world", "camera", "lamp", "camera_data", "lamp_data", "image",  # All those are names!
73         "backup_scene", "backup_world", "backup_camera", "backup_lamp", "backup_camera_data", "backup_lamp_data",
74     ))
75
76     RENDER_PREVIEW_SIZE = bpy.app.render_preview_size
77
78     def render_context_create(engine, objects_ignored):
79         if engine == '__SCENE':
80             backup_scene, backup_world, backup_camera, backup_lamp, backup_camera_data, backup_lamp_data = [()] * 6
81             scene = bpy.context.screen.scene
82             exclude_props = {('world',), ('camera',), ('tool_settings',), ('preview',)}
83             backup_scene = tuple(rna_backup_gen(scene, exclude_props=exclude_props))
84             world = scene.world
85             camera = scene.camera
86             if camera:
87                 camera_data = camera.data
88             else:
89                 backup_camera, backup_camera_data = [None] * 2
90                 camera_data = bpy.data.cameras.new("TEMP_preview_render_camera")
91                 camera = bpy.data.objects.new("TEMP_preview_render_camera", camera_data)
92                 camera.rotation_euler = Euler((1.1635528802871704, 0.0, 0.7853981852531433), 'XYZ')  # (66.67, 0.0, 45.0)
93                 scene.camera = camera
94                 scene.objects.link(camera)
95             # TODO: add lamp if none found in scene?
96             lamp = None
97             lamp_data = None
98         else:
99             backup_scene, backup_world, backup_camera, backup_lamp, backup_camera_data, backup_lamp_data = [None] * 6
100
101             scene = bpy.data.scenes.new("TEMP_preview_render_scene")
102             world = bpy.data.worlds.new("TEMP_preview_render_world")
103             camera_data = bpy.data.cameras.new("TEMP_preview_render_camera")
104             camera = bpy.data.objects.new("TEMP_preview_render_camera", camera_data)
105             lamp_data = bpy.data.lamps.new("TEMP_preview_render_lamp", 'SPOT')
106             lamp = bpy.data.objects.new("TEMP_preview_render_lamp", lamp_data)
107
108             objects_ignored.add((camera.name, lamp.name))
109
110             scene.world = world
111
112             camera.rotation_euler = Euler((1.1635528802871704, 0.0, 0.7853981852531433), 'XYZ')  # (66.67, 0.0, 45.0)
113             scene.camera = camera
114             scene.objects.link(camera)
115
116             lamp.rotation_euler = Euler((0.7853981852531433, 0.0, 1.7453292608261108), 'XYZ')  # (45.0, 0.0, 100.0)
117             lamp_data.falloff_type = 'CONSTANT'
118             lamp_data.spot_size = 1.0471975803375244  # 60
119             scene.objects.link(lamp)
120
121             if engine == 'BLENDER_RENDER':
122                 scene.render.engine = 'BLENDER_RENDER'
123                 scene.render.alpha_mode = 'TRANSPARENT'
124
125                 world.use_sky_blend = True
126                 world.horizon_color = 0.9, 0.9, 0.9
127                 world.zenith_color = 0.5, 0.5, 0.5
128                 world.ambient_color = 0.1, 0.1, 0.1
129                 world.light_settings.use_environment_light = True
130                 world.light_settings.environment_energy = 1.0
131                 world.light_settings.environment_color = 'SKY_COLOR'
132             elif engine == 'CYCLES':
133                 scene.render.engine = 'CYCLES'
134                 scene.cycles.film_transparent = True
135                 # TODO: define Cycles world?
136
137         scene.render.image_settings.file_format = 'PNG'
138         scene.render.image_settings.color_depth = '8'
139         scene.render.image_settings.color_mode = 'RGBA'
140         scene.render.image_settings.compression = 25
141         scene.render.resolution_x = RENDER_PREVIEW_SIZE
142         scene.render.resolution_y = RENDER_PREVIEW_SIZE
143         scene.render.resolution_percentage = 100
144         scene.render.filepath = os.path.join(bpy.app.tempdir, 'TEMP_preview_render.png')
145         scene.render.use_overwrite = True
146         scene.render.use_stamp = False
147
148         image = bpy.data.images.new("TEMP_render_image", RENDER_PREVIEW_SIZE, RENDER_PREVIEW_SIZE, alpha=True)
149         image.source = 'FILE'
150         image.filepath = scene.render.filepath
151
152         return RenderContext(
153             scene.name, world.name if world else None, camera.name, lamp.name if lamp else None,
154             camera_data.name, lamp_data.name if lamp_data else None, image.name,
155             backup_scene, backup_world, backup_camera, backup_lamp, backup_camera_data, backup_lamp_data,
156         )
157
158     def render_context_delete(render_context):
159         # We use try/except blocks here to avoid crash, too much things can go wrong, and we want to leave the current
160         # .blend as clean as possible!
161         success = True
162
163         scene = bpy.data.scenes[render_context.scene, None]
164         try:
165             if render_context.backup_scene is None:
166                 scene.world = None
167                 scene.camera = None
168                 if render_context.camera:
169                     scene.objects.unlink(bpy.data.objects[render_context.camera, None])
170                 if render_context.lamp:
171                     scene.objects.unlink(bpy.data.objects[render_context.lamp, None])
172                 bpy.data.scenes.remove(scene)
173                 scene = None
174             else:
175                 rna_backup_restore(scene, render_context.backup_scene)
176         except Exception as e:
177             print("ERROR:", e)
178             success = False
179
180         if render_context.world is not None:
181             try:
182                 world = bpy.data.worlds[render_context.world, None]
183                 if render_context.backup_world is None:
184                     if scene is not None:
185                         scene.world = None
186                     world.user_clear()
187                     bpy.data.worlds.remove(world)
188                 else:
189                     rna_backup_restore(world, render_context.backup_world)
190             except Exception as e:
191                 print("ERROR:", e)
192                 success = False
193
194         if render_context.camera:
195             try:
196                 camera = bpy.data.objects[render_context.camera, None]
197                 if render_context.backup_camera is None:
198                     if scene is not None:
199                         scene.camera = None
200                         scene.objects.unlink(camera)
201                     camera.user_clear()
202                     bpy.data.objects.remove(camera)
203                     bpy.data.cameras.remove(bpy.data.cameras[render_context.camera_data, None])
204                 else:
205                     rna_backup_restore(camera, render_context.backup_camera)
206                     rna_backup_restore(bpy.data.cameras[render_context.camera_data, None],
207                                        render_context.backup_camera_data)
208             except Exception as e:
209                 print("ERROR:", e)
210                 success = False
211
212         if render_context.lamp:
213             try:
214                 lamp = bpy.data.objects[render_context.lamp, None]
215                 if render_context.backup_lamp is None:
216                     if scene is not None:
217                         scene.objects.unlink(lamp)
218                     lamp.user_clear()
219                     bpy.data.objects.remove(lamp)
220                     bpy.data.lamps.remove(bpy.data.lamps[render_context.lamp_data, None])
221                 else:
222                     rna_backup_restore(lamp, render_context.backup_lamp)
223                     rna_backup_restore(bpy.data.lamps[render_context.lamp_data, None], render_context.backup_lamp_data)
224             except Exception as e:
225                 print("ERROR:", e)
226                 success = False
227
228         try:
229             image = bpy.data.images[render_context.image, None]
230             image.user_clear()
231             bpy.data.images.remove(image)
232         except Exception as e:
233             print("ERROR:", e)
234             success = False
235
236         return success
237
238     def objects_render_engine_guess(obs):
239         for obname in obs:
240             ob = bpy.data.objects[obname, None]
241             for matslot in ob.material_slots:
242                 mat = matslot.material
243                 if mat and mat.use_nodes and mat.node_tree:
244                     for nd in mat.node_tree.nodes:
245                         if nd.shading_compatibility == {'NEW_SHADING'}:
246                             return 'CYCLES'
247         return 'BLENDER_RENDER'
248
249     def object_bbox_merge(bbox, ob, ob_space):
250         if ob.bound_box:
251             ob_bbox = ob.bound_box
252         else:
253             ob_bbox = ((-ob.scale.x, -ob.scale.y, -ob.scale.z), (ob.scale.x, ob.scale.y, ob.scale.z))
254         for v in ob.bound_box:
255             v = ob_space.matrix_world.inverted() * ob.matrix_world * Vector(v)
256             if bbox[0].x > v.x:
257                 bbox[0].x = v.x
258             if bbox[0].y > v.y:
259                 bbox[0].y = v.y
260             if bbox[0].z > v.z:
261                 bbox[0].z = v.z
262             if bbox[1].x < v.x:
263                 bbox[1].x = v.x
264             if bbox[1].y < v.y:
265                 bbox[1].y = v.y
266             if bbox[1].z < v.z:
267                 bbox[1].z = v.z
268
269     def objects_bbox_calc(camera, objects):
270         bbox = (Vector((1e9, 1e9, 1e9)), Vector((-1e9, -1e9, -1e9)))
271         for obname in objects:
272             ob = bpy.data.objects[obname, None]
273             object_bbox_merge(bbox, ob, camera)
274         # Our bbox has been generated in camera local space, bring it back in world one
275         bbox[0][:] = camera.matrix_world * bbox[0]
276         bbox[1][:] = camera.matrix_world * bbox[1]
277         cos = (
278             bbox[0].x, bbox[0].y, bbox[0].z,
279             bbox[0].x, bbox[0].y, bbox[1].z,
280             bbox[0].x, bbox[1].y, bbox[0].z,
281             bbox[0].x, bbox[1].y, bbox[1].z,
282             bbox[1].x, bbox[0].y, bbox[0].z,
283             bbox[1].x, bbox[0].y, bbox[1].z,
284             bbox[1].x, bbox[1].y, bbox[0].z,
285             bbox[1].x, bbox[1].y, bbox[1].z,
286         )
287         return cos
288
289     def preview_render_do(render_context, item_container, item_name, objects):
290         scene = bpy.data.scenes[render_context.scene, None]
291         if objects is not None:
292             camera = bpy.data.objects[render_context.camera, None]
293             lamp = bpy.data.objects[render_context.lamp, None] if render_context.lamp is not None else None
294             cos = objects_bbox_calc(camera, objects)
295             loc, ortho_scale = camera.camera_fit_coords(scene, cos)
296             camera.location = loc
297             if lamp:
298                 loc, ortho_scale = lamp.camera_fit_coords(scene, cos)
299                 lamp.location = loc
300         scene.update()
301
302         bpy.ops.render.render(write_still=True)
303
304         image = bpy.data.images[render_context.image, None]
305         item = getattr(bpy.data, item_container)[item_name, None]
306         image.reload()
307         # Note: we could use struct module here, but not quite sure it'd give any advantage really...
308         pix = tuple((round(r * 255)) + (round(g * 255) << 8) + (round(b * 255) << 16) + (round(a * 255) << 24)
309                     for r, g, b, a in zip(*[iter(image.pixels)] * 4))
310         item.preview.image_size = (RENDER_PREVIEW_SIZE, RENDER_PREVIEW_SIZE)
311         item.preview.image_pixels = pix
312
313     # And now, main code!
314     do_save = True
315
316     if do_data_intern:
317         bpy.ops.wm.previews_clear(id_type=INTERN_PREVIEW_TYPES)
318         bpy.ops.wm.previews_ensure()
319
320     render_contexts = {}
321
322     objects_ignored = set()
323     groups_ignored = set()
324
325     prev_scenename = bpy.context.screen.scene.name
326
327     if do_objects:
328         prev_shown = tuple(ob.hide_render for ob in ids_nolib(bpy.data.objects))
329         for ob in ids_nolib(bpy.data.objects):
330             if ob in objects_ignored:
331                 continue
332             ob.hide_render = True
333         for root in ids_nolib(bpy.data.objects):
334             if root.name in objects_ignored:
335                 continue
336             if root.type not in OBJECT_TYPES_RENDER:
337                 continue
338             objects = (root.name,)
339
340             render_engine = objects_render_engine_guess(objects)
341             render_context = render_contexts.get(render_engine, None)
342             if render_context is None:
343                 render_context = render_context_create(render_engine, objects_ignored)
344                 render_contexts[render_engine] = render_context
345
346             scene = bpy.data.scenes[render_context.scene, None]
347             bpy.context.screen.scene = scene
348
349             for obname in objects:
350                 ob = bpy.data.objects[obname, None]
351                 if obname not in scene.objects:
352                     scene.objects.link(ob)
353                 ob.hide_render = False
354             scene.update()
355
356             preview_render_do(render_context, 'objects', root.name, objects)
357
358             # XXX Hyper Super Uber Suspicious Hack!
359             #     Without this, on windows build, script excepts with following message:
360             #         Traceback (most recent call last):
361             #         File "<string>", line 1, in <module>
362             #         File "<string>", line 451, in <module>
363             #         File "<string>", line 443, in main
364             #         File "<string>", line 327, in do_previews
365             #         OverflowError: Python int too large to convert to C long
366             #    ... :(
367             import sys
368             scene = bpy.data.scenes[render_context.scene, None]
369             for obname in objects:
370                 ob = bpy.data.objects[obname, None]
371                 scene.objects.unlink(ob)
372                 ob.hide_render = True
373
374         for ob, is_rendered in zip(tuple(ids_nolib(bpy.data.objects)), prev_shown):
375             ob.hide_render = is_rendered
376
377     if do_groups:
378         for grp in ids_nolib(bpy.data.groups):
379             if grp.name in groups_ignored:
380                 continue
381             objects = tuple(ob.name for ob in grp.objects)
382
383             render_engine = objects_render_engine_guess(objects)
384             render_context = render_contexts.get(render_engine, None)
385             if render_context is None:
386                 render_context = render_context_create(render_engine, objects_ignored)
387                 render_contexts[render_engine] = render_context
388
389             scene = bpy.data.scenes[render_context.scene, None]
390             bpy.context.screen.scene = scene
391
392             bpy.ops.object.group_instance_add(group=grp.name)
393             grp_ob = next((ob for ob in scene.objects if ob.dupli_group and ob.dupli_group.name == grp.name))
394             grp_obname = grp_ob.name
395             scene.update()
396
397             preview_render_do(render_context, 'groups', grp.name, objects)
398
399             scene = bpy.data.scenes[render_context.scene, None]
400             scene.objects.unlink(bpy.data.objects[grp_obname, None])
401
402     bpy.context.screen.scene = bpy.data.scenes[prev_scenename, None]
403     for render_context in render_contexts.values():
404         if not render_context_delete(render_context):
405             do_save = False  # Do not save file if something went wrong here, we could 'pollute' it with temp data...
406
407     if do_scenes:
408         for scene in ids_nolib(bpy.data.scenes):
409             has_camera = scene.camera is not None
410             bpy.context.screen.scene = scene
411             render_context = render_context_create('__SCENE', objects_ignored)
412             scene.update()
413
414             objects = None
415             if not has_camera:
416                 # We had to add a temp camera, now we need to place it to see interesting objects!
417                 objects = tuple(ob.name for ob in scene.objects
418                                         if (not ob.hide_render) and (ob.type in OBJECT_TYPES_RENDER))
419
420             preview_render_do(render_context, 'scenes', scene.name, objects)
421
422             if not render_context_delete(render_context):
423                 do_save = False
424
425     bpy.context.screen.scene = bpy.data.scenes[prev_scenename, None]
426     if do_save:
427         print("Saving %s..." % bpy.data.filepath)
428         try:
429             bpy.ops.wm.save_mainfile()
430         except Exception as e:
431             # Might fail in some odd cases, like e.g. in regression files we have glsl/ram_glsl.blend which
432             # references an inexistent texture... Better not break in this case, just spit error to console.
433             print("ERROR:", e)
434     else:
435         print("*NOT* Saving %s, because some error(s) happened while deleting temp render data..." % bpy.data.filepath)
436
437
438 def do_clear_previews(do_objects, do_groups, do_scenes, do_data_intern):
439     if do_data_intern:
440         bpy.ops.wm.previews_clear(id_type=INTERN_PREVIEW_TYPES)
441
442     if do_objects:
443         for ob in ids_nolib(bpy.data.objects):
444             ob.preview.image_size = (0, 0)
445
446     if do_groups:
447         for grp in ids_nolib(bpy.data.groups):
448             grp.preview.image_size = (0, 0)
449
450     if do_scenes:
451         for scene in ids_nolib(bpy.data.scenes):
452             scene.preview.image_size = (0, 0)
453
454     print("Saving %s..." % bpy.data.filepath)
455     bpy.ops.wm.save_mainfile()
456
457
458 def main():
459     try:
460         import bpy
461     except ImportError:
462         print("This script must run from inside blender")
463         return
464
465     import sys
466     import argparse
467
468     # Get rid of Blender args!
469     argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
470
471     parser = argparse.ArgumentParser(description="Use Blender to generate previews for currently open Blender file's items.")
472     parser.add_argument('--clear', default=False, action="store_true", help="Clear previews instead of generating them.")
473     parser.add_argument('--no_scenes', default=True, action="store_false", help="Do not generate/clear previews for scene IDs.")
474     parser.add_argument('--no_groups', default=True, action="store_false", help="Do not generate/clear previews for group IDs.")
475     parser.add_argument('--no_objects', default=True, action="store_false", help="Do not generate/clear previews for object IDs.")
476     parser.add_argument('--no_data_intern', default=True, action="store_false",
477                         help="Do not generate/clear previews for mat/tex/image/etc. IDs (those handled by core Blender code).")
478     args = parser.parse_args(argv)
479
480     if args.clear:
481         print("clear!")
482         do_clear_previews(do_objects=args.no_objects, do_groups=args.no_groups, do_scenes=args.no_scenes,
483                           do_data_intern=args.no_data_intern)
484     else:
485         print("render!")
486         do_previews(do_objects=args.no_objects, do_groups=args.no_groups, do_scenes=args.no_scenes,
487                     do_data_intern=args.no_data_intern)
488
489
490 if __name__ == "__main__":
491     print("\n\n *** Running {} *** \n".format(__file__))
492     print(" *** Blend file {} *** \n".format(bpy.data.filepath))
493     main()
494     bpy.ops.wm.quit_blender()