OpenGL

           


by Anton Gerdelan


the main libraries that you need is libGL. there is also a set of utility functions in a library called libGLU, but you more than likely won't need it. in Linux, by default, MESA is used, but typically it is a bit slow, and is currently only supporting OpenGL 3. to get OpenGL 4 support, switch to proprietary hardware drivers (Nvidia, AMD, or Intel) or download from manufacturer website

GLFW is a helper library that will start the OpenGL "context" so that it talks to (almost) any operating system in the same way. the context is a running copy of OpenGL, tied to a window on the operating system

there's a library called GLEW that makes sure that you include the latest version of GL, and not a default [ancient] version on the system path. it also handles GL extensions. include GLEW before GLFW to use the latest GL libraries

// gcc -lglfw -lGL -lGLEW someprg.c 
#include <GL/glew.h> 
#include <GLFW/glfw3.h> 
#include <stdio.h>
#include <stdlib.h>


initialization:
  int main (int argc, char** argv) {

    if (!glfwInit ()) {
      fprintf (stderr, "ERROR: could not start GLFW3\n");
      return 1;
    } 

    // these four strings are really very important for shaders!
    glfwWindowHint (GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint (GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint (GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    glfwWindowHint (GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // antialiasing
    glfwWindowHint (GLFW_SAMPLES, 4);

    // open window in fullscreen mode
    GLFWmonitor* m = glfwGetPrimaryMonitor ();
    const GLFWvidmode* vmode = glfwGetVideoMode (m);
    GLFWwindow* w = glfwCreateWindow (vmode->width, vmode->height, "your title", m, NULL);

    if (!w) {
      fprintf (stderr, "ERROR: could not open window with GLFW3\n");
      glfwTerminate();
      return 1;
    }

    glfwMakeContextCurrent (w);
    glewExperimental = GL_TRUE;
    glewInit ();

    // tell GL to only draw onto a pixel if the shape is closer to the viewer
    glEnable (GL_DEPTH_TEST); // enable depth-testing
    glDepthFunc (GL_LESS);    // depth-testing interprets a smaller value as "closer"


you will copy chunk of memory onto the graphics card in a unit called a vertex buffer object (VBO). to do this you "generate" an empty buffer, set it as the current buffer in OpenGL's state machine by "binding", then copy the points into the currently bound buffer

   float mybuff[] = { 0.0, 0.5, 0.0, 0.5, -0.5, 0.0, -0.5, -0.5, 0.0, };
   
   GLuint vbo;
   glGenBuffers (1, &vbo);
   glBindBuffer (GL_ARRAY_BUFFER, vbo);
   glBufferData (GL_ARRAY_BUFFER, 9 * sizeof (float), mybuff, GL_STATIC_DRAW);

the last line tells GL that the buffer is the size of nine floating point numbers, and gives it the address of the first value

notice that function glBufferData doesn't refer to the id of the VBO, but instead to the current active array buffer

the second parameter specifies the size in bytes. the final parameter is very important and its value depends on the usage of the vertex data:

GL_STATIC_DRAW
the vertex data will be uploaded once and drawn many times
GL_DYNAMIC_DRAW
the vertex data will be changed from time to time, but drawn many times more than that
GL_STREAM_DRAW
the vertex data will change almost every time it's drawn

with VBOs, you copy the whole lot into a buffer before drawing starts, and this sits on the graphics hardware memory. the GL gives you a handle (identifier number) to the whole buffer to interact with, rather than a traditional address to the first element in the buffer

 

vertex attribute object (VAO) remembers all of the vertex buffers that you want to use, and the memory layout of each one. you set up the vertex array object once per mesh. when you want to draw, all you do then is bind the VAO and draw

  GLuint vao;
  glGenVertexArrays (1, &vao);
  glBindVertexArray (vao);
  glEnableVertexAttribArray (0);

  glBindBuffer (GL_ARRAY_BUFFER, vbo);
  glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  glEnableVertexAttribArray (0);

the glVertexAttribPointer function defines the layout of your first vertex buffer:

  • "0" means define the layout for attribute number 0
  • "3" means that the variables are vec3 made from every three floats (GL_FLOAT) in the buffer
  • the first parameter of glVertexAttribPointer asks for an index. this is going to map to the indices in your vertex shader

    you need to use a VAO to tell OpenGL that the array is divided into variables of 3 floats each

    attributes are disabled by default. use a function glEnableVertexAttribArray to enable each one. this function only affects the currently bound vertex array object. if you forget to enable an attribute, the shader won't know where to read the data from and you'll get weird results e.g. black colours


    shaders

    you need to use a shader program, written in OpenGL Shader Language (GLSL), to define how to draw your shape from the vertex attribute object. this shader program is made from the minimum two parts:
  • - a vertex shader, which describes where the 3d points should end up on the display, and
  • - a fragment shader which colours the surfaces
  • shaders are written in a C-style language called GLSL (OpenGL Shading Language). OpenGL will compile your program from source at runtime and copy executable to the graphics card

    the first commented string in GLSL file is really important - it MUST BE and it MUST BE correct. if you leave out the version tag, OpenGL fall back to an earlier default - it's always better to specify the version

      #version 330 core 
    
      in vec3 vp;
    
      void main () {
        gl_Position = vec4 (vp, 1.0);
      }
    

    you can also see the in keyword for input to the program from the previous stage. GLSL also has an out keyword for sending a variable to the next stage

    this vertex shader has one input variable; a vec3 (vector made from three floats) type, which MATCHES UP TO your VAO's ATTRIBUTE POINTER. this means that each vertex shader gets 3 of the all 9 floats from your buffer - therefore 3 vertex shaders WILL RUN CONCURRENTLY; each one positioning 1 of the vertices

    the output has a reserved name gl_Position and expects a 4d float. the 1 at the end just means "don't calculate any perspective"

    the fragment shader has one job - setting the colour of each fragment. it therefore has one output - a 4d vector type, representing a colour made from red, blue, green, and alpha components - each component has a value between 0 and 1. the alpha channel output can be used for a variety of effects, which you define by setting a blend mode in OpenGL. it is commonly used to indicate opacity (for transparent effects), but by default it does nothing

      #version 330 core 
    
      out vec4 fcolour;
      
      void main () {
        fcolour = vec4 (0.5, 0.0, 0.5, 1.0);
      };
    

    again, the first commented string in GLSL file is really important - it MUST BE and it MUST BE correct

    you should load these two shader listings into memory somehow (reading from files, for example)

    before using the shaders you have to create GL shader, load the listing into a GL shader, compile this GL shader

      GLuint vs = glCreateShader (GL_VERTEX_SHADER);
      glShaderSource (vs, 1, &vertex_shader_mem_location_here, NULL);
      glCompileShader (vs);
    
      GLuint fs = glCreateShader (GL_FRAGMENT_SHADER);
      glShaderSource (fs, 1, &fragment_shader_mem_location_here, NULL);
      glCompileShader (fs);
    

    the glShaderSource function can take multiple source strings in an array, but you'll usually have your source code in one char array. the last parameter can contain an array of source code string lengths, passing NULL simply makes it stop at the null terminator

    be aware that if the shader fails to compile, e.g. because of a syntax error, glGetError will not report an error! checking if a shader compiled successfully:

      GLint status;
      glGetShaderiv (vertexShader, GL_COMPILE_STATUS, &status);
    
    if status is equal to GL_TRUE, then your shader was compiled successfully

      char buffer[512];
      glGetShaderInfoLog (vertexShader, 512, NULL, buffer);
    
    this will store the first 511 bytes + null terminator of the compile log in the specified buffer. the log may also report useful warnings even when compiling was successful, so it's useful to check it out from time to time when you develop your shaders

    now, these two compiled shaders must be combined into a single, executable GPU shader program:

      GLuint your_shader_program = glCreateProgram ();
      glAttachShader (your_shader_program, fs);
      glAttachShader (your_shader_program, vs);
      glLinkProgram  (your_shader_program);
    

    now, lets draw triangle:

      glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      glUseProgram (shader_program);
      glBindVertexArray (vao);
      glDrawArrays (GL_TRIANGLES, 0, 3);
      glfwSwapBuffers (w);
    
      sleep (5);
    
      glDeleteShader(vs); glDetachShader(your_shader_program, vs);
      glDeleteShader(fs); glDetachShader(your_shader_program, fs);
    
      glfwTerminate();
    
      return 0;
    }
    
    

    first you clear the drawing surface, then set the shader program that should be "in use" for all further drawing. you set VAO (not the VBO) as the input variables that should be used for all further drawing. then you can draw, and you want to draw in triangles mode (1 triangle for every 3 points), and draw from point number 0, for 3 points

    finally, you flip the swap surface onto the screen. done!

    a shader object can be deleted with glDeleteShader(GLuint), but it will not actually be removed before it has been detached from all programs with glDetachShader(GLuint,GLuint)


    GLFW3 requires that you manually call glfwPollEvents() to update things non-graphical events like key-presses

    GLFW has a function glfwGetTime() which gives us a double-precision floating point number, containing the number of seconds since GLFW started


    a pixel is a "picture element". in OpenGL lingo, pixels are the elements that make up the final 2d image that it draws inside a window on your display

    a fragment is a pixel-sized area of a surface. a fragment shader determines the colour of each one

    sometimes surfaces overlap - you then have more than 1 fragment for 1 pixel. all of the fragments are drawn, even the hidden ones

    note that there is a distinction between a shader, which is a mini-program for just one stage in the hardware pipeline, and a shader program which is a GPU program that comprises several shaders that have been linked together

    to get shaders up and running quickly you can bang this into a minimal GL program:

    lets make function glUniform4f (location, r,g,b,a) to assign an initial colour to your fragment shader. example:

      glUniform4f (colour_loc, 1.0, 0.0, 0.0, 1.0)
    

    for red color

    the only variables that you need to keep track of are

  • the index created by glCreateProgram, and
  • any uniform locations

  • best practice

    all uniform variables are intitialised to 0 when a program links, so you only need to initialise them if the initial value should be something else. example: you might want to set matrices to the identity matrix, rather than a zeroed matrix

    calling glUniform is quite expensive during run-time. structure your program so that glUniform is only called when the value needs to change. this might be the case every time that you draw a new object (e.g. its position might be different), but some uniforms may not change often (e.g. projection matrix)

    calling glGetUniformLocation during run-time can be expensive. it is best to do this during intialisation of the component that updates the uniform e.g. the virtual camera for the projection and view matrices, or a renderable object in the scene for the model matrix. store the uniform locations once, then call glUniform as needed, rather than updating everything every frame

    when calling glGetUniformLocation, it returns -1 if the uniform variable wasn't found to be active. you can check for this. usually it means that either you've made a typo in the name, or the variable isn't actually used anywhere in the shader, and has been "optimised out" by the compiler/linker

    modifying attributes (vertex buffers) during run-time is extremely expensive. avoid

    get your shaders to do as much work as is possible; because of their parallel nature they are much faster than looping on the CPU for most tasks

    drawing lots of separate, small objects at once does not make efficient use of the GPU, as most parallel shader slots will be empty, and separate objects must be drawn in series. where possible, merge many, smaller objects into fewer, larger objects


    you can store more than just 3d points in a vertex buffer. common uses also include

  • 2d texture coordinates that describe how to fit an image to a surface, and
  • 3d normals that describe which way a surface is facing
  • so it's quite likely that most of your objects will have 2 or 3 vertex buffers each

    you can use vertex buffers to hold any data that you like; the key thing is that the data is retrieved once per vertex. if you tell OpenGL to draw an array of 3 points, using a given vertex array object, then it is going to launch 3 vertex shaders in parallel, and each vertex shader will get a one variable from each of the attached arrays; the first vertex shader will get the first 3d point, 2d texture coordinate, and 3d normal, the second vertex shader will get the second 3d point, 2d texture coordinate, and 3d normal, and so on, where the number of variables, and size of each variable is laid out in the vertex array object

    each fragment shader gets an interpolated colour based on its position on the surface. the fragment exactly on the red corner of the triangle will be completely red (1.0, 0.0, 0.0). A fragment exactly half-way between the blue and red vertices, along the edge of the triangle, will be purple; half red, half blue, and no green: (0.5, 0.0, 0.5). a fragment exactly in the middle of the triangle will be an equal mixture of all 3 colours; (0.3333, 0.3333, 0.333)


    example of vertex shader
    #version 330 core
    
    layout(location = 0) in  vec3 vposition;
    layout(location = 1) in  vec3 vcolour;
    layout(location = 2) in  vec3 theta;
    out vec4 colour;
    
    void main () {
      vec3 a = radians (theta);
      vec3 c = cos (a);
      vec3 s = sin (a);
    
      // NB!   column-major matrices in GLSL
    
      mat4 rx = mat4 ( 1.0,  0.0,  0.0,  0.0, 
                       0.0,  c.x,  s.x,  0.0,
                       0.0, -s.x,  c.x,  0.0,
                       0.0,  0.0,  0.0,  1.0 );
    
      mat4 ry = mat4 ( c.y,  0.0, -s.y,  0.0, 
                       0.0,  1.0,  0.0,  0.0,
                       s.y,  0.0,  c.y,  0.0,
                       0.0,  0.0,  0.0,  1.0 );
    
      mat4 rz = mat4 ( c.z, -s.z,  0.0,  0.0, 
                       s.z,  c.z,  0.0,  0.0,
                       0.0,  0.0,  1.0,  0.0,
                       0.0,  0.0,  0.0,  1.0 );
    
      colour = vec4 (vcolour, 1.0);
      gl_Position = rx * ry * rz * vec4 (vposition, 1.0); 
    }
    
    example of fragment shader
    #version 330 core 
    
    in vec4 colour;
    out vec4 frag_colour;
    
    void main () {
      frag_colour = colour;
    }
    

    by Jacobo Rodríguez


    GLSL functions


    GLSL types

    vectors

    vectors are considered column vectors
  • bvec2, bvec3, bvec4 (vectors of 2, 3, and 4 boolean elements)
  • ivec2, ivec3, ivec4 (vectors of 2, 3, and 4 integer elements)
  • uvec2, uvec3, uvec4 (vectors of 2, 3, and 4 unsigned integers)
  • vec2, vec3, vec4 (vectors of 2, 3, and 4 floats, single precision)
  • dvec2, dvec3, dvec4 (vectors of 2, 3, and 4 floats, double precision)

    for vectors, the following are the valid names for the structure's fields (the three groups are synonymous):

  • {x, y, z, w} when accessing vectors that represent positions
  • {r, g, b, a} when accessing vectors that represent colors
  • {s, t, p, q} when accessing vectors that represent texture coordinates

  • position.x = 1.0
    position.y = 1.0
    position.z = 1.0
    position.w = 1.0

    color.r = 1.0
    color.g = 1.0
    color.b = 1.0
    color.a = 1.0

    matrices

    matrices are always made of floating point numbers (the d prefix stands for double precision):

  • mat2, mat3, mat4 (2 x 2, 3 x 3, and 4 x 4 matrices)
  • dmat2, dmat3, dmat4 (2 x 2, 3 x 3, and 4 x 4 matrices)
  • mat2x3, mat2x4, mat3x2, mat3x4, mat4x2, mat4x3
  • dmat2x3, dmat2x4, dmat3x2, dmat3x4, dmat4x2, dmat4x3
  • (first number refers to columns and second to rows)

    a matrix is an array of column vectors and GLSL matrices are column matrices. this means that the first subscript is the column index and the second subscript is the row index:

    mat4 m;
    m[0] = vec4(1.0);         // put the first column to 1.0 in each element
    m[1][1] = 2.0;            // put the second diagonal element to 2.0
    m[2] = color3.xxyy;       // Insert a swizzled vector into the third column
    

    uniform variables

    a parameter that is passed from the application to the shaders is called a uniform variable. those variables are always read-only (constant), global to the shaders that form the current executable program

    the procedure is the following: first, set up the uniform's value, draw some triangles, change the variable (if desired), draw other triangles, and repeat this process until you're finished with your rendering

    let's go to the shader's part:

      #version 330
    
      uniform vec3 myVarName;
    
      void main() {
        ...
      }
    

    and in the host application:

      // enable your shader program. before setting uniforms 
      // the shader must be bound using this function:
      glUseProgram(shader_program_ID);
    
      int your_symbol_location = glGetUniformLocation (shader_program_ID, "myVarName");
    
      // let's create a dummy position, an array of 3
      float myLightPosition[] = {0, 10.0, 0};
    
      // set the values into the uniform's slot
      glUniform3fv(your_symbol_location, myLightPosition);
    

    with this, you have filled the uniform variable with the valid values

    you can pass whatever you want as a uniform variable: single integers, matrices, structures, or even arrays of structures

    these variables' values are constant until you change their values from your OpenGL host application

    using these variables, you can pass the transform matrices to the shader, lighting parameters, or whatever you can think of

    another example:

    upload the variables' values to the GPU in the OpenGL host application:

      // enable your shader program. before setting uniforms 
      // the shader must be bound using this function:
      glUseProgram (p);
    
      GLint x;
      // value to be uploaded to the GPU
      GLfloat variable_01[16] = ... ; // Fill with proper values
      GLfloat variable_02[16] = ... ; // Fill with proper values
    
      // retrieve variable's slots from the shader using the variable's names in the shader
      x = glGetUniformLocation (p, "SomeNameFromShader");
      // upload the values to the shader
      glUniformMatrix2fv (x, 1, GL_FALSE, variable_01);
    
      // retrieve variable's slots from the shader using the variable's names in the shader
      x = glGetUniformLocation (p, "SomeOtherNameFromShader");
      // upload the values to the shader
      glUniformMatrix4fv (x, 1, GL_FALSE, variable_02);
    

    and that's all. the values are ready to be used in the shader:

      #version 330
      #pragma debug(on)
      #pragma optimize(off)
    
      layout (location = 0) in vec4 Position;
    
      // uniform matrices declaration
      uniform mat4 SomeNameFromShader;
      uniform mat4 SomeOtherNameFromShader;
    
      void main() {
        gl_Position = SomeOtherNameFromShader * SomeNameFromShader * Position;
      }
    

    besides uploading a plain array of floats as uniform values, we have declared variables of the mat4 type. GLSL will take care of formatting those arrays into the target type automatically. of course, the data must fit naturally. you can't simply map bool to mat4, or even float[9] to mat4. you must be careful with this

    gl_Position is a special (built-in) value that must hold the transformed vertex position. this is the only requirement for a vertex shader and is its main purpose. not putting a value there will render your shader unusable and ill-formed and you will get a compiler error


    fragment shaders


    using fragment shaders is the only way of painting something on the screen. they are responsible for updating the framebuffer with colors and depth; so, all that you see on the screen is painted by a fragment shader

    two very important constraints about fragment shaders:


    geometry shader


    the geometry shader is optional

    since OpenGL 3.2 there is a third optional type of shader that sits between the vertex and fragment shaders, known as the geometry shader. this shader has the unique ability to create new geometry on the fly using the output of the vertex shader as input

    input types

    whereas a vertex shader processes vertices and a fragment shader processes fragments, a geometry shader processes entire primitives. the first line describes what kind of primitives your shader should process

      layout(points) in;
    

    the available types are listed below, along with their equivalent drawing command types:

    the gl_Position, as set in the vertex shader, can be accessed using the gl_in array in the geometry shader. it is an array of structs that looks like this:

      gl_PerVertex {
        vec4   gl_Position;
        float  gl_PointSize;
        float  gl_ClipDistance[];
      } gl_in[];
    

    output types

    the next line describes the output of the shader. what's interesting about geometry shaders is that they can output an entirely different type of geometry and the number of generated primitives can even vary!

      layout(line_strip, max_vertices = 2) out;
    

    the second line specifies the output type and the maximum amount of vertices it can pass on. this is the maximum amount for the shader invocation, not for a single primitive (line_strip in this case)

    the following output types are available:

    these types seem somewhat restricted, but if you think about it, these types are sufficient to cover all possible types of primitives. for example, a triangle_strip with only 3 vertices is equivalent to a regular triangle

    special functions

    the geometry shader program can call two special functions to generate primitives, EmitVertex and EndPrimitive. each time the program calls EmitVertex, a vertex is added to the current primitive. when all vertices have been added, the program calls EndPrimitive to generate the primitive
    example
      // gcc -lglfw -lGL -lGLEW geom0.c 
      #define GLEW_STATIC
      #include <GL/glew.h> 
      #include <GLFW/glfw3.h> 
      #include <stdio.h>
      #include <stdlib.h>
      #define GLSL(src) "#version 330 core\n" #src
    
      // vertex shader
      const char* vs = GLSL(
    
        in  vec2  pos;
        in  vec3  col;
        in  float sid;
    
        out vec3  vColor;
        out float vSides;
    
        void main() {
          gl_Position = vec4 (pos, 0.0, 1.0);
          vColor = col;
          vSides = sid;
        }
      );
    
      // geometry shader
      const char* gs = GLSL(
    
        layout(points) in;
        layout(line_strip, max_vertices = 64) out;
    
        in  vec3   vColor[];
        in  float  vSides[];
    
        out vec3   fColor;
    
        const float PI = 3.1415926;
    
        void main() {
          fColor = vColor[0];
    
          for (int i = 0; i <= vSides[0]; i++) {
            // angle between each side in radians
            float ang = PI * 2.0 / vSides[0] * i;
            // offset from center of point (0.3 to accomodate for aspect ratio)
            vec4 offset = vec4(cos(ang) * 0.3, -sin(ang) * 0.4, 0.0, 0.0);
            gl_Position = gl_in[0].gl_Position + offset;
            EmitVertex();
          };
    
          EndPrimitive();
        }
      );
    
      // fragment shader
      const char* fs = GLSL(
    
        in  vec3 fColor;
    
        out vec4 outColor;
    
        void main() {
          outColor = vec4 (fColor, 1.0);
        }
      );
    
      GLuint createShader (GLenum type, const GLchar* src) {
        GLuint shader = glCreateShader (type);
        glShaderSource(shader, 1, &src, NULL);
        glCompileShader (shader);
        return shader;
      }
    
      void testShader (GLuint shader) {
        GLint status;
    
        glGetShaderiv (shader, GL_COMPILE_STATUS, &status);
        if (status != GL_TRUE) {
          fprintf (stderr, "shader has not compiled\n");
          char b[1024];
          glGetShaderInfoLog (shader, 1024, NULL, b); 
          fprintf (stderr, "%s\n", b);
          exit (1);
        };
      }
    
      int main (int argc, char** argv) {
        if (!glfwInit ()) {
          fprintf (stderr, "ERROR: could not start GLFW3\n");
          return 1;
        } 
    
        // these four strings are really very important for shaders!
        glfwWindowHint (GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint (GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint (GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
        glfwWindowHint (GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    
        // antialiasing
        glfwWindowHint (GLFW_SAMPLES, 4);
    
        // open window in fullscreen mode
        GLFWmonitor* m = glfwGetPrimaryMonitor ();
        const GLFWvidmode* vmode = glfwGetVideoMode (m);
        GLFWwindow* w = glfwCreateWindow (vmode->width, vmode->height, "Extended GL Init", m, NULL);
    
        if (!w) {
          fprintf (stderr, "ERROR: could not open window with GLFW3\n");
          glfwTerminate();
          return 1;
        }
    
        glfwMakeContextCurrent (w);
        glewExperimental = GL_TRUE;
        glewInit ();
    
        // tell GL to only draw onto a pixel if the shape is closer to the viewer
        glEnable (GL_DEPTH_TEST); // enable depth-testing
        glDepthFunc (GL_LESS);    // depth-testing interprets a smaller value as "closer"
    
    
        GLuint v = createShader (GL_VERTEX_SHADER,   vs); testShader (v);
        GLuint g = createShader (GL_GEOMETRY_SHADER, gs); testShader (g);
        GLuint f = createShader (GL_FRAGMENT_SHADER, fs); testShader (f);
    
        GLuint s = glCreateProgram ();
        glAttachShader (s, v);
        glAttachShader (s, g);
        glAttachShader (s, f);
        glLinkProgram (s);
        glUseProgram (s);
    
        float p[] = {
          //   Coordinates     Color              Sides
              -0.45,  0.45,    1.0, 0.0, 0.0,      4.0,
               0.45,  0.45,    0.0, 1.0, 0.0,      8.0,
               0.45, -0.45,    0.0, 0.0, 1.0,     16.0,
              -0.45, -0.45,    1.0, 1.0, 0.0,     32.0
        };
    
        // create VBO
        GLuint vbo;
        glGenBuffers (1, &vbo);
        glBindBuffer (GL_ARRAY_BUFFER, vbo);
        glBufferData (GL_ARRAY_BUFFER, sizeof (p), p, GL_STATIC_DRAW);
    
        // create VAO
        GLuint vao;
        glGenVertexArrays (1, &vao);
        glBindVertexArray (vao);
    
        // Specify layout of point data
        // postions
        GLint posAttrib = glGetAttribLocation(s, "pos");
        glEnableVertexAttribArray(posAttrib);
        glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE,
                              6 * sizeof(float), 0);
        // colors 
        GLint colAttrib = glGetAttribLocation(s, "col");
        glEnableVertexAttribArray(colAttrib);
        glVertexAttribPointer(colAttrib, 3, GL_FLOAT, GL_FALSE,
                              6 * sizeof(float), (void*) (2 * sizeof(float)));
        // size num
        GLint sideAttrib = glGetAttribLocation(s, "sid");
        glEnableVertexAttribArray(sideAttrib);
        glVertexAttribPointer(sideAttrib, 1, GL_FLOAT, GL_FALSE,
                              6 * sizeof(float), (void*) (5 * sizeof(float)));
    
        glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glBindVertexArray (vao);
        glDrawArrays (GL_POINTS, 0, 4);
        glfwSwapBuffers (w);
    
        sleep  (5);
        glfwTerminate ();
    
        return 0;
      }
    

    result: