bl_previews_render.py 22 KB

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