-- vs.lua
-- A first-order particle motion system, takes a time as input
-- and simply calculates point positions using a vertex shader.

local ffi = require("ffi")
local sf = require("util.shaderfunctions2")

require("util.objfile")

-- Naming this table 'vs' causes errors. Not sure why.
verts = {}
verts.__index = verts

function verts.new(...)
    local self = setmetatable({}, verts)
    if self.init ~= nil and type(self.init) == "function" then
        self:init(...)
    end 
    return self
end

local vert_header = [[
#version 310 es

in vec4 vPosition;
in vec4 vColor;
in vec4 vTexCoord;

out vec3 vfColor;
out vec3 vfTexCoord;
out vec3 vfWorldPos;
#line 1
]]

local vert_body = [[
uniform mat4 modelMtx;
uniform mat4 viewMtx;
uniform mat4 projMtx;

uniform mediump float time;
uniform int numParticles;
uniform float forward;

mat3 rotmat(vec3 v, float angle)
{
    float c = cos(angle);
    float s = sin(angle);
    
    return mat3(c + (1.0 - c) * v.x * v.x, (1.0 - c) * v.x * v.y - s * v.z, (1.0 - c) * v.x * v.z + s * v.y,
        (1.0 - c) * v.x * v.y + s * v.z, c + (1.0 - c) * v.y * v.y, (1.0 - c) * v.y * v.z - s * v.x,
        (1.0 - c) * v.x * v.z - s * v.y, (1.0 - c) * v.y * v.z + s * v.x, c + (1.0 - c) * v.z * v.z
        );
}

// Draw an origin-centered grid
// Returns per-instance offset, scaled by 1.
vec3 getGridOffset(int numInstances, int instanceId)
{
    int side = 12;
    int rings = 14;
    int concrings = side * rings;
    int numRings = numInstances / concrings;

    int spoke = instanceId % side;
    int cyl = (instanceId/side) % rings;
    int wheel = (instanceId/concrings);

    return vec3(
       float(spoke)/float(side),
       float(cyl)/float(rings),
       float(wheel)/float(numRings)
        );
}

float phase = time;

void main()
{
    int index = gl_InstanceID;
    const float TWOPI = 2. * acos(-1.);

    float stepSz = 1.;
    vec3 grid = getGridOffset(numParticles, index);
    float th = TWOPI * grid.x + (.3*sin(3.6*grid.y+.7*phase));
    vec2 rt = 2.*(1.+4.*grid.y) * vec2(sin(th), cos(th));

    vec2 off = stepSz * rt;
    mat3 instMat = rotmat(vec3(0.,0.,1.), TWOPI*grid.x);

    float zoff = grid.z;
    // move towards camera
    float speed = .14;
    zoff -= speed * forward;
    zoff = -fract(zoff);//-.5;
    
    float tunnelLength = 80.;
    vec3 position = vec3(off.x, off.y, tunnelLength*zoff);

    // perturb tube
    //position.xy += 1.*sin(10.*grid.y+4.*forward + 1.*(grid.z+forward));
    float ph = 30.*grid.z;
    position.xy += .5*vec2(sin(ph), cos(ph));
    
    mat4 mvpMatrix = projMtx * viewMtx * modelMtx;
    mat4 vpMatrix = projMtx * viewMtx;
    mat3 normMtx = mat3(modelMtx) * instMat;

    vec4 mPos = vec4(instMat * (vPosition.xyz-vec3(.5)), 1.);
    mPos.xyz *= .51*(1.-grid.y);

    vfColor = abs(normMtx * vColor.rgb);
    vfTexCoord = vec3(1.);//vTexCoord.xyz;
    vec3 wpos = (vec4(.31*(mPos.xyz),1.)).xyz + .3*position.xyz;
    vfWorldPos = (modelMtx * vec4(wpos,1.)).xyz;
    gl_Position = mvpMatrix * vec4(wpos,1.);
}

]]

local basic_frag = [[
#version 310 es

#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif

in vec3 vfColor;
in vec3 vfTexCoord;
out vec4 fragColor;

void main()
{
    fragColor = vec4(vfColor, 1.0);
}
]]

local texcoord_frag = [[
#version 310 es

#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif

in vec3 vfColor;
in vec3 vfTexCoord;
out vec4 fragColor;

void main()
{
    fragColor = vec4(vfTexCoord+.001*vfColor, 1.0);
}
]]

local lighting_frag = [[
#version 310 es

#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif

uniform float time;
uniform float fogDensity;
uniform vec4 fogColor;

in vec3 vfWorldPos;
in vec3 vfColor; // normal
in vec3 vfTexCoord;

out vec4 fragColor;

float shininess = 125.;

vec3 directionalLight(vec3 normal)
{
    vec3 lightDir = normalize(vec3(0.,0.,1.));
    float intensity = max(dot(normal,lightDir), 0.);

#if 1
    float spec = 0.;
    if (intensity > 0.)
    {
        vec3 h = normalize(lightDir);
        float intSpec = max(dot(h,normal), 0.);
        spec = pow(intSpec, shininess);
    }
    return (intensity + spec)*vfTexCoord;
#endif    
}

vec3 pointLight(vec3 pos, vec3 normal)
{
    float r = 12.5;
    float speed = 2.;
    //vec3 lightPos = vec3(r*sin(speed*time), 5.*abs(sin(7.*time)), r*cos(speed*time));
    //vec3 lightPos = vec3(0., 4.*abs(sin(time)), 0.);
    vec3 lightPos = vec3(0., 0., 10.);

    vec3 lightDir = normalize(vec3(lightPos - pos));
    vec3 eye = vec3(0.);

    float spec = 0.;

    float intensity = max(dot(normal,lightDir), 0.);
    if (intensity > 0.)
    {
        vec3 h = normalize(lightDir + eye);
        float intSpec = max(dot(h,normal), 0.);
        spec = pow(intSpec, shininess);
    }
 
    return (intensity + spec)*vfTexCoord;
}

vec3 spotLight(vec3 pos, vec3 normal)
{
    float r = 12.5;
    float speed = 2.;
    //vec3 lightPos = vec3(r*sin(speed*time), 15., r*cos(speed*time));
    //vec3 lightPos = vec3(0., 4.*abs(sin(time)), 0.);
    vec3 lightPos = vec3(10., 15., 10.);

    vec3 spotDir = normalize(vec3(0.,-.0,1.));
    vec3 lightDir = normalize(vec3(lightPos - pos));
    vec3 eye = vec3(0.);

    float spec = 0.;

    float intensity = 0.;
    float spotCutoff = .75;
    if (dot(-spotDir,lightDir) > spotCutoff)
    {
        intensity = max(dot(normal,lightDir), 0.);
        if (intensity > 0.)
        {
            vec3 h = normalize(lightDir + eye);
            float intSpec = max(dot(h,normal), 0.);
            spec = pow(intSpec, shininess);
        }
    }
    return (intensity + spec)*vfTexCoord;
}

void main()
{
    vec3 vWorldPos = vfWorldPos;
    vec3 vVertCol = vfColor; // normal
    vVertCol += .001*vfTexCoord;

    vec3 normal = normalize(vVertCol);

    vec4 sceneCol = vec4(directionalLight(normal), 1.);
    //vec4 sceneCol = vec4(pointLight(vWorldPos, normal), 1.);
    //vec4 sceneCol = vec4(spotLight(vWorldPos, normal), 1.);

    // Fog
    // http://www.ozone3d.net/tutorials/glsl_fog/p04.php
    float density = fogDensity;
    const float LOG2 = 1.442695;
    float z = gl_FragCoord.z / gl_FragCoord.w;
    float fogFactor = exp2( -density * density * z * z * LOG2 );
    fogFactor = clamp(fogFactor, 0.0, 1.0);
    fragColor = mix(fogColor, sceneCol, fogFactor );
}
]]

local texlight_frag = [[
#version 310 es

#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif

uniform float time;
uniform float fogDensity;
uniform sampler2D teximage;

in vec3 vfWorldPos;
in vec3 vfColor; // normal
in vec3 vfTexCoord;

out vec4 fragColor;

float shininess = 125.;

vec3 directionalLight(vec3 normal)
{
    vec3 lightDir = normalize(vec3(0.,0.,1.));
    float intensity = max(dot(normal,lightDir), 0.);

    float spec = 0.;
    if (intensity > 0.)
    {
        vec3 h = normalize(lightDir);
        float intSpec = max(dot(h,normal), 0.);
        spec = pow(intSpec, shininess);
    }
    return vec3(intensity + spec);
}

void main()
{
    vec3 vWorldPos = vfWorldPos;
    vec3 vVertCol = vfColor; // normal
    vVertCol += .001*vfTexCoord;

    vec3 normal = normalize(vVertCol);
    vec4 sceneCol = vec4(directionalLight(normal), 1.);
    fragColor = texture(teximage, vfTexCoord.xy) * sceneCol;
}
]]

function verts:init()
    self.vao = 0
    self.vbos = {}
    self.prog = 0
    self.npts = 0
    self.time = 0

    self.src = vert_body
    self.descr = "Immediate particle system"
    self.shadertype = "vertex"
    self.dynamic_scale = true
    self.num_tris = 0
    self.chosen_frag = lighting_frag
    self.obj = nil
    self.fogDensity = .25
    self.effectGain = 0.8
    self.clearColor = {1,.8,.3,1}
    self.heatup = 1
    self.forward = 1
end

function verts:setDataDirectory(dir)
    self.data_dir = dir
end

function verts:stateString()
    return self.npts..' points'
end

function verts:buildShader(src)
    -- Do we need to bind vao to compile a program?
    --gl.glBindVertexArray(self.vao)
    gl.glDeleteProgram(self.prog)

    self.prog,err = sf.make_shader_from_source({
        vsrc = vert_header..src,
        fsrc = self.chosen_frag,
        })
    if err then print(err) end
    --gl.glBindVertexArray(0)
    return err
end

local vertex_lists = {
    tetra = {
        -- TODO: separate
        verts = {
            0,2.598,0,
            0,0,1.732,
            -1.5,0,-.866,
            1.5,0,-.866,
        },
        norms = {
            1,1,1,
            1,0,0,
            0,1,0,
            0,0,1,
        },
        texs = {
            0,0,0,
            1,0,0,
            0,1,0,
            0,0,1,
        },
        tris = {
            0,1,2,
            0,2,3,
            0,1,3,
            1,3,2,
        },
    },
    square = {
        verts = {
            0,0,0,
            1,0,0,
            1,1,0,
            0,1,0,
        },
        norms = {
        --[[0,0,1,
            1,0,1,
            1,1,1,
            0,1,0,]]
            0,0,1,
            0,0,1,
            0,0,1,
            0,0,1,
        },
        texs = {
            0,0,0,
            1,0,0,
            1,1,0,
            0,1,0,
        },
        tris = {
            0,1,2,
            0,2,3,
        },
    },
    halfcube = {
        verts = {
            0,0,0,
            1,0,0,
            1,1,0,
            0,1,0,

            0,0,0,
            1,0,0,
            1,0,1,
            0,0,1,

            0,0,0,
            0,1,0,
            0,1,1,
            0,0,1,
        },
        norms = {
            0,0,1,
            0,0,1,
            0,0,1,
            0,0,1,

            0,1,0,
            0,1,0,
            0,1,0,
            0,1,0,

            1,0,0,
            1,0,0,
            1,0,0,
            1,0,0,
        },
        texs = {
            0,0,0,
            1,0,0,
            1,1,0,
            0,1,0,

            0,0,0,
            1,0,0,
            1,0,1,
            0,0,1,

            0,0,0,
            0,1,0,
            0,1,1,
            0,0,1,
        },
        tris = {
            0,1,2,
            0,2,3,
            4,5,6,
            4,6,7,
            8,9,10,
            8,10,11,
        },
    },
    cube = {
        verts = {
            0,0,0,
            1,0,0,
            1,1,0,
            0,1,0,

            0,0,0,
            1,0,0,
            1,0,1,
            0,0,1,

            0,0,0,
            0,1,0,
            0,1,1,
            0,0,1,

            0,0,1,
            1,0,1,
            1,1,1,
            0,1,1,

            0,1,0,
            1,1,0,
            1,1,1,
            0,1,1,

            1,0,0,
            1,1,0,
            1,1,1,
            1,0,1,
        },
        norms = {
            0,0,1,
            0,0,1,
            0,0,1,
            0,0,1,

            0,1,0,
            0,1,0,
            0,1,0,
            0,1,0,

            1,0,0,
            1,0,0,
            1,0,0,
            1,0,0,
            -- negative, whatever
            0,0,1,
            0,0,1,
            0,0,1,
            0,0,1,

            0,1,0,
            0,1,0,
            0,1,0,
            0,1,0,

            1,0,0,
            1,0,0,
            1,0,0,
            1,0,0,
        },
        texs = {
            0,0,0,
            1,0,0,
            1,1,0,
            0,1,0,
            
            0,0,0,
            1,0,0,
            1,1,0,
            0,1,0,
            
            0,0,0,
            1,0,0,
            1,1,0,
            0,1,0,
            
            0,0,0,
            1,0,0,
            1,1,0,
            0,1,0,
            
            0,0,0,
            1,0,0,
            1,1,0,
            0,1,0,
            
            0,0,0,
            1,0,0,
            1,1,0,
            0,1,0,
        },
        tris = {
            0,1,2,
            0,2,3,
            4,5,6,
            4,6,7,
            8,9,10,
            8,10,11,
            12,13,14,
            12,14,15,
            16,17,18,
            16,18,19,
            20,21,22,
            20,22,23,
        },
    }
}


-- TODO: Load obj for this
function verts:initSingleInstanceAttributes(dat)
    local glIntv = ffi.typeof('GLint[?]')
    local glUintv = ffi.typeof('GLuint[?]')
    local glFloatv = ffi.typeof('GLfloat[?]')

    local verts = glFloatv(#dat.verts, dat.verts)
    local cols = glFloatv(#dat.norms, dat.norms)
    local texs = glFloatv(#dat.texs, dat.texs)

    local vpos_loc = gl.glGetAttribLocation(self.prog, "vPosition")
    local vcol_loc = gl.glGetAttribLocation(self.prog, "vColor")
    local vtex_loc = gl.glGetAttribLocation(self.prog, "vTexCoord")

    local vvbo = glIntv(0)
    gl.glGenBuffers(1, vvbo)
    gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vvbo[0])
    gl.glBufferData(GL.GL_ARRAY_BUFFER, ffi.sizeof(verts), verts, GL.GL_STATIC_DRAW)
    gl.glVertexAttribPointer(vpos_loc, 3, GL.GL_FLOAT, GL.GL_FALSE, 0, nil)
    table.insert(self.vbos, vvbo)

    local cvbo = glIntv(0)
    gl.glGenBuffers(1, cvbo)
    gl.glBindBuffer(GL.GL_ARRAY_BUFFER, cvbo[0])
    gl.glBufferData(GL.GL_ARRAY_BUFFER, ffi.sizeof(cols), cols, GL.GL_STATIC_DRAW)
    gl.glVertexAttribPointer(vcol_loc, 3, GL.GL_FLOAT, GL.GL_FALSE, 0, nil)
    table.insert(self.vbos, cvbo)

    local tvbo = glIntv(0)
    gl.glGenBuffers(1, tvbo)
    gl.glBindBuffer(GL.GL_ARRAY_BUFFER, tvbo[0])
    gl.glBufferData(GL.GL_ARRAY_BUFFER, ffi.sizeof(texs), texs, GL.GL_STATIC_DRAW)
    gl.glVertexAttribPointer(vtex_loc, 3, GL.GL_FLOAT, GL.GL_FALSE, 0, nil)
    table.insert(self.vbos, tvbo)

    gl.glEnableVertexAttribArray(vpos_loc)
    gl.glEnableVertexAttribArray(vcol_loc)
    gl.glEnableVertexAttribArray(vtex_loc)
    print(vpos_loc)
    print(vcol_loc)
    print(vtex_loc)

    local tris = glIntv(#dat.tris, dat.tris)
    self.num_tris = #dat.tris
    local qvbo = glIntv(0)
    gl.glGenBuffers(1, qvbo)
    gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, qvbo[0])
    gl.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER, ffi.sizeof(tris), tris, GL.GL_STATIC_DRAW)
    table.insert(self.vbos, qvbo)
end

function verts:initSingleInstanceAttributesfromObj(filename)
    local glIntv = ffi.typeof('GLint[?]')
    local glUintv = ffi.typeof('GLuint[?]')
    local glFloatv = ffi.typeof('GLfloat[?]')

    -- Load obj file
    filename = 'models/'..filename..'.obj'
    if self.data_dir then filename = self.data_dir .. "/" .. filename end
    print('initSingleInstanceAttributesfromObj', filename)

    self.obj = objfile.new()
    print("Loading obj "..filename)
    self.obj:loadmodel(filename, true)
    if #self.obj.olist == 0 then return end
    
    local str = (#self.obj.vertlist/3).." vertices*3, "
    str = str..(#self.obj.normlist/3).." normals*3, "
    str = str..(#self.obj.texlist/3).." texcoords*3, "
    str = str..(#self.obj.idxlist/3).." ints*3 for triangle indices."
    print(str)

    -- Delete the old
    gl.glBindVertexArray(self.vao)
    for _,v in pairs(self.vbos) do
        gl.glDeleteBuffers(1,v)
    end
    self.vbos = {}

    --print("Number of objects: "..#self.obj.olist)
    local v = self.obj.vertlist
    local n = self.obj.normlist
    local i = self.obj.idxlist

    local verts = glFloatv(#v,v)
    local cols = glFloatv(#n,n)
    --local verts = glFloatv(#dat.verts, dat.verts)
    --local cols = glFloatv(#dat.norms, dat.norms)

    local vpos_loc = gl.glGetAttribLocation(self.prog, "vPosition")
    local vcol_loc = gl.glGetAttribLocation(self.prog, "vColor")
    local vtex_loc = gl.glGetAttribLocation(self.prog, "vTexCoord")

    local vvbo = glIntv(0)
    gl.glGenBuffers(1, vvbo)
    gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vvbo[0])
    gl.glBufferData(GL.GL_ARRAY_BUFFER, ffi.sizeof(verts), verts, GL.GL_STATIC_DRAW)
    gl.glVertexAttribPointer(vpos_loc, 3, GL.GL_FLOAT, GL.GL_FALSE, 0, nil)
    table.insert(self.vbos, vvbo)

    local cvbo = glIntv(0)
    gl.glGenBuffers(1, cvbo)
    gl.glBindBuffer(GL.GL_ARRAY_BUFFER, cvbo[0])
    gl.glBufferData(GL.GL_ARRAY_BUFFER, ffi.sizeof(cols), cols, GL.GL_STATIC_DRAW)
    gl.glVertexAttribPointer(vcol_loc, 3, GL.GL_FLOAT, GL.GL_FALSE, 0, nil)
    table.insert(self.vbos, cvbo)

    local tvbo = glIntv(0)
    gl.glGenBuffers(1, tvbo)
    gl.glBindBuffer(GL.GL_ARRAY_BUFFER, tvbo[0])
    gl.glBufferData(GL.GL_ARRAY_BUFFER, ffi.sizeof(texs), texs, GL.GL_STATIC_DRAW)
    gl.glVertexAttribPointer(vtex_loc, 3, GL.GL_FLOAT, GL.GL_FALSE, 0, nil)
    table.insert(self.vbos, tvbo)

    gl.glEnableVertexAttribArray(vpos_loc)
    gl.glEnableVertexAttribArray(vcol_loc)
    gl.glEnableVertexAttribArray(vtex_loc)

    local tris = glIntv(#i, i)
    self.num_tris = #i
    local qvbo = glIntv(0)
    gl.glGenBuffers(1, qvbo)
    gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, qvbo[0])
    gl.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER, ffi.sizeof(tris), tris, GL.GL_STATIC_DRAW)
    table.insert(self.vbos, qvbo)

    gl.glBindVertexArray(0)
end

function verts:initGL()
    local vaoId = ffi.new("int[1]")
    gl.glGenVertexArrays(1, vaoId)
    self.vao = vaoId[0]
    gl.glBindVertexArray(self.vao)
    local err = self:buildShader(self.src)
    self:initSingleInstanceAttributes(vertex_lists.cube)
    gl.glBindVertexArray(0)
    if err then print(err) end
end

function verts:exitGL()
    gl.glBindVertexArray(self.vao)
    for _,v in pairs(self.vbos) do
        gl.glDeleteBuffers(1,v)
    end
    self.vbos = {}
    gl.glDeleteProgram(self.prog)
    local vaoId = ffi.new("GLuint[1]", self.vao)
    gl.glDeleteVertexArrays(1, vaoId)
    gl.glBindVertexArray(0)
end

function verts:renderEye(model, view, proj)
    if self.prog == 0 then return end

    local h = self.heatup
    gl.glClearColor(
        self.clearColor[1]*h+.3*(1-h),
        self.clearColor[2]*h+.3*(1-h),
        self.clearColor[3]*h+.3*(1-h),
        self.clearColor[4]*h)
    gl.glClear(GL.GL_COLOR_BUFFER_BIT + GL.GL_DEPTH_BUFFER_BIT)
    gl.glEnable(GL.GL_DEPTH_TEST)

    local um_loc = gl.glGetUniformLocation(self.prog, "modelMtx")
    local uv_loc = gl.glGetUniformLocation(self.prog, "viewMtx")
    local up_loc = gl.glGetUniformLocation(self.prog, "projMtx")
    local un_loc = gl.glGetUniformLocation(self.prog, "numParticles")
    local ut_loc = gl.glGetUniformLocation(self.prog, "time")
    local uf_loc = gl.glGetUniformLocation(self.prog, "fogDensity")
    local ufc_loc = gl.glGetUniformLocation(self.prog, "fogColor")
    local ueg_loc = gl.glGetUniformLocation(self.prog, "effectGain")
    local ufw_loc = gl.glGetUniformLocation(self.prog, "forward")

    gl.glUseProgram(self.prog)

    local glFloatv = ffi.typeof('GLfloat[?]')
    gl.glUniformMatrix4fv(um_loc, 1, GL.GL_FALSE, glFloatv(16, model))
    gl.glUniformMatrix4fv(uv_loc, 1, GL.GL_FALSE, glFloatv(16, view))
    gl.glUniformMatrix4fv(up_loc, 1, GL.GL_FALSE, glFloatv(16, proj))
    gl.glUniform1i(un_loc, self.npts)
    gl.glUniform1f(ut_loc, self.time)
    gl.glUniform1f(uf_loc, self.fogDensity)
    gl.glUniform4f(ufc_loc,
        self.clearColor[1]*h+.3*(1-h),
        self.clearColor[2]*h+.3*(1-h),
        self.clearColor[3]*h+.3*(1-h),
        self.clearColor[4]*h)
    gl.glUniform1f(ufw_loc, self.forward)

    if ueg_loc > -1 then
        gl.glUniform1f(ueg_loc, self.effectGain)
    end

    if self.tex then
        local utex_loc = gl.glGetUniformLocation(self.prog, "teximage")
        gl.glActiveTexture(GL.GL_TEXTURE0)
        gl.glBindTexture(GL.GL_TEXTURE_2D, self.tex)
        gl.glUniform1i(utex_loc, 0)
    end

    --gl.glEnable(GL.GL_BLEND)
    --gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
    gl.glBindVertexArray(self.vao)
    if self.obj then
        for k,v in pairs(self.obj.olist) do
            if v[1] > 0 then
                gl.glDrawElementsInstanced(GL.GL_TRIANGLES, #self.obj.idxlist, GL.GL_UNSIGNED_INT, nil, self.npts)
            end
        end
    else
        gl.glDrawElementsInstanced(GL.GL_TRIANGLES, self.num_tris, GL.GL_UNSIGNED_INT, nil, self.npts)
    end
    gl.glBindVertexArray(0)
    --gl.glDisable(GL.GL_BLEND)

    gl.glUseProgram(0)
end

function verts:render_for_one_eye(view, proj)
    if self.prog ~= 0 then
        gl.glUseProgram(self.prog)

        local ut_loc = gl.glGetUniformLocation(self.prog, "time")
        gl.glUniform1f(ut_loc, self.time)
        local un_loc = gl.glGetUniformLocation(self.prog, "numParticles")
        gl.glUniform1i(un_loc, self.npts)
        local uf_loc = gl.glGetUniformLocation(self.prog, "fogDensity")
        gl.glUniform1f(uf_loc, self.fogDensity)

        local umv_loc = gl.glGetUniformLocation(self.prog, "mvmtx")
        local upr_loc = gl.glGetUniformLocation(self.prog, "prmtx")

        local glFloatv = ffi.typeof('GLfloat[?]')
        gl.glUniformMatrix4fv(umv_loc, 1, GL.GL_FALSE, glFloatv(16, view))
        gl.glUniformMatrix4fv(upr_loc, 1, GL.GL_FALSE, glFloatv(16, proj))
        gl.glBindVertexArray(self.vao)
        gl.glDrawElementsInstanced(GL.GL_TRIANGLES, 4*3, GL.GL_UNSIGNED_INT, nil, self.npts)

        gl.glBindVertexArray(0)
        gl.glUseProgram(0)
    end
end

function verts:timestep(absTime, dt)
    self.time = absTime
end

function verts:onSingleTouch(pointerid, action, x, y)
    --print("points.onSingleTouch",pointerid, action, x, y)
end

function verts:scaleBufferSize(k)
    self.dynamic_scale =  false
    self.npts = self.npts * k
end

function verts:charkeypressed(key)
    if key == '-' then
        self:scaleBufferSize(.5)
    elseif key == '=' then
        self:scaleBufferSize(2)
    end
end

function verts:keypressed(key, scancode, action, mods)
    local func_table = {
        [298] = function (x) self:scaleBufferSize(.5*.5) end, -- F9
        [299] = function (x) self:scaleBufferSize(.5) end, -- F10
        [300] = function (x) self:scaleBufferSize(2) end, -- F11
        [301] = function (x) self:scaleBufferSize(2*2) end, -- F12
    }
    local f = func_table[key]
    if f then f() handled = true end
end

function verts:handleCommand(args)
    if not args then return {'nil args'} end
    if #args == 0 then return {'empty args'} end
    if #args == 1 then return {args[1]..'no args'} end

    local key = args[1]
    local val = args[2]
    if key and val then
        if key == 'dynscale' then
            self.dynamic_scale = true -- TODO size
            return {'Set dynamic scale on'}
        elseif key == 'frag' then
            if val == 'basic' then
                self.chosen_frag = basic_frag
                return {'basic frag shader'}
            elseif val == 'tex' then
                self.chosen_frag = texcoord_frag
                return {'texcoord shader'}
            elseif val == 'lighting' then
                self.chosen_frag = lighting_frag
                return {'lighting frag shader'}
            elseif val == 'texlight' then
                self.chosen_frag = texlight_frag
                return {'texlighting frag shader'}
            else
                return {'Unrecognized shader: '..val}
            end
        elseif key == 'shape' then
            local vlist = vertex_lists[val]
            if vlist then
                gl.glBindVertexArray(self.vao)
                for _,v in pairs(self.vbos) do
                    gl.glDeleteBuffers(1,v)
                end
                self.vbos = {}
                self:initSingleInstanceAttributes(vlist)
                gl.glBindVertexArray(0)
                return {'set shape to '..val}
            else
                return {'Unrecognized shape: '..val}
            end
        elseif key == 'obj' then
            self:initSingleInstanceAttributesfromObj(val)
            return {'set obj to '..val}
        elseif key == 'instances' then
            local n = tonumber(val)
            if n then
                self.dynamic_scale = false
                self.npts = n
                return {'set n points to '..tostring(n)}
            end
        elseif key == 'fog' then
            local n = tonumber(val)
            if n then
                self.fogDensity = n
                return {'Set fog density to '..tostring(self.fogDensity)}
            end
        elseif key == 'fogColor' then
            if args[2] and args[3] and args[4] and args[5] then
                for i=2,5 do
                    self.clearColor[i-1] = tonumber(args[i])
                end
            else
                return {'Not enough args: r g b a'}
            end
        end
    end
end

-- Return a list of strings(lines) describing commands
function verts:commandHelp()
    return {
        'vs - vertex shader help',
        'Commands:',
        '   dynscale <sz> ',
        '   frag <basic|lighting>',
        '   shape <tetra|cube|halfcube|square>',
        '   fog <density> '..self.fogDensity,
        '   fogColor <r g b a>',
        'Instances: '..tostring(self.npts),
    }
end

return verts
