8aaaee301ddcb35dae1625055a33b62e6f4cda7b
[laserbrain_demo] / src / app.cc
1 #include <stdio.h>
2 #include <limits.h>
3 #include <assert.h>
4 #include <goatvr.h>
5 #include "app.h"
6 #include "opengl.h"
7 #include "sdr.h"
8 #include "texture.h"
9 #include "mesh.h"
10 #include "meshgen.h"
11 #include "scene.h"
12 #include "metascene.h"
13 #include "datamap.h"
14 #include "ui.h"
15 #include "opt.h"
16 #include "post.h"
17 #include "renderer.h"
18 #include "avatar.h"
19 #include "vrinput.h"
20 #include "exman.h"
21 #include "blob_exhibit.h"
22 #include "dbg_gui.h"
23 #include "geomdraw.h"
24
25 #define NEAR_CLIP       5.0
26 #define FAR_CLIP        10000.0
27
28 static void draw_scene();
29 static void toggle_flight();
30 static void calc_framerate();
31 static Ray calc_pick_ray(int x, int y);
32
33 long time_msec;
34 int win_width, win_height;
35 float win_aspect;
36 bool fb_srgb;
37 bool opt_gear_wireframe;
38
39 TextureSet texman;
40 SceneSet sceneman;
41
42 unsigned int sdr_ltmap, sdr_ltmap_notex;
43
44 int fpexcept_enabled;
45
46 static Avatar avatar;
47
48 static float cam_dist = 0.0;
49 static float floor_y;   // last floor height
50 static float user_eye_height = 165;
51
52 static float walk_speed = 300.0f;
53 static float mouse_speed = 0.5f;
54 static bool show_walk_mesh, noclip = false;
55
56 static bool have_headtracking, have_handtracking, should_swap;
57
58 static int prev_mx, prev_my;
59 static bool bnstate[8];
60 static bool keystate[256];
61 static bool gpad_bnstate[64];
62 static Vec2 joy_move, joy_look;
63 static float joy_deadzone = 0.01;
64
65 static float framerate;
66
67 static Mat4 view_matrix, mouse_view_matrix, proj_matrix;
68 static MetaScene *mscn;
69 static unsigned int sdr_post_gamma;
70
71 static long prev_msec;
72
73 static ExhibitManager *exman;
74 static BlobExhibit *blobs;
75 static bool show_blobs;
76
77 ExSelection exsel_active, exsel_hover;
78 ExSelection exsel_grab_left, exsel_grab_right;
79 #define exsel_grab_mouse exsel_grab_right
80 static ExhibitSlot exslot_left, exslot_right;
81 #define exslot_mouse exslot_right
82
83 static Renderer *rend;
84
85 static Ray last_pick_ray;
86
87
88 bool app_init(int argc, char **argv)
89 {
90         set_log_file("demo.log");
91
92         char *env = getenv("FPEXCEPT");
93         if(env && atoi(env)) {
94                 info_log("enabling floating point exceptions\n");
95                 fpexcept_enabled = 1;
96                 enable_fpexcept();
97         }
98
99         if(init_opengl() == -1) {
100                 return false;
101         }
102
103         if(!init_options(argc, argv, "demo.conf")) {
104                 return false;
105         }
106         app_resize(opt.width, opt.height);
107         app_fullscreen(opt.fullscreen);
108
109         if(opt.vr) {
110                 if(goatvr_init() == -1) {
111                         return false;
112                 }
113                 goatvr_set_origin_mode(GOATVR_HEAD);
114                 goatvr_set_units_scale(100.0f);
115
116                 goatvr_startvr();
117                 should_swap = goatvr_should_swap() != 0;
118                 user_eye_height = goatvr_get_eye_height();
119                 have_headtracking = goatvr_have_headtracking();
120                 have_handtracking = goatvr_have_handtracking();
121
122                 goatvr_recenter();
123         }
124
125         if(fb_srgb) {
126                 int srgb_capable;
127                 glGetIntegerv(GL_FRAMEBUFFER_SRGB_CAPABLE_EXT, &srgb_capable);
128                 printf("Framebuffer %s sRGB-capable\n", srgb_capable ? "is" : "is not");
129                 if(srgb_capable) {
130                         glEnable(GL_FRAMEBUFFER_SRGB);
131                 } else {
132                         fb_srgb = 0;
133                 }
134         }
135
136         glEnable(GL_MULTISAMPLE);
137         glEnable(GL_DEPTH_TEST);
138         glEnable(GL_CULL_FACE);
139         glEnable(GL_NORMALIZE);
140
141         if(!init_debug_gui()) {
142                 return false;
143         }
144
145         Mesh::use_custom_sdr_attr = false;
146
147         float ambient[] = {0.0, 0.0, 0.0, 0.0};
148         glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient);
149
150         glClearColor(1, 1, 1, 1);
151
152         init_audio();
153
154         if(!init_vrhands()) {
155                 return false;
156         }
157
158         mscn = new MetaScene;
159         if(!mscn->load(opt.scenefile ? opt.scenefile : "data/museum.scene")) {
160                 return false;
161         }
162
163         avatar.pos = mscn->start_pos;
164         Vec3 dir = rotate(Vec3(0, 0, 1), mscn->start_rot);
165         dir.y = 0;
166         avatar.body_rot = rad_to_deg(acos(dot(dir, Vec3(0, 0, 1))));
167
168         exman = new ExhibitManager;
169         /*
170         if(!exman->load(mscn, "data/exhibits")) {
171                 //return false;
172         }
173         */
174
175         blobs = new BlobExhibit;
176         blobs->node = new SceneNode;
177         blobs->init();
178         blobs->node->set_position(Vec3(-680, 160, -100));
179         blobs->node->set_scaling(Vec3(28, 28, 28));
180         blobs->node->update(0);
181
182         exman->add(blobs);
183
184         if(!(sdr_ltmap_notex = create_program_load("sdr/lightmap.v.glsl", "sdr/lightmap-notex.p.glsl"))) {
185                 return false;
186         }
187         set_uniform_int(sdr_ltmap_notex, "texmap", MTL_TEX_DIFFUSE);
188         set_uniform_int(sdr_ltmap_notex, "lightmap", MTL_TEX_LIGHTMAP);
189
190         if(!(sdr_ltmap = create_program_load("sdr/lightmap.v.glsl", "sdr/lightmap-tex.p.glsl"))) {
191                 return false;
192         }
193         set_uniform_int(sdr_ltmap, "texmap", MTL_TEX_DIFFUSE);
194         set_uniform_int(sdr_ltmap, "lightmap", MTL_TEX_LIGHTMAP);
195
196         if(!fb_srgb) {
197                 sdr_post_gamma = create_program_load("sdr/post_gamma.v.glsl", "sdr/post_gamma.p.glsl");
198         }
199
200         rend = new Renderer;
201         rend->set_scene(mscn);
202
203         glUseProgram(0);
204
205         if(opt.vr || opt.fullscreen) {
206                 app_grab_mouse(true);
207         }
208
209         if(mscn->music && opt.music) {
210                 mscn->music->play(AUDIO_PLAYMODE_LOOP);
211         }
212         return true;
213 }
214
215 void app_cleanup()
216 {
217         if(mscn->music) {
218                 mscn->music->stop();
219         }
220         destroy_audio();
221
222         app_grab_mouse(false);
223         if(opt.vr) {
224                 goatvr_shutdown();
225         }
226         destroy_vrhands();
227
228         delete rend;
229
230         /* this must be destroyed before the scene graph to detach exhibit nodes
231          * before the scene tries to delete them recursively
232          */
233         delete exman;
234
235         texman.clear();
236         sceneman.clear();
237
238         cleanup_debug_gui();
239 }
240
241 static bool constrain_walk_mesh(const Vec3 &v, Vec3 *newv)
242 {
243         Mesh *wm = mscn->walk_mesh;
244         if(!wm) {
245                 *newv = v;
246                 return true;
247         }
248
249         Ray downray = Ray(v, Vec3(0, -1, 0));
250         HitPoint hit;
251         if(mscn->walk_mesh->intersect(downray, &hit)) {
252                 *newv = hit.pos;
253                 newv->y += user_eye_height;
254                 return true;
255         }
256         return false;
257 }
258
259 static void update(float dt)
260 {
261         texman.update();
262         sceneman.update();
263
264         mscn->update(dt);
265         exman->update(dt);
266
267         float speed = walk_speed * dt;
268         Vec3 dir;
269
270         // joystick
271         float jdeadsq = joy_deadzone * joy_deadzone;
272         float jmove_lensq = length_sq(joy_move);
273         float jlook_lensq = length_sq(joy_look);
274
275         if(jmove_lensq > jdeadsq) {
276                 float len = sqrt(jmove_lensq);
277                 jmove_lensq -= jdeadsq;
278
279                 float mag = len * len;
280                 dir.x += mag * joy_move.x / len * 2.0 * speed;
281                 dir.z += mag * joy_move.y / len * 2.0 * speed;
282         }
283         if(jlook_lensq > jdeadsq) {
284                 float len = sqrt(jlook_lensq);
285                 jlook_lensq -= jdeadsq;
286
287                 float mag = len * len;
288                 avatar.body_rot += mag * joy_look.x / len * 200.0 * dt;
289                 avatar.head_alt += mag * joy_look.y / len * 100.0 * dt;
290                 if(avatar.head_alt < -90.0f) avatar.head_alt = -90.0f;
291                 if(avatar.head_alt > 90.0f) avatar.head_alt = 90.0f;
292         }
293
294         // keyboard move
295         if(keystate[(int)'w']) {
296                 dir.z -= speed;
297         }
298         if(keystate[(int)'s']) {
299                 dir.z += speed;
300         }
301         if(keystate[(int)'d']) {
302                 dir.x += speed;
303         }
304         if(keystate[(int)'a']) {
305                 dir.x -= speed;
306         }
307         if(keystate[(int)'q'] || gpad_bnstate[GPAD_UP]) {
308                 avatar.pos.y += speed;
309         }
310         if(keystate[(int)'z'] || gpad_bnstate[GPAD_DOWN]) {
311                 avatar.pos.y -= speed;
312         }
313
314         float theta = M_PI * avatar.body_rot / 180.0f;
315         Vec3 newpos;
316         newpos.x = avatar.pos.x + cos(theta) * dir.x - sin(theta) * dir.z;
317         newpos.y = avatar.pos.y;
318         newpos.z = avatar.pos.z + sin(theta) * dir.x + cos(theta) * dir.z;
319
320         if(noclip) {
321                 avatar.pos = newpos;
322         } else {
323                 if(!constrain_walk_mesh(newpos, &avatar.pos)) {
324                         float dtheta = M_PI / 32.0;
325                         float theta = dtheta;
326                         Vec2 dir2d = newpos.xz() - avatar.pos.xz();
327
328                         for(int i=0; i<16; i++) {
329                                 Vec2 dvec = rotate(dir2d, theta);
330                                 Vec3 pos = avatar.pos + Vec3(dvec.x, 0, dvec.y);
331                                 if(constrain_walk_mesh(pos, &avatar.pos)) {
332                                         break;
333                                 }
334                                 dvec = rotate(dir2d, -theta);
335                                 pos = avatar.pos + Vec3(dvec.x, 0, dvec.y);
336                                 if(constrain_walk_mesh(pos, &avatar.pos)) {
337                                         break;
338                                 }
339                                 theta += dtheta;
340                         }
341                 }
342                 floor_y = avatar.pos.y - user_eye_height;
343         }
344
345         // TODO move to the avatar system
346         // calculate mouselook view matrix
347         mouse_view_matrix = Mat4::identity;
348         mouse_view_matrix.pre_translate(0, 0, -cam_dist);
349         if(!have_headtracking) {
350                 mouse_view_matrix.pre_rotate_x(deg_to_rad(avatar.head_alt));
351         }
352         mouse_view_matrix.pre_rotate_y(deg_to_rad(avatar.body_rot));
353         mouse_view_matrix.pre_translate(-avatar.pos.x, -avatar.pos.y, -avatar.pos.z);
354
355         // check if an exhibit is hovered-over by mouse or 6dof (only if we don't have one grabbed)
356         if(!exsel_grab_mouse) {
357                 // XXX note: using previous view/proj matrix lattency shouldn't be an issue but
358                 //           make sure state-creep doesn't get us
359                 // XXX also this mouse-picking probably should only be active in non-VR mode
360                 Ray ray = calc_pick_ray(prev_mx, prev_my);
361                 exsel_hover = exman->select(ray);
362         }
363
364         if(!exslot_left.empty()) exslot_left.node.update(dt);
365         if(!exslot_right.empty()) exslot_right.node.update(dt);
366
367         // update hand-tracking
368         if(have_handtracking) {
369                 update_vrhands(&avatar);
370         }
371 }
372
373 void app_display()
374 {
375         float dt = (float)(time_msec - prev_msec) / 1000.0f;
376         prev_msec = time_msec;
377
378         if(debug_gui) {
379                 ImGui::GetIOPtr()->DeltaTime = dt;
380                 ImGui::NewFrame();
381
382                 ImGui::ShowTestWindow();
383         }
384
385         if(opt.vr) {
386                 // VR mode
387                 goatvr_draw_start();
388                 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
389
390                 update(dt);
391
392                 for(int i=0; i<2; i++) {
393                         // for each eye
394                         goatvr_draw_eye(i);
395
396                         proj_matrix = goatvr_projection_matrix(i, NEAR_CLIP, FAR_CLIP);
397                         glMatrixMode(GL_PROJECTION);
398                         glLoadMatrixf(proj_matrix[0]);
399
400                         view_matrix = mouse_view_matrix * Mat4(goatvr_view_matrix(i));
401                         glMatrixMode(GL_MODELVIEW);
402                         glLoadMatrixf(view_matrix[0]);
403
404                         draw_scene();
405                         if(have_handtracking) {
406                                 draw_vrhands();
407                         }
408
409                         if(debug_gui) {
410                                 ImGui::Render();
411                         }
412                 }
413                 goatvr_draw_done();
414
415                 if(should_swap) {
416                         app_swap_buffers();
417                 }
418
419         } else {
420                 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
421
422                 update(dt);
423
424                 proj_matrix.perspective(deg_to_rad(50.0), win_aspect, NEAR_CLIP, FAR_CLIP);
425                 glMatrixMode(GL_PROJECTION);
426                 glLoadMatrixf(proj_matrix[0]);
427
428                 view_matrix = mouse_view_matrix;
429                 glMatrixMode(GL_MODELVIEW);
430                 glLoadMatrixf(view_matrix[0]);
431
432                 draw_scene();
433
434                 if(!fb_srgb && sdr_post_gamma) {
435                         slow_post(sdr_post_gamma);
436                         glUseProgram(0);
437                 }
438
439                 if(debug_gui) {
440                         ImGui::Render();
441                 }
442                 app_swap_buffers();
443         }
444         assert(glGetError() == GL_NO_ERROR);
445
446         calc_framerate();
447 }
448
449
450 static void draw_scene()
451 {
452         rend->draw();
453         exman->draw();
454
455         /*
456         if(have_handtracking) {
457                 Mat4 head_xform = inverse(mouse_view_matrix);//goatvr_head_matrix();
458                 Mat4 head_dir_xform = head_xform.upper3x3();
459
460                 glUseProgram(0);
461                 glPushAttrib(GL_ENABLE_BIT);
462                 glDisable(GL_LIGHTING);
463                 glBegin(GL_LINES);
464                 for(int i=0; i<2; i++) {
465                         if(hand[i].valid) {
466                                 glColor3f(i, 1 - i, i);
467                         } else {
468                                 glColor3f(0.5, 0.5, 0.5);
469                         }
470                         Vec3 v = head_xform * hand[i].pos;
471                         Vec3 dir = head_dir_xform * rotate(Vec3(0, 0, -1), hand[i].rot) * 20.0f;
472                         Vec3 up = head_dir_xform * rotate(Vec3(0, 1, 0), hand[i].rot) * 10.0f;
473                         Vec3 right = head_dir_xform * rotate(Vec3(1, 0, 0), hand[i].rot) * 10.0f;
474
475                         glVertex3f(v.x, v.y, v.z);
476                         glVertex3f(v.x + dir.x, v.y + dir.y, v.z + dir.z);
477                         glVertex3f(v.x - right.x, v.y - right.y, v.z - right.z);
478                         glVertex3f(v.x + right.x, v.y + right.y, v.z + right.z);
479                         glVertex3f(v.x - up.x, v.y - up.y, v.z - up.z);
480                         glVertex3f(v.x + up.x, v.y + up.y, v.z + up.z);
481                 }
482                 glEnd();
483                 glPopAttrib();
484         }
485         */
486
487         if(debug_gui && dbg_sel_node) {
488                 AABox bvol = dbg_sel_node->get_bounds();
489                 draw_geom_object(&bvol);
490         }
491
492         if(show_walk_mesh && mscn->walk_mesh) {
493                 glPushAttrib(GL_ENABLE_BIT);
494                 glEnable(GL_BLEND);
495                 glBlendFunc(GL_ONE, GL_ONE);
496                 glEnable(GL_POLYGON_OFFSET_FILL);
497
498                 glUseProgram(0);
499
500                 glPolygonOffset(-1, 1);
501                 glDepthMask(0);
502
503                 glColor3f(0.3, 0.08, 0.01);
504                 mscn->walk_mesh->draw();
505
506                 glDepthMask(1);
507
508                 glPopAttrib();
509         }
510
511         print_text(Vec2(9 * win_width / 10, 20), Vec3(1, 1, 0), "fps: %.1f", framerate);
512         draw_ui();
513 }
514
515
516 void app_reshape(int x, int y)
517 {
518         glViewport(0, 0, x, y);
519         goatvr_set_fb_size(x, y, 1.0f);
520         debug_gui_reshape(x, y);
521 }
522
523 void app_keyboard(int key, bool pressed)
524 {
525         unsigned int mod = app_get_modifiers();
526
527         if(debug_gui && !(pressed && (key == '`' || key == 27))) {
528                 debug_gui_key(key, pressed, mod);
529                 return; // ignore all keystrokes when GUI is visible
530         }
531
532         if(pressed) {
533                 switch(key) {
534                 case 27:
535                         app_quit();
536                         break;
537
538                 case '\n':
539                 case '\r':
540                         if(mod & MOD_ALT) {
541                                 app_toggle_fullscreen();
542                         }
543                         break;
544
545                 case '`':
546                         debug_gui = !debug_gui;
547                         show_message("debug gui %s", debug_gui ? "enabled" : "disabled");
548                         break;
549
550                 case 'm':
551                         app_toggle_grab_mouse();
552                         show_message("mouse %s", app_is_mouse_grabbed() ? "grabbed" : "released");
553                         break;
554
555                 case 'w':
556                         if(mod & MOD_CTRL) {
557                                 show_walk_mesh = !show_walk_mesh;
558                                 show_message("walk mesh: %s", show_walk_mesh ? "on" : "off");
559                         }
560                         break;
561
562                 case 'c':
563                         if(mod & MOD_CTRL) {
564                                 noclip = !noclip;
565                                 show_message(noclip ? "no clip" : "clip");
566                         }
567                         break;
568
569                 case 'f':
570                         toggle_flight();
571                         break;
572
573                 case 'p':
574                         if(mod & MOD_CTRL) {
575                                 fb_srgb = !fb_srgb;
576                                 show_message("gamma correction for non-sRGB framebuffers: %s\n", fb_srgb ? "off" : "on");
577                         }
578                         break;
579
580                 case '=':
581                         walk_speed *= 1.25;
582                         show_message("walk speed: %g", walk_speed);
583                         break;
584
585                 case '-':
586                         walk_speed *= 0.75;
587                         show_message("walk speed: %g", walk_speed);
588                         break;
589
590                 case ']':
591                         mouse_speed *= 1.2;
592                         show_message("mouse speed: %g", mouse_speed);
593                         break;
594
595                 case '[':
596                         mouse_speed *= 0.8;
597                         show_message("mouse speed: %g", mouse_speed);
598                         break;
599
600                 case 'b':
601                         show_blobs = !show_blobs;
602                         show_message("blobs: %s\n", show_blobs ? "on" : "off");
603                         break;
604
605                 case ' ':
606                         goatvr_recenter();
607                         show_message("VR recenter\n");
608                         break;
609
610                 case 'x':
611                         exman->load(mscn, "data/exhibits");
612                         break;
613                 }
614         }
615
616         if(key < 256 && !(mod & (MOD_CTRL | MOD_ALT))) {
617                 keystate[key] = pressed;
618         }
619 }
620
621 void app_mouse_button(int bn, bool pressed, int x, int y)
622 {
623         static int press_x, press_y;
624
625         if(debug_gui) {
626                 debug_gui_mbutton(bn, pressed, x, y);
627                 return; // ignore mouse events while GUI is visible
628         }
629
630         prev_mx = x;
631         prev_my = y;
632         bnstate[bn] = pressed;
633
634         if(bn == 0) {
635                 ExSelection sel;
636                 Ray ray = calc_pick_ray(x, y);
637                 sel = exman->select(ray);
638
639                 if(pressed) {
640                         if(sel && (app_get_modifiers() & MOD_CTRL)) {
641                                 exsel_grab_mouse = sel;
642                                 Vec3 pos = sel.ex->node->get_position();
643                                 debug_log("grabbing... (%g %g %g)\n", pos.x, pos.y, pos.z);
644                                 exslot_mouse.node.set_position(pos);
645                                 exslot_mouse.node.set_rotation(sel.ex->node->get_rotation());
646                                 exslot_mouse.attach_exhibit(sel.ex, EXSLOT_ATTACH_TRANSIENT);
647                                 if(exsel_active) {
648                                         exsel_active = ExSelection::null;       // cancel active on grab
649                                 }
650                         }
651                         press_x = x;
652                         press_y = y;
653
654                 } else {
655                         if(exsel_grab_mouse) {
656                                 // cancel grab on mouse release
657                                 Exhibit *ex = exsel_grab_mouse.ex;
658                                 Vec3 pos = exslot_mouse.node.get_position();
659
660                                 debug_log("releasing at %g %g %g ...\n", pos.x, pos.y, pos.z);
661
662                                 exslot_mouse.detach_exhibit();
663
664                                 ExhibitSlot *slot = exman->nearest_empty_slot(pos, 100);
665                                 if(!slot) {
666                                         debug_log("no empty slot nearby\n");
667                                         if(ex->prev_slot && ex->prev_slot->empty()) {
668                                                 slot = ex->prev_slot;
669                                                 debug_log("using previous slot");
670                                         }
671                                 }
672
673                                 if(slot) {
674                                         slot->attach_exhibit(ex);
675                                 } else {
676                                         // nowhere to put it, so stash it for later
677                                         exslot_mouse.detach_exhibit();
678                                         exman->stash_exhibit(ex);
679                                         debug_log("no slots available, stashing\n");
680                                 }
681
682                                 exsel_grab_mouse = ExSelection::null;
683                         }
684
685                         if(abs(press_x - x) < 5 && abs(press_y - y) < 5) {
686                                 exsel_active = sel;     // select or deselect active exhibit
687                                 if(sel) {
688                                         debug_log("selecting...\n");
689                                 } else {
690                                         debug_log("deselecting...\n");
691                                 }
692                         }
693
694                         press_x = press_y = INT_MIN;
695                 }
696         }
697 }
698
699 static inline void mouse_look(float dx, float dy)
700 {
701         float scrsz = (float)win_height;
702         avatar.body_rot += dx * 512.0 / scrsz;
703         avatar.head_alt += dy * 512.0 / scrsz;
704
705         if(avatar.head_alt < -90) avatar.head_alt = -90;
706         if(avatar.head_alt > 90) avatar.head_alt = 90;
707 }
708
709 static void mouse_zoom(float dx, float dy)
710 {
711         cam_dist += dy * 0.1;
712         if(cam_dist < 0.0) cam_dist = 0.0;
713 }
714
715 void app_mouse_motion(int x, int y)
716 {
717         if(debug_gui) {
718                 debug_gui_mmotion(x, y);
719                 return; // ignore mouse events while GUI is visible
720         }
721
722         int dx = x - prev_mx;
723         int dy = y - prev_my;
724         prev_mx = x;
725         prev_my = y;
726
727         if(!dx && !dy) return;
728
729         if(exsel_grab_mouse) {
730                 Vec3 pos = exslot_mouse.node.get_node_position();
731                 Vec3 dir = transpose(view_matrix.upper3x3()) * Vec3(dx * 1.0, dy * -1.0, 0);
732
733                 exslot_mouse.node.set_position(pos + dir);
734         }
735
736         if(bnstate[2]) {
737                 mouse_look(dx, dy);
738         }
739 }
740
741 void app_mouse_delta(int dx, int dy)
742 {
743         if(bnstate[2]) {
744                 mouse_zoom(dx * mouse_speed, dy * mouse_speed);
745         } else {
746                 mouse_look(dx * mouse_speed, dy * mouse_speed);
747         }
748 }
749
750 void app_mouse_wheel(int dir)
751 {
752         if(debug_gui) {
753                 debug_gui_wheel(dir);
754         }
755 }
756
757 void app_gamepad_axis(int axis, float val)
758 {
759         switch(axis) {
760         case 0:
761                 joy_move.x = val;
762                 break;
763         case 1:
764                 joy_move.y = val;
765                 break;
766
767         case 2:
768                 joy_look.x = val;
769                 break;
770         case 3:
771                 joy_look.y = val;
772                 break;
773         }
774 }
775
776 void app_gamepad_button(int bn, bool pressed)
777 {
778         gpad_bnstate[bn] = pressed;
779
780         if(pressed) {
781                 switch(bn) {
782                 case GPAD_LSTICK:
783                         toggle_flight();
784                         break;
785
786                 case GPAD_X:
787                         show_blobs = !show_blobs;
788                         show_message("blobs: %s\n", show_blobs ? "on" : "off");
789                         break;
790
791                 case GPAD_START:
792                         goatvr_recenter();
793                         show_message("VR recenter\n");
794                         break;
795
796                 default:
797                         break;
798                 }
799         }
800 }
801
802 static void toggle_flight()
803 {
804         static float prev_walk_speed = -1.0;
805         if(prev_walk_speed < 0.0) {
806                 noclip = true;
807                 prev_walk_speed = walk_speed;
808                 walk_speed = 1000.0;
809                 show_message("fly mode\n");
810         } else {
811                 noclip = false;
812                 walk_speed = prev_walk_speed;
813                 prev_walk_speed = -1.0;
814                 show_message("walk mode\n");
815         }
816 }
817
818 static void calc_framerate()
819 {
820         //static int ncalc;
821         static int nframes;
822         static long prev_upd;
823
824         long elapsed = time_msec - prev_upd;
825         if(elapsed >= 1000) {
826                 framerate = (float)nframes / (float)(elapsed * 0.001);
827                 nframes = 1;
828                 prev_upd = time_msec;
829
830                 /*if(++ncalc >= 5) {
831                         printf("fps: %f\n", framerate);
832                         ncalc = 0;
833                 }*/
834         } else {
835                 ++nframes;
836         }
837 }
838
839 static Ray calc_pick_ray(int x, int y)
840 {
841         float nx = (float)x / (float)win_width;
842         float ny = (float)(win_height - y) / (float)win_height;
843
844         last_pick_ray = mouse_pick_ray(nx, ny, view_matrix, proj_matrix);
845         return last_pick_ray;
846 }