4c6559b1d42ebc9bfc644e0805d24ad5bf874e9c
[laserbrain_demo] / src / app.cc
1 #include <stdio.h>
2 #include <limits.h>
3 #include <assert.h>
4 #include <goatvr.h>
5 #include <assman.h>
6 #include "app.h"
7 #include "opengl.h"
8 #include "sdr.h"
9 #include "texture.h"
10 #include "mesh.h"
11 #include "meshgen.h"
12 #include "scene.h"
13 #include "metascene.h"
14 #include "datamap.h"
15 #include "ui.h"
16 #include "opt.h"
17 #include "post.h"
18 #include "renderer.h"
19 #include "rtarg.h"
20 #include "avatar.h"
21 #include "vrinput.h"
22 #include "exman.h"
23 #include "blob_exhibit.h"
24 #include "dbg_gui.h"
25 #include "geomdraw.h"
26 #include "ui_exhibit.h"
27
28 #define NEAR_CLIP       5.0
29 #define FAR_CLIP        10000.0
30
31 static void draw_scene();
32 static void toggle_flight();
33 static void calc_framerate();
34 static Ray calc_pick_ray(int x, int y);
35
36 long time_msec;
37 int win_width, win_height;
38 int vp_width, vp_height;
39 float win_aspect;
40 bool fb_srgb;
41 bool opt_gear_wireframe;
42
43 TextureSet texman;
44 SceneSet sceneman;
45
46 int fpexcept_enabled;
47
48 unsigned int dbg_key_pending;
49
50 static Avatar avatar;
51
52 static float cam_dist = 0.0;
53 static float floor_y;   // last floor height
54 static float user_eye_height = 165;
55
56 static float walk_speed = 300.0f;
57 static float mouse_speed = 0.5f;
58 static bool show_walk_mesh, noclip = false;
59
60 static bool have_headtracking, have_handtracking, should_swap;
61
62 static int prev_mx, prev_my;
63 static bool bnstate[8];
64 static bool keystate[256];
65 static bool gpad_bnstate[64];
66 static Vec2 joy_move, joy_look;
67 static float joy_deadzone = 0.01;
68
69 static float framerate;
70
71 static Mat4 view_matrix, mouse_view_matrix, proj_matrix;
72 static MetaScene *mscn;
73 static unsigned int sdr_post_gamma;
74
75 static long prev_msec;
76
77 static ExhibitManager *exman;
78 static bool show_blobs;
79
80 ExSelection exsel_active, exsel_hover;
81 ExSelection exsel_grab_left, exsel_grab_right;
82 #define exsel_grab_mouse exsel_grab_right
83 static ExhibitSlot exslot_left, exslot_right;
84 #define exslot_mouse exslot_right
85
86 static bool pointing;
87
88 static Renderer *rend;
89 static RenderTarget *goatvr_rtarg;
90
91 static Ray last_pick_ray;
92
93 static bool post_scene_init_pending = true;
94
95
96 bool app_init(int argc, char **argv)
97 {
98         set_log_file("demo.log");
99
100         char *env = getenv("FPEXCEPT");
101         if(env && atoi(env)) {
102                 info_log("enabling floating point exceptions\n");
103                 fpexcept_enabled = 1;
104                 enable_fpexcept();
105         }
106
107         if(init_opengl() == -1) {
108                 return false;
109         }
110
111         if(!init_options(argc, argv, "demo.conf")) {
112                 return false;
113         }
114         app_resize(opt.width, opt.height);
115         app_fullscreen(opt.fullscreen);
116
117         if(opt.data_url) {
118                 info_log("Adding URL asset source: %s\n", opt.data_url);
119                 ass_add_url("data", opt.data_url);
120         }
121
122         if(opt.vr) {
123                 if(goatvr_init() == -1) {
124                         return false;
125                 }
126                 goatvr_set_origin_mode(GOATVR_HEAD);
127                 goatvr_set_units_scale(100.0f);
128
129                 goatvr_startvr();
130                 should_swap = goatvr_should_swap() != 0;
131                 user_eye_height = goatvr_get_eye_height();
132                 have_headtracking = goatvr_have_headtracking();
133                 have_handtracking = goatvr_have_handtracking();
134
135                 goatvr_recenter();
136
137                 goatvr_rtarg = new RenderTarget;
138         }
139
140         if(fb_srgb) {
141                 int srgb_capable;
142                 glGetIntegerv(GL_FRAMEBUFFER_SRGB_CAPABLE_EXT, &srgb_capable);
143                 printf("Framebuffer %s sRGB-capable\n", srgb_capable ? "is" : "is not");
144                 if(srgb_capable) {
145                         glEnable(GL_FRAMEBUFFER_SRGB);
146                 } else {
147                         fb_srgb = 0;
148                 }
149         }
150
151         glEnable(GL_MULTISAMPLE);
152         glEnable(GL_DEPTH_TEST);
153         glEnable(GL_CULL_FACE);
154         glEnable(GL_NORMALIZE);
155
156         if(!init_debug_gui()) {
157                 return false;
158         }
159
160         Mesh::use_custom_sdr_attr = false;
161
162         float ambient[] = {0.0, 0.0, 0.0, 0.0};
163         glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient);
164
165         init_audio();
166
167         if(!init_vrhands()) {
168                 return false;
169         }
170
171         mscn = new MetaScene;
172         if(!mscn->load(opt.scenefile ? opt.scenefile : "data/museum.scene")) {
173                 return false;
174         }
175
176         avatar.pos = mscn->start_pos;
177         Vec3 dir = rotate(Vec3(0, 0, 1), mscn->start_rot);
178         dir.y = 0;
179         avatar.body_rot = rad_to_deg(acos(dot(dir, Vec3(0, 0, 1))));
180
181         exman = new ExhibitManager;
182         // exhibits are loaded in post_scene_init, because they need access to the scene graph
183
184         if(!exui_init()) {
185                 error_log("failed to initialize exhibit ui system\n");
186                 return false;
187         }
188         exui_setnode(&exslot_left.node);
189         if(have_handtracking) {
190                 exui_scale(2);
191                 exui_rotation(Vec3(-deg_to_rad(35), 0, 0));
192         }
193
194         if(!fb_srgb) {
195                 sdr_post_gamma = create_program_load("sdr/post_gamma.v.glsl", "sdr/post_gamma.p.glsl");
196         }
197
198         rend = new Renderer;
199         if(!rend->init()) {
200                 return false;
201         }
202         if(opt.reflect) {
203                 rend->ropt |= RENDER_MIRRORS;
204         } else {
205                 rend->ropt &= ~RENDER_MIRRORS;
206         }
207         rend->set_scene(mscn);
208
209         glUseProgram(0);
210
211         if(opt.vr || opt.fullscreen) {
212                 app_grab_mouse(true);
213         }
214
215         if(mscn->music && opt.music) {
216                 mscn->music->play(AUDIO_PLAYMODE_LOOP);
217         }
218         return true;
219 }
220
221 // post_scene_init is called after the scene has completed loading
222 static void post_scene_init()
223 {
224         mscn->update(0);        // update once to calculate node matrices
225
226         int num_mir = mscn->calc_mirror_planes();
227         info_log("found %d mirror planes\n", num_mir);
228
229         exman->load(mscn, "data/exhibits");
230 }
231
232 void app_cleanup()
233 {
234         if(mscn->music) {
235                 mscn->music->stop();
236         }
237         destroy_audio();
238
239         app_grab_mouse(false);
240         if(opt.vr) {
241                 delete goatvr_rtarg;
242                 goatvr_shutdown();
243         }
244         destroy_vrhands();
245
246         delete rend;
247
248         exui_shutdown();
249
250         /* this must be destroyed before the scene graph to detach exhibit nodes
251          * before the scene tries to delete them recursively
252          */
253         delete exman;
254
255         texman.clear();
256         sceneman.clear();
257
258         cleanup_debug_gui();
259 }
260
261 static bool constrain_walk_mesh(const Vec3 &v, Vec3 *newv)
262 {
263         Mesh *wm = mscn->walk_mesh;
264         if(!wm) {
265                 *newv = v;
266                 return true;
267         }
268
269         Ray downray = Ray(v, Vec3(0, -1, 0));
270         HitPoint hit;
271         if(mscn->walk_mesh->intersect(downray, &hit)) {
272                 *newv = hit.pos;
273                 newv->y += user_eye_height;
274                 return true;
275         }
276         return false;
277 }
278
279 static void update(float dt)
280 {
281         texman.update();
282         sceneman.update();
283
284         if(post_scene_init_pending && !sceneman.pending()) {
285                 post_scene_init_pending = false;
286                 post_scene_init();
287         }
288
289         mscn->update(dt);
290         exman->update(dt);
291
292         // use goatvr sticks for joystick input
293         int num_vr_sticks = goatvr_num_sticks();
294         if(num_vr_sticks > 0) {
295                 float p[2];
296                 goatvr_stick_pos(0, p);
297                 joy_move.x = p[0];
298                 joy_move.y = -p[1];
299         }
300         if(num_vr_sticks > 1) {
301                 float p[2];
302                 goatvr_stick_pos(1, p);
303                 joy_look.x = p[0];
304         }
305
306
307         float speed = walk_speed * dt;
308         Vec3 dir;
309
310         // joystick
311         float jdeadsq = joy_deadzone * joy_deadzone;
312         float jmove_lensq = length_sq(joy_move);
313         float jlook_lensq = length_sq(joy_look);
314
315         if(jmove_lensq > jdeadsq) {
316                 float len = sqrt(jmove_lensq);
317                 jmove_lensq -= jdeadsq;
318
319                 float mag = len * len;
320                 dir.x += mag * joy_move.x / len * speed;
321                 dir.z += mag * joy_move.y / len * speed;
322         }
323         if(jlook_lensq > jdeadsq) {
324                 float len = sqrt(jlook_lensq);
325                 jlook_lensq -= jdeadsq;
326
327                 float mag = len * len;
328                 avatar.body_rot += mag * joy_look.x / len * 200.0 * dt;
329                 avatar.head_alt += mag * joy_look.y / len * 100.0 * dt;
330                 if(avatar.head_alt < -90.0f) avatar.head_alt = -90.0f;
331                 if(avatar.head_alt > 90.0f) avatar.head_alt = 90.0f;
332         }
333
334         // keyboard move
335         if(keystate[(int)'w']) {
336                 dir.z -= speed;
337         }
338         if(keystate[(int)'s']) {
339                 dir.z += speed;
340         }
341         if(keystate[(int)'d']) {
342                 dir.x += speed;
343         }
344         if(keystate[(int)'a']) {
345                 dir.x -= speed;
346         }
347         if(keystate[(int)'q'] || gpad_bnstate[GPAD_UP]) {
348                 avatar.pos.y += speed;
349         }
350         if(keystate[(int)'z'] || gpad_bnstate[GPAD_DOWN]) {
351                 avatar.pos.y -= speed;
352         }
353
354         Vec3 walk_dir = avatar.calc_walk_dir(dir.z, dir.x);
355         Vec3 newpos = avatar.pos + walk_dir;
356
357         if(noclip) {
358                 avatar.pos = newpos;
359         } else {
360                 if(!constrain_walk_mesh(newpos, &avatar.pos)) {
361                         float dtheta = M_PI / 32.0;
362                         float theta = dtheta;
363                         Vec2 dir2d = newpos.xz() - avatar.pos.xz();
364
365                         for(int i=0; i<16; i++) {
366                                 Vec2 dvec = rotate(dir2d, theta);
367                                 Vec3 pos = avatar.pos + Vec3(dvec.x, 0, dvec.y);
368                                 if(constrain_walk_mesh(pos, &avatar.pos)) {
369                                         break;
370                                 }
371                                 dvec = rotate(dir2d, -theta);
372                                 pos = avatar.pos + Vec3(dvec.x, 0, dvec.y);
373                                 if(constrain_walk_mesh(pos, &avatar.pos)) {
374                                         break;
375                                 }
376                                 theta += dtheta;
377                         }
378                 }
379                 floor_y = avatar.pos.y - user_eye_height;
380         }
381
382         if(have_headtracking) {
383                 Quat qhead;
384                 goatvr_head_orientation(&qhead.x);
385                 avatar.tracked_head_rotation(qhead);
386         }
387
388         // TODO move to the avatar system
389         // calculate mouselook view matrix
390         mouse_view_matrix = Mat4::identity;
391         mouse_view_matrix.pre_translate(0, 0, -cam_dist);
392         if(!have_headtracking) {
393                 mouse_view_matrix.pre_rotate_x(deg_to_rad(avatar.head_alt));
394         }
395         mouse_view_matrix.pre_rotate_y(deg_to_rad(avatar.body_rot));
396         mouse_view_matrix.pre_translate(-avatar.pos.x, -avatar.pos.y, -avatar.pos.z);
397
398         // update hand-tracking
399         if(have_handtracking) {
400                 update_vrhands(&avatar);
401
402                 ExSelection *exsel_grab[] = { &exsel_grab_left, &exsel_grab_right };
403                 ExhibitSlot *exslot[] = { &exslot_left, &exslot_right };
404
405                 for(int i=0; i<2; i++) {
406                         if(vrhand[i].valid) {
407                                 exslot[i]->node.set_position(vrhand[i].pos);
408                                 exslot[i]->node.set_rotation(vrhand[i].rot * exslot[i]->grab_rot);
409
410                                 bool act_grab = goatvr_action(i, GOATVR_ACTION_GRAB) != 0;
411
412                                 ExSelection sel;
413                                 sel = exman->select(Sphere(vrhand[i].pos, 10));
414
415                                 if(!*exsel_grab[i]) {
416                                         // we don't have an exhibit grabbed
417                                         if(act_grab) {
418                                                 // grab an exhibit
419                                                 *exsel_grab[i] = sel;
420                                                 //SceneNode *objnode = sel.ex->node->find_object_node();
421                                                 //exslot[i]->rotation = normalize(sel.ex->node->get_rotation());
422                                                 exslot[i]->grab_rot = inverse(vrhand[i].rot);
423                                                 exslot[i]->attach_exhibit(sel.ex, EXSLOT_ATTACH_TRANSIENT);
424                                                 if(exsel_active) {
425                                                         exsel_active = ExSelection::null;       // cancel active on grab
426                                                 }
427                                         } else {
428                                                 // just hover
429                                                 exsel_hover = sel;
430                                         }
431                                 } else {
432                                         // we have an exhibit grabbed
433                                         if(!act_grab) {
434                                                 // drop it
435                                                 Exhibit *ex = exsel_grab[i]->ex;
436                                                 exslot[i]->detach_exhibit();
437
438                                                 ExhibitSlot *slot = exman->nearest_empty_slot(vrhand[i].pos, 100);
439                                                 if(!slot) {
440                                                         debug_log("no empty slot nearby\n");
441                                                         if(ex->prev_slot && ex->prev_slot->empty()) {
442                                                                 slot = ex->prev_slot;
443                                                                 debug_log("using previous slot\n");
444                                                         }
445                                                 }
446
447                                                 if(slot) {
448                                                         Quat rot = normalize(exslot[i]->node.get_rotation());
449                                                         ex->node->set_rotation(rot);
450                                                         slot->attach_exhibit(ex);
451                                                 } else {
452                                                         // nowhere to put it, stash it for later
453                                                         exman->stash_exhibit(ex);
454                                                         debug_log("no slots available, stashing\n");
455                                                 }
456
457                                                 *exsel_grab[i] = ExSelection::null;
458                                                 exslot[i]->grab_rot = Quat::identity;
459                                         }
460                                 }
461                         }
462                 }
463
464                 // if there are no grabs, and we're pointing with the right finger, override active
465                 if(!exsel_grab_left && !exsel_grab_right) {
466                         if(goatvr_action(1, GOATVR_ACTION_POINT)) {
467                                 Ray ray;
468                                 ray.origin = vrhand[1].pos;
469                                 ray.dir = rotate(Vec3(0, 0, -1), vrhand[1].rot);
470                                 exsel_active = exman->select(ray);
471                                 pointing = true;
472                         } else {
473                                 exsel_active = ExSelection::null;
474                                 pointing = false;
475                         }
476                 }
477
478         } else {
479                 // check if an exhibit is hovered-over by mouse (only if we don't have one grabbed)
480                 if(!exsel_grab_mouse) {
481                         Ray ray = calc_pick_ray(prev_mx, prev_my);
482                         exsel_hover = exman->select(ray);
483                 }
484
485                 // TODO do this properly
486                 // set the position of the left hand at a suitable position for the exhibit UI
487                 dir = rotate(Vec3(-0.46, -0.1, -1), Vec3(0, 1, 0), deg_to_rad(-avatar.body_rot));
488                 exslot_left.node.set_position(avatar.pos + dir * 30); // magic: distance in front
489                 Quat rot;
490                 rot.set_rotation(Vec3(0, 1, 0), deg_to_rad(-avatar.body_rot));
491                 exslot_left.node.set_rotation(rot);
492         }
493
494         if(!exslot_right.empty()) exslot_right.node.update(dt);
495         // always update the left slot, because it's the anchor point of the exhibit ui
496         exslot_left.node.update(dt);
497
498         // need to call this *after* we have updated the active exhibit (if any)
499         exui_update(dt);
500 }
501
502 void app_display()
503 {
504         float dt = (float)(time_msec - prev_msec) / 1000.0f;
505         prev_msec = time_msec;
506
507         if(debug_gui) {
508                 ImGui::GetIOPtr()->DeltaTime = dt;
509                 ImGui::NewFrame();
510
511                 //ImGui::ShowTestWindow();
512         }
513
514         glClearColor(1, 1, 1, 1);
515
516         if(opt.vr) {
517                 // VR mode
518                 goatvr_draw_start();
519                 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
520
521                 unsigned int gfbo = goatvr_get_fbo();
522
523                 update(dt);
524
525                 for(int i=0; i<2; i++) {
526                         // for each eye
527                         goatvr_draw_eye(i);
528                         if(gfbo) {
529                                 vp_width = goatvr_get_fb_eye_width(i);
530                                 vp_height = goatvr_get_fb_eye_height(i);
531
532                                 // this is a lightweight operation
533                                 goatvr_rtarg->create_wrap_fbo(gfbo, vp_width, vp_height);
534                                 push_render_target(goatvr_rtarg, RT_FAKE);
535                         } else {
536                                 vp_width = win_width / 2;
537                         }
538
539                         proj_matrix = goatvr_projection_matrix(i, NEAR_CLIP, FAR_CLIP);
540                         glMatrixMode(GL_PROJECTION);
541                         glLoadMatrixf(proj_matrix[0]);
542
543                         view_matrix = mouse_view_matrix * Mat4(goatvr_view_matrix(i));
544                         glMatrixMode(GL_MODELVIEW);
545                         glLoadMatrixf(view_matrix[0]);
546
547                         draw_scene();
548                         /*
549                         if(have_handtracking) {
550                                 draw_vrhands();
551                         }
552                         */
553
554                         if(debug_gui) {
555                                 ImGui::Render();
556                         }
557
558                         if(gfbo) {
559                                 pop_render_target(RT_FAKE);
560                         }
561                 }
562
563                 goatvr_draw_done();
564
565                 vp_width = win_width;
566                 vp_height = win_height;
567
568                 if(!gfbo && !fb_srgb && sdr_post_gamma) {
569                         glViewport(0, 0, win_width, win_height);
570                         slow_post(sdr_post_gamma);
571                         glUseProgram(0);
572                 }
573
574                 if(should_swap) {
575                         app_swap_buffers();
576                 }
577
578         } else {
579                 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
580
581                 update(dt);
582
583                 proj_matrix.perspective(deg_to_rad(50.0), win_aspect, NEAR_CLIP, FAR_CLIP);
584                 glMatrixMode(GL_PROJECTION);
585                 glLoadMatrixf(proj_matrix[0]);
586
587                 view_matrix = mouse_view_matrix;
588                 glMatrixMode(GL_MODELVIEW);
589                 glLoadMatrixf(view_matrix[0]);
590
591                 draw_scene();
592
593                 if(!fb_srgb && sdr_post_gamma) {
594                         slow_post(sdr_post_gamma);
595                         glUseProgram(0);
596                 }
597
598                 if(debug_gui) {
599                         ImGui::Render();
600                 }
601                 app_swap_buffers();
602         }
603         assert(glGetError() == GL_NO_ERROR);
604
605         calc_framerate();
606 }
607
608
609 static void draw_scene()
610 {
611         rend->draw();
612         exman->draw();
613
614         if(have_handtracking) {
615                 glUseProgram(0);
616                 glPushAttrib(GL_ENABLE_BIT);
617                 glDisable(GL_LIGHTING);
618                 glBegin(GL_LINES);
619                 for(int i=0; i<2; i++) {
620                         // skip drawing the left hand when we're showing the exhibit gui
621                         if(exsel_active && i == 0) continue;
622
623                         if(vrhand[i].valid) {
624                                 glColor3f(i, 1 - i, i);
625                         } else {
626                                 glColor3f(0.5, 0.5, 0.5);
627                         }
628                         Vec3 v = vrhand[i].pos;
629                         Vec3 dir = rotate(Vec3(0, 0, -1), vrhand[i].rot) * 10.0f;
630                         Vec3 up = rotate(Vec3(0, 1, 0), vrhand[i].rot) * 5.0f;
631                         Vec3 right = rotate(Vec3(1, 0, 0), vrhand[i].rot) * 5.0f;
632
633                         if(i == 1 && pointing) {
634                                 dir *= 1000.0f;
635                         }
636
637                         glVertex3f(v.x, v.y, v.z);
638                         glVertex3f(v.x + dir.x, v.y + dir.y, v.z + dir.z);
639                         glVertex3f(v.x - right.x, v.y - right.y, v.z - right.z);
640                         glVertex3f(v.x + right.x, v.y + right.y, v.z + right.z);
641                         glVertex3f(v.x - up.x, v.y - up.y, v.z - up.z);
642                         glVertex3f(v.x + up.x, v.y + up.y, v.z + up.z);
643                 }
644                 glEnd();
645                 glPopAttrib();
646         }
647
648         if(debug_gui && dbg_sel_node) {
649                 AABox bvol = dbg_sel_node->get_bounds();
650                 draw_geom_object(&bvol);
651         }
652
653         if(show_walk_mesh && mscn->walk_mesh) {
654                 glPushAttrib(GL_ENABLE_BIT);
655                 glEnable(GL_BLEND);
656                 glBlendFunc(GL_ONE, GL_ONE);
657                 glEnable(GL_POLYGON_OFFSET_FILL);
658
659                 glUseProgram(0);
660
661                 glPolygonOffset(-1, 1);
662                 glDepthMask(0);
663
664                 glColor3f(0.3, 0.08, 0.01);
665                 mscn->walk_mesh->draw();
666
667                 glDepthMask(1);
668
669                 glPopAttrib();
670         }
671
672         exui_draw();
673
674         print_text(Vec2(9 * win_width / 10, 20), Vec3(1, 1, 0), "fps: %.1f", framerate);
675         draw_ui();
676 }
677
678
679 void app_reshape(int x, int y)
680 {
681         glViewport(0, 0, x, y);
682         goatvr_set_fb_size(x, y, 1.0f);
683         debug_gui_reshape(x, y);
684
685         vp_width = x;
686         vp_height = y;
687 }
688
689 void app_keyboard(int key, bool pressed)
690 {
691         unsigned int mod = app_get_modifiers();
692
693         if(debug_gui && !(pressed && (key == '`' || key == 27))) {
694                 debug_gui_key(key, pressed, mod);
695                 return; // ignore all keystrokes when GUI is visible
696         }
697
698         if(pressed) {
699                 switch(key) {
700                 case 27:
701                         app_quit();
702                         break;
703
704                 case '\n':
705                 case '\r':
706                         if(mod & MOD_ALT) {
707                                 app_toggle_fullscreen();
708                         }
709                         break;
710
711                 case '`':
712                         debug_gui = !debug_gui;
713                         show_message("debug gui %s", debug_gui ? "enabled" : "disabled");
714                         break;
715
716                 case 'm':
717                         app_toggle_grab_mouse();
718                         show_message("mouse %s", app_is_mouse_grabbed() ? "grabbed" : "released");
719                         break;
720
721                 case 'w':
722                         if(mod & MOD_CTRL) {
723                                 show_walk_mesh = !show_walk_mesh;
724                                 show_message("walk mesh: %s", show_walk_mesh ? "on" : "off");
725                         }
726                         break;
727
728                 case 'c':
729                         if(mod & MOD_CTRL) {
730                                 noclip = !noclip;
731                                 show_message(noclip ? "no clip" : "clip");
732                         }
733                         break;
734
735                 case 'f':
736                         toggle_flight();
737                         break;
738
739                 case 'p':
740                         if(mod & MOD_CTRL) {
741                                 fb_srgb = !fb_srgb;
742                                 show_message("gamma correction for non-sRGB framebuffers: %s\n", fb_srgb ? "off" : "on");
743                         }
744                         break;
745
746                 case '=':
747                         walk_speed *= 1.25;
748                         show_message("walk speed: %g", walk_speed);
749                         break;
750
751                 case '-':
752                         walk_speed *= 0.75;
753                         show_message("walk speed: %g", walk_speed);
754                         break;
755
756                 case ']':
757                         mouse_speed *= 1.2;
758                         show_message("mouse speed: %g", mouse_speed);
759                         break;
760
761                 case '[':
762                         mouse_speed *= 0.8;
763                         show_message("mouse speed: %g", mouse_speed);
764                         break;
765
766                 case 'b':
767                         show_blobs = !show_blobs;
768                         show_message("blobs: %s\n", show_blobs ? "on" : "off");
769                         break;
770
771                 case ' ':
772                         goatvr_recenter();
773                         show_message("VR recenter\n");
774                         break;
775
776                 case KEY_UP:
777                         exui_scroll(-1);
778                         break;
779
780                 case KEY_DOWN:
781                         exui_scroll(1);
782                         break;
783
784                 case KEY_LEFT:
785                         exui_change_tab(-1);
786                         break;
787
788                 case KEY_RIGHT:
789                         exui_change_tab(1);
790                         break;
791
792                 case '\t':
793                         if(exsel_grab_mouse) {
794                                 Exhibit *ex = exsel_grab_mouse.ex;
795                                 exslot_mouse.detach_exhibit();
796                                 exman->stash_exhibit(ex);
797                                 exsel_grab_mouse = ExSelection::null;
798                         } else {
799                                 Exhibit *ex = exman->unstash_exhibit();
800                                 if(ex) {
801                                         exslot_mouse.attach_exhibit(ex, EXSLOT_ATTACH_TRANSIENT);
802                                         exsel_grab_mouse = ex;
803
804                                         Vec3 fwd = avatar.get_body_fwd();
805                                         exslot_mouse.node.set_position(avatar.pos + fwd * 100);
806                                 }
807                         }
808                         break;
809
810                 case KEY_F5:
811                 case KEY_F6:
812                 case KEY_F7:
813                 case KEY_F8:
814                         dbg_key_pending |= 1 << (key - KEY_F5);
815                         break;
816                 }
817         }
818
819         if(key < 256 && !(mod & (MOD_CTRL | MOD_ALT))) {
820                 keystate[key] = pressed;
821         }
822 }
823
824 void app_mouse_button(int bn, bool pressed, int x, int y)
825 {
826         static int press_x, press_y;
827
828         if(debug_gui) {
829                 debug_gui_mbutton(bn, pressed, x, y);
830                 return; // ignore mouse events while GUI is visible
831         }
832
833         prev_mx = x;
834         prev_my = y;
835         bnstate[bn] = pressed;
836
837         if(bn == 0) {
838                 ExSelection sel;
839                 Ray ray = calc_pick_ray(x, y);
840                 sel = exman->select(ray);
841
842                 if(pressed) {
843                         if(sel && (app_get_modifiers() & MOD_CTRL)) {
844                                 exsel_grab_mouse = sel;
845                                 Vec3 pos = sel.ex->node->get_position();
846                                 debug_log("grabbing... (%g %g %g)\n", pos.x, pos.y, pos.z);
847                                 exslot_mouse.node.set_position(pos);
848                                 exslot_mouse.node.set_rotation(sel.ex->node->get_rotation());
849                                 exslot_mouse.attach_exhibit(sel.ex, EXSLOT_ATTACH_TRANSIENT);
850                                 if(exsel_active) {
851                                         exsel_active = ExSelection::null;       // cancel active on grab
852                                 }
853                         }
854                         press_x = x;
855                         press_y = y;
856
857                 } else {
858                         if(exsel_grab_mouse) {
859                                 // cancel grab on mouse release
860                                 Exhibit *ex = exsel_grab_mouse.ex;
861                                 Vec3 pos = exslot_mouse.node.get_position();
862
863                                 debug_log("releasing at %g %g %g ...\n", pos.x, pos.y, pos.z);
864
865                                 exslot_mouse.detach_exhibit();
866
867                                 ExhibitSlot *slot = exman->nearest_empty_slot(pos, 300);
868                                 if(!slot) {
869                                         debug_log("no empty slot nearby\n");
870                                         if(ex->prev_slot && ex->prev_slot->empty()) {
871                                                 slot = ex->prev_slot;
872                                                 debug_log("using previous slot\n");
873                                         }
874                                 }
875
876                                 if(slot) {
877                                         slot->attach_exhibit(ex);
878                                 } else {
879                                         // nowhere to put it, so stash it for later
880                                         exslot_mouse.detach_exhibit();
881                                         exman->stash_exhibit(ex);
882                                         debug_log("no slots available, stashing\n");
883                                 }
884
885                                 exsel_grab_mouse = ExSelection::null;
886                         }
887
888                         if(abs(press_x - x) < 5 && abs(press_y - y) < 5) {
889                                 exsel_active = sel;     // select or deselect active exhibit
890                                 if(sel) {
891                                         debug_log("selecting...\n");
892                                 } else {
893                                         debug_log("deselecting...\n");
894                                 }
895                         }
896
897                         press_x = press_y = INT_MIN;
898                 }
899         }
900 }
901
902 static inline void mouse_look(float dx, float dy)
903 {
904         float scrsz = (float)win_height;
905         avatar.set_body_rotation(avatar.body_rot + dx * 512.0 / scrsz);
906         avatar.head_alt += dy * 512.0 / scrsz;
907
908         if(avatar.head_alt < -90) avatar.head_alt = -90;
909         if(avatar.head_alt > 90) avatar.head_alt = 90;
910 }
911
912 static void mouse_zoom(float dx, float dy)
913 {
914         cam_dist += dy * 0.1;
915         if(cam_dist < 0.0) cam_dist = 0.0;
916 }
917
918 void app_mouse_motion(int x, int y)
919 {
920         if(debug_gui) {
921                 debug_gui_mmotion(x, y);
922                 return; // ignore mouse events while GUI is visible
923         }
924
925         int dx = x - prev_mx;
926         int dy = y - prev_my;
927         prev_mx = x;
928         prev_my = y;
929
930         if(!dx && !dy) return;
931
932         if(exsel_grab_mouse) {
933                 Vec3 pos = exslot_mouse.node.get_node_position();
934                 Vec3 dir = transpose(view_matrix.upper3x3()) * Vec3(dx * 1.0, dy * -1.0, 0);
935
936                 exslot_mouse.node.set_position(pos + dir);
937         }
938
939         if(bnstate[2]) {
940                 mouse_look(dx, dy);
941         }
942 }
943
944 void app_mouse_delta(int dx, int dy)
945 {
946         if(bnstate[2]) {
947                 mouse_zoom(dx * mouse_speed, dy * mouse_speed);
948         } else {
949                 mouse_look(dx * mouse_speed, dy * mouse_speed);
950         }
951 }
952
953 void app_mouse_wheel(int dir)
954 {
955         if(debug_gui) {
956                 debug_gui_wheel(dir);
957         }
958 }
959
960 void app_gamepad_axis(int axis, float val)
961 {
962         switch(axis) {
963         case 0:
964                 joy_move.x = val;
965                 break;
966         case 1:
967                 joy_move.y = val;
968                 break;
969
970         case 2:
971                 joy_look.x = val;
972                 break;
973         case 3:
974                 joy_look.y = val;
975                 break;
976         }
977 }
978
979 void app_gamepad_button(int bn, bool pressed)
980 {
981         gpad_bnstate[bn] = pressed;
982
983         if(pressed) {
984                 switch(bn) {
985                 case GPAD_LSTICK:
986                         toggle_flight();
987                         break;
988
989                 case GPAD_X:
990                         show_blobs = !show_blobs;
991                         show_message("blobs: %s\n", show_blobs ? "on" : "off");
992                         break;
993
994                 case GPAD_START:
995                         goatvr_recenter();
996                         show_message("VR recenter\n");
997                         break;
998
999                 default:
1000                         break;
1001                 }
1002         }
1003 }
1004
1005 static void toggle_flight()
1006 {
1007         static float prev_walk_speed = -1.0;
1008         if(prev_walk_speed < 0.0) {
1009                 noclip = true;
1010                 prev_walk_speed = walk_speed;
1011                 walk_speed = 1000.0;
1012                 show_message("fly mode\n");
1013         } else {
1014                 noclip = false;
1015                 walk_speed = prev_walk_speed;
1016                 prev_walk_speed = -1.0;
1017                 show_message("walk mode\n");
1018         }
1019 }
1020
1021 static void calc_framerate()
1022 {
1023         //static int ncalc;
1024         static int nframes;
1025         static long prev_upd;
1026
1027         long elapsed = time_msec - prev_upd;
1028         if(elapsed >= 1000) {
1029                 framerate = (float)nframes / (float)(elapsed * 0.001);
1030                 nframes = 1;
1031                 prev_upd = time_msec;
1032
1033                 /*if(++ncalc >= 5) {
1034                         printf("fps: %f\n", framerate);
1035                         ncalc = 0;
1036                 }*/
1037         } else {
1038                 ++nframes;
1039         }
1040 }
1041
1042 static Ray calc_pick_ray(int x, int y)
1043 {
1044         float nx = (float)x / (float)win_width;
1045         float ny = (float)(win_height - y) / (float)win_height;
1046
1047         last_pick_ray = mouse_pick_ray(nx, ny, view_matrix, proj_matrix);
1048         return last_pick_ray;
1049 }