/// LSU EE 4702-1 (Fall 2023), GPU Programming
//
 /// Textures, Blending, the Alpha Test

/// Purpose
//
//   Demonstrate use of Textures and Frame Buffer Update Operations

/// References
//


#if 0
/// Background -- Textures
//

// :ogl46: Chapter 8
// :v1.2: Chapter 13
//

//  :Def: Texture
//   An image applied to a primitive, like wallpaper.
//   Has many uses.
//
//   Textures are used to show the course syllabus on the platform
//   floor in the classroom examples.
//
//  Textures use lots of time, resources.
//
//  :Def: Texel
//  A point on a texture image. (Name is mix of texture and pixel.)

 /// Basic Procedure
//
//     During Initialization
//     - Specify a texture, perhaps from an image file like myhouse.jpeg.
//     - Specify how texels should be blended with primitive's color.
//
//     During a Rendering Pass
//     - Provide a texture coordinate for each vertex, indicating
//       what part of the texture image corresponds to the vertex location.
//       :Practice Problems:
//        Fall 2017 Midterm Exam Problem 3
//        https://www.ece.lsu.edu/koppel/gpup/2017/mt.pdf
//        https://www.ece.lsu.edu/koppel/gpup/2017/mt_sol.pdf
//
 /// What Happens (In a Simple Case)
//
//     - Texture coordinates are passed along as vertex attributes.
//
//     - Fragment shader uses texture coordinates to retrieve a texel.
//       (Texel is retrieved and filtered by fixed-function hardware.)
//
//     - Fragment shader combines texel with lighted color. If
//       the fragment survives, the result will be written to
//       the frame buffer.

 /// Texturing Concepts
//
//   :Def: Texel Fetch
//   The retrieval of a texel value at some coordinates.
//
//   OpenGL Shading Language texel fetch function:
     vec4 texel = texture( tex_unit_0, texture_coor );
//
//   texture_coor is a 2-D vector of floats. Coordinates space is [0,1]
//
//   :Example:  Very simple texture fetch.
//      Suppose original texture image is 1000 by 1000 pixels.
//      Suppose texture_coor = { 0.50, 0.070 }.
//      Fetch would return value of image texel at 50,70.
//
//      Suppose an adjacent fragment had texture_coor = { 0.150, 0.070 }
//      Fetch would return value of image texel at 150,070 ---
//        --- SKIPPING 100 image texels on the horizontal axis.

//
//   :Def: Texture Filtering
//   The computing of a texel value at some coordinate using some
//   combination of image texel values.
//
//   Texture filtering has a very strong impact on image appearance.
//
//   Texture filtering is compute intensive, and is one of the few
//   graphics operations that still (in 2019) uses special-purpose GPU
//   hardware.





/// Background -- Fragment Tests, Blending, FB Update

 /// This material not yet updated for Vulkan. 

 //  Vulkan provides capabilities that are similar to those provided
 //  by OpenGL, but with a more flexible API.

 // :ogl46: Chapter 14: Fixed-Function Primitive Assembly and Rasterization
 // :ogl46: Chapter 17: Writing fragments and samples to the framebuffer.

 /// Major Steps
 //
 //  -- Early per-fragment tests.
 //  -- Fragment Processing: User-provided shader or compatibility routine.
 //  -- Late per-fragment tests.

 //  Early Per-Fragment Tests
 //
 //    -- Pixel Ownership Test
 //       Is it in window-manager assigned area?

 //    -- Scissoring.
 //       Is it in the view volume?
 //       Is it within an additional user-defined area?

 //  Late Per-Fragment Tests
 //
 //    Tests are performed in the order below.
 //    Not every test is listed.
 //    A fragment that fails a test is not processed further.
 //
 //    -- Alpha Test  (Deprecated)
 //    -- Stencil Test (Not used in this set.)
 //    -- Depth Buffer (z) Test
 //    -- Blending

 ///  Depth Buffer Test
 //
 //   Operates on depth (z) layer of frame buffer.
 //
 //   Turn on and off depth test.
      glEnable(GL_DEPTH_TEST);
      glDisable(GL_DEPTH_TEST);

 //   Specify type of test:
      glDepthFunc(FUNC);
 //   FUNC -> GL_NEVER, GL_ALWAYS, GL_LESS, GL_LEQUAL, GL_EQUAL,
 //           GL_GEQUAL, GL_NOTEQUAL
 //
      glDepthFunc(GL_LESS); // Pass if fragment z < pixel z.


 ///  Blending
 //
 //   Method of combining fragment colors with pixel colors.
 //
 //   Turn on and off.
      glEnable(GL_BLEND);
      glDisable(GL_BLEND);
 //
 //   Specify blend operator (Equation) and blend factors (BF)
 //
      glBlendEquation( EQ );
 //   Specifies how fragment (source) and pixel (dest) should be combined.
 //
 //   EQ -> GL_FUNC_ADD, GL_FUNC_SUBTRACT, GL_MIN, GL_MAX
 //
      glBlendFunc( SRC_BF, DST_BF );
 //   Specifies how fragment and pixel components should be weighted.
 //   
 //   BF -> GL_ZERO, GL_ONE, GL_SRC_COLOR, GL_DST_COLOR, 
 //
 //
 //   Let s denote fragment being processed ..
 //   .. and d denote the current pixel value (the destination).
 //   Let s.a indicate the fragment alpha channel, etc.
 //
 //







#endif

///  Keyboard Commands
 //
 /// Object (Eye, Light, Ball) Location or Push
 //   Arrows, Page Up, Page Down
 //   Will move object or push ball, depending on mode:
 //   'e': Move eye.
 //   'l': Move light.
 //
 /// Eye Direction
 //   Home, End, Delete, Insert
 //   Turn the eye direction.
 //   Home should rotate eye direction up, End should rotate eye
 //   down, Delete should rotate eye left, Insert should rotate eye
 //   right.  The eye direction vector is displayed in the upper left.

 /// Simulation Options
 //  (Also see variables below.)
 //
 //  'm'    Change method used to apply textures.
 //  'r'    Toggle vertex re-computation on and off.
 //  'F11'  Change size of text.
 //  'F12'  Write screenshot to file.

 /// Variables
 //   Selected program variables can be modified using the keyboard.
 //   Use "Tab" to cycle through the variable to be modified, the
 //   name of the variable is displayed next to "VAR" on the bottom
 //   line of green text.

 //  'Tab' Cycle to next variable.
 //  '`'   Cycle to previous variable.
 //  '+'   Increase variable value.
 //  '-'   Decrease variable value.
 //
 //  VAR Light Intensity - The light intensity.

// Include files provided for this course.
//
#define MAIN_INCLUDE
#include <vhelper.h>

#include <vstroke.h>
#include <gp/coord.h>
#include <gp/pstring.h>
#include <gp/misc.h>
#include <gp/colors.h>

#include <vutil-texture.h>
#include <vutil-pipeline.h>
#include "shapes.h"

 ///
 ///  Create and Initialize a Texture Object from a File
 ///
enum { PT_Invert = 1, PT_To_Alpha = 2 };

const int FL_NONE = 0;

#define ENUM_LABEL(c) { #c, c }

// Note names below are taken from OpenGL and the OpenGL-defined
// functionality is implemented by a fragment shader running under
// Vulkan written for this demo code.
template <typename T>
struct pEnum_Label { const char *label; T value; };
pEnum_Label<int> texture_env_modes[] = {
  ENUM_LABEL(FL_NONE),
  ENUM_LABEL(GL_REPLACE),
  ENUM_LABEL(GL_MODULATE),
  ENUM_LABEL(GL_DECAL),   // Use alpha channel of tex to select.
  ENUM_LABEL(GL_BLEND),   // Blend in a constant color using separate alpha.
  ENUM_LABEL(GL_ADD),     // Sum of colors, product of alphas.
  {NULL,0}
};

struct Uni_TMode {
  vec4 tex_env_color;
  int texture_mode;
};

pEnum_Label<vk::SamplerMipmapMode> texture_mipmap_mode[] = {
  ENUM_LABEL( vk::SamplerMipmapMode::eNearest ),
  ENUM_LABEL( vk::SamplerMipmapMode::eLinear ),
  {NULL,{}}
};

pEnum_Label<vk::Filter> texture_min_filters[] = {
  ENUM_LABEL( vk::Filter::eNearest ),
  ENUM_LABEL( vk::Filter::eLinear ),
  {NULL,{}}
};

pEnum_Label<vk::Filter> texture_mag_filters[] = {
  ENUM_LABEL( vk::Filter::eNearest ),
  ENUM_LABEL( vk::Filter::eLinear ),
  {NULL,{}}
};


class World {
public:
  World(pVulkan_Helper &vh)
    :vh(vh),ff_state(vh.qs),shapes(ff_state),frame_timer(vh.frame_timer),
     transform(vh.qs){ init(); }
  void init();
  void run();
  void render(vk::CommandBuffer& cb);
  void cb_keyboard();
  void modelview_update();

  // Class providing utilities, such as showing text.
  //
  pVulkan_Helper& vh;
  VFixed_Function_State_Manager ff_state;

  Shapes shapes;

  // Class for easy keyboard control of variables.
  //
  pVariable_Control variable_control;

  // Class for showing frame timing.
  //
  pFrame_Timer& frame_timer;

  pCoor light_location;
  float opt_light_intensity;
  enum { MI_Eye, MI_Light, MI_Ball, MI_Ball_V, MI_COUNT } opt_move_item;

  bool global_transform_stale;

  VTransform transform;
  VBufferV<Uni_Lighting> uni_light;
  struct pColor_Pair {pColor front, back;};
  VBufferV<pColor_Pair> uni_material_color_sphere;
  VPipeline pipe_lonely;
  VPipeline pipe_sphere;
  VVertex_Buffer_Set bset_lonely, bset_sphere;

  pCoor sphere_location;
  float sphere_size;

  pCoor eye_location;
  pVect eye_direction;
  pMatrix modelview;

  int opt_method;
  bool opt_recompute;

  bool lonely_triangle_stale;
  float tex_scale;  // Amount by which to scale the texture on the triangle.
  float tri_tilt;   // Amount by which to tilt single triangle.

  vk::Sampler sampler;
  VTexture texture_id_syllabus;
  VTexture texture_id_image;

  string shader_gl_defines;
  VBufferV<Uni_TMode> uni_texture_mode;

  bool opt_blend;
  bool opt_alpha;
  int opt_texture_env_mode;
  int opt_texture_min_filter;
  int opt_texture_mag_filter;
  int opt_texture_mipmap_mode;
  int opt_perm;
  bool opt_lod_zero;
  bool sampler_dirty;

};

void
World::init()
{
  vh.init();
  vh.opt_record_every_time = true;
  vh.display_cb_set([&](){});
  vh.cbs_cmd_record.push_back( [&](vk::CommandBuffer& cb){ render(cb); });

#define ENUM_TO_INT(e) \
    shader_gl_defines += pStringF("const int %s = %d;\n", #e, e).ss();
  ENUM_TO_INT(FL_NONE);
  ENUM_TO_INT(GL_REPLACE);
  ENUM_TO_INT(GL_MODULATE);
  ENUM_TO_INT(GL_DECAL);
  ENUM_TO_INT(GL_BLEND);
  ENUM_TO_INT(GL_ADD);
  ENUM_TO_INT(GL_COMBINE);
#undef ENUM_TO_INT

  opt_method = 0;
  opt_recompute = false;

  eye_location = pCoor(2.6,0.5,9);
  eye_direction = pVect(0,0,-1);

  opt_light_intensity = 7.2;
  light_location = pCoor(7,4.0,-0.3);

  sphere_location = pCoor(0,0,-5);
  sphere_size = 5;

  lonely_triangle_stale = true;
  tex_scale = 1;
  variable_control
    .insert(tex_scale,"Texture Scaling")
    .set_on_change(lonely_triangle_stale);

  tri_tilt = 0;
  variable_control
    .insert(tri_tilt,"Triangle Tilt",.2,false)
    .set_on_change(lonely_triangle_stale);

  variable_control.insert(opt_light_intensity,"Light Intensity");

  opt_move_item = MI_Eye;

  uni_light.init(vh.qs,vk::BufferUsageFlagBits::eUniformBuffer);
  uni_texture_mode.init(vh.qs,vk::BufferUsageFlagBits::eUniformBuffer);
  uni_texture_mode->texture_mode = opt_texture_env_mode = 0;
  uni_texture_mode->tex_env_color = vec4(0,0,0,0);
  opt_texture_min_filter = 0;
  opt_texture_mag_filter = 0;
  opt_texture_mipmap_mode = 0;
  opt_perm = 0;
  opt_lod_zero = false;
  // Type: vk::Sampler
  sampler = nullptr;
  sampler_dirty = true;
  opt_blend = false;
  opt_alpha = false;

  texture_id_syllabus.init( vh.qs, P_Image_Read("gpup.png",255) );
  texture_id_image.init( vh.qs, P_Image_Read("mult.png",255) );

  uni_material_color_sphere
    .init(vh.qs, vk::BufferUsageFlagBits::eUniformBuffer )
    .set_val({ color_lsu_spirit_gold, color_red })
    .to_dev();

  modelview_update();
}

void
World::modelview_update()
{
  pMatrix_Translate center_eye(-eye_location);
  pMatrix_Rotation rotate_eye(eye_direction,pVect(0,0,-1));
  modelview = rotate_eye * center_eye;
}

void
World::run()
{
  vh.message_loop_spin();

  uni_light.destroy();
  uni_texture_mode.destroy();
  uni_material_color_sphere.destroy();

  bset_sphere.destroy();
  bset_lonely.destroy();
  pipe_sphere.destroy();
  pipe_lonely.destroy();

  texture_id_syllabus.destroy();
  texture_id_image.destroy();
  vh.dev.destroySampler(sampler);
  shapes.destroy();
  transform.destroy();

  vh.finish();
}

void
World::render(vk::CommandBuffer& cb)
{
  // This routine called whenever window needs to be updated.

  // Get any waiting keyboard commands.
  //
  cb_keyboard();

  if ( opt_recompute ) bset_sphere.reset();

  vh.fbprintf("%s\n",frame_timer.frame_rate_text_get());

  vh.fbprintf
    ("Eye location: [%5.1f, %5.1f, %5.1f]  "
     "Eye direction: [%+.2f, %+.2f, %+.2f]\n",
     eye_location.x, eye_location.y, eye_location.z,
     eye_direction.x, eye_direction.y, eye_direction.z);

  vh.fbprintf
    ("Light location: [%5.1f, %5.1f, %5.1f]  "
     "Sphere Location[%5.1f, %5.1f, %5.1f]\n",
     light_location.x, light_location.y, light_location.z,
     sphere_location.x, sphere_location.y, sphere_location.z
     );

  vh.fbprintf("Texture Mode: %s  ('m')   Tri TC Permutation: %d ('p')\n",
              texture_env_modes[opt_texture_env_mode].label,
              opt_perm);
  vh.fbprintf("Use Mipmap Levels: %s ('d')\n",
              opt_lod_zero ? " NO" : "YES" );
  vh.fbprintf("Min Filter: %s  ('i')\n",
              texture_min_filters[opt_texture_min_filter].label);
  vh.fbprintf("Mipmap Mode: %s ('I')\n",
              texture_mipmap_mode[opt_texture_mipmap_mode].label);
  vh.fbprintf("Mag Filter: %s  ('a')\n",
              texture_mag_filters[opt_texture_mag_filter].label);
  vh.fbprintf("Blending %s ('b')  Alpha Test %s ('p')\n",
              opt_blend ? "ON" : "OFF", opt_alpha ? "ON" : "OFF");

  pVariable_Control_Elt* const cvar = variable_control.current;
  vh.fbprintf("VAR %s = %.5f  (TAB or '`' to change, +/- to adjust)\n",
              cvar->name,cvar->var[0]);

  const int win_width = vh.get_width();
  const int win_height = vh.get_height();
  const float aspect = float(win_width) / win_height;

  transform.eye_from_global_set( modelview );
  transform.clip_from_eye_set
    ( pMatrix_Frustum(-.8,.8,-.8/aspect,.8/aspect,1,5000) );

  pColor white(1,1,1);
  pColor ambient_color(0x555555);

  /// Lighting
  //
  auto& light0 = uni_light->cgl_LightSource[0];
  uni_light->cgl_LightModel.ambient = ambient_color;
  light0.diffuse = white * opt_light_intensity;
  light0.position = transform.eye_from_global * light_location;
  light0.constantAttenuation = 0;
  light0.linearAttenuation = 1;
  light0.quadraticAttenuation = 0.25;
  light0.ambient = color_black;
  uni_light.to_dev();
  uni_texture_mode.to_dev();

  if ( sampler_dirty )
    {
      sampler_dirty = false;

      // Since the old sampler might currently be in use, wait for any
      // pending frames to be rendered, after which it will be okay to
      // destroy the old sampler.
      //
      vh.frame_serial_need();
      if ( sampler ) vh.dev.destroySampler(sampler);

      // Type: vk::Sampler
      // See :v1.3: Chapter 13
      //
      sampler = vh.qs.dev.createSampler
        ( { {},
            //
            // Magnification and Minification Filters:
            //   Possible values: vk::Filter::eNearest, vk::Filter::eLinear
            //
            texture_mag_filters[opt_texture_mag_filter].value,
            texture_min_filters[opt_texture_min_filter].value,
            //
            // Mipmap Mode (Specifies which mipmap level to use.)
            //   Possible values: vk::SamplerMipmapMode::eNearest, 
            //                    vk::SamplerMipmapMode::eLinear 
            //
            texture_mipmap_mode[opt_texture_mipmap_mode].value,
            //
            // Texture Coordinate Overflow (E.g., 1.2, -.3) Handling
            //
            //   vk::SamplerAddressMode::eRepeat
            //   vk::SamplerAddressMode::eMirroredRepeat
            //   vk::SamplerAddressMode::eClampToEdge
            //   vk::SamplerAddressMode::eClampToBorder
            //   vk::SamplerAddressMode::eMirrorClampToEdge
            //
            vk::SamplerAddressMode::eRepeat,
            vk::SamplerAddressMode::eRepeat,
            vk::SamplerAddressMode::eRepeat,
            0.0f,  // mipLodBias: Fudge factor to add to LOD (MIPMAP level).
            false, // anisotropyEnable,
            16.0f,
            false,
            vk::CompareOp::eNever,
            // min and max LOD 
            0.0f, opt_lod_zero ? 0.0f : VK_LOD_CLAMP_NONE,
            vk::BorderColor::eFloatOpaqueBlack } );
    }

  ///
  /// Paint Single Triangle.
  ///

  if ( !pipe_lonely )
    {
      pipe_lonely
        .init( vh.qs )
        .ds_follow( transform )
        .ds_use_texture()
        .ds_uniform_follow( "BIND_LIGHT", uni_light )
        .ds_uniform_follow( "BIND_TEXTURE_MODE", uni_texture_mode )
        .shader_inputs_info_set<pCoor,pNorm,pTCoor,pColor>()
        .shader_code_set
        ("demo-08-shdr.cc", "vs_main();", nullptr, "fs_main();",
         shader_gl_defines + "#define PIPE_LONELY\n")
        .topology_set( vk::PrimitiveTopology::eTriangleList )
        .create();
    }

  if ( !bset_lonely || lonely_triangle_stale )
    {
      bset_lonely.reset( pipe_lonely );
      lonely_triangle_stale = false;
      // Specify vertices for a triangle.
      //
      pCoor p1( 11,   -4,  -2.8 );
      pCoor p2( 11 + tri_tilt,    5,  -2.8 );
      pCoor p3(  2,    5,  -2.8 );

#if 1

      pTCoor tc1( tex_scale * 1.0, tex_scale * 1.0 );
      pTCoor tc2( tex_scale * 1.0, tex_scale * 0.0 );
      pTCoor tc3( tex_scale * 0.0, tex_scale * 0.0 );

      bset_lonely << p1          << p2        << p3;
      bset_lonely << color_green << color_red << color_blue;
      bset_lonely << tc1         << tc2       << tc3;

#else
      bset_lonely
        << pCoor( 11, -4, -2.8 ) << pCoor( 11, 5, -2.8 ) << pCoor( 2, 5, -2.8 )
        << pTCoor( 1.0, 1.0 )    << pTCoor( 1.0, 0.0 )   << pTCoor( 0.0, 0.0 )
        << color_green           << color_red            << color_blue;
#endif


      pNorm tnorm = cross(p1,p2,p3);
      bset_lonely << tnorm << tnorm << tnorm;

      bset_lonely.to_dev();
    }

  pipe_lonely
    .ds_use(sampler, texture_id_syllabus )
    .record_draw(cb, bset_lonely);

  if ( !pipe_sphere )
    {
      pipe_sphere
        .init( vh.qs )
        .ds_use_texture()
        .ds_uniform_use( "BIND_MAT_COLOR", uni_material_color_sphere )
        .ds_uniform_follow( "BIND_LIGHT", uni_light )
        .ds_uniform_follow( "BIND_TEXTURE_MODE", uni_texture_mode )
        .shader_inputs_info_set<pCoor,pTCoor>()
        .shader_code_set
        ("demo-08-shdr.cc", "vs_main_sphere();", nullptr, "fs_main();",
         shader_gl_defines  + "#define PIPE_SPHERE\n")
        .topology_set( vk::PrimitiveTopology::eTriangleStrip )
        .create();
    }

  ///
  /// Construct a Sphere
  ///

  if ( !bset_sphere )
    {
      bset_sphere.reset( pipe_sphere );

      const int slices = 40;
      const double delta_eta = M_PI / slices;

      for ( double eta = 0; eta < M_PI - 0.0001 - delta_eta; eta += delta_eta )
        {
          const double eta1 = eta + delta_eta;
          const float  y0 = cos(eta),        y1 = cos(eta1);
          const double slice_r0 = sin(eta),  slice_r1 = sin(eta1);
          const double delta_theta = delta_eta;

          const float t0 = eta / M_PI;
          const float t1 = eta1 / M_PI;

          // Add first two vertices of triangle strip.
          //

          // Mapping of Texture Coordinates to Position on Sphere
          //
          //  Assuming that the texture image is a rectangle ..
          //  .. there is no right way to assign a texture coordinate ..
          //  .. to a position on a sphere.
          //
          //  The method used below is simple.
          //
          //  See rectangular images in https://xkcd.com/977/

          // Vertex 1
          //
          bset_sphere << pCoor( slice_r0, y0, 0 )
                      << pTCoor( 0, t0 );

          // Vertex 2
          //
          bset_sphere << pCoor( slice_r1, y1, 0 )
                      << pTCoor( 0, t1 );


          for ( double theta = 0; theta < 2 * M_PI; theta += delta_theta )
            {
              const double theta1 = theta + delta_theta;

              // Vertex 3  (Used for three triangles.)
              //
              bset_sphere
                << pCoor( slice_r0 * sin(theta1), y0, slice_r0 * cos(theta1) )
                << pTCoor( theta1 / ( 2 * M_PI ), t0 );

              // Vertex 4  (Used for three triangles.)
              //
              bset_sphere
                << pCoor( slice_r1 * sin(theta1), y1, slice_r1 * cos(theta1) )
                << pTCoor( theta1 / ( 2 * M_PI ), t1 );
            }
        }
      bset_sphere.to_dev();
    }

  ///
  /// Paint a Sphere
  ///
  pipe_sphere
    .ds_use(sampler, texture_id_image )
    .ds_set( transform
          * pMatrix_Translate( sphere_location )
          * pMatrix_Scale(sphere_size)
          * pMatrix_Rotation( pVect(0,1,0), 2.6 ) )
    .record_draw( cb, bset_sphere );

  // Render Marker for Light Source
  //
  shapes.record_tetrahedron(cb,transform,light_location,0.5);
}


void
World::cb_keyboard()
{
  const int key = vh.keyboard_key_get();
  if ( !key ) return;
  pVect adjustment(0,0,0);
  pVect user_rot_axis(0,0,0);
  const bool kb_mod_s = vh.keyboard_shift;
  const bool kb_mod_c = vh.keyboard_control;
  const float move_amt = kb_mod_s ? 2.0 : kb_mod_c ? 0.08 : 0.4;

  switch ( key ) {
  case FB_KEY_LEFT: adjustment.x = -move_amt; break;
  case FB_KEY_RIGHT: adjustment.x = move_amt; break;
  case FB_KEY_PAGE_UP: adjustment.y = move_amt; break;
  case FB_KEY_PAGE_DOWN: adjustment.y = -move_amt; break;
  case FB_KEY_DOWN: adjustment.z = move_amt; break;
  case FB_KEY_UP: adjustment.z = -move_amt; break;
  case FB_KEY_DELETE: user_rot_axis.y = 1; break;
  case FB_KEY_INSERT: user_rot_axis.y =  -1; break;
  case FB_KEY_HOME: user_rot_axis.x = 1; break;
  case FB_KEY_END: user_rot_axis.x = -1; break;

  case 'b': case 'B': opt_blend = !opt_blend; break;
  case 'P': opt_alpha = !opt_alpha; break;

  case 'd': case 'D': opt_lod_zero = !opt_lod_zero; sampler_dirty = true; break;

  case 'm': opt_texture_env_mode++;
    if ( !texture_env_modes[opt_texture_env_mode].label )
      opt_texture_env_mode = 0;
    uni_texture_mode->texture_mode
      = texture_env_modes[opt_texture_env_mode].value;
    break;
  case 'i': opt_texture_min_filter++;
    sampler_dirty = true;
    if ( !texture_min_filters[opt_texture_min_filter].label )
      opt_texture_min_filter = 0;
    break;
  case 'I': opt_texture_mipmap_mode++;
    sampler_dirty = true;
    if ( !texture_mipmap_mode[opt_texture_mipmap_mode].label )
      opt_texture_mipmap_mode = 0;
    break;
  case 'a': opt_texture_mag_filter++;
    sampler_dirty = true;
    if ( !texture_mag_filters[opt_texture_mag_filter].label )
      opt_texture_mag_filter = 0;
    break;

  case 's': case 'S': opt_move_item = MI_Ball; break;
  case 'e': case 'E': opt_move_item = MI_Eye; break;
  case 'l': case 'L': opt_move_item = MI_Light; break;

  case 'p': opt_perm++; if ( opt_perm > 5 ) opt_perm = 0;

  case 'r': case 'R': opt_recompute = !opt_recompute; break;

  case FB_KEY_TAB:
    if ( !kb_mod_s ) { variable_control.switch_var_right(); break; }
  case 96: variable_control.switch_var_left(); break;
  case '-':case '_': variable_control.adjust_lower(); break;
  case '+':case '=': variable_control.adjust_higher(); break;
  default: printf("Unknown key, %d\n",key); break;
  }

  // Update eye_direction based on keyboard command.
  //
  if ( user_rot_axis.x || user_rot_axis.y )
    {
      pMatrix_Rotation rotall(eye_direction,pVect(0,0,-1));
      user_rot_axis *= invert(rotall);
      eye_direction *= pMatrix_Rotation(user_rot_axis, M_PI * 0.03);
      modelview_update();
    }

  // Update eye_location based on keyboard command.
  //
  if ( adjustment.x || adjustment.y || adjustment.z )
    {
      const double angle =
        fabs(eye_direction.y) > 0.99
        ? 0 : atan2(eye_direction.x,-eye_direction.z);
      pMatrix_Rotation rotall(pVect(0,1,0),-angle);
      adjustment *= rotall;

      switch ( opt_move_item ){
      case MI_Light: light_location += adjustment; break;
      case MI_Eye: eye_location += adjustment; break;
      case MI_Ball: sphere_location += adjustment; break;
      default: break;
      }
      modelview_update();
    }
}


int
main(int argv, char **argc)
{
  pVulkan_Helper pvulkan_helper(argv,argc);
  World world(pvulkan_helper);

  world.run();

  return 0;
}