Newer
Older
Import / research / 3d-experiments / Framework / Graphics.cpp
//  BlockyFroggy
//  Copyright © 2017 John Ryland.
//  All rights reserved.

// TODO: there is a real mix of levels of logic here
// Need to seperate out in to probably 3 classes or something
//      1) All the low level GL calls should be seperate
//      2) Voxel model related code seperated
//      3) game level logic - camera control - which models etc

#include "Graphics.h"
#include "SystemInformation.h"

#include "Shaders.h"
#include "Log.h"
#include "Debug.h"
#include <cstdlib>
#include <cstring>
#include <functional>
//#include "Font6.h"
//#include "Font7.h"
#include "PngImage.h"
#include "ResourceLoader.h"
#include "ObjModel.h"
#include "Utilities.h"

#ifdef USE_OPENGLES2
#include <OpenGLES/ES2/gl.h>
#include <OpenGLES/ES2/glext.h>
#include <GLKit/GLKMatrix4.h>
#include <GLKit/GLKMatrix3.h>
#include <GLKit/GLKMathUtils.h>
#endif

#include "TargetConditionals.h"


DECLARE_LOG_CONTEXT(GFX)


#define TIME_BASED 1
#define SHADOW_DIM 1024 // 2048 // 512
#define USE_ONE_BIG_VERTEX_ARRAY 0   // On iPhone6 it was slower to upload one big vert array per frame instead of having more draw calls but less to upload

/*
 
    Potential ways to slice the code here:

        Program class abstraction around a glProgram
        Model class which abstracts vertex data
        Camera class
        Clipping
        HUD

     Some concepts:

        Models
        Scene   - composition of models (varying formats etc)
        Camera  - players perspective (is it part of the scene?)
        Lights  - lighting data (also, is it part of the scene?)
        Effects - last with alpha in sorted order with z-test and no z-writes
        Shaders
        Program
        HUD

        Submit scene to be rendered by shaders from camera, also from lights
        Then submit HUD over the top

        For lights, need to know the camera to work out the frustrum to find a light frustum that fits
 
 */


bool toggleDebugCameraView = false;
bool debugCameraView = false;
bool debugCollisions = false;
bool showDebugging = true;//false;
bool modelEditing = true;

#ifdef OLD_CAMERA

#if 0
const int cameraTransitionFrames = 30; // Fast for quicker debugging
const int cameraWaitFrames = 50; // Fast for quicker debugging
#else
const int cameraTransitionFrames = 150; // Nice value
const int cameraWaitFrames = 250;
#endif
int m_frame = 0;

#endif


const int vertSize = sizeof(PolyVertex);
const int fltsPerVert = vertSize / sizeof(GLfloat);
const int trisPerCube = 12;
const int vertsPerCube = 3*trisPerCube;

size_t m_hudVertexBufferSize = 0;


inline void addDebugString(GameUi::DrawItems& items, int line, int column, const char* string)
{
  items.addContrastString(column*75, 90 + 19*line, string,             1);
  items.addString(        column*75, 90 + 19*line, string, 0xff00ff00, 1);
}


// One big triangle that covers the entire screen, each vertex is 2d (x,y)
GLfloat gOverlayVertexData[6] = { 1.0f, 1.0f, -3.0f, 1.0f, 1.0f, -3.0f };
GLfloat gBaseCubeVertexData[fltsPerVert * vertsPerCube];


uint32_t colorForObjectType(GameSim::ObjectType a_type)
{
  uint32_t color = 0;
  switch (a_type)
  {
      // Terrain
    case GameSim::Ground:        color = 0x47bd37; break;
    case GameSim::Road:          color = 0x6a6963; break;
    case GameSim::Rail:          color = 0x464441; break;
    case GameSim::Water:         color = 0x3a31f5; break;
    case GameSim::WaterLane:     color = 0x3a31f5; break;
      
      // Obstacles
    case GameSim::Tree:          color = 0x508c23; break;
    case GameSim::Shrub:         color = 0x508c23; break;
    case GameSim::Building:      color = 0xd38b37; break;
    case GameSim::Rocks:         color = 0x5c4932; break;
      
      // Vehicles
    case GameSim::Bike:          color = 0xdf1f24; break;
    case GameSim::Car:           color = 0xf52542; break;
    case GameSim::Van:           color = 0x3a4b88; break;
    case GameSim::Truck:         color = 0xdfd41f; break;
      
      // Trains
    case GameSim::Train1:        color = 0x8bbb38; break;
    case GameSim::Train2:        color = 0xa2d051; break;
    case GameSim::Train3:        color = 0x6ca50a; break;
    case GameSim::Train4:        color = 0x0a53a5; break;
      
      // Vessels
    case GameSim::Log:           color = 0x6b5331; break;
    case GameSim::Boat:          color = 0xd5d5d5; break;
    case GameSim::Crocodile:     color = 0x597e3e; break;
    case GameSim::LilyPad:       color = 0x71df1f; break;
    case GameSim::SteppingStone: color = 0x71df1f; break;
      
    case GameSim::Player:        color = 0xffffff; break;
      
      // Pickups
    case GameSim::Coin:          color = 0xffffff; break;
    case GameSim::Food:          color = 0xffffff; break;
    case GameSim::Life:          color = 0xffffff; break;
      //case Bonus:         color = 0xffffff; break;
      
    default:
      break;
  }
  return color;
}


void initCubeVerticesFromObject(PolyVertex* cubePtr, const GameSim::GameObject& obj, float scale)
{
  uint32_t color = (obj.m_type == GameSim::Car) ? obj.m_color : colorForObjectType(obj.m_type);
  for (int v = 0; v < 36; v++)
  {
    cubePtr->m_animParams.y = obj.m_w;
    cubePtr->m_animParams.x = cubePtr->m_position.x - 0.5f;

    cubePtr->m_normal.x += cubePtr->m_position.x;
    cubePtr->m_normal.y += cubePtr->m_position.y;
    cubePtr->m_normal.z += cubePtr->m_position.z;
    
    cubePtr->m_position.x *= obj.m_w*scale;
    cubePtr->m_position.y *= obj.m_h*scale;
    cubePtr->m_position.z *= (obj.m_d < 0.0f) ? -obj.m_d*scale : obj.m_d*scale;
    
    cubePtr->m_position.x += 0.5f*obj.m_w*scale;
    cubePtr->m_position.y += 0.5f*obj.m_h*scale;
    cubePtr->m_position.z += (obj.m_d < 0.0f) ? -0.5f*obj.m_d*scale : 0.5f*obj.m_d*scale;
    
#if TIME_BASED
    cubePtr->m_position.x += obj.m_x*scale;
    cubePtr->m_position.y += obj.m_y*scale;
    cubePtr->m_position.z += obj.m_z*scale;
#endif
    
    cubePtr->m_delta.x = obj.m_dx;
    cubePtr->m_delta.y = obj.m_dy;
    cubePtr->m_delta.z = 0.0f;
    
    cubePtr->m_color.r = GLfloat((color >> 16) & 0xff) / 255.0;
    cubePtr->m_color.g = GLfloat((color >>  8) & 0xff) / 255.0;
    cubePtr->m_color.b = GLfloat((color >>  0) & 0xff) / 255.0;
    cubePtr->m_color.a = 1.0f;
    
    cubePtr++;
  }
}


void initCubeVerticesFromVoxel(PolyVertex* cubePtr, const VoxelBox& obj, float scale)
{
  uint32_t color = obj.m_color;
  for (int v = 0; v < 36; v++)
  {
    cubePtr->m_animParams.y = obj.m_w;
    cubePtr->m_animParams.x = cubePtr->m_position.x - 0.5f;
    
    cubePtr->m_position.x *= obj.m_w*scale;
    cubePtr->m_position.y *= obj.m_h*scale;
    cubePtr->m_position.z *= (obj.m_d < 0.0f) ? -obj.m_d*scale : obj.m_d*scale;
    
    cubePtr->m_position.x += 0.5*obj.m_w*scale;
    cubePtr->m_position.y += 0.5*obj.m_h*scale;
    cubePtr->m_position.z += (obj.m_d < 0.0f) ? -0.5f*obj.m_d*scale : 0.5f*obj.m_d*scale;
    
    cubePtr->m_position.x += obj.m_x*scale;
    cubePtr->m_position.y += obj.m_y*scale;
    cubePtr->m_position.z += obj.m_z*scale;
    
    cubePtr->m_delta.x = 0.0f;
    cubePtr->m_delta.y = 0.0f;
    cubePtr->m_delta.z = 0.0f;
    
    cubePtr->m_color.r = GLfloat((color >> 16) & 0xff) / 255.0;
    cubePtr->m_color.g = GLfloat((color >>  8) & 0xff) / 255.0;
    cubePtr->m_color.b = GLfloat((color >>  0) & 0xff) / 255.0;
    cubePtr->m_color.a = 1.0f;
    
    cubePtr++;
  }
}

void GameGraphics::resetVertexBuffers(const std::vector<GameSim::GameObjectList*>& a_objectLists /*,
                                      const Game::GameObjectList& a_carModels*/ )
{
  m_scene.reset(a_objectLists);

#ifdef OLD_CAMERA
  m_frame = 0;
#endif

  size_t m_cubeCount = 0;
  m_cubeCount = m_scene.m_rest.size();
  //    for (Game::GameObjectList* objList : a_objectLists)
  //        if (objList->back().m_type != Game::Player)
  //            m_cubeCount += objList->size();
  size_t m_triCount = 12 * m_cubeCount;
  size_t vertCount = 3 * m_triCount;

  m_staticPolyVertexArray.m_vertexData.clear();
  m_staticPolyVertexArray.m_vertexData.resize(vertCount);

  // A cube instance
  // Probably don't need to do this every reset
  Cube cube;
  memcpy(gBaseCubeVertexData, cube.m_baseCubeData.data(), vertSize*vertsPerCube);

  size_t index = 0;
  /*    
        for (Game::GameObjectList* objList : a_objectLists)
        {
        if (objList->back().m_type != Game::Player)
        for (unsigned i = 0; i < objList->size(); i++)
        {
        const Game::GameObject& obj = objList->at(i);
   */
  {
    for (const GameSim::GameObject& obj : m_scene.m_rest)
    {

      PolyVertex* cubePtr = &m_staticPolyVertexArray.m_vertexData[index*vertsPerCube];
      memcpy(cubePtr, gBaseCubeVertexData, vertSize*vertsPerCube);
      initCubeVerticesFromObject(cubePtr, obj, 1.0);
      index++;
    }
  }
}


GameGraphics::GameGraphics()
{
  m_viewMap["RotatingFrog"] = std::bind(&GameGraphics::drawRotatingFrog, this);
  m_viewMap["ModelEditView"] = std::bind(&GameGraphics::drawModelEditLayer, this);
  m_viewMap["RotatingModelPreview"] = std::bind(&GameGraphics::drawModelRotatingPreview, this);
  m_viewMap["CaroselTest"] = std::bind(&GameGraphics::drawCaroselTest, this);
}


GameGraphics::~GameGraphics()
{
}


// Uniform index.
enum
{
    UNIFORM_HUD_TEXTURE,
    UNIFORM_HUD_INV_SCREEN,
    NUM_UNIFORMS
};
GLint uniforms[NUM_UNIFORMS];


// Attribute index.
enum
{
    ATTRIB_VERTEX,
    ATTRIB_COLOR,
    ATTRIB_CHAR,
    ATTRIB_TEXTURE_IDX,
    NUM_ATTRIBUTES
};


/*
struct Program
{
    GLProgram   m_program;
    // ... uniforms ....
};
*/


ObjScene frogModel;
//bool    frogModelLoaded = false;
uint8_t hudTextureIdx = 0;
uint8_t frogModelTextureIdx = 0;
uint8_t vehiclesTextureIdx1 = 0;
uint8_t vehiclesTextureIdx2 = 0;
uint8_t vehiclesTextureIdx3 = 0;
uint8_t rampTextureIdx1 = 0;
uint8_t rampTextureIdx2 = 0;

uint8_t playersTextureIdx1 = 0;
uint8_t playersTextureIdx2 = 0;
uint8_t playersTextureIdx3 = 0;


void GameGraphics::reloadShaders()
{
  m_normalContext.setup(s_vertexShader, s_fragmentShader, Medium, m_options);
  m_shadowContext.setup(s_shadowVertexShader, s_shadowFragmentShader, Medium, m_options);

  // Setup hud program
  m_hudProgram.deleteProgram();
  m_hudProgram.loadShaders(s_hudVertexShader, s_hudFragmentShader, Medium, m_options,
    [](GLuint a_program) {
      auto bindAttribFunc = [a_program](const char* name, GLuint index, GLsizei, GLenum, GLboolean, GLsizei, const void*) {
        glBindAttribLocation(a_program, index, name);
      };
      GameUi::UiVertex::visit(bindAttribFunc);
    }, [](GLuint a_program) {
      uniforms[UNIFORM_HUD_TEXTURE] = glGetUniformLocation(a_program, "texture");
      uniforms[UNIFORM_HUD_INV_SCREEN] = glGetUniformLocation(a_program, "invScreen");
    });

}


void objToVertexBuffer(std::vector<PolyVertex> &objVerts, const std::vector<Object>& obj)
{
  size_t vertCount = 0;
  for (const Object& o : obj)
    vertCount += o.faces.size() * 3;
  objVerts.resize(vertCount);
  int x = 0;
  for (const Object& o : frogModel.objects) {
    for (const Face& f : o.faces) {
      for (int i = 0; i < 3; i++) {

        Position v = { 0.0f, 0.0f, 0.0f };
        size_t vIdx = f.indices[i].vertexIndex;
        if (vIdx && o.vertices.size() >= vIdx)
          v = o.vertices[vIdx-1];

        Normal n = { 0.0f, 1.0f, 0.0f };
        size_t nIdx = f.indices[i].normalIndex;
        if (nIdx && o.normals.size() >= nIdx)
          n = o.normals[nIdx-1];

        UV uv = { 0.0f, 0.0f };
        size_t uIdx = f.indices[i].uvIndex;
        if (uIdx && o.uvs.size() >= uIdx)
          uv = o.uvs[uIdx-1];

        objVerts[x].m_animParams.x = 0.0f;
        objVerts[x].m_animParams.y = 0.0f;
        objVerts[x].m_animParams.z = 0.0f;

        // X,Y,Z
        objVerts[x].m_position.x = -v.y * 1.5f + 3.0f;
        objVerts[x].m_position.y =  v.x * 1.5f + 0.3f;
        objVerts[x].m_position.z =  v.z * 1.5f + 6.0f;

        // DX,DY,DZ
        objVerts[x].m_delta.x = 0.0f;
        objVerts[x].m_delta.y = 0.0f;
        objVerts[x].m_delta.z = 0.0f;

        // Normal
        objVerts[x].m_normal.x = -n.y;
        objVerts[x].m_normal.y =  n.x;
        objVerts[x].m_normal.z =  n.z;

        // RGB
        objVerts[x].m_color.r = uv.u;
        objVerts[x].m_color.g = 1.0f - uv.v;
        objVerts[x].m_color.b = float(frogModelTextureIdx - 0);
        objVerts[x].m_color.a = 0.0f;

        // Barycentric
        objVerts[x].m_barycentric.x = (i==0) ? 1.0f : 0.0f;
        objVerts[x].m_barycentric.y = (i==1) ? 1.0f : 0.0f;
        objVerts[x].m_barycentric.z = (i==2) ? 1.0f : 0.0f;

        x++;
      }
    }
  }
}


void GameGraphics::initialize()
{
  // Setup default options
  m_options["debugTriangles"] = false;
#if TARGET_IPHONE_SIMULATOR
  m_options["enableShadows"] = false;//true;// false;
  m_options["enableLighting"] = false;// true;//false;//true;
#else
  m_options["enableShadows"] = true;//true;// false;
  m_options["enableLighting"] = true;// true;//false;//true;
#endif
  m_options["enableTextures"] = true;// true;//false;//true;
  m_options["enableShadowAttenuation"] = true;//false;//true;
  m_options["enableDistanceFog"] = false;//true;//false;//true;
  m_options["enableRampShading"] = false;//true;//false;//true;
  m_options["enableDarkenedSides"] = false;//true;//false;//true;
  m_options["enableBevels"] = false;// true;//false;//true;
  
  glEnable(GL_DEPTH_TEST);
  //glClearDepthf(1.0f);
  glClearColor(0.65f, 0.65f, 0.65f, 1.0f);
  
  glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&m_defaultFramebufferId);

  //glGenFramebuffers(1, &m_defaultFramebufferId);
  //glBindFramebuffer(GL_FRAMEBUFFER, m_defaultFramebufferId);

  //    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_shadowTexId, 0);
  //glBindFramebuffer(GL_FRAMEBUFFER, 0);
  
  //m_vertBufferSize = vertSize * m_staticPolyVertexArray.m_vertexData.size();

  reloadShaders();

  m_cuboidVertexList.createVertexBuffer(); 
  m_staticPolyVertexArray.createVertexBuffer();
  m_dynamicPolyVertexArray.createVertexBuffer();
  for (int i = 0; i < MODEL_COUNT; i++)
    m_modelsVertexArrays[i].createVertexBuffer();
  for (int i = 0; i < PLAYER_MODEL_COUNT; i++)
    m_playerModelsVertexArrays[i].createVertexBuffer();

#if USE_ONE_BIG_VERTEX_ARRAY
  m_combinedModelsVertexArrays.createVertexBuffer();
#endif

  bool okay = readObjFile(frogModel, "froggy.obj", [this](bool okay){
    LogTag(LL_Info, "OBJ", "Loaded test model called back with %s", okay ? "okay" : "not okay");
    
    if (!okay)
      // Might want to set a flag to retry loading later
      return;

    // The conversion of the frog model from obj to triangle soup can be done once off
    // Perhaps a uniform to position the frog as player
    objToVertexBuffer(m_dynamicPolyVertexArray.m_vertexData, frogModel.objects);
    m_dynamicPolyVertexArray.sync();
  });
  LogTag(LL_Info, "OBJ", "Loading test model started %s", okay ? "okay" : "not okay");
  

  if (m_hudEnabled)
  {
    glGenVertexArraysOES(1, &m_auxVertexArray);
    glBindVertexArrayOES(m_auxVertexArray);
    glGenBuffers(1, &m_auxVertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, m_auxVertexBuffer);
    GameUi::UiVertex::visit(AttribPointerVisitor);

    glGenVertexArraysOES(1, &m_hudVertexArray);
    glBindVertexArrayOES(m_hudVertexArray);
    glGenBuffers(1, &m_hudVertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, m_hudVertexBuffer);
    GameUi::UiVertex::visit(AttribPointerVisitor);
    glBindVertexArrayOES(0);

    glGenTextures(1, &m_hudTexId);
    glBindTexture(GL_TEXTURE_2D, m_hudTexId);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);//_MIPMAP_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    /*
    std::vector<unsigned char> fontData;
    unsigned long fontW, fontH;
    int error = ::decodePNG(fontData, fontW, fontH, Font7_png, Font7_png_len);
    if (error != 0 || fontW != 256 || fontH != 256)
      Log(LL_Error, "error: %d", error);
    */
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2048, 2048, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    //glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 256, 256, GL_RGBA, GL_UNSIGNED_BYTE, (const GLvoid *)&fontData[0]);
  }

  if (m_shadowsEnabled)
  {
    // Depth texture. Slower than a depth buffer, but you can sample it later in your shader
    glGenTextures(1, &m_shadowTexId);
    glBindTexture(GL_TEXTURE_2D, m_shadowTexId);
    
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);//GL_NEAREST);
    //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);//GL_NEAREST);
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    //glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE); // automatic mipmap
//        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC_EXT, GL_LEQUAL);
//        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE_EXT, GL_COMPARE_REF_TO_TEXTURE_EXT);
    //glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 1024, 1024, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, 0);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, SHADOW_DIM, SHADOW_DIM, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, 0);
//    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, SHADOW_DIM, SHADOW_DIM, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
    
    //glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1024, 1024, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);

    glBindTexture(GL_TEXTURE_2D, 0);

    /*
    // create a renderbuffer object to store depth info
    GLuint rboId;
    glGenRenderbuffers(1, &rboId);
    glBindRenderbuffer(GL_RENDERBUFFER, rboId);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, SHADOW_DIM, SHADOW_DIM);
    */
    
    // The framebuffer, which regroups 0, 1, or more textures, and 0 or 1 depth buffer.
    glGenFramebuffers(1, &m_shadowFramebufferId);
    glBindFramebuffer(GL_FRAMEBUFFER, m_shadowFramebufferId);
    //glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboId);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowTexId, 0);


    //glBindRenderbuffer(GL_RENDERBUFFER, 0);

    
    //glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, 1024, 1024);
    //glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_shadowFramebufferId);
    
    //GLenum bufs[1] = {GL_NONE};
    //glDrawBuffers(1, bufs); // No color buffer is drawn to.
    
    /*
#ifndef USE_OPENGLES2
    // gllsBuffer ?
    glDrawBuffer(GL_NONE); // No color buffer is drawn to.
    glReadBuffer(GL_NONE); // No color buffer is read from.
#endif
     */

    // Always check that our framebuffer is ok
    GLenum res = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (res != GL_FRAMEBUFFER_COMPLETE)
    {
      Log(LL_Error, "Error setting up FBO, error: %i\n", res);
    }

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
  }


  // This finds the texture of the frog model, loads it from file, then loads it to the GPU in a texture slot
  GameUi::UpdateState items;
  //frogModelTextureIdx = items.getTextureIdForAsset(frogModel.materialLibrary[frogModel.objects[0].materialName].map_Kd.c_str());
  //vehiclesTextureIdx  = items.getTextureIdForAsset("models-texture.png");
  hudTextureIdx       = items.getTextureIdForAsset("ui.png");
  vehiclesTextureIdx1 = items.getTextureIdForAsset("models.png");
  rampTextureIdx1     = items.getTextureIdForAsset("ramp.png");
  //rampTextureIdx2     = items.getTextureIdForAsset("ramp2.png");
  rampTextureIdx2     = items.getTextureIdForAsset("tile-map.png");
  frogModelTextureIdx = items.getTextureIdForAsset("froggy-spots.png");
  frogModelTextureIdx = items.getTextureIdForAsset("froggy-strips.png");
  frogModelTextureIdx = items.getTextureIdForAsset("froggy-common.png");
  for (auto job : items.m_loadTexturesQueue)
    LoadPendingTexture(job);

  // Just reserve these texture slots, don't actually load anything in to them
  vehiclesTextureIdx2  = items.getTextureIdForAsset("models-x-axis");
  vehiclesTextureIdx3  = items.getTextureIdForAsset("models-z-axis");

  playersTextureIdx1 = items.getTextureIdForAsset("player-models.png");
  playersTextureIdx2 = items.getTextureIdForAsset("player-models-x-axis");
  playersTextureIdx3 = items.getTextureIdForAsset("player-models-z-axis");

  m_models.reloadDataFromFile("models.png", true);
  m_playerModels.reloadDataFromFile("player-models.png", true);
}


void GameGraphics::shutdown()
{
  m_staticPolyVertexArray.destroyVertexBuffer();
  m_dynamicPolyVertexArray.destroyVertexBuffer();
  m_cuboidVertexList.destroyVertexBuffer();
  // TODO: car models
  glDeleteVertexArraysOES(1, &m_hudVertexArray);
  
  m_normalContext.shutdown();
  m_shadowContext.shutdown();
}


// May need a way to collect unused textures to free up space
void GameGraphics::LoadPendingTexture(GameUi::TextureLoadJob& job)
{
  int idx = job.texIdx;
  //Resource* res = LoadFile(job.assetName.c_str(), true);
  loadFileAsync(job.assetName.c_str(), true, [idx, this](Resource* res) {
      if (res && res->data.size() > 1) {
          glBindTexture(GL_TEXTURE_2D, m_hudTexId);
          std::vector<unsigned char> texData;
          unsigned long texW, texH;
          int error = ::decodePNG(texData, texW, texH, res->data.data(), res->data.size());
          if (error != 0 || texW != 256 || texH != 256)
              Log(LL_Error, "error: %d", error);
          // job.texIdx;
          glTexSubImage2D(GL_TEXTURE_2D, 0, (idx%8)*256, (idx/8)*256, 256, 256, GL_RGBA, GL_UNSIGNED_BYTE, (const GLvoid *)&texData[0]);
        //  glTexSubImage2D(GL_TEXTURE_2D, 0, 256, 0, 256, 256, GL_RGBA, GL_UNSIGNED_BYTE, (const GLvoid *)&texData[0]);
      }
  });
}



void GameGraphics::loadVoxelModel(VertexArray<PolyVertex>& vertexArray, const char* name, int x, int y, int w, int h, int d, bool direction, int scaling, bool asCubes,
   int yClamp0, int yClamp1)
{
  float modelScale = 1.0f;
//  int scaling = 8;


  // loading asCubes is inefficient, but could have benefits if voxel editing just one model
  if (asCubes)
  {
    VoxelBoxList& voxelModel  = m_models.getModel(name, x, y, w, h, d, direction, m_model_x_axis, m_model_z_axis);

    vertexArray.m_vertexData.clear();
    int size = 0;
    for (unsigned i = 0; i < voxelModel.size(); i++)
      if (voxelModel.at(i).m_z >= yClamp0 && voxelModel.at(i).m_z < yClamp1)
        size++;
    vertexArray.m_vertexData.resize(size * 12 * 3);

    int cube = 0;
    for (unsigned i = 0; i < voxelModel.size(); i++)
    {
      if (voxelModel.at(i).m_z >= yClamp0 && voxelModel.at(i).m_z < yClamp1)
      {
        PolyVertex* cubePtr = &vertexArray.m_vertexData[cube*vertsPerCube];
        memcpy(cubePtr, gBaseCubeVertexData, vertSize*vertsPerCube);
        initCubeVerticesFromVoxel(cubePtr, voxelModel.at(i), modelScale);
        cube++;
      }
    }
    return;
  }

  
  if (scaling == 2)
  {
    modelScale = 2.0f;
    x /= 2;
    x += 128;
    y /= 2;
    y += 128;
    w /= 2;
    h /= 2;
    d /= 2;
  }
  else if (scaling == 4)
  {
    modelScale = 4.0f;
    x /= 4;
    x += 128 + 64;
    y /= 4;
    y += 128 + 64;
    w /= 4;
    h /= 4;
    d /= 4;
  }
  else if (scaling == 8)
  {
    modelScale = 8.0f;
    x /= 8;
    x += 128 + 64 + 32;
    y /= 8;
    y += 128 + 64 + 32;
    w /= 8;
    h /= 8;
    d /= 8;
  }


  // TODO: is this needed?
  if (m_model_x_axis == nullptr) {
    m_model_x_axis = new uint32_t[256*256];
    for (int j = 0; j < 256; j++)
    for (int i = 0; i < 256; i++)
      m_model_x_axis[j*256+i] = (0xff<<24)|(i<<16)|(j<<8);
    glBindTexture(GL_TEXTURE_2D, m_hudTexId);
    glTexSubImage2D(GL_TEXTURE_2D, 0, (vehiclesTextureIdx2%8)*256, (vehiclesTextureIdx2/8)*256, 256, 256, GL_RGBA, GL_UNSIGNED_BYTE, (const GLvoid *)m_model_x_axis);
  }
  if (m_model_z_axis == nullptr) {
    m_model_z_axis = new uint32_t[256*256];
    for (int j = 0; j < 256; j++)
    for (int i = 0; i < 256; i++)
      m_model_x_axis[j*256+i] = (0xff<<24)|(i<<8)|j;
    glBindTexture(GL_TEXTURE_2D, m_hudTexId);
    glTexSubImage2D(GL_TEXTURE_2D, 0, (vehiclesTextureIdx3%8)*256, (vehiclesTextureIdx3/8)*256, 256, 256, GL_RGBA, GL_UNSIGNED_BYTE, (const GLvoid *)m_model_z_axis);
  }


  VoxelTriangleList& voxelModel  = m_models.getModelTriangles(name, x, y, w, h, d, direction,  m_model_x_axis, m_model_z_axis);
  
  
  vertexArray.m_vertexData.clear();
  vertexArray.m_vertexData.resize(voxelModel.size() * 3);
 
  unsigned i = 0; 
  for (unsigned t = 0; t < voxelModel.size(); t++)
  {
    const VoxelTriangle& tri = voxelModel.at(t);
    if (tri.m_verts[0].m_z >= yClamp0 && tri.m_verts[0].m_z < yClamp1) {

      // lookup the normal based on the axis
      float normals[6][4] = {
        { 0.0,0.0,-1.0,0.0 },
        { -1.0,0.0,0.0,0.0 },
        { 0.0,-1.0,0.0,0.0 },
        { 0.0,0.0,1.0,0.0 },
        { 1.0,0.0,0.0,0.0 },
        { 0.0,1.0,0.0,0.0 },
      };
      int texIdxs[6] = {
        vehiclesTextureIdx3,
        vehiclesTextureIdx2,
        vehiclesTextureIdx1,
        vehiclesTextureIdx3,
        vehiclesTextureIdx2,
        vehiclesTextureIdx1,
      };
      float normal[4];
      memcpy(normal, normals[tri.m_axis], sizeof(float)*4);
      int texIdx;// = vehiclesTextureIdx1;
      texIdx = texIdxs[tri.m_axis];

      /*
      // calculate the normal
      float vt[2][4];
      vt[0][0] = tri.m_verts[1].m_x - tri.m_verts[0].m_x;
      vt[0][1] = tri.m_verts[1].m_y - tri.m_verts[0].m_y;
      vt[0][2] = tri.m_verts[1].m_z - tri.m_verts[0].m_z;
      vt[1][0] = tri.m_verts[1].m_x - tri.m_verts[2].m_x;
      vt[1][1] = tri.m_verts[1].m_y - tri.m_verts[2].m_y;
      vt[1][2] = tri.m_verts[1].m_z - tri.m_verts[2].m_z;
      Math::normalizeVector(vt[0]);
      Math::normalizeVector(vt[1]);
      float normal[4];
      Math::crossProduct(normal, vt[1], vt[0]);
       */

      for (int v = 0; v < 3; v++)
      {
        PolyVertex& vert = vertexArray.m_vertexData[i*3 + v];

        vert.m_animParams.y = 0.0;//obj.m_w;
        vert.m_animParams.x = 0.0;//cubePtr->m_position.x - 0.5;

        vert.m_position.x = tri.m_verts[v].m_x * modelScale;
        vert.m_position.y = tri.m_verts[v].m_y * modelScale;
        vert.m_position.z = tri.m_verts[v].m_z * modelScale;

        vert.m_barycentric.x = (v == 0) ? 1.0 : 0.0;
        vert.m_barycentric.y = (v == 1) ? 1.0 : 0.1;
        vert.m_barycentric.z = (v == 2) ? 1.0 : 0.0;

        if ( y == 128  ||  (tri.m_axis%3) == 0) // roads and stuff
        {
          vert.m_normal.x = normal[0];
          vert.m_normal.y = normal[1];
          vert.m_normal.z = normal[2];
        }
        else
        {
          vert.m_normal.x = normal[0]*0.5 + 0.5;
          vert.m_normal.y = normal[1]*0.5 + 0.5;
          vert.m_normal.z = normal[2]*0.5 + 0.5;

          vert.m_normal.x += 1.2*(vert.m_position.x - w/2)/w;
          vert.m_normal.y += 1.2*(vert.m_position.y - d/2)/d;
          vert.m_normal.z += 1.2*(vert.m_position.z - h/2)/h;

/*
          vert.m_normal.x = normal[0];
          vert.m_normal.y = normal[1];
          vert.m_normal.z = normal[2];
          vert.m_normal.x = 2.0*(vert.m_position.x - w/2)/w;
          vert.m_normal.y = 2.0*(vert.m_position.y - d/2)/d;
          vert.m_normal.z = 2.0*(vert.m_position.z - h/2)/h;
*/

          /*
             vert.m_normal.x = vert.m_position.x - w/2;
             vert.m_normal.y = vert.m_position.y - d/2;
             vert.m_normal.z = vert.m_position.z - h/2;
           */
        }
        // Math::normalizeVector(&vert.m_normal.x);

        vert.m_delta.x = 0.0f;
        vert.m_delta.y = 0.0f;
        vert.m_delta.z = 0.0f;

        vert.m_color.r = (GLfloat(tri.m_uvs[v].m_u)-0.0) / 256.0;
        vert.m_color.g = (GLfloat(tri.m_uvs[v].m_v)+0.0) / 256.0;
        vert.m_color.b = GLfloat(texIdx);
        vert.m_color.a = 0.0f;
      }
    }
    i++;
  }
}


struct ModelInfo {
  const char* name;
  int x, y;
  int w, h, d;
  bool direction;
  int scaling;
};
ModelInfo g_modelInfos[MODEL_COUNT] = {
  { "car-left",    0,   0,  16, 16, 8, false , 1 },
  { "car-right",   0,   0,  16, 16, 8, true  , 1 },
  { "bus-left",   16,   0,  32, 16, 8, false , 1 },
  { "bus-right",  16,   0,  32, 16, 8, true  , 1 },
  { "bike-left",  48,   0,  16, 16, 8, false , 1 },
  { "bike-right", 48,   0,  16, 16, 8, true  , 1 },
  { "van-left",   64,   0,  16, 16, 8, false , 1 },
  { "van-right",  64,   0,  16, 16, 8, true  , 1 },
  { "trk-left",   80,   0,  48, 16, 8, false , 1 },
  { "trk-right",  80,   0,  48, 16, 8, true  , 1 },
  { "trn-left",  128,   0, 128, 16, 8, false , 1 },
  { "trn-right", 128,   0, 128, 16, 8, true  , 1 },
  { "road",        0, 128,  16,  8,16, true  , 1 },
  { "water",      16, 128,  16,  8,16, true  , 1 },
  { "tracks",     32, 128,  16,  8,16, true  , 1 },
  { "road-2",     48, 128,  16,  8,16, true  , 1 },
  { "tree",       64, 128,  16, 16, 8, true  , 1 },

  { "shrub",      64+16*2, 128,  16, 16, 8, true  , 1 },
  { "building",   64+16*3, 128,  16, 16, 8, true  , 1 },
  { "rock",       64+16*4, 128,  16, 16, 8, true  , 1 },

  { "logs",       64+16*5, 128,  16, 16, 8, false , 1 },
  { "logs",       64+16*5, 128,  16, 16, 8, true  , 1 },

  { "boats",      64+16*6, 128,  16, 16, 8, false , 1 },
  { "boats",      64+16*6, 128,  16, 16, 8, true  , 1 },

  { "crocs",      64+16*7, 128,  16, 16, 8, false , 1 },
  { "crocs",      64+16*7, 128,  16, 16, 8, true  , 1 },

  { "edit-slot",  80, 128,  16, 16, 8, true  , 1 },
};

/*
void GetBool(const GameUi::DrawItems& a_uiItems, const char* name)
{
  if (a_uiItems.m_variables.count(name))
    if (a_uiItems.m_variables.at(name) == "on")
      return true;
  return false;
}
*/

// Doesn't change the value if it is not in a_uiItems, otherwise sets it to what it is set as
bool MapBool(bool& var, const GameUi::UpdateState& state, const char* name)
{
  if (state.m_variables.count(name)) {
    std::string str = state.m_variables.at(name);
    if (str == "on" && !var) {
      var = true;
      return true;
    }
    if (str == "off" && var) {
      var = false;
      return true;
    }
  }
  return false;
}


void GameGraphics::prepareState(GameUi::UpdateState& a_state)
{
  a_state.m_variables["DEBUG_TRIANGLES"]       = m_options["debugTriangles"] ? "on" : "off";
  a_state.m_variables["ENABLE_SHADOWS"]        = m_options["enableShadows"] ? "on" : "off";
  a_state.m_variables["ENABLE_SHADOW_ATTN"]    = m_options["enableShadowAttenuation"] ? "on" : "off";
  a_state.m_variables["ENABLE_DISTANCE_FOG"]   = m_options["enableDistanceFog"] ? "on" : "off";
  a_state.m_variables["ENABLE_RAMP_SHADING"]   = m_options["enableRampShading"] ? "on" : "off";
  a_state.m_variables["ENABLE_DARKENED_SIDES"] = m_options["enableDarkenedSides"] ? "on" : "off";
  a_state.m_variables["ENABLE_TEXTURES"]       = m_options["enableTextures"] ? "on" : "off";
  a_state.m_variables["ENABLE_BEVELS"]         = m_options["enableBevels"] ? "on" : "off";
  a_state.m_variables["ENABLE_LIGHTING"]       = m_options["enableLighting"] ? "on" : "off";
}


void GameGraphics::update(float a_elapsed, float a_aspect, const GameUi::UpdateState& a_state, float a_playerPos[3]
                          , float a_cameraStyle[8], float a_light[8], float a_debugCamera[8])
{
  MapBool(debugCameraView, a_state, "DEBUG_CAMERA");
  MapBool(debugCollisions, a_state, "DEBUG_COLLISIONS");
  MapBool(showDebugging,   a_state, "DEBUG");
  MapBool(modelEditing,    a_state, "MODEL_EDITING");

  bool needToReloadShaders = false;
  needToReloadShaders |= MapBool(m_options["debugTriangles"],          a_state, "DEBUG_TRIANGLES");
  needToReloadShaders |= MapBool(m_options["enableShadows"],           a_state, "ENABLE_SHADOWS");
  needToReloadShaders |= MapBool(m_options["enableShadowAttenuation"], a_state, "ENABLE_SHADOW_ATTN");
  needToReloadShaders |= MapBool(m_options["enableDistanceFog"],       a_state, "ENABLE_DISTANCE_FOG");
  needToReloadShaders |= MapBool(m_options["enableRampShading"],       a_state, "ENABLE_RAMP_SHADING");
  needToReloadShaders |= MapBool(m_options["enableDarkenedSides"],     a_state, "ENABLE_DARKENED_SIDES");
  needToReloadShaders |= MapBool(m_options["enableTextures"],          a_state, "ENABLE_TEXTURES");
  needToReloadShaders |= MapBool(m_options["enableBevels"],            a_state, "ENABLE_BEVELS");
  needToReloadShaders |= MapBool(m_options["enableLighting"],          a_state, "ENABLE_LIGHTING");

  if (needToReloadShaders)
    reloadShaders();

  //Log(LL_Trace, "debug = %s", debugStr.c_str());

  // allow it to be changed by key strokes too
  if (toggleDebugCameraView)
    debugCameraView = !debugCameraView;

  const bool reloadModels = true;
  if (reloadModels && m_models.dataUpdated())
  {
    m_models.ackDataUpdated();
    for (int i = 0; i < MODEL_COUNT; i++) {
      ModelInfo &mi = g_modelInfos[i];
      loadVoxelModel(m_modelsVertexArrays[i], mi.name, mi.x, mi.y, mi.w, mi.h, mi.d, mi.direction, mi.scaling, (i == (MODEL_COUNT-1)));
      
      m_modelsVertexArraysCopy[i] = m_modelsVertexArrays[i];
    }

    glBindTexture(GL_TEXTURE_2D, m_hudTexId);
    glTexSubImage2D(GL_TEXTURE_2D, 0, (vehiclesTextureIdx1%8)*256, (vehiclesTextureIdx1/8)*256, 256, 256, GL_RGBA, GL_UNSIGNED_BYTE, (const GLvoid *)m_models.y_axis());
    glTexSubImage2D(GL_TEXTURE_2D, 0, (vehiclesTextureIdx2%8)*256, (vehiclesTextureIdx2/8)*256, 256, 256, GL_RGBA, GL_UNSIGNED_BYTE, (const GLvoid *)m_model_x_axis);
    glTexSubImage2D(GL_TEXTURE_2D, 0, (vehiclesTextureIdx3%8)*256, (vehiclesTextureIdx3/8)*256, 256, 256, GL_RGBA, GL_UNSIGNED_BYTE, (const GLvoid *)m_model_z_axis);
  }
  
  m_staticPolyVertexArray.sync();
  for (int i = 0; i < MODEL_COUNT; i++)
    m_modelsVertexArrays[i].sync();

#if USE_ONE_BIG_VERTEX_ARRAY
  struct ObjMap {
    const GameSim::GameObjectList& obj;
    int modelIndex;
  } objsMap[] = {
    { m_scene.m_cars, 0},
    { m_scene.m_bikes, 4},
    { m_scene.m_vans, 6},
    { m_scene.m_trucks, 8},
    { m_scene.m_trains, 10},
    { m_scene.m_trees, 16},
    { m_scene.m_shrubs, 17},
    { m_scene.m_buildings, 18},
    { m_scene.m_rocks, 19},
    { m_scene.m_logs, 20},
    { m_scene.m_boats, 22},
    { m_scene.m_crocs, 24},
  };
  
  // build up the new combined vertex arrays and sync
  m_combinedModelsVertexArrays.m_vertexData.clear();
  m_combinedModelsVertexArrays.m_vertexData.shrink_to_fit();
  
  auto combiner = [this](const GameSim::GameObjectList& a_objs, int a_voxelModelIndex, int zoff, int wid) {
    for (const GameSim::GameObject& obj : a_objs) {
      for (int j = 0; j < int(obj.m_h/10.0); j++)
      {
        for (int i = 0; i < 1000/wid; i++)
        {
          float x = obj.m_x + i*wid;
          if (!m_camera.isClipped(x, obj.m_y + j*10.0f + 1.0f, obj.m_z + zoff, wid, obj.m_h, obj.m_d))
          {
            // optimized version which moves invariant out of the loop
            float pos[4] = { x, obj.m_y + j*10.0f + 0.0f, obj.m_z + zoff, 0.0f };
            size_t oldSize = m_combinedModelsVertexArrays.m_vertexData.size();
            size_t appendSize = m_modelsVertexArraysCopy[a_voxelModelIndex].m_vertexData.size();
            m_combinedModelsVertexArrays.m_vertexData.resize(oldSize + appendSize);
            for (int j = 0; j < appendSize; j++) {
              auto newVertex = m_modelsVertexArraysCopy[a_voxelModelIndex].m_vertexData[j];
              for (int t = 0; t < 3; t++)
                newVertex.m_position.attrib[t] += pos[t];
              m_combinedModelsVertexArrays.m_vertexData[oldSize + j] = newVertex;
            }
          }
        }
      }
    }
  };

  for (int i = 0; i < sizeof(objsMap)/sizeof(ObjMap); i++)
  {
   int modelIndex = objsMap[i].modelIndex;
   for (const GameSim::GameObject& obj : objsMap[i].obj) {
    float x = Math::modulus(obj.m_x + obj.m_dx * m_time, 1000.0f + obj.m_w);
    if (!m_camera.isClipped(x, obj.m_y, obj.m_z, obj.m_w, obj.m_h, obj.m_d))
    {
      float pos[4] = { x, obj.m_y, obj.m_z, 0.0f };
      size_t oldSize = m_combinedModelsVertexArrays.m_vertexData.size();
      if (obj.m_dx <= 0.0) {
        size_t appendSize = m_modelsVertexArraysCopy[modelIndex].m_vertexData.size();
        m_combinedModelsVertexArrays.m_vertexData.resize(oldSize + appendSize);
        for (int j = 0; j < appendSize; j++) {
          auto newVertex = m_modelsVertexArraysCopy[modelIndex].m_vertexData[j];
          for (int t = 0; t < 3; t++)
            newVertex.m_position.attrib[t] += pos[t];
          m_combinedModelsVertexArrays.m_vertexData[oldSize + j] = newVertex;
        }
      } else {
        size_t appendSize = m_modelsVertexArraysCopy[modelIndex+1].m_vertexData.size();
        m_combinedModelsVertexArrays.m_vertexData.resize(oldSize + appendSize);
        for (int j = 0; j < appendSize; j++) {
          auto newVertex = m_modelsVertexArraysCopy[modelIndex+1].m_vertexData[j];
          for (int t = 0; t < 3; t++)
            newVertex.m_position.attrib[t] += pos[t];
          m_combinedModelsVertexArrays.m_vertexData[oldSize + j] = newVertex;
        }
      }
    }
   }
  }
  combiner(m_scene.m_road,   12, 0, 16);
  combiner(m_scene.m_water,  13, 0, 16);
  combiner(m_scene.m_tracks, 14, 0, 15);
  m_combinedModelsVertexArrays.sync();
#endif
  
  modelIdx = int(a_playerPos[0]);
  
  float currentPlayerX = a_playerPos[0];
  float currentPlayerY = a_playerPos[1];
  float currentPlayerZ = a_playerPos[2];
  static float prevPlayerX = currentPlayerX;
  static float prevPlayerY = currentPlayerY;
  static float lastPlayerX = currentPlayerX;
  static float lastPlayerY = currentPlayerY;
  static float lastPlayerZ = currentPlayerZ;
  static float jumpTime = 0.0;
  static bool inJump = false;
  
  //const float playerLerpSpeed = 0.5;
  // lerp frog position - avoids too jumpy movement
  float playerX = currentPlayerX; // Math::lerp(lastPlayerX, currentPlayerX, a_elapsed / playerLerpSpeed);
  float playerY = currentPlayerY; // Math::lerp(lastPlayerY, currentPlayerY, a_elapsed / playerLerpSpeed);
  float playerZ = 0.0;
  
  if (currentPlayerX != prevPlayerX || currentPlayerY != prevPlayerY || inJump) {
    jumpTime += a_elapsed;
    float ratio = jumpTime * 2.0;
    if (ratio >= 1.0) {
      inJump = false;
      ratio = 1.0;
    } else {
      playerX = Math::lerp(lastPlayerX, currentPlayerX, easeInBackCurve(ratio));
      playerY = Math::lerp(lastPlayerY, currentPlayerY, easeInBackCurve(ratio));
      inJump = true;
    }
    playerZ = Math::lerp(lastPlayerZ, currentPlayerZ, ratio) + 2.0;
    if (ratio < 0.5f)
      playerZ += easeInOutBackCurve(ratio * 2.0) * 5.0;
    else
      playerZ += easeInBackCurve(1.0 - (ratio-0.5)*2.0) * 5.0;
  } else {
    inJump = false;
    jumpTime = 0.0;
    lastPlayerZ = currentPlayerZ;
    playerZ = currentPlayerZ;
  }
  
  
  lastPlayerX = playerX;
  lastPlayerY = playerY;
  prevPlayerX = currentPlayerX;
  prevPlayerY = currentPlayerY;

  m_playerX = playerX;
  m_playerY = playerY;
  m_playerZ = playerZ;
  
  
//    setDebugValue("triangles:", int(m_triCount));
//    setDebugValue("vertices:", int(m_triCount * 3));

#ifdef OLD_CAMERA
    //const Game::GameObject& player = a_objectLists.back()->front();
  
  m_cameraX = -a_playerPos[0] + 20;
  m_cameraY = -a_playerPos[1] - 100;
  

  float xOff = -300.0;
  float yOff = -100.0;
  float zOff =   20.0;  // height of camera, smaller is higher

    xOff = m_cameraX;//-player.m_x + 20;
    yOff = m_cameraY;//-player.m_y - 100;

    // lerp camera pos towards the player's position (avoids jumpy camera movements, feels nicer)
    static float lastXOff = xOff;
    static float lastYOff = yOff;
    const float cameraLerpSpeed = 10.0;
#define LERP(A, B, Ratio)         ((A) + (((B) - (A)) * (Ratio)))
    xOff = Math::lerp(lastXOff, xOff, a_elapsed / cameraLerpSpeed);
    yOff = Math::lerp(lastYOff, yOff, a_elapsed / cameraLerpSpeed);
    lastXOff = xOff;
    lastYOff = yOff;


    //  Layout of camera style parameters:
    // float cameraStyle[8] = { fov, rx, ry, rz, s, tx, ty, tz };

    // Frogger mode (perspective devolved to ortho and with top down view)
    float debugCameraStyle[8]      = { 1.0f, 0.0f, 0.0f, 0.0f, 0.0035f, -20.0f, 20.0f, 80.0f }; // Debug Style - 2D top down mode
//  float topDownCameraStyle[8]    = { 1.0f, 0.0f, 0.0f, 0.0f, 0.02f, -20.0f, 20.0f, 80.0f }; // Frogger Style - 2D top down mode
    float topDownCameraStyle[8]    = { 0.5f, 0.0f, 0.0f, 0.0f, 0.015f, -20.0f, 40.0f, 80.0f }; // Frogger Style - 2D top down mode
    float isoCameraStyle[8]        = { 0.5f, -45.0f, 0.0f, -20.0f, 0.015f, 0.0f, 0.0f, 0.0f }; // Crossy Road Style - nearly isometric / orthographic
    float perspectiveCameraStyle[8] = { 65.0f, -45.0f, 0.0f, -20.0f, 1.0f, 0.0f, 0.0f, 0.0f }; // Normal as designed style with perspective

    float* defaultCameraStyle = perspectiveCameraStyle;
  
    bool useIso = true;
    if (useIso)
      defaultCameraStyle = isoCameraStyle;

    const int cwf = cameraWaitFrames;
    const int ctf = cameraTransitionFrames;
    float cameraStyle[8];
    if (m_frame < cwf)
      for (int i = 0; i < 8; i++)
        cameraStyle[i] = topDownCameraStyle[i];
    else if (m_frame < cwf+ctf)
      for (int i = 0; i < 8; i++)
        cameraStyle[i] = Math::lerp(topDownCameraStyle[i], isoCameraStyle[i], (m_frame-cwf) / float(ctf));
    else if (m_frame < 2*cwf+ctf)
      for (int i = 0; i < 8; i++)
        cameraStyle[i] = isoCameraStyle[i];
    else if (m_frame < 2*(cwf+ctf))
      for (int i = 0; i < 8; i++)
        cameraStyle[i] = Math::lerp(isoCameraStyle[i], defaultCameraStyle[i], (m_frame-(2*cwf+ctf)) / float(ctf));
    else
      for (int i = 0; i < 8; i++)
        cameraStyle[i] = defaultCameraStyle[i];

/*
    if (debugCameraView)
      for (int i = 0; i < 8; i++)
        cameraStyle[i] = debugCameraStyle[i];
*/

    m_polyUniforms.m_shadowStrength = float(m_frame) / (3*cwf+2*ctf);
    if (float(m_frame) >= (3*cwf+2*ctf))
      m_polyUniforms.m_shadowStrength = 1.0f;

    m_frame++;

    m_camera.build(cameraStyle[0], a_aspect, &cameraStyle[1], cameraStyle[4] * 0.01f, xOff + cameraStyle[5], yOff + cameraStyle[6], zOff + cameraStyle[7]);
  
    Log(LL_Trace, "mvp[] = {");
    Log(LL_Trace, "   %f  %f  %f  %f", m_modelViewProjectionMatrix[0],  m_modelViewProjectionMatrix[1],  m_modelViewProjectionMatrix[2],  m_modelViewProjectionMatrix[3]);
    Log(LL_Trace, "   %f  %f  %f  %f", m_modelViewProjectionMatrix[4],  m_modelViewProjectionMatrix[5],  m_modelViewProjectionMatrix[6],  m_modelViewProjectionMatrix[7]);
    Log(LL_Trace, "   %f  %f  %f  %f", m_modelViewProjectionMatrix[8],  m_modelViewProjectionMatrix[9],  m_modelViewProjectionMatrix[10], m_modelViewProjectionMatrix[11]);
    Log(LL_Trace,  "   %f  %f  %f  %f", m_modelViewProjectionMatrix[12], m_modelViewProjectionMatrix[13], m_modelViewProjectionMatrix[14], m_modelViewProjectionMatrix[15]);
    Log(LL_Trace, "};");

    if (debugCameraView)
    {
      for (int i = 0; i < 8; i++)
        cameraStyle[i] = debugCameraStyle[i];
      m_debugCamera.build(cameraStyle[0], a_aspect, &cameraStyle[1], cameraStyle[4] * 0.01f, xOff + cameraStyle[5], yOff + cameraStyle[6], zOff + cameraStyle[7]);
    }
  
#else
  
  m_normalContext.m_uniforms.m_shadowStrength = 1.0f;
  m_normalContext.m_uniforms.m_mixNoise = false;

  m_camera.build(a_cameraStyle[0], a_aspect, &a_cameraStyle[1], a_cameraStyle[4], a_cameraStyle[5], a_cameraStyle[6], a_cameraStyle[7]);
  m_light.build(a_light[0], 1.0f, &a_light[1], a_light[4], a_light[5], a_light[6], a_light[7], true);
  if (debugCameraView)
  {
    m_debugCamera.build(a_debugCamera[0], a_aspect, &a_debugCamera[1], a_debugCamera[4], a_debugCamera[5], a_debugCamera[6], a_debugCamera[7]);
  }
  
#endif

  if (!modelEditing)
  {
    // This continually refreshes the model data from file - probably only want to do this quite periodically
    const int fastRefresh = 50;
    const int normalRefresh = 1000; // @60fps this is every 16s
    static int tick = normalRefresh;
    int refresh = (debugCameraView) ? fastRefresh : normalRefresh;
    if (tick++ >= refresh) {
      // etag checking would make this faster / better
      m_models.reloadDataFromFile("models.png", true);
      tick = 0;
    }
  }
}


void GameGraphics::drawDebug(GameUi::DrawItems& a_uiItems)
{
  if (m_hudEnabled)
  {
    std::string fps = getDebugVariables().at("Average FPS:");
    int fpsi = int(atof(fps.c_str()));
    fps = std::to_string(fpsi);
    a_uiItems.addContrastString(7*75, 90, fps.c_str(), 2);
    a_uiItems.addString(        7*75, 90, fps.c_str(), (fpsi>58)?0xff00ff00:0xff0000ff, 2);
   
    if (showDebugging)
    {
      addDebugString(a_uiItems, 2, 4, "Debugging variables:");
      int line = 3;
      for (auto var : getDebugVariables()) {
        addDebugString(a_uiItems, line, 5, var.first.c_str());
        addDebugString(a_uiItems, line, 7, var.second.c_str());
        line++;
      }
    }

    //Log(LL_Debug, "Doing update with %ld items", items.m_items.size());
    glBindBuffer(GL_ARRAY_BUFFER, m_hudVertexBuffer);
    m_hudItemCount = a_uiItems.m_items.size();
    //if (m_hudItemCount > m_hudVertexBufferSize) {
    glBufferData(GL_ARRAY_BUFFER, sizeof(GameUi::UiVertex)*m_hudItemCount, (void*)&a_uiItems.m_items[0], GL_DYNAMIC_DRAW);
    m_hudVertexBufferSize = m_hudItemCount;
    //} else {
    //  glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(GameUi::UiVertex)*m_hudItemCount, (void*)&hudItems->m_items[0]);
    //}
  }
}


void GameGraphics::drawCube(float x, float y, float z, float w, float h, float d, uint32_t /*color*/)
{
  float pnts[8][4];
  for (int i = 0; i < 8; i++) {
    pnts[i][0] = x + ((i&1) ? w : 0);
    pnts[i][1] = y + ((i&2) ? h : 0);
    pnts[i][2] = z + ((i&4) ? d : 0);
  }
  drawCuboid(pnts);
}


void GameGraphics::drawObjectListHelper(const GameSim::GameObjectList& a_objs, GLsizei siz, bool direction)
{
  // Rules of thumb - 100 to 1000 to 4000 tris per draw call (depends who you ask)
  for (const GameSim::GameObject& obj : a_objs) {
    if ((obj.m_dx <= 0.0) == direction) {
      float x = Math::modulus(obj.m_x + obj.m_dx * m_time, 1000.0f + obj.m_w);
      //if (!isClipped(fabs(fmod(obj.m_x + obj.m_dx * m_time, 1000.0)), obj.m_y, obj.m_z, obj.m_w, obj.m_h, obj.m_d))
      //if (!isClipped(Math::modulus(obj.m_x + obj.m_dx * m_time, 1000.0), obj.m_y, obj.m_z, obj.m_w, obj.m_h, obj.m_d))
      if (!m_camera.isClipped(x, obj.m_y, obj.m_z, obj.m_w, obj.m_h, obj.m_d))
      {
        //glVertexAttrib4f(deltaIndex, obj.m_dx, obj.m_dy, 0.0, 0.0);
        //glVertexAttrib4f(deltaIndex, 0.0, 0.0, 0.0, 0.0);
        float pos[4] = { x, obj.m_y, obj.m_z, 0.0f };
        m_currentContext->m_uniforms.setUniforms();
        glUniform4fv(m_currentContext->m_uniforms.m_translation_index, 1, (GLfloat*)pos);
        glDrawArrays(GL_TRIANGLES, 0, siz); // tri-strips are better
        //m_modelsVertexArrays[obj.m_dx > 0.0 ? a_voxelModelIndex+1 : a_voxelModelIndex].draw();
      }
    }
  }
}


void GameGraphics::drawObjectTypeList(const GameSim::GameObjectList& a_objs, int a_voxelModelIndex)
{
    GLint deltaIndex = VERTEX_ATTRIB_INDEX(PolyVertex, delta);
    glDisableVertexAttribArray(deltaIndex);
  
    glVertexAttrib4f(deltaIndex, 0.0, 0.0, 0.0, 0.0);

    // part of the optimization
    GLsizei siz = GLsizei(m_modelsVertexArrays[a_voxelModelIndex].m_vertexDataSize);
    if (!siz)
      return;
    glBindVertexArrayOES(m_modelsVertexArrays[a_voxelModelIndex].m_vertexArray);
  
    drawObjectListHelper(a_objs, siz, true);

    // part of the optimization
    siz = GLsizei(m_modelsVertexArrays[a_voxelModelIndex+1].m_vertexDataSize);
    if (!siz)
      return;
    glBindVertexArrayOES(m_modelsVertexArrays[a_voxelModelIndex+1].m_vertexArray);

    drawObjectListHelper(a_objs, siz, false);

    if (debugCollisions || debugCameraView)
    {
      for (const GameSim::GameObject& obj : a_objs) {
        float x = Math::modulus(obj.m_x + obj.m_dx * m_time, 1000.0f + obj.m_w);
        bool clipped = m_camera.isClipped(x, obj.m_y, obj.m_z, obj.m_w, obj.m_h, obj.m_d);
        if ((clipped && debugCameraView) || (debugCollisions && x > obj.m_w && x < (1000-obj.m_w)))
          drawCube(x, obj.m_y, obj.m_z, obj.m_w, obj.m_h, obj.m_d, 0x00ff00);
      }
    }


    //glEnableVertexAttribArray(deltaIndex);
    glVertexAttrib4f(deltaIndex, 0.0, 0.0, 0.0, 0.0);
}

void GameGraphics::drawObjectTypeListTiled(const GameSim::GameObjectList& a_objs, int a_voxelModelIndex, int zoff, int wid)
{
    GLint deltaIndex = VERTEX_ATTRIB_INDEX(PolyVertex, delta);
    glDisableVertexAttribArray(deltaIndex);

/*
    if (!shadowPass) {
#if !TARGET_IPHONE_SIMULATOR
      m_polyUniforms.m_mixNoise = (a_voxelModelIndex==13); // if water, mix noise
#endif
      m_polyUniforms.setUniforms();
    }
*/
  
    glVertexAttrib4f(deltaIndex, 0.0, 0.0, 0.0, 0.0);

    // part of the optimization
    GLsizei siz = GLsizei(m_modelsVertexArrays[a_voxelModelIndex].m_vertexDataSize);
    if (!siz)
      return;
    glBindVertexArrayOES(m_modelsVertexArrays[a_voxelModelIndex].m_vertexArray);

    for (const GameSim::GameObject& obj : a_objs) {

      for (int j = 0; j < int(obj.m_h/10.0); j++)
      {
        for (int i = 0; i < 1000/wid; i++)
        {
          //float x = Math::modulus(obj.m_x + 650.0f + obj.m_dx * m_time, 1000.0 + obj.m_w);
          float x = obj.m_x + i*wid;// obj.m_x + 650.0f;
          if (!m_camera.isClipped(x, obj.m_y + j*10.0f + 1.0f, obj.m_z + zoff, wid, obj.m_h, obj.m_d))
          {
/*
            m_polyUniforms.m_translation = vec4attrib{ obj.m_x + i*10.0f, obj.m_y + j*10.0f, obj.m_z + (zoff/3) - 6.0f, 0.0f };
            glVertexAttrib4f(deltaIndex, obj.m_dx, obj.m_dy, 0.0f, 0.0f);
            m_polyUniforms.setUniforms();
            m_modelsVertexArrays[obj.m_dx > 0.0f ? a_voxelModelIndex+1 : a_voxelModelIndex].draw();
*/
            // optimized version which moves invariant out of the loop
            float pos[4] = { x, obj.m_y + j*10.0f + 0.0f, obj.m_z + zoff, 0.0f };
            m_currentContext->m_uniforms.setUniforms();
            glUniform4fv(m_currentContext->m_uniforms.m_translation_index, 1, (GLfloat*)pos);
            glDrawArrays(GL_TRIANGLES, 0, siz); // tri-strips are better
          }
        }
        // float m_w, m_h, m_d;
      }

    }
    //glEnableVertexAttribArray(deltaIndex);
    glVertexAttrib4f(deltaIndex, 0.0, 0.0, 0.0, 0.0);
}


#define setProfileLocation(name) \
    thisTime = GameTime::now(); \
    setDebugValue(name ":", std::string(std::to_string(GameTime::Duration(thisTime - lastTime).count() * 0.000001f) + " ms").c_str()); \
    lastTime = thisTime


void GameGraphics::preDraw(float a_time)
{
  m_time = a_time;

  glDisable(GL_BLEND);
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_CULL_FACE);
  glDepthMask(GL_TRUE);

  // Get some parameters
  GLint vp[4];
  glGetIntegerv(GL_VIEWPORT, vp);
  m_w = vp[2];
  m_h = vp[3];
}


void GameGraphics::drawScene()
{
  m_staticPolyVertexArray.draw();
  setDebugValue("Tri count static:", int(m_staticPolyVertexArray.m_vertexDataSize));

  m_currentContext->m_uniforms.m_translation = vec4attrib{ m_playerX, m_playerY, m_playerZ, 0.0f };
  m_currentContext->m_uniforms.setUniforms();
  m_dynamicPolyVertexArray.draw();

  setDebugValue("Tri count dynamic:", int(m_dynamicPolyVertexArray.m_vertexDataSize));


  /*
  // instancing test
  glUniform4fv(m_polyUniforms.m_translation_index, 100, (GLfloat*)trans);
  glBindVertexArrayOES(m_carRightVertexArray.m_vertexArray);
  glDrawArraysInstancedEXT(GL_TRIANGLES, 0, 100, GLsizei(m_carRightVertexArray.m_vertexDataSize)); // tri-strips are better
   */


#if USE_ONE_BIG_VERTEX_ARRAY

  GLint deltaIndex = VERTEX_ATTRIB_INDEX(PolyVertex, delta);
  glDisableVertexAttribArray(deltaIndex);
  if (!shadowPass) { m_polyUniforms.setUniforms(); }
  glVertexAttrib4f(deltaIndex, 0.0, 0.0, 0.0, 0.0);
  GLsizei siz = GLsizei(m_combinedModelsVertexArrays.m_vertexDataSize);
  glBindVertexArrayOES(m_combinedModelsVertexArrays.m_vertexArray);
  float pos[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
  glUniform4fv((shadowPass) ? m_shadowUniforms.m_translation_index : m_polyUniforms.m_translation_index, 1, (GLfloat*)pos);
  glDrawArrays(GL_TRIANGLES, 0, siz);
  glVertexAttrib4f(deltaIndex, 0.0, 0.0, 0.0, 0.0);

#else

  drawObjectTypeList(m_scene.m_cars,   0);
  // TODO: add buses
  drawObjectTypeList(m_scene.m_bikes,  4);
  drawObjectTypeList(m_scene.m_vans,   6);
  drawObjectTypeList(m_scene.m_trucks, 8);
  drawObjectTypeList(m_scene.m_trains, 10);
  drawObjectTypeList(m_scene.m_trees,  16);

  drawObjectTypeList(m_scene.m_shrubs, 17);
  drawObjectTypeList(m_scene.m_buildings, 18);
  drawObjectTypeList(m_scene.m_rocks, 19);
  drawObjectTypeList(m_scene.m_logs, 20);
  drawObjectTypeList(m_scene.m_boats, 22);
  drawObjectTypeList(m_scene.m_crocs, 24);

  // TODO: below takes time (3ms)
  drawObjectTypeListTiled(m_scene.m_road,   12, 0, 16);
  drawObjectTypeListTiled(m_scene.m_water,  13, 0, 16);
  drawObjectTypeListTiled(m_scene.m_tracks, 14, 0, 15);

#endif
}


void GameGraphics::prepareShadowContext()
{
  glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&m_defaultFramebufferId);
  
  // Render the objects from light POV to make shadow map
#ifndef USE_OPENGLES2
  glDrawBuffer(GL_NONE); // No color buffer is drawn to.
  glReadBuffer(GL_NONE); // No color buffer is read from.
#endif

  //setProfileLocation("Draw Point B0");

  glBindFramebuffer(GL_FRAMEBUFFER, m_shadowFramebufferId);
  glViewport(0, 0, SHADOW_DIM, SHADOW_DIM);

  //setProfileLocation("Draw Point B1");

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  //glClear(GL_DEPTH_BUFFER_BIT);

  //setProfileLocation("Draw Point B2");

  glCullFace(GL_FRONT);

  //glBindVertexArrayOES(m_dynamicPolyVertexArray.m_vertexArray);
  //GLKVector3 lightInvDir = GLKVector3Make(0.5f,2,2);
  // Compute the MVP matrix from the light's point of view
  //const Game::GameObjec-75.0ft& player = m_sim.objectLists().back()->front();

  //setProfileLocation("Draw Point B3");

#ifdef OLD_CAMERA
  /*
  float rotations[3] = { -75.0f, 20.0f, -30.0f };
  // TODO: the light position in the shader(s) and here should line up for better realism
  m_light.build(1.0f, 1.0f, rotations, 0.0175f, m_cameraX-20.0, m_cameraY+45.0, 20.0, true);
   */
#endif

  m_shadowContext.m_program.useProgram();
  memcpy(m_shadowContext.m_uniforms.m_modelViewProjectionMatrix.m, m_light.m_modelViewProjectionMatrix, sizeof(float)*16);
  m_shadowContext.m_uniforms.m_time = m_time;
  m_shadowContext.m_uniforms.m_translation = vec4attrib{ 0.0f, 0.0f, 0.0f, 0.0f };
  m_shadowContext.m_uniforms.setUniforms();
}


void GameGraphics::renderShadows()
{
  if (m_options["enableShadows"])
  {
    prepareShadowContext();
    
    m_currentContext = &m_shadowContext;
    drawScene();
    m_currentContext = &m_normalContext;
    
    // restore original framebuffer and viewport
    glBindFramebuffer(GL_FRAMEBUFFER, m_defaultFramebufferId);
  }
}


void GameGraphics::prepareScreenContext()
{
  //glViewport(vp[0], vp[1], vp[2], vp[3]);
  glViewport(0, 0, m_w, m_h);
  //glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

#ifndef USE_OPENGLES2
  glDrawBuffer(GL_BACK);
  glReadBuffer(GL_NONE);
#endif

  // Render the object again with ES2
  m_normalContext.m_program.useProgram();

  if (debugCameraView)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  else
    glClear(GL_DEPTH_BUFFER_BIT);

  // setProfileLocation("Draw Point C2");

  glCullFace(GL_BACK);

  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, m_shadowTexId);
  glActiveTexture(GL_TEXTURE1);
  glBindTexture(GL_TEXTURE_2D, m_hudTexId);

  // TODO: work out why the shadows 'swim'

  float biasedShadowMVP[16] = { 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.5, 0.5, 0.5, 1.0 };
  Math::multiplyMatrix4x4(m_normalContext.m_uniforms.m_shadowMapMVP.m, biasedShadowMVP, m_light.m_modelViewProjectionMatrix);
  if (debugCameraView)
    m_normalContext.m_uniforms.m_modelViewProjectionMatrix = m_debugCamera.m_modelViewProjectionMatrix;
  else
    m_normalContext.m_uniforms.m_modelViewProjectionMatrix = m_camera.m_modelViewProjectionMatrix;
  m_normalContext.m_uniforms.m_shadowMap = 0;
  m_normalContext.m_uniforms.m_texture = 1;
  m_normalContext.m_uniforms.m_time = m_time;
  m_normalContext.m_uniforms.m_translation = vec4attrib{ 0.0f, 0.0f, 0.0f, 0.0f };
  m_normalContext.m_uniforms.setUniforms();
  /*
 */
}


void GameGraphics::renderScreen()
{
  prepareScreenContext();
  //setProfileLocation("Draw Point C3");
  //glEnable(GL_BLEND);
  //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  drawScene();
  //glDisable(GL_ALPHA_TEST);
  //setProfileLocation("Draw Point C43");
}


void GameGraphics::draw()
{
  GameTime::TimePoint startOfDraw = GameTime::now();
  GameTime::TimePoint lastTime = startOfDraw;
  GameTime::TimePoint thisTime = startOfDraw;

  setProfileLocation("Draw Point A1");

  renderShadows();

  setProfileLocation("Draw Point C0");

  renderScreen();

  /*
  // seems to just clip in screen space
  int p = 0;
  GLdouble plane[4] = { m_clipPlanes[p][0], m_clipPlanes[p][1], m_clipPlanes[p][2], m_clipPlanes[p][3] };
  glClipPlane(GL_CLIP_PLANE0, plane);
  glEnable(GL_CLIP_PLANE0);
   */


  if (debugCameraView)
  {
    float frustumPts[8][4];
    m_camera.calcFrustum(frustumPts);
    drawCuboid(frustumPts);
    m_light.calcFrustum(frustumPts);
    drawCuboid(frustumPts);
  }

  
  if (m_options["enableShadows"])
  {
    glBindFramebuffer(GL_FRAMEBUFFER, m_shadowFramebufferId);
    //glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    //glClear(GL_DEPTH_BUFFER_BIT);
    const GLenum discards[] = {GL_DEPTH_ATTACHMENT};
#ifndef USE_OPENGLES2
    glInvalidateFramebuffer(GL_READ_FRAMEBUFFER, 1, discards);
#else
    glDiscardFramebufferEXT(GL_READ_FRAMEBUFFER_APPLE, 1, discards);
    //glDiscardFramebufferEXT(GL_DRAW_FRAMEBUFFER_APPLE, 1, discards);
#endif
    // restore original framebuffer and viewport
    glBindFramebuffer(GL_FRAMEBUFFER, m_defaultFramebufferId);
    //glViewport(vp[0], vp[1], vp[2], vp[3]);
  }

  //glFlush();
  //glFinish();
}


void GameGraphics::prepareHUDContext()
{
  glDisable(GL_CULL_FACE);
  glDisable(GL_DEPTH_TEST);
  glDepthMask(GL_FALSE);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_DST_COLOR);
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, m_hudTexId);
  m_hudProgram.useProgram();
  glUniform1i(uniforms[UNIFORM_HUD_TEXTURE], 0);

  //glUniform2f(uniforms[UNIFORM_HUD_INV_SCREEN], 2.0f / m_w, -2.0f / m_h);
  glUniform2f(uniforms[UNIFORM_HUD_INV_SCREEN], 2.0f / 640, -2.0f / 960);
}


void GameGraphics::drawHUD()
{
  if (m_hudEnabled)
  {
    prepareHUDContext();
    glBindVertexArrayOES(m_hudVertexArray);
    glDrawArrays(GL_TRIANGLES, 0, GLsizei(m_hudItemCount));

    glDisable(GL_BLEND);
    glDepthMask(GL_TRUE);
    glEnable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);
  }
}


void GameGraphics::drawAuxItems(const GameUi::DrawItems& a_uiItems)
{
  prepareHUDContext();

  glBindVertexArrayOES(m_auxVertexArray);
  glBindBuffer(GL_ARRAY_BUFFER, m_auxVertexBuffer);
  size_t itemCount = a_uiItems.m_items.size();
  glBufferData(GL_ARRAY_BUFFER, sizeof(GameUi::UiVertex)*itemCount, (void*)&a_uiItems.m_items[0], GL_DYNAMIC_DRAW);
  glDrawArrays(GL_TRIANGLES, 0, GLsizei(itemCount));

  glDisable(GL_BLEND);
  glDepthMask(GL_TRUE);
  glEnable(GL_CULL_FACE);
  glEnable(GL_DEPTH_TEST);
}


#include <functional>

void GameGraphics::drawViews(const std::vector<GameUi::View*>& a_views)
{
  bool prepared = false;
  for (GameUi::View* view : a_views)
  {
    if (m_viewMap.count(view->m_view)) {
      if (!prepared) {
        prepareScreenContext();
        prepared = true;
      }
      //Log(LL_Info, "drawing view: %s", view->m_view.c_str());
      GLint vp[4];
      glGetIntegerv(GL_VIEWPORT, vp);
      glViewport(view->m_geometry.x, vp[3]-(view->m_geometry.y+view->m_geometry.h), view->m_geometry.w, view->m_geometry.h);
      m_w = view->m_geometry.w;
      m_h = view->m_geometry.h;
      m_viewMap[view->m_view]();
      glViewport(vp[0], vp[1], vp[2], vp[3]);
      m_w = vp[2];
      m_h = vp[3];
    }
  }
}


void GameGraphics::drawRotatingFrog()
{
  // Rotating model in center
  static float zr = 0.0f;
  float modelScale = 8.0f;
  float aspectRatio = (m_h) ? (float(m_w)/float(m_h)) : 1.0;
  float isoCameraStyle[8] = { 25.0f, -60.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f }; // Crossy Road Style - nearly isometric / orthographic
  Camera camera;

  camera.build(25.0f, aspectRatio, &isoCameraStyle[1], 0.005f, 0.0f, 0.0f, 0.0f);
  float baseModelViewMatrix[16];
  Math::makeIdentityMatrix4x4(baseModelViewMatrix);
  Math::translateMatrix4x4(baseModelViewMatrix, 0.0f, 0.0f, -3.5f);
  float modelViewMatrix[16];
  Math::multiplyMatrix4x4(modelViewMatrix, baseModelViewMatrix, camera.m_cameraMatrix);
  Math::translateMatrix4x4(modelViewMatrix, 0,  35, -35);
  Math::rotateMatrix4x4(modelViewMatrix, zr, 0.0f, 0.0f, 1.0f);
  Math::translateMatrix4x4(modelViewMatrix, -350.0f, -28.0f, 0);
  Math::scaleMatrix4x4(modelViewMatrix, modelScale, modelScale, modelScale);

  Math::multiplyMatrix4x4(m_normalContext.m_uniforms.m_modelViewProjectionMatrix.m, camera.m_projectionMatrix, modelViewMatrix);
  m_normalContext.m_uniforms.m_translation = vec4attrib{ 40, 0, 0, 0.0f };
  m_normalContext.m_uniforms.setUniforms();

  // draw the frog model
  m_dynamicPolyVertexArray.draw();

  zr += 0.03f;
}


void GameGraphics::drawModelEditLayer()
{
  // Rotating model in center
  static float zr = 10.0f;//-0.5f;
  float modelScale = 4.0f;
  float aspectRatio = (m_h) ? (float(m_w)/float(m_h)) : 1.0;
  float isoCameraStyle[8] = { 25.0f, -60.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f }; // Crossy Road Style - nearly isometric / orthographic
  int idx = Math::modulus(m_editModel, MODEL_COUNT);
  ModelInfo &mi = g_modelInfos[idx];

  Camera camera;
  camera.build(25.0f, aspectRatio, &isoCameraStyle[1], 0.005f, 0.0f, 0.0f, 0.0f);
  Math::translateMatrix4x4(camera.m_cameraMatrix, 0,  200+154+35, -55-55-55-55-5 );
  Math::rotateMatrix4x4(camera.m_cameraMatrix, zr, 0.0f, 0.0f, 1.0f);
  Math::translateMatrix4x4(camera.m_cameraMatrix, -mi.w * 0.5*modelScale, -mi.d * 0.5*modelScale, -mi.h * 0.5*modelScale);
  Math::scaleMatrix4x4(camera.m_cameraMatrix, modelScale, modelScale, modelScale);
  Math::multiplyMatrix4x4(m_normalContext.m_uniforms.m_modelViewProjectionMatrix.m, camera.m_projectionMatrix, camera.m_cameraMatrix);
  m_normalContext.m_uniforms.m_translation = vec4attrib{ 0, 0, 0, 0 };
  m_normalContext.m_uniforms.setUniforms();

  int editLayer = (mi.h-1) - int((mi.h-1) * m_editLayer + 0.0);
  
  //if (m_editModelDirty)
  {
    loadVoxelModel(m_editModelVertexArray1, mi.name, mi.x, mi.y, mi.w, mi.h, mi.d, mi.direction, mi.scaling, true, 0, editLayer+1);
    m_editModelVertexArray1.update();
  }
  
  glClear(GL_DEPTH_BUFFER_BIT);
  glEnable(GL_DEPTH_TEST);
  glDepthMask(GL_TRUE);
  //m_editModelVertexArray1.sync();
  //m_modelsVertexArrays[idx].update();
  m_editModelVertexArray1.draw();

  if (editLayer <= mi.h-1)
  {
    //if (m_editModelDirty)
    {
      loadVoxelModel(m_editModelVertexArray2, mi.name, mi.x, mi.y, mi.w, mi.h, mi.d, mi.direction, mi.scaling, true, editLayer+1, 256);
      //m_editModelVertexArray2.sync();
    }
    glDisable(GL_DEPTH_TEST);
    glDepthMask(GL_FALSE);
    glDisable(GL_CULL_FACE);
    glEnable(GL_BLEND);
/*
    glBlendFunc(GL_ONE_MINUS_CONSTANT_ALPHA, GL_CONSTANT_ALPHA);
    glBlendColor(0.7, 0.7, 0.7, 0.95);
*/
/*
#define GL_SRC_COLOR                      0x0300
#define GL_ONE_MINUS_SRC_COLOR            0x0301
#define GL_SRC_ALPHA                      0x0302
#define GL_ONE_MINUS_SRC_ALPHA            0x0303
#define GL_DST_ALPHA                      0x0304
#define GL_ONE_MINUS_DST_ALPHA            0x0305
#define GL_DST_COLOR                      0x0306
#define GL_ONE_MINUS_DST_COLOR            0x0307
#define GL_SRC_ALPHA_SATURATE             0x0308
#define GL_CONSTANT_COLOR                 0x8001
#define GL_ONE_MINUS_CONSTANT_COLOR       0x8002
#define GL_CONSTANT_ALPHA                 0x8003
#define GL_ONE_MINUS_CONSTANT_ALPHA       0x8004
*/

    glBlendFuncSeparate(GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA, GL_CONSTANT_ALPHA);
    glBlendColor(0.7, 0.7, 0.7, 0.05);

    //m_editModelVertexArray2.sync();
    m_editModelVertexArray2.update();
    m_editModelVertexArray2.draw();

    glBlendColor(0.7, 0.7, 0.7, 0.15);
    drawSolidCuboid( 0, 0, editLayer+1, mi.w, mi.d, 0.001, 0x3f3fff, 1.0);
    glDisable(GL_BLEND);

    glEnable(GL_CULL_FACE);
  }
  
  m_editModelDirty = false;

/*
  //m_models.editPixel(mi.x, mi.y, 0);
  //m_models.invalidateCache();
  //loadVoxelModel(m_modelsVertexArrays[idx], mi.name, mi.x, mi.y, mi.w, mi.h, mi.d, mi.direction, mi.scaling, true);
*/


 /* 
  float cuboid[8][4] = {
    { float(   0), float(   0), float(editLayer  ), 0.0 },
    { float(   0), float(mi.d), float(editLayer  ), 0.0 },
    { float(mi.w), float(   0), float(editLayer  ), 0.0 },
    { float(mi.w), float(mi.d), float(editLayer  ), 0.0 },
    { float(   0), float(   0), float(editLayer+1), 0.0 },
    { float(   0), float(mi.d), float(editLayer+1), 0.0 },
    { float(mi.w), float(   0), float(editLayer+1), 0.0 },
    { float(mi.w), float(mi.d), float(editLayer+1), 0.0 },
  };

  drawCuboid(cuboid);
  */
}


void GameGraphics::drawCaroselTest()
{
  // Rotating model in center
  static float zr = 0.0f;
  float modelScale = 0.2f;
  float aspectRatio = (m_h) ? (float(m_w)/float(m_h)) : 1.0;
  float isoCameraStyle[8] = { 25.0f, -60.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f }; // Crossy Road Style - nearly isometric / orthographic

  glClear(GL_DEPTH_BUFFER_BIT);
  glEnable(GL_DEPTH_TEST);
  glDepthMask(GL_TRUE);

  //int mIdx = Math::modulus(m_editModel, MODEL_COUNT);
  int mIdx = m_editModel;
  static int lastEditModel = mIdx;
  static int savedLastEditModel = mIdx;
  static int frame = 0;

  if (mIdx != lastEditModel) {
    savedLastEditModel = lastEditModel;
    lastEditModel = mIdx;
    frame = 0;
  } else {
    frame++;
    if (frame > 50) {
      frame = 50;
    }
  }

  int f = frame;
  if (savedLastEditModel < lastEditModel) {
    f = -f + 25;
  } else {
    f = f - 75;
  }

  for (int i = 0; i < MODEL_COUNT; i++)
  {
    const float radius = 250.0f;
    float radian = (float(i) + 0.5 + (f/50.0)) * 2 * 3.14 / MODEL_COUNT - 0.5*3.14;
    int idx = Math::modulus(i + m_editModel, MODEL_COUNT);
    float x = cos(radian) * radius;
    float y = (1.0 + sin(radian)) * radius;
    int curIdx = Math::modulus(mIdx, MODEL_COUNT);
    int lastIdx = Math::modulus(savedLastEditModel, MODEL_COUNT);
    int f2 = 0;
    if ( idx == curIdx ) {
      f2 = frame;
    } else if ( idx == lastIdx ) {
      f2 = 50 - frame;
    }
    float zRot = Math::modulus(zr,3.14f*2) * (f2/50.0);
    y = y * ((50-f2)/50.0) - f2;
    modelScale = 0.2 + 0.8 * (f2/50.0);

    //Log(LL_Info, "model idx: %i", idx);
    ModelInfo &mi = g_modelInfos[idx];
    Camera camera;
    camera.build(28.0f, aspectRatio, &isoCameraStyle[1], 0.005f, 0.0f, 0.0f, 0.0f);
    Math::rotateMatrix4x4(camera.m_cameraMatrix, -0.25, 1.0f, 0.0f, 0.0f);
    Math::translateMatrix4x4(camera.m_cameraMatrix, x*0.2,  100+200+154+35 + y*modelScale, -105-15 );
    Math::rotateMatrix4x4(camera.m_cameraMatrix, zRot, 0.0f, 0.0f, 1.0f);
    Math::translateMatrix4x4(camera.m_cameraMatrix, (-100*modelScale) -mi.w * 0.5*modelScale, -mi.d * 0.5*modelScale, -mi.h * 0.5*modelScale);
    Math::scaleMatrix4x4(camera.m_cameraMatrix, modelScale, modelScale, modelScale);
    Math::multiplyMatrix4x4(m_normalContext.m_uniforms.m_modelViewProjectionMatrix.m, camera.m_projectionMatrix, camera.m_cameraMatrix);
    m_normalContext.m_uniforms.m_translation = vec4attrib{ 100, 0, -5, 0 };
    m_normalContext.m_uniforms.setUniforms();
    
    //loadVoxelModel(m_modelsVertexArrays[idx], mi.name, mi.x, mi.y, mi.w, mi.h, mi.d, mi.direction, mi.scaling, false);
    //m_modelsVertexArrays[idx].update();
    
    m_modelsVertexArrays[idx].draw();
  }

  zr += 0.03f;
}


void GameGraphics::drawModelRotatingPreview()
{
  // Rotating model in center
  static float zr = 0.0f;
  float modelScale = 4.0f;
  float aspectRatio = (m_h) ? (float(m_w)/float(m_h)) : 1.0;
  float isoCameraStyle[8] = { 25.0f, -60.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f }; // Crossy Road Style - nearly isometric / orthographic
  int idx = Math::modulus(m_editModel, MODEL_COUNT);
  ModelInfo &mi = g_modelInfos[idx];

  Camera camera;
  camera.build(25.0f, aspectRatio, &isoCameraStyle[1], 0.005f, 0.0f, 0.0f, 0.0f);
  Math::translateMatrix4x4(camera.m_cameraMatrix, 0,  200+154+35, -55-55-55-55 );
  Math::rotateMatrix4x4(camera.m_cameraMatrix, zr, 0.0f, 0.0f, 1.0f);
  Math::translateMatrix4x4(camera.m_cameraMatrix, -mi.w * 0.5*modelScale, -mi.d * 0.5*modelScale, -mi.h * 0.5*modelScale);
  Math::scaleMatrix4x4(camera.m_cameraMatrix, modelScale, modelScale, modelScale);
  Math::multiplyMatrix4x4(m_normalContext.m_uniforms.m_modelViewProjectionMatrix.m, camera.m_projectionMatrix, camera.m_cameraMatrix);
  m_normalContext.m_uniforms.m_translation = vec4attrib{ 0, 0, 0, 0 };
  m_normalContext.m_uniforms.setUniforms();

  //loadVoxelModel(m_modelsVertexArrays[idx], mi.name, mi.x, mi.y, mi.w, mi.h, mi.d, mi.direction, mi.scaling, false);
  glClear(GL_DEPTH_BUFFER_BIT);
  glEnable(GL_DEPTH_TEST);
  glDepthMask(GL_TRUE);
  //m_modelsVertexArrays[idx].sync();
  //m_modelsVertexArrays[idx].update();
  m_modelsVertexArrays[idx].draw();

  setDebugValue("ModelTris:", int(m_modelsVertexArrays[idx].m_vertexDataSize/3));
  setDebugValue("ModelVerts:", int(m_modelsVertexArrays[idx].m_vertexDataSize));

  zr += 0.03f;
}


void GameGraphics::getEditObjectPixels(float a_editLayer, std::vector<uint32_t>& pixels, int& w, int& h, int& d)
{
  int idx = Math::modulus(m_editModel, MODEL_COUNT);
  ModelInfo &mi = g_modelInfos[idx];
  int editLayer = (mi.h-1) - int((mi.h-1) * a_editLayer + 0.0);
  w = mi.w;
  h = mi.d;
  d = mi.h;
  pixels.clear();
  pixels.resize(w*h);
  for (int i = 0; i < w*h; i++)
    pixels[i] = 0x3f000000;

  for (int y = 0; y < h; y++)
    for (int x = 0; x < w; x++) {
      int z = editLayer;
      uint32_t col = m_models.getPixel( mi.x+x, mi.y+(y*mi.h)+(15-z));
      pixels[y*w+x] = col;
    }
/*
  for (unsigned i = 0; i < voxelModel.size(); i++)
    if (voxelModel.at(i).m_z == editLayer)
    {
      uint32_t col = voxelModel.at(i).m_color;
      pixels[voxelModel.at(i).m_y * w + voxelModel.at(i).m_x] = ((col&0xff)<<16)|(col&0xff00)|((col>>16)&0xff)|0xff000000;
    }
*/
}


void GameGraphics::setEditObjectPixels(float a_editLayer, const std::vector<uint32_t>& pixels, int w, int h)
{
  int idx = Math::modulus(m_editModel, MODEL_COUNT);
  ModelInfo &mi = g_modelInfos[idx];
  int editLayer = (mi.h-1) - int((mi.h-1) * a_editLayer + 0.0);
  for (int y = 0; y < h; y++)
    for (int x = 0; x < w; x++) {
      int z = editLayer;
      uint32_t col = pixels[y*w+x];
      if (col == 0x3f000000)
        col = 0;
      m_models.editPixel( mi.x+x, mi.y+(y*mi.h)+(15-z), col);
    }
  m_models.invalidateCache();
}


void GameGraphics::getPalettePixels(std::vector<uint32_t>& pixels)
{
  pixels.clear();
  pixels.resize(256);
  for (int i = 0; i < 256; i++)
    pixels[i] = m_models.getPixel(255-(i%16), 255-15+(i/16)) | 0xff000000;
}


void GameGraphics::drawCuboid(float cuboid[8][4])
{
  m_normalContext.m_uniforms.m_translation = vec4attrib{ 0, 0, 0, 0.0f };
  m_normalContext.m_uniforms.setUniforms();
  const int lineSegments[12][2] = {{0,1},{1,3},{3,2},{2,0}, {4,5},{5,7},{7,6},{6,4}, {0,4},{1,5},{3,7},{2,6}};
  std::vector<PolyVertex> &objVerts = m_cuboidVertexList.m_vertexData;
  objVerts.resize(24);
  for (int i = 0; i < 12; i++) {
    for (int p = 0; p < 2; p++) {
      memset(&objVerts[i*2+p], 0, sizeof(PolyVertex));
      objVerts[i*2+p].m_color.r = 1.0;
      objVerts[i*2+p].m_color.g = 0.5;
      objVerts[i*2+p].m_color.b = 0.5;
      objVerts[i*2+p].m_color.a = 1.0;
      objVerts[i*2+p].m_position.x = cuboid[lineSegments[i][p]][0];
      objVerts[i*2+p].m_position.y = cuboid[lineSegments[i][p]][1];
      objVerts[i*2+p].m_position.z = cuboid[lineSegments[i][p]][2];
    }
  }
  m_cuboidVertexList.update();
  glBindVertexArrayOES(m_cuboidVertexList.m_vertexArray);
  glDrawArrays(GL_LINES, 0, 24);
}


void GameGraphics::drawSolidCuboid(float x, float y, float z, float w, float h, float d, uint32_t col, float scale)
{
  m_normalContext.m_uniforms.m_translation = vec4attrib{ 0, 0, 0, 0.0f };
  m_normalContext.m_uniforms.setUniforms();
  std::vector<PolyVertex> &objVerts = m_cuboidVertexList.m_vertexData;
  objVerts.resize(12 * 3);
  PolyVertex* cubePtr = &objVerts[0];
  memcpy(cubePtr, gBaseCubeVertexData, vertSize*vertsPerCube);
  VoxelBox obj = { x, y, z, 0.0, w, d, h, col };
  initCubeVerticesFromVoxel(cubePtr, obj, scale);
  for (int v = 0; v < 36; v++)
    cubePtr->m_color.a = 0.9;
  m_cuboidVertexList.update();
  glBindVertexArrayOES(m_cuboidVertexList.m_vertexArray);
  //glDrawArrays(GL_LINES, 0, 24);
  glDrawArrays(GL_TRIANGLES, 0, 36);
}