WebGL rotating F

From Rosetta Code
Revision as of 14:03, 11 March 2022 by Petelomax (talk | contribs) (Created page with "{{draft task}} Using WebGL or OpenGL ES 2.0 compatible routines draw a rotating F shape.<br> Specifically avoid glBegin, glVertex,...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
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.

Phix

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

--
-- 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 specifc 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|compete) 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()