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