ya2 · news · projects · code · about

f27e7b43c927698052aeee943a7e1a2c6996dcac
[pmachines.git] / pmachines / scene.py
1 from os.path import exists
2 from glob import glob
3 from importlib import import_module
4 from inspect import isclass
5 from panda3d.core import AmbientLight, DirectionalLight, Point3, Texture, \
6 TextPropertiesManager, TextNode, Spotlight, PerspectiveLens, BitMask32
7 from panda3d.bullet import BulletPlaneShape, BulletGhostNode
8 from direct.gui.OnscreenImage import OnscreenImage
9 from direct.gui.OnscreenText import OnscreenText
10 from direct.gui.DirectGui import DirectButton, DirectFrame
11 from direct.gui.DirectGuiGlobals import FLAT, DISABLED, NORMAL
12 from direct.showbase.DirectObject import DirectObject
13 from pmachines.items.background import Background
14 from pmachines.sidepanel import SidePanel
15 from lib.engine.gui.cursor import MouseCursor
16 from lib.lib.p3d.gfx import P3dGfxMgr
17
18
19 class Scene(DirectObject):
20
21 def __init__(self, world, exit_cb, auto_close_instr, dbg_items, reload_cb):
22 super().__init__()
23 self._world = world
24 self._exit_cb = exit_cb
25 self._dbg_items = dbg_items
26 self._reload_cb = reload_cb
27 self._set_camera()
28 self._cursor = MouseCursor(
29 'assets/buttons/arrowUpLeft.png', (.04, 1, .04), (.5, .5, .5, 1),
30 (.01, .01))
31 self._set_gui()
32 self._set_lights()
33 self._set_input()
34 self._set_mouse_plane()
35 self.items = []
36 self.reset()
37 self._paused = False
38 self._item_active = None
39 if auto_close_instr:
40 self.__store_state()
41 self.__restore_state()
42 else:
43 self._set_instructions()
44 self._bg = Background()
45 self._side_panel = SidePanel(world, self._mouse_plane_node, (-5, 4), (-3, 1), 1, self.items)
46 self._scene_tsk = taskMgr.add(self.on_frame, 'on_frame')
47
48 @staticmethod
49 def name():
50 return ''
51
52 def _instr_txt(self):
53 return ''
54
55 def _set_items(self):
56 self.items = []
57
58 def screenshot(self, task=None):
59 tex = Texture('screenshot')
60 buffer = base.win.make_texture_buffer('screenshot', 512, 512, tex, True )
61 cam = base.make_camera(buffer)
62 cam.reparent_to(render)
63 cam.node().get_lens().set_fov(base.camLens.get_fov())
64 cam.set_pos(0, -20, 0)
65 cam.look_at(0, 0, 0)
66 import simplepbr
67 simplepbr.init(
68 window=buffer,
69 camera_node=cam,
70 use_normal_maps=True,
71 use_emission_maps=False,
72 use_occlusion_maps=True,
73 msaa_samples=4,
74 enable_shadows=True)
75 base.graphicsEngine.renderFrame()
76 base.graphicsEngine.renderFrame()
77 fname = self.__class__.__name__
78 buffer.save_screenshot('assets/images/scenes/%s.png' % fname)
79 # img = DirectButton(
80 # frameTexture=buffer.get_texture(), relief=FLAT,
81 # frameSize=(-.2, .2, -.2, .2))
82 return buffer.get_texture()
83
84 def current_bottom(self):
85 curr_bottom = 1
86 for item in self.items:
87 if item.repos_done:
88 curr_bottom = min(curr_bottom, item.get_bottom())
89 return curr_bottom
90
91 def reset(self):
92 [itm.destroy() for itm in self.items]
93 self._set_items()
94 self._commands = []
95 self._command_idx = 0
96 if hasattr(self, '_success_txt'):
97 self._success_txt.destroy()
98 del self._success_txt
99 self.__right_btn['state'] = NORMAL
100
101 def destroy(self):
102 self._unset_gui()
103 self._unset_lights()
104 self._unset_input()
105 self._unset_mouse_plane()
106 [itm.destroy() for itm in self.items]
107 self._bg.destroy()
108 self._side_panel.destroy()
109 self._cursor.destroy()
110 taskMgr.remove(self._scene_tsk)
111 if hasattr(self, '_success_txt'):
112 self._success_txt.destroy()
113
114 def _set_camera(self):
115 base.camera.set_pos(0, -20, 0)
116 base.camera.look_at(0, 0, 0)
117
118 def __load_img_btn(self, path, col):
119 img = OnscreenImage('assets/buttons/%s.png' % path)
120 img.set_transparency(True)
121 img.set_color(col)
122 img.detach_node()
123 return img
124
125 def _set_gui(self):
126 def load_images_btn(path, col):
127 colors = {
128 'gray': [
129 (.6, .6, .6, 1), # ready
130 (1, 1, 1, 1), # press
131 (.8, .8, .8, 1), # rollover
132 (.4, .4, .4, .4)],
133 'green': [
134 (.1, .68, .1, 1),
135 (.1, 1, .1, 1),
136 (.1, .84, .1, 1),
137 (.4, .1, .1, .4)]}[col]
138 return [self.__load_img_btn(path, col) for col in colors]
139 abl, abr = base.a2dBottomLeft, base.a2dBottomRight
140 btn_info = [
141 ('home', self.on_home, NORMAL, abl, 'gray'),
142 ('information', self._set_instructions, NORMAL, abl, 'gray'),
143 ('right', self.on_play, NORMAL, abr, 'green'),
144 ('next', self.on_next, DISABLED, abr, 'gray'),
145 ('previous', self.on_prev, DISABLED, abr, 'gray'),
146 ('rewind', self.reset, NORMAL, abr, 'gray')]
147 num_l = num_r = 0
148 btns = []
149 for binfo in btn_info:
150 imgs = load_images_btn(binfo[0], binfo[4])
151 if binfo[3] == base.a2dBottomLeft:
152 sign, num = 1, num_l
153 num_l += 1
154 else:
155 sign, num = -1, num_r
156 num_r += 1
157 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
158 btn = DirectButton(
159 image=imgs, scale=.05, pos=(sign * (.06 + .11 * num), 1, .06),
160 parent=binfo[3], command=binfo[1], state=binfo[2], relief=FLAT,
161 frameColor=fcols[0] if binfo[2] == NORMAL else fcols[1],
162 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
163 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
164 btn.set_transparency(True)
165 btns += [btn]
166 self.__home_btn, self.__info_btn, self.__right_btn, self.__next_btn, \
167 self.__prev_btn, self.__rewind_btn = btns
168 if self._dbg_items:
169 self._info_txt = OnscreenText(
170 '', parent=base.a2dTopRight, scale=0.04,
171 pos=(-.03, -.06), fg=(.9, .9, .9, 1), align=TextNode.A_right)
172
173 def _unset_gui(self):
174 btns = [
175 self.__home_btn, self.__info_btn, self.__right_btn,
176 self.__next_btn, self.__prev_btn, self.__rewind_btn]
177 [btn.destroy() for btn in btns]
178 if self._dbg_items:
179 self._info_txt.destroy()
180
181 def _set_spotlight(self, name, pos, look_at, color, shadows=False):
182 light = Spotlight(name)
183 if shadows:
184 light.setLens(PerspectiveLens())
185 light_np = render.attach_new_node(light)
186 light_np.set_pos(pos)
187 light_np.look_at(look_at)
188 light.set_color(color)
189 render.set_light(light_np)
190 return light_np
191
192 def _set_lights(self):
193 alight = AmbientLight('alight') # for ao
194 alight.set_color((.15, .15, .15, 1))
195 self._alnp = render.attach_new_node(alight)
196 render.set_light(self._alnp)
197 self._key_light = self._set_spotlight(
198 'key light', (-5, -80, 5), (0, 0, 0), (2.8, 2.8, 2.8, 1))
199 self._shadow_light = self._set_spotlight(
200 'key light', (-5, -80, 5), (0, 0, 0), (.58, .58, .58, 1), True)
201 self._shadow_light.node().set_shadow_caster(True, 2048, 2048)
202 self._shadow_light.node().get_lens().set_film_size(2048, 2048)
203 self._shadow_light.node().get_lens().set_near_far(1, 256)
204 self._shadow_light.node().set_camera_mask(BitMask32(0x01))
205
206 def _unset_lights(self):
207 for light in [self._alnp, self._key_light, self._shadow_light]:
208 render.clear_light(light)
209 light.remove_node()
210
211 def _set_input(self):
212 self.accept('mouse1', self.on_click_l)
213 self.accept('mouse1-up', self.on_release)
214 self.accept('mouse3', self.on_click_r)
215 self.accept('mouse3-up', self.on_release)
216
217 def _unset_input(self):
218 for evt in ['mouse1', 'mouse1-up', 'mouse3', 'mouse3-up']:
219 self.ignore(evt)
220
221 def _set_mouse_plane(self):
222 shape = BulletPlaneShape((0, -1, 0), 0)
223 #self._mouse_plane_node = BulletRigidBodyNode('mouse plane')
224 self._mouse_plane_node = BulletGhostNode('mouse plane')
225 self._mouse_plane_node.addShape(shape)
226 #np = render.attachNewNode(self._mouse_plane_node)
227 #self._world.attachRigidBody(self._mouse_plane_node)
228 self._world.attach_ghost(self._mouse_plane_node)
229
230 def _unset_mouse_plane(self):
231 self._world.remove_ghost(self._mouse_plane_node)
232
233 def _get_hits(self):
234 if not base.mouseWatcherNode.has_mouse(): return []
235 p_from, p_to = P3dGfxMgr.world_from_to(base.mouseWatcherNode.get_mouse())
236 return self._world.ray_test_all(p_from, p_to).get_hits()
237
238 def _update_info(self, item):
239 txt = ''
240 if item:
241 txt = '%.3f %.3f\n%.3f°' % (
242 item._np.get_x(), item._np.get_z(), item._np.get_r())
243 self._info_txt['text'] = txt
244
245 def _on_click(self, method):
246 if self._paused:
247 return
248 for hit in self._get_hits():
249 if hit.get_node() == self._mouse_plane_node:
250 pos = hit.get_hit_pos()
251 for hit in self._get_hits():
252 for item in [i for i in self.items if hit.get_node() == i.node and i.interactable]:
253 if not self._item_active:
254 self._item_active = item
255 getattr(item, method)(pos)
256 img = 'move' if method == 'on_click_l' else 'rotate'
257 if not (img == 'rotate' and not item._instantiated):
258 self._cursor.set_image('assets/buttons/%s.png' % img)
259
260 def on_click_l(self):
261 self._on_click('on_click_l')
262
263 def on_click_r(self):
264 self._on_click('on_click_r')
265
266 def on_release(self):
267 if self._item_active and not self._item_active._first_command:
268 self._commands = self._commands[:self._command_idx]
269 self._commands += [self._item_active]
270 self._command_idx += 1
271 self.__prev_btn['state'] = NORMAL
272 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
273 self.__prev_btn['frameColor'] = fcols[0]
274 if self._item_active._command_idx == len(self._item_active._commands) - 1:
275 self.__next_btn['state'] = DISABLED
276 self.__next_btn['frameColor'] = fcols[1]
277 self._item_active = None
278 [item.on_release() for item in self.items]
279 self._cursor.set_image('assets/buttons/arrowUpLeft.png')
280
281 def repos(self):
282 for item in self.items:
283 item.repos_done = False
284 self.items = sorted(self.items, key=lambda itm: itm.__class__.__name__)
285 [item.on_aspect_ratio_changed() for item in self.items]
286 self._side_panel.update(self.items)
287 max_x = -float('inf')
288 for item in self.items:
289 if not item._instantiated:
290 max_x = max(item._np.get_x(), max_x)
291 for item in self.items:
292 if not item._instantiated:
293 item.repos_x(max_x)
294
295 def on_aspect_ratio_changed(self):
296 self.repos()
297
298 def _end_condition(self):
299 pass
300
301 def on_frame(self, task):
302 hits = self._get_hits()
303 pos = None
304 for hit in self._get_hits():
305 if hit.get_node() == self._mouse_plane_node:
306 pos = hit.get_hit_pos()
307 hit_nodes = [hit.get_node() for hit in hits]
308 if self._item_active:
309 items_hit = [self._item_active]
310 else:
311 items_hit = [itm for itm in self.items if itm.node in hit_nodes]
312 items_no_hit = [itm for itm in self.items if itm not in items_hit]
313 [itm.on_mouse_on() for itm in items_hit]
314 [itm.on_mouse_off() for itm in items_no_hit]
315 if pos and self._item_active:
316 self._item_active.on_mouse_move(pos)
317 if self._dbg_items:
318 self._update_info(items_hit[0] if items_hit else None)
319 if self._end_condition():
320 self._set_end()
321 if any(itm._overlapping for itm in self.items):
322 self._cursor.cursor_img.img.set_color(.9, .1, .1, 1)
323 else:
324 self._cursor.cursor_img.img.set_color(.9, .9, .9, 1)
325 return task.cont
326
327 def cb_inst(self, item):
328 self.items += [item]
329
330 def on_play(self):
331 self.__prev_btn['state'] = DISABLED
332 self.__next_btn['state'] = DISABLED
333 self.__right_btn['state'] = DISABLED
334 [itm.play() for itm in self.items]
335
336 def on_next(self):
337 self._commands[self._command_idx].redo()
338 self._command_idx += 1
339 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
340 self.__prev_btn['state'] = NORMAL
341 self.__prev_btn['frameColor'] = fcols[0]
342 more_commands = self._command_idx < len(self._commands)
343 self.__next_btn['state'] = NORMAL if more_commands else DISABLED
344 self.__next_btn['frameColor'] = fcols[0] if more_commands else fcols[1]
345
346 def on_prev(self):
347 self._command_idx -= 1
348 self._commands[self._command_idx].undo()
349 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
350 self.__next_btn['state'] = NORMAL
351 self.__next_btn['frameColor'] = fcols[0]
352 self.__prev_btn['state'] = NORMAL if self._command_idx else DISABLED
353 self.__prev_btn['frameColor'] = fcols[0] if self._command_idx else fcols[1]
354
355 def on_home(self):
356 self._exit_cb()
357
358 def _set_instructions(self):
359 self._paused = True
360 self.__store_state()
361 mgr = TextPropertiesManager.get_global_ptr()
362 for name in ['mouse_l', 'mouse_r']:
363 graphic = OnscreenImage('assets/buttons/%s.png' % name)
364 graphic.set_scale(.5)
365 graphic.get_texture().set_minfilter(Texture.FTLinearMipmapLinear)
366 graphic.get_texture().set_anisotropic_degree(2)
367 mgr.set_graphic(name, graphic)
368 graphic.set_z(-.2)
369 graphic.set_transparency(True)
370 graphic.detach_node()
371 frm = DirectFrame(frameColor=(.4, .4, .4, .06),
372 frameSize=(-.6, .6, -.3, .3))
373 font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
374 font.clear()
375 font.set_pixels_per_unit(60)
376 font.set_minfilter(Texture.FTLinearMipmapLinear)
377 font.set_outline((0, 0, 0, 1), .8, .2)
378 self._txt = OnscreenText(
379 self._instr_txt(), parent=frm, font=font, scale=0.06,
380 fg=(.9, .9, .9, 1), align=TextNode.A_left)
381 u_l = self._txt.textNode.get_upper_left_3d()
382 l_r = self._txt.textNode.get_lower_right_3d()
383 w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
384 btn_scale = .05
385 mar = .06 # margin
386 z = h / 2 - font.get_line_height() * self._txt['scale'][1]
387 z += (btn_scale + 2 * mar) / 2
388 self._txt['pos'] = -w / 2, z
389 u_l = self._txt.textNode.get_upper_left_3d()
390 l_r = self._txt.textNode.get_lower_right_3d()
391 c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
392 fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
393 frm['frameSize'] = fsz
394 colors = [
395 (.6, .6, .6, 1), # ready
396 (1, 1, 1, 1), # press
397 (.8, .8, .8, 1), # rollover
398 (.4, .4, .4, .4)]
399 imgs = [self.__load_img_btn('exitRight', col) for col in colors]
400 btn = DirectButton(
401 image=imgs, scale=btn_scale,
402 pos=(l_r[0] - btn_scale, 1, l_r[2] - mar - btn_scale),
403 parent=frm, command=self.__on_close_instructions, extraArgs=[frm],
404 relief=FLAT, frameColor=(.6, .6, .6, .08),
405 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
406 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
407 btn.set_transparency(True)
408
409 def _set_end(self):
410 loader.load_sfx('assets/audio/sfx/success.ogg').play()
411 self._paused = True
412 self.__store_state()
413 frm = DirectFrame(frameColor=(.4, .4, .4, .06),
414 frameSize=(-.6, .6, -.3, .3))
415 font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
416 font.clear()
417 font.set_pixels_per_unit(60)
418 font.set_minfilter(Texture.FTLinearMipmapLinear)
419 font.set_outline((0, 0, 0, 1), .8, .2)
420 self._txt = OnscreenText(
421 _('You win!'),
422 parent=frm,
423 font=font, scale=0.2,
424 fg=(.9, .9, .9, 1))
425 u_l = self._txt.textNode.get_upper_left_3d()
426 l_r = self._txt.textNode.get_lower_right_3d()
427 w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
428 btn_scale = .05
429 mar = .06 # margin
430 z = h / 2 - font.get_line_height() * self._txt['scale'][1]
431 z += (btn_scale + 2 * mar) / 2
432 self._txt['pos'] = 0, z
433 u_l = self._txt.textNode.get_upper_left_3d()
434 l_r = self._txt.textNode.get_lower_right_3d()
435 c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
436 fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
437 frm['frameSize'] = fsz
438 colors = [
439 (.6, .6, .6, 1), # ready
440 (1, 1, 1, 1), # press
441 (.8, .8, .8, 1), # rollover
442 (.4, .4, .4, .4)]
443 imgs = [self.__load_img_btn('home', col) for col in colors]
444 btn = DirectButton(
445 image=imgs, scale=btn_scale,
446 pos=(-2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
447 parent=frm, command=self._on_end_home, extraArgs=[frm],
448 relief=FLAT, frameColor=(.6, .6, .6, .08),
449 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
450 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
451 btn.set_transparency(True)
452 imgs = [self.__load_img_btn('rewind', col) for col in colors]
453 btn = DirectButton(
454 image=imgs, scale=btn_scale,
455 pos=(0, 1, l_r[2] - mar - btn_scale),
456 parent=frm, command=self._on_restart, extraArgs=[frm],
457 relief=FLAT, frameColor=(.6, .6, .6, .08),
458 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
459 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
460 btn.set_transparency(True)
461 scenes = []
462 for _file in glob('pmachines/scenes/*.py'):
463 _fn = _file.replace('.py', '').replace('/', '.')
464 for member in import_module(_fn).__dict__.values():
465 if isclass(member) and issubclass(member, Scene) and \
466 member != Scene:
467 scenes += [member]
468 scenes = sorted(scenes, key=lambda elm: elm.sorting)
469 enabled = scenes.index(self.__class__) < len(scenes) - 1
470 if enabled:
471 next_scene = scenes[scenes.index(self.__class__) + 1]
472 else:
473 next_scene = None
474 imgs = [self.__load_img_btn('right', col) for col in colors]
475 btn = DirectButton(
476 image=imgs, scale=btn_scale,
477 pos=(2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
478 parent=frm, command=self._on_next_scene,
479 extraArgs=[frm, next_scene], relief=FLAT,
480 frameColor=(.6, .6, .6, .08),
481 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
482 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
483 btn['state'] = NORMAL if enabled else DISABLED
484 btn.set_transparency(True)
485
486 def _on_restart(self, frm):
487 self.__on_close_instructions(frm)
488 self.reset()
489
490 def _on_end_home(self, frm):
491 self.__on_close_instructions(frm)
492 self.on_home()
493
494 def _on_next_scene(self, frm, scene):
495 self.__on_close_instructions(frm)
496 self._reload_cb(scene)
497
498 def __store_state(self):
499 btns = [
500 self.__home_btn, self.__info_btn, self.__right_btn,
501 self.__next_btn, self.__prev_btn, self.__rewind_btn]
502 self.__btn_state = [btn['state'] for btn in btns]
503 for btn in btns:
504 btn['state'] = DISABLED
505 [itm.store_state() for itm in self.items]
506
507 def __restore_state(self):
508 btns = [
509 self.__home_btn, self.__info_btn, self.__right_btn,
510 self.__next_btn, self.__prev_btn, self.__rewind_btn]
511 for btn, state in zip(btns, self.__btn_state):
512 btn['state'] = state
513 [itm.restore_state() for itm in self.items]
514 self._paused = False
515
516 def __on_close_instructions(self, frm):
517 frm.remove_node()
518 self.__restore_state()