WebGL rotating F

From Rosetta Code
WebGL rotating F is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

Using WebGL or OpenGL ES 2.0 compatible routines draw a rotating F shape.
Specifically avoid glBegin, glVertex, glPush/PopMatrix, or glu(t/fw), however all functions needed are available in (eg) opengl32.dll.
While the Phix example below uses a somewhat fudged "Hello!" graphic, any texture or even just plain colours is perfectly acceptable.

A detailed tutorial (if code below does not suffice) can be found at https://webglfundamentals.org - specifically 2nd 3D and 1st textures sections.
Be warned that site can and probably will stress your CPU, but closing the page returns things to normal pretty quickly. The OpenGL task entries for JavaScript, Phix, and Rust could also be used as starting points, as can OpenGL_pixel_shader entries for JavaScript and Phix.

Optional extra

Allow toggling between the F shape and a classic Utah teapot.


J

Near as I can tell, the webgl requirement means that the task is asking for a wrapper for javascript (possibly encapsulated in a library).

Here, we'll use ray marching as that "feels sort of like J":

webgl_close=: {{ wd'pclose'}}
wd {{)n
  pc webgl; cc w webview;
  pmove 0 0 518 518;
  pshow;
  set w html *
<html><head><title>test</title></head>
  <script>
  function paint(t) {
    gl.uniform1f(timeNdx, t/1e3);
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    requestAnimationFrame(paint);
  }
  function run(){
    c= document.querySelector("#c");
    gl= c.getContext("webgl");
    P= gl.createProgram();
    function setShader(typ, src) {
      shader= gl.createShader(typ);
      gl.shaderSource(shader, src);
      gl.compileShader(shader);
      gl.attachShader(P, shader);
    }
    setShader(gl.VERTEX_SHADER, "attribute vec4 pos;void main(){gl_Position=pos;}")
    setShader(gl.FRAGMENT_SHADER, `
      precision mediump float;
      uniform vec2 res;
      uniform float time;
      mat3 rotateY(float theta) {
          float c= cos(theta);
          float s= sin(theta);
          return mat3(
              vec3(c, 0, s),
              vec3(0, 1, 0),
              vec3(-s, 0, c)
          );
      }
      const float MAX_DIST= 100.0;
      float sdBox(vec3 position, vec3 box, vec3 offset) {
        vec3 q= abs(position - offset) - box/2.0;
        return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
      }
      float sdScene(vec3 p) { // distance to closest object
        float dist=      sdBox(p, vec3(1,4,1),   vec3(0, -0.5, 0));
        dist=  min(dist, sdBox(p, vec3(1.5,1,1), vec3(0.75, -0.5, 0)));
        return min(dist, sdBox(p, vec3(2.5,1,1), vec3(0.75, 1.5, 0)));
      }
      float rayMarch(vec3 ro, vec3 rd, float depth, float end) {
        for (int i= 0; i< 100 /* MAX_MARCHING_STEPS */; i++) {
          if (depth < end) depth+= sdScene(ro + depth * rd);
        }
        return depth;
      }
      vec3 calcNormal(in vec3 p) {
          vec2 e= vec2(1.0, -1.0) * 0.0005; // epsilon
          return normalize(
            e.xyy * sdScene(p + e.xyy) +
            e.yyx * sdScene(p + e.yyx) +
            e.yxy * sdScene(p + e.yxy) +
            e.xxx * sdScene(p + e.xxx));
      }
      void main(){
        vec2 uv= (gl_FragCoord.xy-.5*res)/res.x;
        mat3 rot= rotateY(time); 
        vec3 backgroundColor= vec3(0.835, 1, 1);
        vec3 color;
        vec3 ro= vec3(0, 0, 7)*rot; // ray origin that represents camera position
        vec3 rd= normalize(vec3(uv, -1)*rot); // ray direction
        float sd= rayMarch(ro, rd, 0.0 /* MIN_DIST */, MAX_DIST); // closest object
        if (sd > MAX_DIST) {
          color= backgroundColor; // ray didn't hit anything
        } else {
          vec3 p= ro + rd * sd; // point on cube we discovered from ray marching
          vec3 normal= calcNormal(p);
          vec3 lightPosition= vec3(2, 2, 7)*rot;
          vec3 lightDirection= normalize(lightPosition - p);
          float dif= clamp(dot(normal, lightDirection), 0.3, 1.); // diffuse reflection
          color= dif * vec3(1,0,0) + backgroundColor * .2; // Add a bit of background color to the diffuse color
        }
        gl_FragColor= vec4(color, 1.0);
      }
    `); /* end of FRAGMENT_SHADER */
    gl.linkProgram(P);
    posNdx= gl.getAttribLocation(P, "pos");
    timeNdx= gl.getUniformLocation(P, "time");
    posBuffer= gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,  -1,1,  1,-1,  1,1]), gl.STATIC_DRAW);
    gl.viewport(0,0,c.width,c.height);
    gl.useProgram(P);
    gl.enableVertexAttribArray(posNdx);
    gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
    gl.vertexAttribPointer(posNdx, 2, gl.FLOAT, false, 0, 0);
    gl.uniform2f(gl.getUniformLocation(P, "res"), c.width, c.height);
    requestAnimationFrame(paint);
  }
  </script>
</head><body onload="run()" style="margin: 0">
  <canvas id="c" width=500 height=500></canvas>
</body></html>
}}

In other words, we construct the letter F using three rectangular blocks near the origin of our coordinate system. And we construct a rectangle covering the screen using triangle strip. Finally, for each pixel on the screen we project a vector representing a potential pixel from our camera position towards the blocks.

In ray marching we find the minimum distance from an origin point to any point in the geometric structure, move in the ray direction that distance and repeat (in this case we repeat this operation 100 time for each pixel). After these iterations we have either shot past the geometry or we're within some small epsilon distance from the surface of the object. We can inspect the distance the array has progressed to determine whether we should use background color or surface color. And we can check distances with some minor variations on this ray to determine the orientation of the surface at that point.

Finally, to achieve the "rotating" effect, we spin our camera (and light source) so that it rotates around the blocks which form our letter "F", with the camera always facing towards the origin.

Tested in J9.03 and a J9.04 beta.

See also: https://tinyurl.com/rotatingF for a j-playground variation of this implementation (hit Run in the upper right corner).

Julia

Makie can use OpenGL as a backend for plotting (GLMakie). The code plots an 'F' as three rectangular solid-shaped scatter plots with overlapping markers, then rotates the resulting graph.

using Colors
using GeometryBasics: Rect3f
using GLMakie

topbar = vec([Point3f(i / 5, j / 5, k / 5) for i in 1:7, j in 8:3:28, k in 45:50])
stem = vec([Point3f(i / 5, j / 5, k / 5) for i in 1:7, j in 1:7, k in 1:8:56])
midbar = vec([Point3f(i / 5, j / 5, k / 5) for i in 1:7, j in 8:2:21, k in 25:29])
fig = Figure(resolution = (800, 400))
pos = fig[1, 1]
meshscatter(pos, topbar; marker = Rect3f(Vec3f(-0.5), Vec3f(16)), transparency = true,
   color = [RGBA(topbar[i]..., 0.5) for i in 1:length(topbar)])
meshscatter!(pos, stem; marker = Rect3f(Vec3f(-0.5), Vec3f(16)), transparency = true,
   color = [RGBA(stem[i]..., 0.5) for i in 1:length(stem)])
meshscatter!(pos, midbar; marker = Rect3f(Vec3f(-0.5), Vec3f(16)), transparency = true,
   color = [RGBA(midbar[i]..., 0.5) for i in 1:length(midbar)])

for _ in 1:28
    display(fig.scene)
    rotate!(Accum, fig.scene, 0.25)
    sleep(0.25)
end

Phix

You can run this online here (which gives me around 10% CPU load, with space toggling the timer for no extra load). No teapot, sorry.

--
-- demo\pGUI\HelloF.exw
-- ====================
--
--  Translation of https://webglfundamentals.org/webgl/webgl-3d-textures-good-npot.html
--          but using a manually generated texture rather than one loaded from an image.
--
--  Converted to pGUI by Pete Lomax
--
--  This can be run online here, and also works on desktop/Phix
--
with javascript_semantics   -- (OpenGL ES 2.0 compatible) [YEAH!!]
requires("1.0.1")
include pGUI.e
include opengl.e
--
-- Pete's super quick intro to OpenGL/WebGL		(copied from the docs)
-- ========================================
--
--  First and foremost you need to define all the points/vertices and faces of the model.
--  For a 2D 'F' draw three rectangles, one upright and two rungs. We will treat that as 3 faces.
--  There are four distinct x (left/right) values and five distinct y (up/down) values, and 12 vertices, 
--  two of which are the same point (where the upright and the top rung meet). 
--  For a 3D 'F' replicate that with a different z (front/back), now we have 24 vertices.
--  The right hand side of the upright needs (splitting into) two faces and the left hand side of both rungs have none, plus
--  the top of the upright and the top of the top rung are in practice merged together into a single face.
--  In total we have 16 faces[1], each of which we are going to specify as two triangles[2].
--  [1] Just to clarify and not of any great significance, 6 of those faces are flat and 5 each are horizonal/vertical edge-on.
--  [2] While OpenGL 1.0's glBegin() supported quads and polygons, OpenGL ES 2.0 and WebGL stop at triangles.  
--
--  Let that all sink in. If you cannot imagine or arrange a few suitable things on the desktop in front of you to resemble 
--  a 3D 'F', and verify the numbers above (esp 16), then I'm afraid to say you are a lost cause.
--  Don't worry about the units: internally OpenGL maps everything to 0.0..1.0 or -1.0..+1.0 as needed anyway (I think),
--  along with the "camera" position, so just use the numbers you find easiest. Do however bear in mind that we are
--  (probably) going to be rotating about {0,0,0}, so making that the "centre of gravity" usually helps, though of
--  course you can specify things more naturally and then add/subtract some {dx,dy,dz} to/from every point.
--  See sequence F_positions below.
--
--  It is important to note that each face can be drawn using two triangles in 144 different ways, which could have a significant
--  impact when it comes to applying textures. There are two diagonals that split a rectangle into two triangles, but you can start 
--  on any one of those four, and draw by starting on any of the three corners of a triangle and go clockwise or anticlockwise, so 
--  4*3*2*3*2=144. On top of that we have different pixel coordinates for Canvas Draw and OpenGL, as well as low-level differences 
--  between OpenGL (on the desktop) and WebGL (as in a browser). We need to take the simplest possible route here (and we do).
--
--  Next we want to apply a texture to each of those faces. This is a simple matter of matching each and every vertex previously 
--  specified to the corresponding position on the texture. We can make things much simpler by declaring the faces in a consistent
--  manner and applying textures using a standard platform-specific approach, which means opengl.e vs opengl.js in this context.
--  Applying a world map to a sphere needs a bit more math and probably more triangles, but it is basically the same. 
--
--  The rest is fairly standard: store it with glBufferData(), and specify what it all means first by calling glVertexAttribPointer() 
--  with the right size, type, and possibly stride settings, and finally glDrawArrays() with the desired mode, here GL_TRIANGLES, not 
--  forgetting the total length. Of course there are all sorts of other things to achieve smoothing, rotation, lighting effects, and 
--  so on, but this quick little intro endeth here. The following notes are all very specific to this particular program.
--
-- First define a 2D 'F' as follows
--
--      [1] {0,150}  {30,150}  {100,150}
--            +---------+----------+
--            |         |          |
--      [2]   | {30,120}+----------+{100,120}
--            |         |
--            |         |
--      [3]   |  {30,90}+------+{67,90}
--            |         |  X   |
--      [4]   |  {30,60}+------+{67,60}
--            |         |
--            |         |
--            |         |
--            +---------+
--      [5] {0,0}    {30,0}
--
-- Use twelve 3-letter contants named (up|tr|mr)((l|r)|(t|b)),
--              eg upl is the x co-ord for the upright's left.
--
-- [1] aka {upl,upt}, {upr,upt}=={trl,trt}, {trr,trt}
-- [2] aka                       {trl,trb}, {trr,trb}
-- [3] aka                       {mrl,mrt}, {mrr,mrt}
-- [4] aka                       {mrl,mrb}, {mrr,mrb}
-- [5] aka {upl,upb}, {upr,upb}
--
-- The translations simply shift {x,y,z} such that {0,0,0} is the
--  "centre of gravity" (X) rather than front upright botton left,
--  because rotating about that X point looks so very much better.
--
-- Obviously replicate that with z of 0 and 30 for the front and 
--  back panes, then out of that mess define all of the 16 faces 
--  that we need to draw the whole thing properly.
--
constant tx = -50, ty = -75, tz = -15, -- translations
          f = 0+tz, b = 30+tz,    -- front and back planes
-- name the left/right/top/bottom on the 3 front faces:
 upl = 0+tx, upr =  30+tx, upt = 150+ty, upb =   0+ty,  -- upright 
 trl =  upr, trr = 100+tx, trt =    upt, trb = 120+ty,  -- top rung
 mrl =  upr, mrr =  67+tx, mrt =  90+ty, mrb =  60+ty   -- middle rung

-- Define all 24 vertices:- (up|tr|mr)(t|b)(l|r)(f|b)
constant uptlf = {upl,upt,f},   uptrf = {upr,upt,f},    
         upblf = {upl,upb,f},   upbrf = {upr,upb,f},    
         trtlf = {trl,trt,f},   trtrf = {trr,trt,f},    
         trblf = {trl,trb,f},   trbrf = {trr,trb,f},    
         mrtlf = {mrl,mrt,f},   mrtrf = {mrr,mrt,f},    
         mrblf = {mrl,mrb,f},   mrbrf = {mrr,mrb,f},    

         uptlb = {upl,upt,b},   uptrb = {upr,upt,b},
         upblb = {upl,upb,b},   upbrb = {upr,upb,b},
         trtlb = {trl,trt,b},   trtrb = {trr,trt,b},
         trblb = {trl,trb,b},   trbrb = {trr,trb,b},
         mrtlb = {mrl,mrt,b},   mrtrb = {mrr,mrt,b},
         mrblb = {mrl,mrb,b},   mrbrb = {mrr,mrb,b}
--
-- Referring back to the above diagram, we now have:
-- [1] aka uptlf/b, uptrf/b==trtlf/b, trtrf/b
-- [2] aka                   trblf/b, trbrf/b
-- [3] aka                   mrtlf/b, mrtrf/b
-- [4] aka                   mrblf/b, mrbrf/b
-- [5] aka upblf/b, upbrf/b
--
-- Now define the 16 faces.
--
-- Note that while the 24 vertices were named corresponding
-- to two upright 'F's, when we define each face we will 
-- use the corners in an order suitable for texture mapping.
-- I hereby define the "A7" format as {bl,tl,br,tl,tr,br},
-- ie the six points (aka two triangles) needed to draw it, 
-- when looking at it from the texture side and orientated
-- the right way up for that, as a way to simplify things.
-- The two front faces of the rungs are orientated the same
-- as the vertex naming, the rest all [up]side[down]ways.
--
-- While the following is fairly difficult to type in, and
-- the polar opposite of flexible, the hope is it will be
-- less daunting than the more usual "wall of 288 numbers". 
-- Trust me I had a furrowed brow and used lots of pencil
-- and paper and rotated my notepad in order to pen these.
-- Of course you can generate all the vertices and faces 
-- programmatically in a much more abstract fashion, too.
--
constant faces = {{upbrf,upblf,uptrf,upblf,uptlf,uptrf},    -- left column front
                  {trblf,trtlf,trbrf,trtlf,trtrf,trbrf},    -- top rung front
                  {mrblf,mrtlf,mrbrf,mrtlf,mrtrf,mrbrf},    -- middle rung front
                  {upblb,upbrb,uptlb,upbrb,uptrb,uptlb},    -- left column back
                  {trbrb,trtrb,trblb,trtrb,trtlb,trblb},    -- top rung back
                  {mrbrb,mrtrb,mrblb,mrtrb,mrtlb,mrblb},    -- middle rung back
                  {uptlf,uptlb,trtrf,uptlb,trtrb,trtrf},    -- joined up top
                  {trbrf,trtrf,trbrb,trtrf,trtrb,trbrb},    -- top rung right
                  {trblb,trblf,trbrb,trblf,trbrf,trbrb},    -- under top rung
                  {mrtlb,mrtlf,trblb,mrtlf,trblf,trblb},    -- between rungs right
                  {mrtlf,mrtlb,mrtrf,mrtlb,mrtrb,mrtrf},    -- middle rung top 
                  {mrbrf,mrtrf,mrbrb,mrtrf,mrtrb,mrbrb},    -- middle rung right
                  {mrblb,mrblf,mrbrb,mrblf,mrbrf,mrbrb},    -- under middle rung
                  {upbrb,upbrf,mrblb,upbrf,mrblf,mrblb},    -- below rungs right
                  {upblb,upblf,upbrb,upblf,upbrf,upbrb},    -- upright bottom (match under rungs)
        --        {upbrb,upblb,upbrf,upblb,upblf,upbrf},    -- upright bottom (match the uprights)
                  {upblf,upblb,uptlf,upblb,uptlb,uptlf}},   -- left hand side
        --
        -- And sqidge together into the 32 triangles / 96 points that OpenGL likes:
        --
        F_positions = flatten(faces,{}), -- (nb be sure to ask for a dword-sequence)
        --
        -- Now for the final flourish: since we defined our faces in a consistent manner
        -- we can reach out to opengl.e/js for a standard texture mapping for all faces.
        -- More details can be found in the documentation for glSimpleA7texcoords().
        --
        F_texture_coordinates = glSimpleA7texcoords(length(faces))

Ihandln dlg
Ihandle canvas, timer
cdCanvas cd_canvas

constant fragment_shader = `
precision mediump float;

// Passed in from the vertex shader.
varying vec2 v_texcoord;

// The texture.
uniform sampler2D u_texture;

void main() {
   gl_FragColor = texture2D(u_texture, v_texcoord);
}`

constant vertex_shader = `
attribute vec4 a_position;
attribute vec2 a_texcoord;

uniform mat4 u_matrix;

varying vec2 v_texcoord;

void main() {
  // Multiply the position by the matrix.
  gl_Position = u_matrix * a_position;

  // Pass the texcoord to the fragment shader.
  v_texcoord = a_texcoord;
}`

function load_shader(string shaderSource, integer shaderType)
    integer shader = glCreateShader(shaderType)
    glShaderSource(shader, shaderSource)
    glCompileShader(shader)
    integer compiled = glGetShaderParameter(shader, GL_COMPILE_STATUS)
    if not compiled then
        string fmt = "*** Error compiling shader \'%d\': %s\n",
               msg = sprintf(fmt,{shader,glGetShaderInfoLog(shader)})
        sequence lines = split(shaderSource,'\n',false)
        for i=1 to length(lines) do
            lines[i] = sprintf("%d: %s\n",{i,lines[i]})
        end for
        puts(1,msg & join(lines,"\n"))
        shader = glDeleteShader(shader)
    end if
    return shader
end function

function create_program(sequence shaders)
    integer program = glCreateProgram()
    for i=1 to length(shaders) do
      glAttachShader(program, shaders[i])
    end for
    glLinkProgram(program)
    integer linked = glGetProgramParameter(program, GL_LINK_STATUS)
    if not linked then
        string lastError = glGetProgramInfoLog(program)
        puts(1,"Error in program linking:" & lastError)
        program = glDeleteProgram(program)
    end if
    return program
end function

integer program = 0

function canvas_action_cb(Ihandle /*canvas*/)

    if program=0 then -- (do this lot once only! but not b4 desktop/Phix is ready)

        program = create_program({load_shader(vertex_shader, GL_VERTEX_SHADER),
                                  load_shader(fragment_shader, GL_FRAGMENT_SHADER)})

        // Create a buffer for the F_positions and fill it
        integer position_buffer = glCreateBuffer()
        // Bind to ARRAY_BUFFER (think of it as ARRAY_BUFFER:=position_buffer)
        glBindBuffer(GL_ARRAY_BUFFER, position_buffer)
        {integer size, atom pData} = glFloat32Array(F_positions)
        glBufferData(GL_ARRAY_BUFFER, size, pData, GL_STATIC_DRAW)

        // Create a matching buffer for the F_texture_coordinates and fill it
        integer texcoord_buffer = glCreateBuffer()
        glBindBuffer(GL_ARRAY_BUFFER, texcoord_buffer)
        {size, pData} = glFloat32Array(F_texture_coordinates)
        glBufferData(GL_ARRAY_BUFFER, size, pData, GL_STATIC_DRAW)

        // Put some text in the center of a canvas/pixel array.
        // note this is a (bit of a|complete) hack, see docs...
        integer w = 100, h = 26
        atom cdx = glCanvasSpecialText(cd_canvas,w,h,20,"Hello!")
    
        integer texture = glCreateTexture()
        glBindTexture(GL_TEXTURE_2D, texture)
        glTexImage2Dc(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, cdx)
        // make sure we can render it even if it's not a power of 2
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)

        integer position_location = glGetAttribLocation(program, "a_position")
        glEnableVertexAttribArray(position_location)    // Turn on the position attribute
        glBindBuffer(GL_ARRAY_BUFFER, position_buffer)  // Bind the position buffer.
        // Tell the position attribute how to get data out of position_buffer (ARRAY_BUFFER)
        atom size3 = 3,         // 3 components per iteration (obvs you can inline all these)
          datatype = GL_FLOAT,  // the data is 32bit floats
         normalize = false,     // don't normalize the data
            stride = 0,         // 0 = move fwd size*sizeof(type) each iteration to get next
            offset = 0          // start at the beginning of the buffer
        glVertexAttribPointer(position_location, size3, datatype, normalize, stride, offset)

        integer texcoord_location = glGetAttribLocation(program, "a_texcoord")
        glEnableVertexAttribArray(texcoord_location)    // Turn on the texcoord attribute
        glBindBuffer(GL_ARRAY_BUFFER, texcoord_buffer)  // bind the texcoord buffer.
        // Tell the texcoord attribute how to get data out of texcoord_buffer (ARRAY_BUFFER)
        atom size2 = 2          // 2 components per iteration (the rest are same as above)
        glVertexAttribPointer(texcoord_location, size2, datatype, normalize, stride, offset)

        glEnable(GL_CULL_FACE)
        glEnable(GL_DEPTH_TEST)

        // Tell it to use our program (pair of shaders)
        glUseProgram(program)
    end if
    return IUP_DEFAULT
end function

function map_cb(Ihandle /*ih*/)
    IupGLMakeCurrent(canvas)
    cd_canvas = cdCreateCanvas(CD_IUP, canvas)
    return IUP_DEFAULT
end function

atom field_of_view_radians = 60*CD_DEG2RAD,     -- (currently fixed)
     model_x_rotation_radians = 20*CD_DEG2RAD,  -- +0.3 per second
     model_y_rotation_radians = 0*CD_DEG2RAD,   -- -0.2 per second
     last_time = time()

function timer_cb(Ihandle /*ih*/)

    atom this_time = time(),
        delta_time = this_time-last_time
    last_time = this_time   // (remember for the next frame)

    // Animate the rotation
    model_x_rotation_radians += 0.3 * delta_time
    model_y_rotation_radians -= 0.2 * delta_time

    // Tell WebGL how to convert from clip space to pixels
    integer {w, h} = IupGetIntInt(canvas, "RASTERSIZE")
    glViewport(0, 0, w, h)

    // Clear the canvas AND the depth buffer.
    glClear(GL_COLOR_BUFFER_BIT || GL_DEPTH_BUFFER_BIT)
    glClearColor(0.3, 0.3, 0.3, 1.0)

    // Compute the projection, camera, and final view matrices.
    // Use your favourite search engine for more info on this,
    // that is the core matrix usage, rather than the m4_xxx().
    atom aspect = w/h
    sequence projection = m4_perspective(field_of_view_radians, aspect, 1, 2000),
        camera_position = {0, 0, 200}, up = {0, 1, 0}, target = {0, 0, 0},
                 camera = m4_lookAt(camera_position, target, up),
        view_projection = m4_multiply(projection, m4_inverse(camera)),
        rotated_about_x = m4_xRotate(view_projection, model_x_rotation_radians),
      final_view_matrix = m4_yRotate(rotated_about_x, model_y_rotation_radians)

    // Set the final view matrix.
    {integer size, atom pData} = glFloat32Array(final_view_matrix)
    glUniformMatrix4fv(glGetUniformLocation(program, "u_matrix"), false, pData)

    // Tell the shader to use texture unit 0 for u_texture
    glUniform1i(glGetUniformLocation(program, "u_texture"), 0)

    integer num_points = length(F_positions)/3,        -- {x,y,z} per
            num_coords = length(F_texture_coordinates)/2 -- {x,y} per
    assert(num_points==num_coords) -- sanity check

    // Draw the geometry.
    glDrawArrays(GL_TRIANGLES, 0, num_points)

    // Check for errors.
    while true do
        integer e = glGetError()
        if e=GL_NO_ERROR then exit end if
        printf(1,"GL error %d\n", e)
    end while

    glFlush()
    return IUP_DEFAULT
end function

function key_cb(Ihandle ih, atom c)
    if c=' ' then
        IupSetInt(timer,"RUN",not IupGetInt(timer,"RUN"))
        last_time = time() -- (avoids sudden jumping)
    end if
    return iff(c=K_ESC?IUP_CLOSE:IUP_CONTINUE)
end function

procedure main()
    IupOpen()
    canvas = IupGLCanvas("RASTERSIZE=640x480")
    IupSetCallbacks(canvas, {"MAP_CB", Icallback("map_cb"),
                             "ACTION", Icallback("canvas_action_cb"),
                             "KEY_CB", Icallback("key_cb")})
    dlg = IupDialog(canvas, `TITLE="Hello! F", SHRINK=YES, MINSIZE=120x90`)
    IupShow(dlg)
    timer = IupTimer(Icallback("timer_cb"), 1000/40)
    if platform()!=JS then
        IupMainLoop()
        dlg = IupDestroy(dlg)
        IupClose()
    end if
end procedure
main()

Wren

Library: FreeGLUT
Library: Wren-math

The following is based on the WebGL tutorial javascript animation code here but is written as an OpenGL ES 2.0 desktop application. It just uses plain colors, without any textures and looks exactly the same as the WebGL version when run.

Although the task description says to avoid it, I've used Freeglut to create and manage a desktop window (but no drawing nor callbacks) as that's what I'm most familiar with. However, if that's a problem I can try and use something else (such as SDL).

Assuming it's started from a terminal session, the only way to kill the program is by pressing Ctrl-C. The window close button doesn't work, probably due to the use of usleep to create a delay between drawing frames. It does work if I use a freeglut timer function instead but that creates callback issues (the Wren VM is not re-entrant) and I wanted to minimize the use of freeglut in any case.

As with any Open GL aplication, the Wren code needs to be embedded in a host application which can communicate directly with the relevant libraries and which I've written here in C.

/* WebGL_rotating_F.wren */

import "./math" for Math

var GL_ARRAY_BUFFER     = 0x8892
var GL_STATIC_DRAW      = 0x88E4
var GL_VERTEX_SHADER    = 0x8B31
var GL_FRAGMENT_SHADER  = 0x8B30
var GL_COLOR_BUFFER_BIT = 0x4000
var GL_DEPTH_BUFFER_BIT = 0x0100
var GL_CULL_FACE        = 0x0B44
var GL_DEPTH_TEST       = 0x0B71
var GL_UNSIGNED_BYTE    = 0x1403
var GL_FLOAT            = 0x1406
var GL_TRIANGLES        = 0x0004

var GLUT_ACTION_ON_WINDOW_CLOSE = 0x01F9
var GLUT_ACTION_GLUTMAINLOOP_RETURNS = 0x0001
var GLUT_SINGLE = 0x0000
var GLUT_RGB    = 0x0000
var GLUT_DEPTH  = 0x0010

class Gl {
    foreign static bufferData(target, size, data, usage)
    foreign static createShader(type)
    foreign static shaderSource(shader, count, string, length)
    foreign static compileShader(shader)
    foreign static createProgram()
    foreign static attachShader(program, shader)
    foreign static linkProgram(program)
    foreign static getAttribLocation(program, name)
    foreign static getUniformLocation(program, name)
    foreign static genBuffers(n, buffers)
    foreign static bindBuffer(target, buffer)
    foreign static viewport(x, y, width, height)
    foreign static clear(mask)
    foreign static enable(cap)
    foreign static useProgram(program)
    foreign static drawArrays(mode, first, count)
    foreign static enableVertexAttribArray(index)
    foreign static vertexAttribPtr(index, size, type, normalized, stride, ptr)
    foreign static uniformMatrix4fv(location, count, transpose, value)
    foreign static flush()
}

class Glut {
    foreign static initDisplayMode(mode)
    foreign static initWindowSize(width, height)
    foreign static createWindow(name)
    foreign static setOption(eWhat, value)
}

class M4 {
    static perspective(fieldOfViewInRadians, aspect, near, far, dst) {
        if (!dst) dst = List.filled(16, 0)

        var f = (Num.pi * 0.5 - 0.5 * fieldOfViewInRadians).tan
        var rangeInv = 1 / (near - far)

        dst[ 0] = f / aspect
        dst[ 1] = 0
        dst[ 2] = 0
        dst[ 3] = 0
        dst[ 4] = 0
        dst[ 5] = f
        dst[ 6] = 0
        dst[ 7] = 0
        dst[ 8] = 0
        dst[ 9] = 0
        dst[10] = (near + far) * rangeInv
        dst[11] = -1
        dst[12] = 0
        dst[13] = 0
        dst[14] = near * far * rangeInv * 2
        dst[15] = 0

        return dst
    }

    static translate(m, tx, ty, tz, dst) {
        if (!dst) dst = List.filled(16, 0)

        var m00 = m[0]
        var m01 = m[1]
        var m02 = m[2]
        var m03 = m[3]
        var m10 = m[1 * 4 + 0]
        var m11 = m[1 * 4 + 1]
        var m12 = m[1 * 4 + 2]
        var m13 = m[1 * 4 + 3]
        var m20 = m[2 * 4 + 0]
        var m21 = m[2 * 4 + 1]
        var m22 = m[2 * 4 + 2]
        var m23 = m[2 * 4 + 3]
        var m30 = m[3 * 4 + 0]
        var m31 = m[3 * 4 + 1]
        var m32 = m[3 * 4 + 2]
        var m33 = m[3 * 4 + 3]

        if (m != dst) {
            dst[ 0] = m00
            dst[ 1] = m01
            dst[ 2] = m02
            dst[ 3] = m03
            dst[ 4] = m10
            dst[ 5] = m11
            dst[ 6] = m12
            dst[ 7] = m13
            dst[ 8] = m20
            dst[ 9] = m21
            dst[10] = m22
            dst[11] = m23
        }

        dst[12] = m00 * tx + m10 * ty + m20 * tz + m30
        dst[13] = m01 * tx + m11 * ty + m21 * tz + m31
        dst[14] = m02 * tx + m12 * ty + m22 * tz + m32
        dst[15] = m03 * tx + m13 * ty + m23 * tz + m33

        return dst
    }

    static xRotate(m, angleInRadians, dst) {
        if (!dst) dst = List.filled(16, 0)

        var m10 = m[4]
        var m11 = m[5]
        var m12 = m[6]
        var m13 = m[7]
        var m20 = m[8]
        var m21 = m[9]
        var m22 = m[10]
        var m23 = m[11]
        var c = angleInRadians.cos
        var s = angleInRadians.sin

        dst[4]  = c * m10 + s * m20
        dst[5]  = c * m11 + s * m21
        dst[6]  = c * m12 + s * m22
        dst[7]  = c * m13 + s * m23
        dst[8]  = c * m20 - s * m10
        dst[9]  = c * m21 - s * m11
        dst[10] = c * m22 - s * m12
        dst[11] = c * m23 - s * m13

        if (m != dst) {
            dst[ 0] = m[ 0]
            dst[ 1] = m[ 1]
            dst[ 2] = m[ 2]
            dst[ 3] = m[ 3]
            dst[12] = m[12]
            dst[13] = m[13]
            dst[14] = m[14]
            dst[15] = m[15]
        }

        return dst
    }

    static yRotate(m, angleInRadians, dst) {
        if (!dst) dst = List.filled(16, 0)

        var m00 = m[0 * 4 + 0]
        var m01 = m[0 * 4 + 1]
        var m02 = m[0 * 4 + 2]
        var m03 = m[0 * 4 + 3]
        var m20 = m[2 * 4 + 0]
        var m21 = m[2 * 4 + 1]
        var m22 = m[2 * 4 + 2]
        var m23 = m[2 * 4 + 3]
        var c = angleInRadians.cos
        var s = angleInRadians.sin

        dst[ 0] = c * m00 - s * m20
        dst[ 1] = c * m01 - s * m21
        dst[ 2] = c * m02 - s * m22
        dst[ 3] = c * m03 - s * m23
        dst[ 8] = c * m20 + s * m00
        dst[ 9] = c * m21 + s * m01
        dst[10] = c * m22 + s * m02
        dst[11] = c * m23 + s * m03

        if (m != dst) {
            dst[ 4] = m[ 4]
            dst[ 5] = m[ 5]
            dst[ 6] = m[ 6]
            dst[ 7] = m[ 7]
            dst[12] = m[12]
            dst[13] = m[13]
            dst[14] = m[14]
            dst[15] = m[15]
        }

        return dst
    }

    static zRotate(m, angleInRadians, dst) {
        if (!dst) dst = List.filled(16, 0)

        var m00 = m[0 * 4 + 0]
        var m01 = m[0 * 4 + 1]
        var m02 = m[0 * 4 + 2]
        var m03 = m[0 * 4 + 3]
        var m10 = m[1 * 4 + 0]
        var m11 = m[1 * 4 + 1]
        var m12 = m[1 * 4 + 2]
        var m13 = m[1 * 4 + 3]
        var c = angleInRadians.cos
        var s = angleInRadians.sin

        dst[ 0] = c * m00 + s * m10
        dst[ 1] = c * m01 + s * m11
        dst[ 2] = c * m02 + s * m12
        dst[ 3] = c * m03 + s * m13
        dst[ 4] = c * m10 - s * m00
        dst[ 5] = c * m11 - s * m01
        dst[ 6] = c * m12 - s * m02
        dst[ 7] = c * m13 - s * m03

        if (m != dst) {
          dst[ 8] = m[ 8]
          dst[ 9] = m[ 9]
          dst[10] = m[10]
          dst[11] = m[11]
          dst[12] = m[12]
          dst[13] = m[13]
          dst[14] = m[14]
          dst[15] = m[15]
        }

        return dst
    }

    static scale(m, sx, sy, sz, dst) {
        if (!dst) dst = List.filled(16, 0)

        dst[ 0] = sx * m[0 * 4 + 0]
        dst[ 1] = sx * m[0 * 4 + 1]
        dst[ 2] = sx * m[0 * 4 + 2]
        dst[ 3] = sx * m[0 * 4 + 3]
        dst[ 4] = sy * m[1 * 4 + 0]
        dst[ 5] = sy * m[1 * 4 + 1]
        dst[ 6] = sy * m[1 * 4 + 2]
        dst[ 7] = sy * m[1 * 4 + 3]
        dst[ 8] = sz * m[2 * 4 + 0]
        dst[ 9] = sz * m[2 * 4 + 1]
        dst[10] = sz * m[2 * 4 + 2]
        dst[11] = sz * m[2 * 4 + 3]

        if (m != dst) {
            dst[12] = m[12]
            dst[13] = m[13]
            dst[14] = m[14]
            dst[15] = m[15]
        }

        return dst
    }
}

class C {
    foreign static usleep(usec)
}

// Fill the buffer with the values that define a letter 'F'.
var setGeometry = Fn.new {
    Gl.bufferData(
        GL_ARRAY_BUFFER,
        4, // size of 32 bit float
        [
            // left column front
             0,    0,   0,
             0,  150,   0,
            30,    0,   0,
             0,  150,   0,
            30,  150,   0,
            30,    0,   0,

            // top rung front
             30,   0,   0,
             30,  30,   0,
            100,   0,   0,
             30,  30,   0,
            100,  30,   0,
            100,   0,   0,

            // middle rung front
             30,  60,   0,
             30,  90,   0,
             67,  60,   0,
             30,  90,   0,
             67,  90,   0,
             67,  60,   0,

            // left column back
              0,   0,  30,
             30,   0,  30,
              0, 150,  30,
              0, 150,  30,
             30,   0,  30,
             30, 150,  30,

            // top rung back
             30,   0,  30,
            100,   0,  30,
             30,  30,  30,
             30,  30,  30,
            100,   0,  30,
            100,  30,  30,

            // middle rung back
             30,  60,  30,
             67,  60,  30,
             30,  90,  30,
             30,  90,  30,
             67,  60,  30,
             67,  90,  30,

            // top
              0,   0,   0,
            100,   0,   0,
            100,   0,  30,
              0,   0,   0,
            100,   0,  30,
              0,   0,  30,

            // top rung right
            100,   0,   0,
            100,  30,   0,
            100,  30,  30,
            100,   0,   0,
            100,  30,  30,
            100,   0,  30,

            // under top rung
             30,  30,   0,
             30,  30,  30,
            100,  30,  30,
             30,  30,   0,
            100,  30,  30,
            100,  30,   0,

            // between top rung and middle
             30,  30,   0,
             30,  60,  30,
             30,  30,  30,
             30,  30,   0,
             30,  60,   0,
             30,  60,  30,

            // top of middle rung
             30,  60,   0,
             67,  60,  30,
             30,  60,  30,
             30,  60,   0,
             67,  60,   0,
             67,  60,  30,

            // right of middle rung
             67,  60,   0,
             67,  90,  30,
             67,  60,  30,
             67,  60,   0,
             67,  90,   0,
             67,  90,  30,

            // bottom of middle rung.
             30,  90,   0,
             30,  90,  30,
             67,  90,  30,
             30,  90,   0,
             67,  90,  30,
             67,  90,   0,

            // right of bottom
             30,  90,   0,
             30, 150,  30,
             30,  90,  30,
             30,  90,   0,
             30, 150,   0,
             30, 150,  30,

            // bottom
              0, 150,   0,
              0, 150,  30,
             30, 150,  30,
              0, 150,   0,
             30, 150,  30,
             30, 150,   0,

            // left side
              0,   0,   0,
              0,   0,  30,
              0, 150,  30,
              0,   0,   0,
              0, 150,  30,
              0, 150,   0
        ],
        GL_STATIC_DRAW
    )
}

// Fill the buffer with colors for the 'F'.
var setColors = Fn.new {
    Gl.bufferData(
        GL_ARRAY_BUFFER,
        1, // size of unsigned byte
        [
            // left column front
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,

            // top rung front
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,

            // middle rung front
            200,  70, 120, 
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,

            // left column back
             80,  70, 200,
             80,  70, 200,
             80,  70, 200,
             80,  70, 200,
             80,  70, 200,
             80,  70, 200,

            // top rung back
             80,  70, 200,
             80,  70, 200,
             80,  70, 200,
             80,  70, 200,
             80,  70, 200,
             80,  70, 200,

            // middle rung back
             80,  70, 200,
             80,  70, 200,
             80,  70, 200,
             80,  70, 200,
             80,  70, 200,
             80,  70, 200,

            // top
             70, 200, 210,
             70, 200, 210,
             70, 200, 210,
             70, 200, 210,
             70, 200, 210,
             70, 200, 210,

            // top rung right
            200, 200,  70,
            200, 200,  70,
            200, 200,  70,
            200, 200,  70,
            200, 200,  70,
            200, 200,  70,

            // under top rung
            210, 100,  70,
            210, 100,  70,
            210, 100,  70,
            210, 100,  70,
            210, 100,  70,
            210, 100,  70,

            // between top rung and middle
            210, 160,  70,
            210, 160,  70,
            210, 160,  70,
            210, 160,  70,
            210, 160,  70,
            210, 160,  70,

            // top of middle rung
             70, 180, 210,
             70, 180, 210,
             70, 180, 210,
             70, 180, 210,
             70, 180, 210,
             70, 180, 210,

            // right of middle rung
            100,  70, 210,
            100,  70, 210,
            100,  70, 210,
            100,  70, 210,
            100,  70, 210,
            100,  70, 210,

            // bottom of middle rung.
             76, 210, 100,
             76, 210, 100,
             76, 210, 100,
             76, 210, 100,
             76, 210, 100,
             76, 210, 100,

            // right of bottom
            140, 210,  80,
            140, 210,  80,
            140, 210,  80,
            140, 210,  80,
            140, 210,  80,
            140, 210,  80,
 
            // bottom
             90, 130, 110,
             90, 130, 110,
             90, 130, 110,
             90, 130, 110,
             90, 130, 110,
             90, 130, 110,

            // left side
            160, 160, 220,
            160, 160, 220,
            160, 160, 220,
            160, 160, 220,
            160, 160, 220,
            160, 160, 220
        ],
        GL_STATIC_DRAW
    )
}

var Program
var PositionLocation
var ColorLocation
var MatrixLocation
var PositionBuffer
var ColorBuffer

var init = Fn.new {
    // create vertex shader
    var vShaderSource = """
    #version 100
    attribute vec4 a_position;
    attribute vec4 a_color;

    uniform mat4 u_matrix;

    varying vec4 v_color;

    void main() {
      // Multiply the position by the matrix.
      gl_Position = u_matrix * a_position;

      // Pass the color to the fragment shader.
      v_color = a_color;
    }
    """
    var vShader = Gl.createShader(GL_VERTEX_SHADER)
    if (vShader == 0) Fiber.abort("Error creating vertex shader.")
    Gl.shaderSource(vShader, 1, vShaderSource, null)
    Gl.compileShader(vShader)

    // create fragment shader
    var fShaderSource = """
    #version 100
    precision mediump float;

    // Passed in from the vertex shader.
    varying vec4 v_color;

    void main() {
       gl_FragColor = v_color;
    }
    """
    var fShader = Gl.createShader(GL_FRAGMENT_SHADER)
    if (fShader == 0) Fiber.abort("Error creating fragment shader.")
    Gl.shaderSource(fShader, 1, fShaderSource, null)
    Gl.compileShader(fShader)

    // setup GLSL program
    Program = Gl.createProgram()
    Gl.attachShader(Program, vShader)
    Gl.attachShader(Program, fShader)
    Gl.linkProgram(Program)

    // look up where the vertex data needs to go
    PositionLocation = Gl.getAttribLocation(Program, "a_position")
    ColorLocation = Gl.getAttribLocation(Program, "a_color")
    // lookup uniforms
    MatrixLocation = Gl.getUniformLocation(Program, "u_matrix")
    // generate 2 buffer object names
    var buffers = List.filled(2, 0)
    Gl.genBuffers(2, buffers)

    // Create a buffer to put positions in
    PositionBuffer = buffers[0]
    // Bind it to ARRAY_BUFFER
    Gl.bindBuffer(GL_ARRAY_BUFFER, PositionBuffer)
    // Put geometry data into buffer
    setGeometry.call()

    // Create a buffer to put colors in
    ColorBuffer = buffers[1]
    // Bind it to ARRAY_BUFFER
    Gl.bindBuffer(GL_ARRAY_BUFFER, ColorBuffer)
    // Put color data into buffer
    setColors.call()
}

var Translation = [0, 0, -360]
var Rotation = [Math.radians(190), Math.radians(40), Math.radians(320)]
var Scale = [1, 1, 1]
var FieldOfViewRadians = Math.radians(60)
var WindowWidth = 640
var WindowHeight = 480

var drawScene // recursive function
drawScene = Fn.new {
    Rotation[1] = Rotation[1] + 0.01
    if (Rotation[1] >= 360) Rotation[1] = 0

    // tell GLES how to convert from clip space to pixels
    Gl.viewport(0, 0, WindowWidth, WindowHeight)

    // clear the window and the depth buffer
    Gl.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    // turn on culling. By default backfacing triangles will be culled
    Gl.enable(GL_CULL_FACE)

    // Enable the depth buffer
    Gl.enable(GL_DEPTH_TEST)

    // Tell it to use our program (pair of shaders)
    Gl.useProgram(Program)

    // Turn on the position attribute
    Gl.enableVertexAttribArray(PositionLocation)

    // Bind the position buffer.
    Gl.bindBuffer(GL_ARRAY_BUFFER, PositionBuffer)

    // tell the position attribute how to get data out of positionBuffer (ARRAY_BUFFER)
    var size = 3           // 3 components per iteration
    var type = GL_FLOAT    // the data is 32 bit floats
    var normalize = false  // don't normalize the data
    var stride = 0         // 0 = move forward size * sizeof(type) each iteration to get the next position
    var offset = 0         // start at the beginning of the buffer
    Gl.vertexAttribPtr(PositionLocation, size, type, normalize, stride, offset)

    // Turn on the color attribute
    Gl.enableVertexAttribArray(ColorLocation)

    // Bind the color buffer.
    Gl.bindBuffer(GL_ARRAY_BUFFER, ColorBuffer)

    // Tell the attribute how to get data out of colorBuffer (ARRAY_BUFFER)
    Gl.vertexAttribPtr(ColorLocation, size, type, normalize, stride, offset)

    // compute the matrices
    var aspect = WindowWidth / WindowHeight
    var matrix = M4.perspective(FieldOfViewRadians, aspect, 1, 2000, null)
    matrix = M4.translate(matrix, Translation[0], Translation[1], Translation[2], null)
    matrix = M4.xRotate(matrix, Rotation[0], null)
    matrix = M4.yRotate(matrix, Rotation[1], null)
    matrix = M4.zRotate(matrix, Rotation[2], null)
    matrix = M4.scale(matrix, Scale[0], Scale[1], Scale[2], null)

    // set the matrix
    Gl.uniformMatrix4fv(MatrixLocation, 1, false, matrix)

    // draw the geometry
    var primitiveType = GL_TRIANGLES
    var count = 16 * 6
    Gl.drawArrays(primitiveType, offset, count)
    Gl.flush()
    C.usleep(10000) // wait for 10 msec
    drawScene.call()
}

Glut.initDisplayMode(GLUT_SINGLE | GLUT_RGB | GLUT_DEPTH)
Glut.initWindowSize(640, 480)
Glut.createWindow("Rotating F")
Glut.setOption(GLUT_ACTION_ON_WINDOW_CLOSE, GLUT_ACTION_GLUTMAINLOOP_RETURNS)
init.call()
drawScene.call()


We now embed the above code in the following C program, compile and run.

/* gcc WebGL_rotating_F.c -o WebGL_rotating_F -lglut -lGLESv2 -lm -lwren */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <GLES2/gl2.h>
#include <GL/freeglut.h>
#include <unistd.h>
#include "wren.h"

/* Gl functions */

void C_bufferData(WrenVM* vm) {
    GLenum target = (GLenum)wrenGetSlotDouble(vm, 1);
    GLsizeiptr size = (GLsizeiptr)wrenGetSlotDouble(vm, 2);
    int count = wrenGetListCount(vm, 3);
    GLenum usage = (GLenum)wrenGetSlotDouble(vm, 4);
    float data[count];
    for (int i = 0; i < count; ++i) {
        wrenGetListElement(vm, 3, i, 1);
        data[i] = (float)wrenGetSlotDouble(vm, 1);
        if (size == 1) data[i] /= 255.0;
    }
    glBufferData(target, 4 * count, data, usage);
}

void C_createShader(WrenVM* vm) {
    GLenum shaderType = (GLenum)wrenGetSlotDouble(vm, 1);
    GLuint shader = glCreateShader(shaderType);
    wrenSetSlotDouble(vm, 0, (double)shader);
}

void C_shaderSource(WrenVM* vm) {
    GLuint shader = (GLuint)wrenGetSlotDouble(vm, 1);
    GLsizei count = (GLsizei)wrenGetSlotDouble(vm, 2);
    // assume for simplicity there will always be one shader string and the fourth parameter will be null
    const GLchar *string = (const GLchar *)wrenGetSlotString(vm, 3);
    glShaderSource(shader, count, &string, 0);
}

void C_compileShader(WrenVM* vm) {
    GLuint shader = (GLuint)wrenGetSlotDouble(vm, 1);
    glCompileShader(shader);
    // check shader compiled ok
    GLint shader_compiled;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &shader_compiled);
    if (shader_compiled != GL_TRUE) {
        GLsizei log_length = 0;
        GLchar message[1024];
        glGetShaderInfoLog(shader, 1024, &log_length, message);
        printf("%s\n", message);
    }
}

void C_createProgram(WrenVM* vm) {
    GLuint program = glCreateProgram();
    wrenSetSlotDouble(vm, 0, (double)program);
}

void C_attachShader(WrenVM* vm) {
    GLuint program = (GLuint)wrenGetSlotDouble(vm, 1);
    GLuint shader  = (GLuint)wrenGetSlotDouble(vm, 2);
    glAttachShader(program, shader);
}

void C_linkProgram(WrenVM* vm) {
    GLuint program = (GLuint)wrenGetSlotDouble(vm, 1);
    // check program linked ok
    glBindAttribLocation(program, 0, "position");
    glBindAttribLocation(program, 1, "texcoord");
    glBindAttribLocation(program, 2, "normal");
    glBindAttribLocation(program, 3, "color");
    glLinkProgram(program);
    GLint program_linked;
    glGetProgramiv(program, GL_LINK_STATUS, &program_linked);
    if (program_linked != GL_TRUE) {
        GLsizei log_length = 0;
        GLchar message[1024];
        glGetProgramInfoLog(program, 1024, &log_length, message);
        printf("%s\n", message);
    }
}

void C_getAttribLocation(WrenVM* vm) {
    GLuint program = (GLuint)wrenGetSlotDouble(vm, 1);
    const GLchar *name = (const GLchar *)wrenGetSlotString(vm, 2);
    GLint location = glGetAttribLocation(program, name);
    wrenSetSlotDouble(vm, 0, (double)location);
}

void C_getUniformLocation(WrenVM* vm) {
    GLuint program = (GLuint)wrenGetSlotDouble(vm, 1);
    const GLchar *name = (const GLchar *)wrenGetSlotString(vm, 2);
    GLint location = glGetUniformLocation(program, name);
    wrenSetSlotDouble(vm, 0, (double)location);
}

void C_genBuffers(WrenVM* vm) {
    GLsizei n = (GLsizei)wrenGetSlotDouble(vm, 1);
    int count = wrenGetListCount(vm, 2);
    GLuint buffers[count];
    glGenBuffers(n, buffers);
    for (int i = 0; i < count; ++i) {
        wrenSetSlotDouble(vm, 1, (double)buffers[i]);
        wrenSetListElement(vm, 2, i, 1);
    }
}

void C_bindBuffer(WrenVM* vm) {
    GLenum target = (GLenum)wrenGetSlotDouble(vm, 1);
    GLuint buffer = (GLuint)wrenGetSlotDouble(vm, 2);
    glBindBuffer(target, buffer);
}

void C_viewport(WrenVM* vm) {
    GLint x = (GLint)wrenGetSlotDouble(vm, 1);
    GLint y = (GLint)wrenGetSlotDouble(vm, 2);
    GLsizei width = (GLsizei)wrenGetSlotDouble(vm, 3);
    GLsizei height = (GLsizei)wrenGetSlotDouble(vm, 4);
    glViewport(x, y, width, height);
}

void C_clear(WrenVM* vm) {
    glClearColor(1, 1, 1, 1); // always clear to white background
    GLbitfield mask = (GLbitfield)wrenGetSlotDouble(vm, 1);
    glClear(mask);
}

void C_enable(WrenVM* vm) {
    GLenum cap = (GLenum)wrenGetSlotDouble(vm, 1);
    glEnable(cap);
}

void C_useProgram(WrenVM* vm) {
    GLuint program = (GLuint)wrenGetSlotDouble(vm, 1);
    glUseProgram(program);
}

void C_drawArrays(WrenVM* vm) {
    GLenum mode = (GLenum)wrenGetSlotDouble(vm, 1);
    GLint first = (GLint)wrenGetSlotDouble(vm, 2);
    GLsizei count = (GLsizei)wrenGetSlotDouble(vm, 3);
    glDrawArrays(mode, first, count);
}

void C_enableVertexAttribArray(WrenVM* vm) {
    GLuint index = (GLuint)wrenGetSlotDouble(vm, 1);
    glEnableVertexAttribArray(index);
}

void C_vertexAttribPtr(WrenVM* vm) {
    GLuint index = (GLuint)wrenGetSlotDouble(vm, 1);
    GLint size = (GLint)wrenGetSlotDouble(vm, 2);
    GLenum type = (GLenum)wrenGetSlotDouble(vm, 3);
    GLboolean normalized = (GLboolean)wrenGetSlotBool(vm, 4);
    GLsizei stride = (GLsizei)wrenGetSlotDouble(vm, 5);
    // assume for simplicity sixth parameter will always be 0
    glVertexAttribPointer(index, size, type, normalized, stride, 0);
}

void C_uniformMatrix4fv(WrenVM* vm) {
    GLint location = (GLint)wrenGetSlotDouble(vm, 1);
    GLsizei count = (GLsizei)wrenGetSlotDouble(vm, 2);
    GLboolean transpose = (GLboolean)wrenGetSlotBool(vm, 3);
    int len = wrenGetListCount(vm, 4);
    GLfloat value[len];
    for (int i = 0; i < len; ++i) {
        wrenGetListElement(vm, 4, i, 1);
        value[i] = (GLfloat)wrenGetSlotDouble(vm, 1);
    }
    glUniformMatrix4fv(location, count, transpose, value);
}

void C_flush(WrenVM* vm) {
    glFlush();
}

/* Glut functions */

void C_initDisplayMode(WrenVM* vm) {
    unsigned int mode = (unsigned int)wrenGetSlotDouble(vm, 1);
    glutInitDisplayMode(mode);
}

void C_initWindowSize(WrenVM* vm) {
    int width  = (int)wrenGetSlotDouble(vm, 1);
    int height = (int)wrenGetSlotDouble(vm, 2);
    glutInitWindowSize(width, height);
}

void C_createWindow(WrenVM* vm) {
    const char *name = wrenGetSlotString(vm, 1);
    glutCreateWindow(name);
}

void C_setOption(WrenVM* vm) {
    GLenum eWhat = (GLenum)wrenGetSlotDouble(vm, 1);
    int value    = (int)wrenGetSlotDouble(vm, 2);
    glutSetOption(eWhat, value);
}

/* C function */

void C_usleep(WrenVM* vm) {
    useconds_t usec = (useconds_t)wrenGetSlotDouble(vm, 1);
    usleep(usec);
}

WrenForeignMethodFn bindForeignMethod(
    WrenVM* vm,
    const char* module,
    const char* className,
    bool isStatic,
    const char* signature) {
    if (strcmp(module, "main") == 0) {
        if (strcmp(className, "Gl") == 0) {
            if (isStatic && strcmp(signature, "bufferData(_,_,_,_)") == 0)     return C_bufferData;
            if (isStatic && strcmp(signature, "createShader(_)") == 0)         return C_createShader;
            if (isStatic && strcmp(signature, "shaderSource(_,_,_,_)") == 0)   return C_shaderSource;
            if (isStatic && strcmp(signature, "compileShader(_)") == 0)        return C_compileShader;
            if (isStatic && strcmp(signature, "createProgram()") == 0)         return C_createProgram;
            if (isStatic && strcmp(signature, "attachShader(_,_)") == 0)       return C_attachShader;
            if (isStatic && strcmp(signature, "linkProgram(_)") == 0)          return C_linkProgram;
            if (isStatic && strcmp(signature, "getAttribLocation(_,_)") == 0)  return C_getAttribLocation;
            if (isStatic && strcmp(signature, "getUniformLocation(_,_)") == 0) return C_getUniformLocation;
            if (isStatic && strcmp(signature, "genBuffers(_,_)") == 0)         return C_genBuffers;
            if (isStatic && strcmp(signature, "bindBuffer(_,_)") == 0)         return C_bindBuffer;
            if (isStatic && strcmp(signature, "viewport(_,_,_,_)") == 0)       return C_viewport;
            if (isStatic && strcmp(signature, "clear(_)") == 0)                return C_clear;
            if (isStatic && strcmp(signature, "enable(_)") == 0)               return C_enable;
            if (isStatic && strcmp(signature, "useProgram(_)") == 0)           return C_useProgram;
            if (isStatic && strcmp(signature, "drawArrays(_,_,_)") == 0)       return C_drawArrays;
            if (isStatic && strcmp(signature, "flush()") == 0)                 return C_flush;

            if (isStatic && strcmp(signature, "enableVertexAttribArray(_)") == 0)   return C_enableVertexAttribArray;
            if (isStatic && strcmp(signature, "vertexAttribPtr(_,_,_,_,_,_)") == 0) return C_vertexAttribPtr;
            if (isStatic && strcmp(signature, "uniformMatrix4fv(_,_,_,_)") == 0)    return C_uniformMatrix4fv;
        } else if (strcmp(className, "Glut") == 0) {
            if (isStatic && strcmp(signature, "initDisplayMode(_)") == 0)  return C_initDisplayMode;
            if (isStatic && strcmp(signature, "initWindowSize(_,_)") == 0) return C_initWindowSize;
            if (isStatic && strcmp(signature, "createWindow(_)") == 0)     return C_createWindow;
            if (isStatic && strcmp(signature, "setOption(_,_)") == 0)      return C_setOption;
        } else if (strcmp(className, "C") == 0) {
            if (isStatic && strcmp(signature, "usleep(_)") == 0)           return C_usleep;
        }
    }
    return NULL;
}

static void writeFn(WrenVM* vm, const char* text) {
    printf("%s", text);
}

void errorFn(WrenVM* vm, WrenErrorType errorType, const char* module, const int line, const char* msg) {
    switch (errorType) {
        case WREN_ERROR_COMPILE:
            printf("[%s line %d] [Error] %s\n", module, line, msg);
            break;
        case WREN_ERROR_STACK_TRACE:
            printf("[%s line %d] in %s\n", module, line, msg);
            break;
        case WREN_ERROR_RUNTIME:
            printf("[Runtime Error] %s\n", msg);
            break;
    }
}

char *readFile(const char *fileName) {
    FILE *f = fopen(fileName, "r");
    fseek(f, 0, SEEK_END);
    long fsize = ftell(f);
    rewind(f);
    char *script = malloc(fsize + 1);
    fread(script, 1, fsize, f);
    fclose(f);
    script[fsize] = 0;
    return script;
}

static void loadModuleComplete(WrenVM* vm, const char* module, WrenLoadModuleResult result) {
    if( result.source) free((void*)result.source);
}

WrenLoadModuleResult loadModule(WrenVM* vm, const char* name) {
    WrenLoadModuleResult result = {0};
    if (strcmp(name, "random") != 0 && strcmp(name, "meta") != 0) {
        result.onComplete = loadModuleComplete;
        char fullName[strlen(name) + 6];
        strcpy(fullName, name);
        strcat(fullName, ".wren");
        result.source = readFile(fullName);
    }
    return result;
}

int main(int argc, char **argv) {
    glutInit(&argc, argv);
    glutInitContextVersion(2, 0);
    WrenConfiguration config;
    wrenInitConfiguration(&config);
    config.writeFn = &writeFn;
    config.errorFn = &errorFn;
    config.bindForeignMethodFn = &bindForeignMethod;
    config.loadModuleFn = &loadModule;
    WrenVM* vm = wrenNewVM(&config);
    const char* module = "main";
    const char* fileName = "WebGL_rotating_F.wren";
    char *script = readFile(fileName);
    WrenInterpretResult result = wrenInterpret(vm, module, script);
    switch (result) {
        case WREN_RESULT_COMPILE_ERROR:
            printf("Compile Error!\n");
            break;
        case WREN_RESULT_RUNTIME_ERROR:
            printf("Runtime Error!\n");
            break;
        case WREN_RESULT_SUCCESS:
            break;
    }
    glutMainLoop();
    wrenFreeVM(vm);
    free(script);
    return 0;
}