ya2 · news · projects · code · about

multithreaded render
[pmachines.git] / pmachines / scene.py
CommitLineData
31237524 1from os.path import exists
2aaa10d3 2from panda3d.core import AmbientLight, DirectionalLight, Point3, Texture, \
bdfa6dda 3 TextPropertiesManager, TextNode, Spotlight, PerspectiveLens, BitMask32
36099535
FC
4from panda3d.bullet import BulletPlaneShape, BulletGhostNode
5from direct.gui.OnscreenImage import OnscreenImage
2aaa10d3
FC
6from direct.gui.OnscreenText import OnscreenText
7from direct.gui.DirectGui import DirectButton, DirectFrame
36099535 8from direct.gui.DirectGuiGlobals import FLAT, DISABLED, NORMAL
1be87278
FC
9from direct.showbase.DirectObject import DirectObject
10from pmachines.items.background import Background
11from pmachines.items.box import Box
d5932612 12from pmachines.items.shelf import Shelf
b41381b2 13from pmachines.items.domino import Domino, TargetDomino
8254721d 14from pmachines.items.basketball import Basketball
ea38777c 15from pmachines.items.teetertooter import TeeterTooter
a5dc83f4 16from pmachines.sidepanel import SidePanel
79f81d48 17from lib.engine.gui.cursor import MouseCursor
13263131 18from lib.lib.p3d.gfx import P3dGfxMgr
1be87278
FC
19
20
21class Scene(DirectObject):
22
31237524 23 def __init__(self, world, exit_cb, auto_close_instr, dbg_items):
1be87278
FC
24 super().__init__()
25 self._world = world
5964572b 26 self._exit_cb = exit_cb
31237524 27 self._dbg_items = dbg_items
1be87278 28 self._set_camera()
79f81d48
FC
29 self._cursor = MouseCursor(
30 'assets/buttons/arrowUpLeft.png', (.04, 1, .04), (.5, .5, .5, 1),
31 (.01, .01))
36099535 32 self._set_gui()
1be87278
FC
33 self._set_lights()
34 self._set_input()
c8d8653f 35 self._set_mouse_plane()
d5932612
FC
36 self.items = []
37 self.reset()
9830561d 38 self._paused = False
7c0a81ae 39 self._item_active = None
e669403e 40 if auto_close_instr:
32aa4dae 41 self.__store_state()
e669403e
FC
42 self.__restore_state()
43 else:
44 self._set_instructions()
5964572b
FC
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
0d5a5427 49 def screenshot(self, task=None):
ecdf8933
FC
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))
0d5a5427 72 return buffer.get_texture()
ecdf8933 73
d5932612
FC
74 def current_bottom(self):
75 curr_bottom = 1
76 for item in self.items:
77 if item.repos_done:
78 curr_bottom = min(curr_bottom, item.get_bottom())
79 return curr_bottom
80
05fd23ec 81 def reset(self):
d5932612 82 [itm.destroy() for itm in self.items]
ef3c36bf
FC
83 self.items = []
84 #self.items += [Box(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, count=3)]
b41381b2
FC
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 += [Shelf(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, mass=0, pos=(1.2, 0, -.6))]
88 self.items += [Domino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=2)]
89 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)]
90 self.items += [TargetDomino(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, pos=(-.49, 0, -.04), tgt_degrees=60)]
91 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)]
92 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)]
93 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)]
ef3c36bf
FC
94 #self.items += [Basketball(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
95 #self.items += [TeeterTooter(self._world, self._mouse_plane_node, self.cb_inst, self.current_bottom, self.repos, count=3)]
32aa4dae
FC
96 self._commands = []
97 self._command_idx = 0
b41381b2
FC
98 if hasattr(self, '_success_txt'):
99 self._success_txt.destroy()
100 del self._success_txt
101 self.__right_btn['state'] = NORMAL
05fd23ec 102
5964572b
FC
103 def destroy(self):
104 self._unset_gui()
105 self._unset_lights()
106 self._unset_input()
107 self._unset_mouse_plane()
108 [itm.destroy() for itm in self.items]
109 self._bg.destroy()
110 self._side_panel.destroy()
407412a5 111 self._cursor.destroy()
5964572b 112 taskMgr.remove(self._scene_tsk)
1be87278
FC
113
114 def _set_camera(self):
115 base.camera.set_pos(0, -20, 0)
116 base.camera.look_at(0, 0, 0)
117
a0acba9a
FC
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
36099535 125 def _set_gui(self):
01b221a6
FC
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]
a0acba9a 138 return [self.__load_img_btn(path, col) for col in colors]
36099535
FC
139 abl, abr = base.a2dBottomLeft, base.a2dBottomRight
140 btn_info = [
5964572b 141 ('home', self.on_home, NORMAL, abl, 'gray'),
9830561d 142 ('information', self._set_instructions, NORMAL, abl, 'gray'),
01b221a6
FC
143 ('right', self.on_play, NORMAL, abr, 'green'),
144 ('next', self.on_next, DISABLED, abr, 'gray'),
145 ('previous', self.on_prev, DISABLED, abr, 'gray'),
05fd23ec 146 ('rewind', self.reset, NORMAL, abr, 'gray')]
36099535 147 num_l = num_r = 0
a0acba9a 148 btns = []
36099535 149 for binfo in btn_info:
01b221a6 150 imgs = load_images_btn(binfo[0], binfo[4])
36099535
FC
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,
54a1397e
FC
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'))
36099535 164 btn.set_transparency(True)
a0acba9a
FC
165 btns += [btn]
166 self.__home_btn, self.__info_btn, self.__right_btn, self.__next_btn, \
167 self.__prev_btn, self.__rewind_btn = btns
31237524
FC
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)
36099535 172
5964572b
FC
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]
31237524
FC
178 if self._dbg_items:
179 self._info_txt.destroy()
5964572b 180
64eae9c7
FC
181 def _set_spotlight(self, name, pos, look_at, color, shadows=False):
182 light = Spotlight(name)
183 if shadows:
184 light.setLens(PerspectiveLens())
1be87278 185 light_np = render.attach_new_node(light)
64eae9c7
FC
186 light_np.set_pos(pos)
187 light_np.look_at(look_at)
1be87278
FC
188 light.set_color(color)
189 render.set_light(light_np)
5964572b 190 return light_np
1be87278
FC
191
192 def _set_lights(self):
193 alight = AmbientLight('alight') # for ao
64eae9c7
FC
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))
5964572b
FC
205
206 def _unset_lights(self):
64eae9c7 207 for light in [self._alnp, self._key_light, self._shadow_light]:
5964572b
FC
208 render.clear_light(light)
209 light.remove_node()
1be87278
FC
210
211 def _set_input(self):
49c79300 212 self.accept('mouse1', self.on_click_l)
c8d8653f 213 self.accept('mouse1-up', self.on_release)
49c79300
FC
214 self.accept('mouse3', self.on_click_r)
215 self.accept('mouse3-up', self.on_release)
c8d8653f 216
5964572b
FC
217 def _unset_input(self):
218 for evt in ['mouse1', 'mouse1-up', 'mouse3', 'mouse3-up']:
219 self.ignore(evt)
220
c8d8653f 221 def _set_mouse_plane(self):
651713a9 222 shape = BulletPlaneShape((0, -1, 0), 0)
c8d8653f
FC
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)
06af3aa9 228 self._world.attach_ghost(self._mouse_plane_node)
1be87278 229
5964572b
FC
230 def _unset_mouse_plane(self):
231 self._world.remove_ghost(self._mouse_plane_node)
232
37ba71d8
FC
233 def _get_hits(self):
234 if not base.mouseWatcherNode.has_mouse(): return []
13263131 235 p_from, p_to = P3dGfxMgr.world_from_to(base.mouseWatcherNode.get_mouse())
37ba71d8
FC
236 return self._world.ray_test_all(p_from, p_to).get_hits()
237
31237524
FC
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
49c79300 245 def _on_click(self, method):
a0acba9a
FC
246 if self._paused:
247 return
c8d8653f
FC
248 for hit in self._get_hits():
249 if hit.get_node() == self._mouse_plane_node:
250 pos = hit.get_hit_pos()
37ba71d8 251 for hit in self._get_hits():
ef3c36bf 252 for item in [i for i in self.items if hit.get_node() == i.node and i.interactable]:
7c0a81ae
FC
253 if not self._item_active:
254 self._item_active = item
49c79300 255 getattr(item, method)(pos)
79f81d48 256 img = 'move' if method == 'on_click_l' else 'rotate'
a6843832
FC
257 if not (img == 'rotate' and not item._instantiated):
258 self._cursor.set_image('assets/buttons/%s.png' % img)
49c79300
FC
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')
c8d8653f
FC
265
266 def on_release(self):
32aa4dae
FC
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]
7c0a81ae 277 self._item_active = None
49c79300 278 [item.on_release() for item in self.items]
79f81d48 279 self._cursor.set_image('assets/buttons/arrowUpLeft.png')
37ba71d8 280
e1438449 281 def repos(self):
d5932612
FC
282 for item in self.items:
283 item.repos_done = False
e1438449 284 self.items = sorted(self.items, key=lambda itm: itm.__class__.__name__)
651713a9 285 [item.on_aspect_ratio_changed() for item in self.items]
a5dc83f4 286 self._side_panel.update(self.items)
7790323d 287 max_x = -float('inf')
e1438449
FC
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()
651713a9 297
37ba71d8 298 def on_frame(self, task):
c8d8653f
FC
299 hits = self._get_hits()
300 pos = None
301 for hit in self._get_hits():
302 if hit.get_node() == self._mouse_plane_node:
303 pos = hit.get_hit_pos()
304 hit_nodes = [hit.get_node() for hit in hits]
7c0a81ae
FC
305 if self._item_active:
306 items_hit = [self._item_active]
307 else:
308 items_hit = [itm for itm in self.items if itm.node in hit_nodes]
37ba71d8
FC
309 items_no_hit = [itm for itm in self.items if itm not in items_hit]
310 [itm.on_mouse_on() for itm in items_hit]
311 [itm.on_mouse_off() for itm in items_no_hit]
7c0a81ae
FC
312 if pos and self._item_active:
313 self._item_active.on_mouse_move(pos)
31237524
FC
314 if self._dbg_items:
315 self._update_info(items_hit[0] if items_hit else None)
b41381b2
FC
316 if all(itm.end_condition() for itm in self.items) and not hasattr(self, '_success_txt'):
317 loader.load_sfx('assets/audio/sfx/success.ogg').play()
318 font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
319 font.clear()
320 font.set_pixels_per_unit(60)
321 font.set_minfilter(Texture.FTLinearMipmapLinear)
322 font.set_outline((0, 0, 0, 1), .8, .2)
323 self._success_txt = OnscreenText(
324 _('You win!'), font=font, scale=0.2, fg=(.9, .9, .9, 1))
37ba71d8 325 return task.cont
c8d8653f 326
7850ccc4
FC
327 def cb_inst(self, item):
328 self.items += [item]
329
c8d8653f 330 def on_play(self):
b41381b2
FC
331 self.__prev_btn['state'] = DISABLED
332 self.__next_btn['state'] = DISABLED
333 self.__right_btn['state'] = DISABLED
c8d8653f 334 [itm.play() for itm in self.items]
36099535
FC
335
336 def on_next(self):
32aa4dae
FC
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]
36099535
FC
345
346 def on_prev(self):
32aa4dae
FC
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]
36099535 354
36099535 355 def on_home(self):
5964572b 356 self._exit_cb()
36099535 357
2aaa10d3 358 def _set_instructions(self):
9830561d
FC
359 self._paused = True
360 self.__store_state()
2aaa10d3
FC
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 txt = _('keep \5mouse_l\5 pressed to drag an item\n\n'
379 'keep \5mouse_r\5 pressed to rotate an item')
380 self._txt = OnscreenText(
381 txt, parent=frm, font=font, scale=0.06, fg=(.9, .9, .9, 1),
382 align=TextNode.A_left)
383 u_l = self._txt.textNode.get_upper_left_3d()
384 l_r = self._txt.textNode.get_lower_right_3d()
385 w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
a0acba9a
FC
386 btn_scale = .05
387 mar = .06 # margin
2aaa10d3 388 z = h / 2 - font.get_line_height() * self._txt['scale'][1]
a0acba9a 389 z += (btn_scale + 2 * mar) / 2
2aaa10d3
FC
390 self._txt['pos'] = -w / 2, z
391 u_l = self._txt.textNode.get_upper_left_3d()
392 l_r = self._txt.textNode.get_lower_right_3d()
a0acba9a
FC
393 c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
394 fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
2aaa10d3 395 frm['frameSize'] = fsz
a0acba9a
FC
396 colors = [
397 (.6, .6, .6, 1), # ready
398 (1, 1, 1, 1), # press
399 (.8, .8, .8, 1), # rollover
400 (.4, .4, .4, .4)]
401 imgs = [self.__load_img_btn('exitRight', col) for col in colors]
402 btn = DirectButton(
403 image=imgs, scale=btn_scale,
404 pos=(l_r[0] - btn_scale, 1, l_r[2] - mar - btn_scale),
405 parent=frm, command=self.__on_close_instructions, extraArgs=[frm],
406 relief=FLAT, frameColor=(.6, .6, .6, .08),
407 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
408 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
409 btn.set_transparency(True)
410
411 def __store_state(self):
412 btns = [
413 self.__home_btn, self.__info_btn, self.__right_btn,
414 self.__next_btn, self.__prev_btn, self.__rewind_btn]
415 self.__btn_state = [btn['state'] for btn in btns]
416 for btn in btns:
417 btn['state'] = DISABLED
418 [itm.store_state() for itm in self.items]
419
420 def __restore_state(self):
421 btns = [
422 self.__home_btn, self.__info_btn, self.__right_btn,
423 self.__next_btn, self.__prev_btn, self.__rewind_btn]
424 for btn, state in zip(btns, self.__btn_state):
425 btn['state'] = state
426 [itm.restore_state() for itm in self.items]
427 self._paused = False
428
429 def __on_close_instructions(self, frm):
430 frm.remove_node()
431 self.__restore_state()