added minimum turn option, to discretize turns in VR
[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.1;
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
329                 if(opt.min_turn > 0.0f) {
330                         static long last_turn;
331                         if(len > 0.5 && time_msec - last_turn > 350) {
332                                 float sign = joy_look.x > 0.0f ? 1.0f : -1.0f;
333                                 avatar.body_rot += opt.min_turn * sign;
334                                 last_turn = time_msec;
335                         }
336                 } else {
337                         avatar.body_rot += mag * joy_look.x / len * 200.0 * dt;
338                 }
339
340                 avatar.head_alt += mag * joy_look.y / len * 100.0 * dt;
341                 if(avatar.head_alt < -90.0f) avatar.head_alt = -90.0f;
342                 if(avatar.head_alt > 90.0f) avatar.head_alt = 90.0f;
343         }
344
345         // keyboard move
346         if(keystate[(int)'w']) {
347                 dir.z -= speed;
348         }
349         if(keystate[(int)'s']) {
350                 dir.z += speed;
351         }
352         if(keystate[(int)'d']) {
353                 dir.x += speed;
354         }
355         if(keystate[(int)'a']) {
356                 dir.x -= speed;
357         }
358         if(keystate[(int)'q'] || gpad_bnstate[GPAD_UP]) {
359                 avatar.pos.y += speed;
360         }
361         if(keystate[(int)'z'] || gpad_bnstate[GPAD_DOWN]) {
362                 avatar.pos.y -= speed;
363         }
364
365         Vec3 walk_dir = avatar.calc_walk_dir(dir.z, dir.x);
366         Vec3 newpos = avatar.pos + walk_dir;
367
368         if(noclip) {
369                 avatar.pos = newpos;
370         } else {
371                 if(!constrain_walk_mesh(newpos, &avatar.pos)) {
372                         float dtheta = M_PI / 32.0;
373                         float theta = dtheta;
374                         Vec2 dir2d = newpos.xz() - avatar.pos.xz();
375
376                         for(int i=0; i<16; i++) {
377                                 Vec2 dvec = rotate(dir2d, theta);
378                                 Vec3 pos = avatar.pos + Vec3(dvec.x, 0, dvec.y);
379                                 if(constrain_walk_mesh(pos, &avatar.pos)) {
380                                         break;
381                                 }
382                                 dvec = rotate(dir2d, -theta);
383                                 pos = avatar.pos + Vec3(dvec.x, 0, dvec.y);
384                                 if(constrain_walk_mesh(pos, &avatar.pos)) {
385                                         break;
386                                 }
387                                 theta += dtheta;
388                         }
389                 }
390                 floor_y = avatar.pos.y - user_eye_height;
391         }
392
393         if(have_headtracking) {
394                 Quat qhead;
395                 goatvr_head_orientation(&qhead.x);
396                 avatar.tracked_head_rotation(qhead);
397         }
398
399         // TODO move to the avatar system
400         // calculate mouselook view matrix
401         mouse_view_matrix = Mat4::identity;
402         mouse_view_matrix.pre_translate(0, 0, -cam_dist);
403         if(!have_headtracking) {
404                 mouse_view_matrix.pre_rotate_x(deg_to_rad(avatar.head_alt));
405         }
406         mouse_view_matrix.pre_rotate_y(deg_to_rad(avatar.body_rot));
407         mouse_view_matrix.pre_translate(-avatar.pos.x, -avatar.pos.y, -avatar.pos.z);
408
409         // update hand-tracking
410         if(have_handtracking) {
411                 update_vrhands(&avatar);
412
413                 ExSelection *exsel_grab[] = { &exsel_grab_left, &exsel_grab_right };
414                 ExhibitSlot *exslot[] = { &exslot_left, &exslot_right };
415
416                 for(int i=0; i<2; i++) {
417                         if(vrhand[i].valid) {
418                                 exslot[i]->node.set_position(vrhand[i].pos);
419                                 exslot[i]->node.set_rotation(vrhand[i].rot * exslot[i]->grab_rot);
420
421                                 bool act_grab = goatvr_action(i, GOATVR_ACTION_GRAB) != 0;
422
423                                 ExSelection sel;
424                                 sel = exman->select(Sphere(vrhand[i].pos, 10));
425
426                                 if(!*exsel_grab[i]) {
427                                         // we don't have an exhibit grabbed
428                                         if(act_grab) {
429                                                 // grab an exhibit
430                                                 *exsel_grab[i] = sel;
431                                                 //SceneNode *objnode = sel.ex->node->find_object_node();
432                                                 //exslot[i]->rotation = normalize(sel.ex->node->get_rotation());
433                                                 exslot[i]->grab_rot = inverse(vrhand[i].rot);
434                                                 exslot[i]->attach_exhibit(sel.ex, EXSLOT_ATTACH_TRANSIENT);
435                                                 if(exsel_active) {
436                                                         exsel_active = ExSelection::null;       // cancel active on grab
437                                                 }
438                                         } else {
439                                                 // just hover
440                                                 exsel_hover = sel;
441                                         }
442                                 } else {
443                                         // we have an exhibit grabbed
444                                         if(!act_grab) {
445                                                 // drop it
446                                                 Exhibit *ex = exsel_grab[i]->ex;
447                                                 exslot[i]->detach_exhibit();
448
449                                                 ExhibitSlot *slot = exman->nearest_empty_slot(vrhand[i].pos, 100);
450                                                 if(!slot) {
451                                                         debug_log("no empty slot nearby\n");
452                                                         if(ex->prev_slot && ex->prev_slot->empty()) {
453                                                                 slot = ex->prev_slot;
454                                                                 debug_log("using previous slot\n");
455                                                         }
456                                                 }
457
458                                                 if(slot) {
459                                                         Quat rot = normalize(exslot[i]->node.get_rotation());
460                                                         ex->node->set_rotation(rot);
461                                                         slot->attach_exhibit(ex);
462                                                 } else {
463                                                         // nowhere to put it, stash it for later
464                                                         exman->stash_exhibit(ex);
465                                                         debug_log("no slots available, stashing\n");
466                                                 }
467
468                                                 *exsel_grab[i] = ExSelection::null;
469                                                 exslot[i]->grab_rot = Quat::identity;
470                                         }
471                                 }
472                         }
473                 }
474
475                 // if there are no grabs, and we're pointing with the right finger, override active
476                 if(!exsel_grab_left && !exsel_grab_right) {
477                         if(goatvr_action(1, GOATVR_ACTION_POINT)) {
478                                 Ray ray;
479                                 ray.origin = vrhand[1].pos;
480                                 ray.dir = rotate(Vec3(0, 0, -1), vrhand[1].rot);
481                                 exsel_active = exman->select(ray);
482                                 pointing = true;
483                         } else {
484                                 exsel_active = ExSelection::null;
485                                 pointing = false;
486                         }
487                 }
488
489         } else {
490                 // check if an exhibit is hovered-over by mouse (only if we don't have one grabbed)
491                 if(!exsel_grab_mouse) {
492                         Ray ray = calc_pick_ray(prev_mx, prev_my);
493                         exsel_hover = exman->select(ray);
494                 }
495
496                 // TODO do this properly
497                 // set the position of the left hand at a suitable position for the exhibit UI
498                 dir = rotate(Vec3(-0.46, -0.1, -1), Vec3(0, 1, 0), deg_to_rad(-avatar.body_rot));
499                 exslot_left.node.set_position(avatar.pos + dir * 30); // magic: distance in front
500                 Quat rot;
501                 rot.set_rotation(Vec3(0, 1, 0), deg_to_rad(-avatar.body_rot));
502                 exslot_left.node.set_rotation(rot);
503         }
504
505         if(!exslot_right.empty()) exslot_right.node.update(dt);
506         // always update the left slot, because it's the anchor point of the exhibit ui
507         exslot_left.node.update(dt);
508
509         // need to call this *after* we have updated the active exhibit (if any)
510         exui_update(dt);
511 }
512
513 void app_display()
514 {
515         float dt = (float)(time_msec - prev_msec) / 1000.0f;
516         prev_msec = time_msec;
517
518         if(debug_gui) {
519                 ImGui::GetIOPtr()->DeltaTime = dt;
520                 ImGui::NewFrame();
521
522                 //ImGui::ShowTestWindow();
523         }
524
525         glClearColor(1, 1, 1, 1);
526
527         if(opt.vr) {
528                 // VR mode
529                 goatvr_draw_start();
530                 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
531
532                 unsigned int gfbo = goatvr_get_fbo();
533
534                 update(dt);
535
536                 for(int i=0; i<2; i++) {
537                         // for each eye
538                         goatvr_draw_eye(i);
539                         if(gfbo) {
540                                 vp_width = goatvr_get_fb_eye_width(i);
541                                 vp_height = goatvr_get_fb_eye_height(i);
542
543                                 // this is a lightweight operation
544                                 goatvr_rtarg->create_wrap_fbo(gfbo, vp_width, vp_height);
545                                 push_render_target(goatvr_rtarg, RT_FAKE);
546                         } else {
547                                 vp_width = win_width / 2;
548                         }
549
550                         proj_matrix = goatvr_projection_matrix(i, NEAR_CLIP, FAR_CLIP);
551                         glMatrixMode(GL_PROJECTION);
552                         glLoadMatrixf(proj_matrix[0]);
553
554                         view_matrix = mouse_view_matrix * Mat4(goatvr_view_matrix(i));
555                         glMatrixMode(GL_MODELVIEW);
556                         glLoadMatrixf(view_matrix[0]);
557
558                         draw_scene();
559                         /*
560                         if(have_handtracking) {
561                                 draw_vrhands();
562                         }
563                         */
564
565                         if(debug_gui) {
566                                 ImGui::Render();
567                         }
568
569                         if(gfbo) {
570                                 pop_render_target(RT_FAKE);
571                         }
572                 }
573
574                 goatvr_draw_done();
575
576                 vp_width = win_width;
577                 vp_height = win_height;
578
579                 if(!gfbo && !fb_srgb && sdr_post_gamma) {
580                         glViewport(0, 0, win_width, win_height);
581                         slow_post(sdr_post_gamma);
582                         glUseProgram(0);
583                 }
584
585                 if(should_swap) {
586                         app_swap_buffers();
587                 }
588
589         } else {
590                 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
591
592                 update(dt);
593
594                 proj_matrix.perspective(deg_to_rad(50.0), win_aspect, NEAR_CLIP, FAR_CLIP);
595                 glMatrixMode(GL_PROJECTION);
596                 glLoadMatrixf(proj_matrix[0]);
597
598                 view_matrix = mouse_view_matrix;
599                 glMatrixMode(GL_MODELVIEW);
600                 glLoadMatrixf(view_matrix[0]);
601
602                 draw_scene();
603
604                 if(!fb_srgb && sdr_post_gamma) {
605                         slow_post(sdr_post_gamma);
606                         glUseProgram(0);
607                 }
608
609                 if(debug_gui) {
610                         ImGui::Render();
611                 }
612                 app_swap_buffers();
613         }
614         assert(glGetError() == GL_NO_ERROR);
615
616         calc_framerate();
617 }
618
619
620 static void draw_scene()
621 {
622         rend->draw();
623         exman->draw();
624
625         if(have_handtracking) {
626                 glUseProgram(0);
627                 glPushAttrib(GL_ENABLE_BIT);
628                 glDisable(GL_LIGHTING);
629                 glBegin(GL_LINES);
630                 for(int i=0; i<2; i++) {
631                         // skip drawing the left hand when we're showing the exhibit gui
632                         if(exsel_active && i == 0) continue;
633
634                         if(vrhand[i].valid) {
635                                 glColor3f(i, 1 - i, i);
636                         } else {
637                                 glColor3f(0.5, 0.5, 0.5);
638                         }
639                         Vec3 v = vrhand[i].pos;
640                         Vec3 dir = rotate(Vec3(0, 0, -1), vrhand[i].rot) * 10.0f;
641                         Vec3 up = rotate(Vec3(0, 1, 0), vrhand[i].rot) * 5.0f;
642                         Vec3 right = rotate(Vec3(1, 0, 0), vrhand[i].rot) * 5.0f;
643
644                         if(i == 1 && pointing) {
645                                 dir *= 1000.0f;
646                         }
647
648                         glVertex3f(v.x, v.y, v.z);
649                         glVertex3f(v.x + dir.x, v.y + dir.y, v.z + dir.z);
650                         glVertex3f(v.x - right.x, v.y - right.y, v.z - right.z);
651                         glVertex3f(v.x + right.x, v.y + right.y, v.z + right.z);
652                         glVertex3f(v.x - up.x, v.y - up.y, v.z - up.z);
653                         glVertex3f(v.x + up.x, v.y + up.y, v.z + up.z);
654                 }
655                 glEnd();
656                 glPopAttrib();
657         }
658
659         if(debug_gui && dbg_sel_node) {
660                 AABox bvol = dbg_sel_node->get_bounds();
661                 draw_geom_object(&bvol);
662         }
663
664         if(show_walk_mesh && mscn->walk_mesh) {
665                 glPushAttrib(GL_ENABLE_BIT);
666                 glEnable(GL_BLEND);
667                 glBlendFunc(GL_ONE, GL_ONE);
668                 glEnable(GL_POLYGON_OFFSET_FILL);
669
670                 glUseProgram(0);
671
672                 glPolygonOffset(-1, 1);
673                 glDepthMask(0);
674
675                 glColor3f(0.3, 0.08, 0.01);
676                 mscn->walk_mesh->draw();
677
678                 glDepthMask(1);
679
680                 glPopAttrib();
681         }
682
683         exui_draw();
684
685         print_text(Vec2(9 * win_width / 10, 20), Vec3(1, 1, 0), "fps: %.1f", framerate);
686         draw_ui();
687 }
688
689
690 void app_reshape(int x, int y)
691 {
692         glViewport(0, 0, x, y);
693         goatvr_set_fb_size(x, y, 1.0f);
694         debug_gui_reshape(x, y);
695
696         vp_width = x;
697         vp_height = y;
698 }
699
700 void app_keyboard(int key, bool pressed)
701 {
702         unsigned int mod = app_get_modifiers();
703
704         if(debug_gui && !(pressed && (key == '`' || key == 27))) {
705                 debug_gui_key(key, pressed, mod);
706                 return; // ignore all keystrokes when GUI is visible
707         }
708
709         if(pressed) {
710                 switch(key) {
711                 case 27:
712                         app_quit();
713                         break;
714
715                 case '\n':
716                 case '\r':
717                         if(mod & MOD_ALT) {
718                                 app_toggle_fullscreen();
719                         }
720                         break;
721
722                 case '`':
723                         debug_gui = !debug_gui;
724                         show_message("debug gui %s", debug_gui ? "enabled" : "disabled");
725                         break;
726
727                 case 'm':
728                         app_toggle_grab_mouse();
729                         show_message("mouse %s", app_is_mouse_grabbed() ? "grabbed" : "released");
730                         break;
731
732                 case 'w':
733                         if(mod & MOD_CTRL) {
734                                 show_walk_mesh = !show_walk_mesh;
735                                 show_message("walk mesh: %s", show_walk_mesh ? "on" : "off");
736                         }
737                         break;
738
739                 case 'c':
740                         if(mod & MOD_CTRL) {
741                                 noclip = !noclip;
742                                 show_message(noclip ? "no clip" : "clip");
743                         }
744                         break;
745
746                 case 'f':
747                         toggle_flight();
748                         break;
749
750                 case 'p':
751                         if(mod & MOD_CTRL) {
752                                 fb_srgb = !fb_srgb;
753                                 show_message("gamma correction for non-sRGB framebuffers: %s\n", fb_srgb ? "off" : "on");
754                         }
755                         break;
756
757                 case '=':
758                         walk_speed *= 1.25;
759                         show_message("walk speed: %g", walk_speed);
760                         break;
761
762                 case '-':
763                         walk_speed *= 0.75;
764                         show_message("walk speed: %g", walk_speed);
765                         break;
766
767                 case ']':
768                         mouse_speed *= 1.2;
769                         show_message("mouse speed: %g", mouse_speed);
770                         break;
771
772                 case '[':
773                         mouse_speed *= 0.8;
774                         show_message("mouse speed: %g", mouse_speed);
775                         break;
776
777                 case 'b':
778                         show_blobs = !show_blobs;
779                         show_message("blobs: %s\n", show_blobs ? "on" : "off");
780                         break;
781
782                 case ' ':
783                         goatvr_recenter();
784                         show_message("VR recenter\n");
785                         break;
786
787                 case KEY_UP:
788                         exui_scroll(-1);
789                         break;
790
791                 case KEY_DOWN:
792                         exui_scroll(1);
793                         break;
794
795                 case KEY_LEFT:
796                         exui_change_tab(-1);
797                         break;
798
799                 case KEY_RIGHT:
800                         exui_change_tab(1);
801                         break;
802
803                 case '\t':
804                         if(exsel_grab_mouse) {
805                                 Exhibit *ex = exsel_grab_mouse.ex;
806                                 exslot_mouse.detach_exhibit();
807                                 exman->stash_exhibit(ex);
808                                 exsel_grab_mouse = ExSelection::null;
809                         } else {
810                                 Exhibit *ex = exman->unstash_exhibit();
811                                 if(ex) {
812                                         exslot_mouse.attach_exhibit(ex, EXSLOT_ATTACH_TRANSIENT);
813                                         exsel_grab_mouse = ex;
814
815                                         Vec3 fwd = avatar.get_body_fwd();
816                                         exslot_mouse.node.set_position(avatar.pos + fwd * 100);
817                                 }
818                         }
819                         break;
820
821                 case KEY_F5:
822                 case KEY_F6:
823                 case KEY_F7:
824                 case KEY_F8:
825                         dbg_key_pending |= 1 << (key - KEY_F5);
826                         break;
827                 }
828         }
829
830         if(key < 256 && !(mod & (MOD_CTRL | MOD_ALT))) {
831                 keystate[key] = pressed;
832         }
833 }
834
835 void app_mouse_button(int bn, bool pressed, int x, int y)
836 {
837         static int press_x, press_y;
838
839         if(debug_gui) {
840                 debug_gui_mbutton(bn, pressed, x, y);
841                 return; // ignore mouse events while GUI is visible
842         }
843
844         prev_mx = x;
845         prev_my = y;
846         bnstate[bn] = pressed;
847
848         if(bn == 0) {
849                 ExSelection sel;
850                 Ray ray = calc_pick_ray(x, y);
851                 sel = exman->select(ray);
852
853                 if(pressed) {
854                         if(sel && (app_get_modifiers() & MOD_CTRL)) {
855                                 exsel_grab_mouse = sel;
856                                 Vec3 pos = sel.ex->node->get_position();
857                                 debug_log("grabbing... (%g %g %g)\n", pos.x, pos.y, pos.z);
858                                 exslot_mouse.node.set_position(pos);
859                                 exslot_mouse.node.set_rotation(sel.ex->node->get_rotation());
860                                 exslot_mouse.attach_exhibit(sel.ex, EXSLOT_ATTACH_TRANSIENT);
861                                 if(exsel_active) {
862                                         exsel_active = ExSelection::null;       // cancel active on grab
863                                 }
864                         }
865                         press_x = x;
866                         press_y = y;
867
868                 } else {
869                         if(exsel_grab_mouse) {
870                                 // cancel grab on mouse release
871                                 Exhibit *ex = exsel_grab_mouse.ex;
872                                 Vec3 pos = exslot_mouse.node.get_position();
873
874                                 debug_log("releasing at %g %g %g ...\n", pos.x, pos.y, pos.z);
875
876                                 exslot_mouse.detach_exhibit();
877
878                                 ExhibitSlot *slot = exman->nearest_empty_slot(pos, 300);
879                                 if(!slot) {
880                                         debug_log("no empty slot nearby\n");
881                                         if(ex->prev_slot && ex->prev_slot->empty()) {
882                                                 slot = ex->prev_slot;
883                                                 debug_log("using previous slot\n");
884                                         }
885                                 }
886
887                                 if(slot) {
888                                         slot->attach_exhibit(ex);
889                                 } else {
890                                         // nowhere to put it, so stash it for later
891                                         exslot_mouse.detach_exhibit();
892                                         exman->stash_exhibit(ex);
893                                         debug_log("no slots available, stashing\n");
894                                 }
895
896                                 exsel_grab_mouse = ExSelection::null;
897                         }
898
899                         if(abs(press_x - x) < 5 && abs(press_y - y) < 5) {
900                                 exsel_active = sel;     // select or deselect active exhibit
901                                 if(sel) {
902                                         debug_log("selecting...\n");
903                                 } else {
904                                         debug_log("deselecting...\n");
905                                 }
906                         }
907
908                         press_x = press_y = INT_MIN;
909                 }
910         }
911 }
912
913 static inline void mouse_look(float dx, float dy)
914 {
915         float scrsz = (float)win_height;
916         avatar.set_body_rotation(avatar.body_rot + dx * 512.0 / scrsz);
917         avatar.head_alt += dy * 512.0 / scrsz;
918
919         if(avatar.head_alt < -90) avatar.head_alt = -90;
920         if(avatar.head_alt > 90) avatar.head_alt = 90;
921 }
922
923 static void mouse_zoom(float dx, float dy)
924 {
925         cam_dist += dy * 0.1;
926         if(cam_dist < 0.0) cam_dist = 0.0;
927 }
928
929 void app_mouse_motion(int x, int y)
930 {
931         if(debug_gui) {
932                 debug_gui_mmotion(x, y);
933                 return; // ignore mouse events while GUI is visible
934         }
935
936         int dx = x - prev_mx;
937         int dy = y - prev_my;
938         prev_mx = x;
939         prev_my = y;
940
941         if(!dx && !dy) return;
942
943         if(exsel_grab_mouse) {
944                 Vec3 pos = exslot_mouse.node.get_node_position();
945                 Vec3 dir = transpose(view_matrix.upper3x3()) * Vec3(dx * 1.0, dy * -1.0, 0);
946
947                 exslot_mouse.node.set_position(pos + dir);
948         }
949
950         if(bnstate[2]) {
951                 mouse_look(dx, dy);
952         }
953 }
954
955 void app_mouse_delta(int dx, int dy)
956 {
957         if(bnstate[2]) {
958                 mouse_zoom(dx * mouse_speed, dy * mouse_speed);
959         } else {
960                 mouse_look(dx * mouse_speed, dy * mouse_speed);
961         }
962 }
963
964 void app_mouse_wheel(int dir)
965 {
966         if(debug_gui) {
967                 debug_gui_wheel(dir);
968         }
969 }
970
971 void app_gamepad_axis(int axis, float val)
972 {
973         switch(axis) {
974         case 0:
975                 joy_move.x = val;
976                 break;
977         case 1:
978                 joy_move.y = val;
979                 break;
980
981         case 2:
982                 joy_look.x = val;
983                 break;
984         case 3:
985                 joy_look.y = val;
986                 break;
987         }
988 }
989
990 void app_gamepad_button(int bn, bool pressed)
991 {
992         gpad_bnstate[bn] = pressed;
993
994         if(pressed) {
995                 switch(bn) {
996                 case GPAD_LSTICK:
997                         toggle_flight();
998                         break;
999
1000                 case GPAD_X:
1001                         show_blobs = !show_blobs;
1002                         show_message("blobs: %s\n", show_blobs ? "on" : "off");
1003                         break;
1004
1005                 case GPAD_START:
1006                         goatvr_recenter();
1007                         show_message("VR recenter\n");
1008                         break;
1009
1010                 default:
1011                         break;
1012                 }
1013         }
1014 }
1015
1016 static void toggle_flight()
1017 {
1018         static float prev_walk_speed = -1.0;
1019         if(prev_walk_speed < 0.0) {
1020                 noclip = true;
1021                 prev_walk_speed = walk_speed;
1022                 walk_speed = 1000.0;
1023                 show_message("fly mode\n");
1024         } else {
1025                 noclip = false;
1026                 walk_speed = prev_walk_speed;
1027                 prev_walk_speed = -1.0;
1028                 show_message("walk mode\n");
1029         }
1030 }
1031
1032 static void calc_framerate()
1033 {
1034         //static int ncalc;
1035         static int nframes;
1036         static long prev_upd;
1037
1038         long elapsed = time_msec - prev_upd;
1039         if(elapsed >= 1000) {
1040                 framerate = (float)nframes / (float)(elapsed * 0.001);
1041                 nframes = 1;
1042                 prev_upd = time_msec;
1043
1044                 /*if(++ncalc >= 5) {
1045                         printf("fps: %f\n", framerate);
1046                         ncalc = 0;
1047                 }*/
1048         } else {
1049                 ++nframes;
1050         }
1051 }
1052
1053 static Ray calc_pick_ray(int x, int y)
1054 {
1055         float nx = (float)x / (float)win_width;
1056         float ny = (float)(win_height - y) / (float)win_height;
1057
1058         last_pick_ray = mouse_pick_ray(nx, ny, view_matrix, proj_matrix);
1059         return last_pick_ray;
1060 }