ya2 · news · projects · code · about

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