Import images as planes: use Principled BSDF for emission mode
[blender-addons.git] / system_demo_mode / demo_mode.py
blob8c45448bba7162199358dd15c916ed6952c5ee16
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 # <pep8 compliant>
21 """
22 Even though this is in a package this can run as a stand alone scripts.
24 # --- example usage
25 blender --python release/scripts/addons/system_demo_mode/demo_mode.py
27 looks for demo.py textblock or file in the same path as the blend:
28 # --- example
29 config = [
30 dict(anim_cycles=1, anim_render=False, anim_screen_switch=0.0, anim_time_max=10.0, anim_time_min=4.0, mode='AUTO', display_render=4.0, file='/l/19534_simplest_mesh_2.blend'),
31 dict(anim_cycles=1, anim_render=False, anim_screen_switch=0.0, anim_time_max=10.0, anim_time_min=4.0, mode='AUTO', display_render=4.0, file='/l/252_pivotConstraint_01.blend'),
33 # ---
34 /data/src/blender/lib/tests/rendering/
35 """
37 import bpy
38 import time
39 import os
41 DEMO_CFG = "demo.py"
43 # populate from script
44 global_config_files = []
46 global_config = dict(
47 anim_cycles=1,
48 anim_render=False,
49 anim_screen_switch=0.0,
50 anim_time_max=60.0,
51 anim_time_min=4.0,
52 mode='AUTO',
53 display_render=4.0,
56 # switch to the next file in 2 sec.
57 global_config_fallback = dict(
58 anim_cycles=1,
59 anim_render=False,
60 anim_screen_switch=0.0,
61 anim_time_max=60.0,
62 anim_time_min=4.0,
63 mode='AUTO',
64 display_render=4.0,
67 global_state = {
68 "init_time": 0.0,
69 "last_switch": 0.0,
70 "reset_anim": False,
71 "anim_cycles": 0, # count how many times we played the anim
72 "last_frame": -1,
73 "is_render": False,
74 "render_time": "", # time render was finished.
75 "timer": None,
76 "basedir": "", # demo.py is stored here
77 "demo_index": 0,
78 "exit": False,
82 # -----------------------------------------------------------------------------
83 # render handler - maintain "is_render"
85 def handle_render_clear():
86 for ls in (bpy.app.handlers.render_complete, bpy.app.handlers.render_cancel):
87 while handle_render_done_cb in ls:
88 ls.remove(handle_render_done_cb)
91 def handle_render_done_cb(self):
92 global_state["is_render"] = True
95 def handle_render_init():
96 handle_render_clear()
97 bpy.app.handlers.render_complete.append(handle_render_done_cb)
98 bpy.app.handlers.render_cancel.append(handle_render_done_cb)
99 global_state["is_render"] = False
102 def demo_mode_auto_select():
104 play_area = 0
105 render_area = 0
107 totimg = 0
109 for area in bpy.context.window.screen.areas:
110 size = area.width * area.height
111 if area.type in {'VIEW_3D', 'GRAPH_EDITOR', 'DOPESHEET_EDITOR', 'NLA_EDITOR', 'TIMELINE'}:
112 play_area += size
113 elif area.type in {'IMAGE_EDITOR', 'SEQUENCE_EDITOR', 'NODE_EDITOR'}:
114 render_area += size
116 if area.type == 'IMAGE_EDITOR':
117 totimg += 1
119 # since our test files have this as defacto standard
120 scene = bpy.context.scene
121 if totimg >= 2 and (scene.camera or scene.render.use_sequencer):
122 mode = 'RENDER'
123 else:
124 if play_area >= render_area:
125 mode = 'PLAY'
126 else:
127 mode = 'RENDER'
129 if 0:
130 return 'PLAY'
132 return mode
135 def demo_mode_next_file(step=1):
137 # support for temp
138 if global_config_files[global_state["demo_index"]].get("is_tmp"):
139 del global_config_files[global_state["demo_index"]]
140 global_state["demo_index"] -= 1
142 print(global_state["demo_index"])
143 demo_index_next = (global_state["demo_index"] + step) % len(global_config_files)
145 if global_state["exit"] and step > 0:
146 # check if we cycled
147 if demo_index_next < global_state["demo_index"]:
148 import sys
149 sys.exit(0)
151 global_state["demo_index"] = demo_index_next
152 print(global_state["demo_index"], "....")
153 print("func:demo_mode_next_file", global_state["demo_index"])
154 filepath = global_config_files[global_state["demo_index"]]["file"]
155 bpy.ops.wm.open_mainfile(filepath=filepath)
158 def demo_mode_timer_add():
159 global_state["timer"] = bpy.context.window_manager.event_timer_add(0.8, window=bpy.context.window)
162 def demo_mode_timer_remove():
163 if global_state["timer"]:
164 bpy.context.window_manager.event_timer_remove(global_state["timer"])
165 global_state["timer"] = None
168 def demo_mode_load_file():
169 """ Take care, this can only do limited functions since its running
170 before the file is fully loaded.
171 Some operators will crash like playing an animation.
173 print("func:demo_mode_load_file")
174 DemoMode.first_run = True
175 bpy.ops.wm.demo_mode('EXEC_DEFAULT')
178 def demo_mode_temp_file():
179 """ Initialize a temp config for the duration of the play time.
180 Use this so we can initialize the demo intro screen but not show again.
182 assert(global_state["demo_index"] == 0)
184 temp_config = global_config_fallback.copy()
185 temp_config["anim_time_min"] = 0.0
186 temp_config["anim_time_max"] = 60.0
187 temp_config["anim_cycles"] = 0 # ensures we switch when hitting the end
188 temp_config["mode"] = 'PLAY'
189 temp_config["is_tmp"] = True
191 global_config_files.insert(0, temp_config)
194 def demo_mode_init():
195 print("func:demo_mode_init")
196 DemoKeepAlive.ensure()
198 if 1:
199 global_config.clear()
200 global_config.update(global_config_files[global_state["demo_index"]])
202 print(global_config)
204 demo_mode_timer_add()
206 if global_config["mode"] == 'AUTO':
207 global_config["mode"] = demo_mode_auto_select()
209 if global_config["mode"] == 'PLAY':
210 global_state["last_frame"] = -1
211 global_state["anim_cycles"] = 0
212 bpy.ops.screen.animation_play()
214 elif global_config["mode"] == 'RENDER':
215 print(" render")
217 # setup scene.
218 scene = bpy.context.scene
219 scene.render.filepath = "TEMP_RENDER"
220 scene.render.image_settings.file_format = 'AVI_JPEG' if global_config["anim_render"] else 'PNG'
221 scene.render.use_file_extension = False
222 scene.render.use_placeholder = False
223 try:
224 # XXX - without this rendering will crash because of a bug in blender!
225 bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
226 if global_config["anim_render"]:
227 bpy.ops.render.render('INVOKE_DEFAULT', animation=True)
228 else:
229 bpy.ops.render.render('INVOKE_DEFAULT') # write_still=True, no need to write now.
231 handle_render_init()
233 except RuntimeError: # no camera for eg:
234 import traceback
235 traceback.print_exc()
237 else:
238 raise Exception("Unsupported mode %r" % global_config["mode"])
240 global_state["init_time"] = global_state["last_switch"] = time.time()
241 global_state["render_time"] = -1.0
244 def demo_mode_update():
245 time_current = time.time()
246 time_delta = time_current - global_state["last_switch"]
247 time_total = time_current - global_state["init_time"]
249 # --------------------------------------------------------------------------
250 # ANIMATE MODE
251 if global_config["mode"] == 'PLAY':
252 frame = bpy.context.scene.frame_current
253 # check for exit
254 if time_total > global_config["anim_time_max"]:
255 demo_mode_next_file()
256 return
257 # above cycles and minimum display time
258 if (
259 (time_total > global_config["anim_time_min"]) and
260 (global_state["anim_cycles"] > global_config["anim_cycles"])
262 # looped enough now.
263 demo_mode_next_file()
264 return
266 # run update funcs
267 if global_state["reset_anim"]:
268 global_state["reset_anim"] = False
269 bpy.ops.screen.animation_cancel(restore_frame=False)
270 bpy.ops.screen.animation_play()
272 # warning, switching the screen can switch the scene
273 # and mess with our last-frame/cycles counting.
274 if global_config["anim_screen_switch"]:
275 # print(time_delta, 1)
276 if time_delta > global_config["anim_screen_switch"]:
278 screen = bpy.context.window.screen
279 index = bpy.data.screens.keys().index(screen.name)
280 screen_new = bpy.data.screens[(index if index > 0 else len(bpy.data.screens)) - 1]
281 bpy.context.window.screen = screen_new
283 global_state["last_switch"] = time_current
285 # if we also switch scenes then reset last frame
286 # otherwise it could mess up cycle calc.
287 if screen.scene != screen_new.scene:
288 global_state["last_frame"] = -1
290 #if global_config["mode"] == 'PLAY':
291 if 1:
292 global_state["reset_anim"] = True
294 # did we loop?
295 if global_state["last_frame"] > frame:
296 print("Cycle!")
297 global_state["anim_cycles"] += 1
299 global_state["last_frame"] = frame
301 # --------------------------------------------------------------------------
302 # RENDER MODE
303 elif global_config["mode"] == 'RENDER':
304 if global_state["is_render"]:
305 # wait until the time has passed
306 # XXX, todo, if rendering an anim we need some way to check its done.
307 if global_state["render_time"] == -1.0:
308 global_state["render_time"] = time.time()
309 else:
310 if time.time() - global_state["render_time"] > global_config["display_render"]:
311 handle_render_clear()
312 demo_mode_next_file()
313 return
314 else:
315 raise Exception("Unsupported mode %r" % global_config["mode"])
317 # -----------------------------------------------------------------------------
318 # modal operator
321 class DemoKeepAlive:
322 secret_attr = "_keepalive"
324 @staticmethod
325 def ensure():
326 if DemoKeepAlive.secret_attr not in bpy.app.driver_namespace:
327 bpy.app.driver_namespace[DemoKeepAlive.secret_attr] = DemoKeepAlive()
329 @staticmethod
330 def remove():
331 if DemoKeepAlive.secret_attr in bpy.app.driver_namespace:
332 del bpy.app.driver_namespace[DemoKeepAlive.secret_attr]
334 def __del__(self):
335 """ Hack, when the file is loaded the drivers namespace is cleared.
337 if DemoMode.enabled:
338 demo_mode_load_file()
341 class DemoMode(bpy.types.Operator):
342 bl_idname = "wm.demo_mode"
343 bl_label = "Demo"
345 enabled = False
346 first_run = True
348 def cleanup(self, disable=False):
349 demo_mode_timer_remove()
350 DemoMode.first_run = True
352 if disable:
353 DemoMode.enabled = False
354 DemoKeepAlive.remove()
356 def modal(self, context, event):
357 # print("DemoMode.modal", global_state["anim_cycles"])
358 if not DemoMode.enabled:
359 self.cleanup(disable=True)
360 return {'CANCELLED'}
362 if event.type == 'ESC':
363 self.cleanup(disable=True)
364 # disable here and not in cleanup because this is a user level disable.
365 # which should stay disabled until explicitly enabled again.
366 return {'CANCELLED'}
368 # print(event.type)
369 if DemoMode.first_run:
370 DemoMode.first_run = False
372 demo_mode_init()
373 else:
374 demo_mode_update()
376 return {'PASS_THROUGH'}
378 def execute(self, context):
379 print("func:DemoMode.execute:", len(global_config_files), "files")
381 use_temp = False
383 # load config if not loaded
384 if not global_config_files:
385 load_config()
386 use_temp = True
388 if not global_config_files:
389 self.report({'INFO'}, "No configuration found with text or file: %s. Run File -> Demo Mode Setup" % DEMO_CFG)
390 return {'CANCELLED'}
392 if use_temp:
393 demo_mode_temp_file() # play this once through then never again
395 # toggle
396 if DemoMode.enabled and DemoMode.first_run is False:
397 # this actually cancells the previous running instance
398 # should never happen now, DemoModeControl is for this.
399 return {'CANCELLED'}
400 else:
401 DemoMode.enabled = True
403 context.window_manager.modal_handler_add(self)
404 return {'RUNNING_MODAL'}
406 def cancel(self, context):
407 print("func:DemoMode.cancel")
408 # disable here means no running on file-load.
409 self.cleanup()
411 # call from DemoModeControl
412 @classmethod
413 def disable(cls):
414 if cls.enabled and cls.first_run is False:
415 # this actually cancells the previous running instance
416 # should never happen now, DemoModeControl is for this.
417 cls.enabled = False
420 class DemoModeControl(bpy.types.Operator):
421 bl_idname = "wm.demo_mode_control"
422 bl_label = "Control"
424 mode: bpy.props.EnumProperty(
425 items=(('PREV', "Prev", ""),
426 ('PAUSE', "Pause", ""),
427 ('NEXT', "Next", "")),
428 name="Mode"
431 def execute(self, context):
432 mode = self.mode
433 if mode == 'PREV':
434 demo_mode_next_file(-1)
435 elif mode == 'NEXT':
436 demo_mode_next_file(1)
437 else: # pause
438 DemoMode.disable()
439 return {'FINISHED'}
442 def menu_func(self, context):
443 # print("func:menu_func - DemoMode.enabled:", DemoMode.enabled, "bpy.app.driver_namespace:", DemoKeepAlive.secret_attr not in bpy.app.driver_namespace, 'global_state["timer"]:', global_state["timer"])
444 layout = self.layout
445 layout.operator_context = 'EXEC_DEFAULT'
446 row = layout.row(align=True)
447 row.label(text="Demo Mode:")
448 if not DemoMode.enabled:
449 row.operator("wm.demo_mode", icon='PLAY', text="")
450 else:
451 row.operator("wm.demo_mode_control", icon='REW', text="").mode = 'PREV'
452 row.operator("wm.demo_mode_control", icon='PAUSE', text="").mode = 'PAUSE'
453 row.operator("wm.demo_mode_control", icon='FF', text="").mode = 'NEXT'
456 def register():
457 bpy.utils.register_class(DemoMode)
458 bpy.utils.register_class(DemoModeControl)
459 bpy.types.INFO_HT_header.append(menu_func)
462 def unregister():
463 bpy.utils.unregister_class(DemoMode)
464 bpy.utils.unregister_class(DemoModeControl)
465 bpy.types.INFO_HT_header.remove(menu_func)
468 # -----------------------------------------------------------------------------
469 # parse args
471 def load_config(cfg_name=DEMO_CFG):
472 namespace = {}
473 del global_config_files[:]
474 basedir = os.path.dirname(bpy.data.filepath)
476 text = bpy.data.texts.get(cfg_name)
477 if text is None:
478 demo_path = os.path.join(basedir, cfg_name)
479 if os.path.exists(demo_path):
480 print("Using config file: %r" % demo_path)
481 demo_file = open(demo_path, "r")
482 demo_data = demo_file.read()
483 demo_file.close()
484 else:
485 demo_data = ""
486 else:
487 print("Using config textblock: %r" % cfg_name)
488 demo_data = text.as_string()
489 demo_path = os.path.join(bpy.data.filepath, cfg_name) # fake
491 if not demo_data:
492 print("Could not find %r textblock or %r file." % (DEMO_CFG, demo_path))
493 return False
495 namespace["__file__"] = demo_path
497 exec(demo_data, namespace, namespace)
499 demo_config = namespace["config"]
500 demo_search_path = namespace.get("search_path")
501 global_state["exit"] = namespace.get("exit", False)
503 if demo_search_path is None:
504 print("reading: %r, no search_path found, missing files wont be searched." % demo_path)
505 if demo_search_path.startswith("//"):
506 demo_search_path = bpy.path.abspath(demo_search_path)
507 if not os.path.exists(demo_search_path):
508 print("reading: %r, search_path %r does not exist." % (demo_path, demo_search_path))
509 demo_search_path = None
511 blend_lookup = {}
512 # initialize once, case insensitive dict
514 def lookup_file(filepath):
515 filename = os.path.basename(filepath).lower()
517 if not blend_lookup:
518 # ensure only ever run once.
519 blend_lookup[None] = None
521 def blend_dict_items(path):
522 for dirpath, dirnames, filenames in os.walk(path):
523 # skip '.git'
524 dirnames[:] = [d for d in dirnames if not d.startswith(".")]
525 for filename in filenames:
526 if filename.lower().endswith(".blend"):
527 filepath = os.path.join(dirpath, filename)
528 yield (filename.lower(), filepath)
530 blend_lookup.update(dict(blend_dict_items(demo_search_path)))
532 # fallback to original file
533 return blend_lookup.get(filename, filepath)
534 # done with search lookup
536 for filecfg in demo_config:
537 filepath_test = filecfg["file"]
538 if not os.path.exists(filepath_test):
539 filepath_test = os.path.join(basedir, filecfg["file"])
540 if not os.path.exists(filepath_test):
541 filepath_test = lookup_file(filepath_test) # attempt to get from searchpath
542 if not os.path.exists(filepath_test):
543 print("Cant find %r or %r, skipping!")
544 continue
546 filecfg["file"] = os.path.normpath(filepath_test)
548 # sanitize
549 filecfg["file"] = os.path.abspath(filecfg["file"])
550 filecfg["file"] = os.path.normpath(filecfg["file"])
551 print(" Adding: %r" % filecfg["file"])
552 global_config_files.append(filecfg)
554 print("found %d files" % len(global_config_files))
556 global_state["basedir"] = basedir
558 return bool(global_config_files)
561 # support direct execution
562 if __name__ == "__main__":
563 register()
565 demo_mode_load_file() # kick starts the modal operator