src/gl/fb_tiny.c

    Tiny OpenGL implementation

    This provides the minimal set of OpenGL functions necessary for Basilisk View. It has no dependencies other than the standard C library.

    The “hardware” for this implementation is the tiny renderer originally written by Dmitry V. Sokolov.

    Also this page: https://fgiesen.wordpress.com/2013/02/06/the-barycentric-conspirac/

    #include <math.h>
    #include <stdlib.h>
    #include <string.h>
    #include <assert.h>
    #include <stdio.h>
    #include "tinygl.h"
    #include "tinyrenderer/geometry.h"
    #include "tinyrenderer/tiny.h"

    “Unused” OpenGL functions

    void glBindTexture (GLenum target, GLuint texture) {
      // fixme: add "undefined" message option
    }
    
    void glDisable (GLenum cap) {}
    void glEnable (GLenum cap) {}
    void glFinish (void) {}
    void glGetDoublev (GLenum pname, GLdouble * params) {}
    void glHint (GLenum target, GLenum mode) {}
    void glLightModeli (GLenum pname, GLint param) {}
    void glOrtho (GLdouble left, GLdouble right, GLdouble bottom, GLdouble top,
    	      GLdouble nearVal, GLdouble farVal) {}
    void glShadeModel (GLenum mode) {}
    void glTexCoord2f (GLfloat s, GLfloat t) {}
    void glTexParameteri (GLenum target, GLenum pname, GLint param) {}
    
    #define TEXTURE_WIDTH 256
    static float texture[3*TEXTURE_WIDTH];
    
    void glTexImage1D (GLenum target, GLint level, GLint internalFormat,
    		   GLsizei width, GLint border, GLenum format, GLenum type,
    		   const void * data)
    {
      assert (target == GL_TEXTURE_1D && level == 0 && internalFormat == GL_RGB &&
    	  width == TEXTURE_WIDTH && border == 0 && format == GL_RGB && type == GL_FLOAT);
      memcpy (texture, data, 3*TEXTURE_WIDTH*sizeof (float));
    }
    
    static real modelview0[16] = {
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1
    }, * modelview = modelview0;
    
    static real projection0[16] = {
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1
    }, * projection = projection0;;

    Lights

    Very loosely based on the Mesa implementation in mesa-17.2.4/src/mesa/tnl/t_vb_lighttmp.h:light_fast_rgba_single(). Note that we allow only for “grayscale” materials (i.e. the material properties are floats not vec3). This could be changed easily if necessary.

    A single light (GL_LIGHT0) is allowed and specular reflection is not implemented.

    typedef struct {
      float ambient, diffuse;
      int two_sides;
      vec4 position;
      vec3 _VP_inf_norm; // in modelview coordinates  
    } light_t;
    
    static
    light_t Light0 = {
      .ambient = 0.2, .diffuse = 1., // OpenGL defaults
      .two_sides = 1, // fixme: there seems to be an orientation problem compared to OSMesa when this is unset
      .position = {0, 0, 50, 0} // not OpenGL default
    };
    
    static const int not_implemented = 0;
    
    void glLightfv (GLenum light, GLenum pname, const GLfloat *params)
    {
      assert (light == GL_LIGHT0); // only one light is implemented
      switch (pname) {
        
      case GL_POSITION:
        Light0.position = (vec4){ params[0], params[1], params[2], params[3] };
        Light0._VP_inf_norm = vec3_normalized (vec4_proj3 (mat4_mul (*((mat4 *)modelview), Light0.position)));
        break;
    
    #if 0 // fixme: does not seem to match with OSMesa when changed from the default (0.2) above   
      case GL_AMBIENT:
        assert (params[0] == params[1] && params[1] == params[2]); // only white lights are implemented
        Light0.ambient = params[0];
        break;
    #endif
        
      case GL_DIFFUSE:
        assert (params[0] == params[1] && params[1] == params[2]); // only white lights are implemented
        Light0.diffuse = params[0];
        break;
        
      default:
        assert (not_implemented); // only the attributes above are implemented
      }
    }
    
    #define clamp(a,b,c) ((a) < (b) ? (b) : (a) > (c) ? (c) : (a))
    
    static inline
    float normal_shade (const vec3 normal)
    {
      float n_dot_VP = vec3_scalar (normal, Light0._VP_inf_norm); // normal . light direction
      float sum = Light0.ambient;
      if (n_dot_VP < 0) {
        if (Light0.two_sides)
          n_dot_VP = - n_dot_VP;
        else
          n_dot_VP = 0.;
      }
      sum += n_dot_VP*Light0.diffuse;
      return sum > 1. ? 1. : sum;
    }

    Matrix transformations

    static real * current = projection0;
    
    GLenum glGetError (void) { return GL_NO_ERROR; }
    
    void glGetFloatv (GLenum pname, GLfloat * params)
    {
      switch (pname) {
    
      case GL_MODELVIEW_MATRIX:
        for (int i = 0; i < 16; i++)
          params[i] = modelview[i];
        Light0._VP_inf_norm = vec3_normalized (vec4_proj3 (mat4_mul (*((mat4 *)modelview), Light0.position)));
        break;
        
      case GL_PROJECTION_MATRIX:
        for (int i = 0; i < 16; i++)
          params[i] = projection[i];
        break;
    
      default:
        assert (not_implemented);
      }
    }
    
    void glMultMatrixf (const GLfloat * m)
    {
      GLfloat r[16];
      int i, j, k;
      for (i = 0; i < 4; i++)
        for (j = 0; j < 4; j++) {
          GLfloat a = 0.;
          for (k = 0; k < 4; k++)
    	a += current[4*k + i]*m[4*j + k];
          r[4*j + i] = a;
        }
      for (i = 0; i < 16; i++)
        current[i] = r[i];
    }
    
    void glLoadIdentity (void)
    {
      static GLdouble identity[16] = {
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1
      };
      int i;
      for (i = 0; i < 16; i++)
        current[i] = identity[i];
    }
    
    void glLoadMatrixd (const GLdouble * m)
    {
      int i;
      for (i = 0; i < 16; i++)
        current[i] = m[i];
    }
    
    void glScalef (GLfloat x, GLfloat y, GLfloat z)
    {
      glMultMatrixf ((GLfloat []){
          x, 0, 0, 0,
          0, y, 0, 0,
          0, 0, z, 0,
          0, 0, 0, 1
      });
    }
    
    void glTranslatef (GLfloat x, GLfloat y, GLfloat z)
    {
      glMultMatrixf ((GLfloat []){
          1, 0, 0, 0,
          0, 1, 0, 0,
          0, 0, 1, 0,
          x, y, z, 1
      });
    }
    
    void glRotatef (GLfloat angle, GLfloat x, GLfloat y, GLfloat z)
    {
      angle *= M_PI/180.;
      float c = cos(angle), s = sin(angle), n = sqrt(x*x + y*y + z*z);
      if (n > 0.)
        x /= n, y /= n, z /= n;
      glMultMatrixf ((GLfloat []){
          x*x*(1. - c) + c,    y*x*(1. - c) + z*s,  x*z*(1. - c) - y*s,   0,
          x*y*(1. - c) - z*s,  y*y*(1. - c) + c,    y*z*(1. - c) + x*s,   0,
          x*z*(1. - c) + y*s,  y*z*(1. - c) - x*s,  z*z*(1. - c) + c,     0,
          0,                   0,                   0,                    1
      });  
    }
    
    void glMatrixMode (GLenum mode)
    {
      switch (mode) {
      case GL_MODELVIEW:  current = modelview;  break;
      case GL_PROJECTION: current = projection; break;
      default: assert (0);
      }
    }
    
    typedef struct {
      void * stack;
      int len;
    } Stack;
    
    static Stack modelview_stack = {0}, projection_stack = {0};
    
    #define SIZE (16*sizeof (real))
    
    void glPopMatrix (void)
    {
      Stack * s = (current == modelview) ? &modelview_stack : &projection_stack;
      assert (s->len >= SIZE);
      memcpy (current, s->stack + s->len - SIZE, SIZE);
      s->len -= SIZE;
      if (s->len == 0)
        free (s->stack), s->stack = NULL;
    }
    
    void glPushMatrix (void)
    {
      Stack * s = (current == modelview) ? &modelview_stack : &projection_stack;
      s->stack = realloc (s->stack, s->len + SIZE);
      memcpy (s->stack + s->len, current, SIZE);
      s->len += SIZE;
    }
    
    void glGetIntegerv (GLenum pname, GLint * params)
    {
      switch (pname) {
    
      case GL_VIEWPORT:
        assert (TinyFramebuffer);
        params[0] = 0;
        params[1] = 0;
        params[2] = TinyFramebuffer->width;
        params[3] = TinyFramebuffer->height;
        break;
        
      default: assert (not_implemented);
      }
    }

    Drawing primitives

    static int Mode = -1;
    static int nvertex = 0, nnormal = 0, ncolor = 0, ntexture = 0;
    
    static inline
    void reset_vertices() {
      nvertex = nnormal = ncolor = ntexture = 0;
    }
    
    void glBegin (GLenum mode) {
      assert (Mode < 0); // glBegins cannot be imbricated
      Mode = mode;
      reset_vertices();
    }
    
    static TinyColor clear = {255,255,255,0};
    
    void glClear (GLbitfield mask)
    {
      assert (TinyFramebuffer);
      if (mask & GL_COLOR_BUFFER_BIT) {
        unsigned char * p = TinyFramebuffer->image;
        for (int i = 0; i < TinyFramebuffer->width*TinyFramebuffer->height; i++, p += 4)
          for (int j = 0; j < 4; j++)
    	*(p + j) = ((unsigned char *)&clear)[j];
      }
      if (mask & GL_DEPTH_BUFFER_BIT) {
        real * p = TinyFramebuffer->zbuffer;
        for (int i = 0; i < TinyFramebuffer->width*TinyFramebuffer->height; i++, p++)
          *p = 1e30;
      }
    }
    
    void glClearColor (float red, float green, float blue, float alpha) {
      //  fprintf (stderr, "%g %g %g %g\n", red, green, blue, alpha);
      clear.r = red*255;
      clear.g = green*255;
      clear.b = blue*255;
      clear.a = alpha*255;
    }
    
    static float LineWidth = 1.;
    
    void glLineWidth (GLfloat width) {
      LineWidth = width;
    }
    
    static float PointSize = 4.;
    
    void glPointSize (GLfloat size) {
      PointSize = size;
    }
    
    // Maximum number of vertices in any single POLYGON or TRIANGLE_FAN
    #define NVERTMAX 1024
    static vec3 color[NVERTMAX];
    static TinyColor FgColor = {0, 0, 0, 255 };
    
    void glColor3f (GLfloat red, GLfloat green, GLfloat blue)
    {
      assert (TinyFramebuffer);
      assert (ncolor < NVERTMAX);
      color[ncolor++] = (vec3){red, green, blue};
      FgColor.r = red*255;
      FgColor.g = green*255;
      FgColor.b = blue*255;
    }
    
    static
    int Face = 0;
    
    void glColorMaterial (GLenum face, GLenum mode) {
      switch (face) {
      case GL_FRONT_AND_BACK: Face =  0; break;
      case GL_FRONT:          Face =  1; break;
      case GL_BACK:           Face = -1; break;
      default: assert (0);
      }
    }

    Shaders

    static real texcoord[NVERTMAX];
    
    void glTexCoord1d (GLdouble s)
    {
      assert (TinyFramebuffer);
      assert (ntexture < NVERTMAX);
      texcoord[ntexture++] = s;
    }
    
    static vec4 vertex[NVERTMAX], startv = {0,0,0,1};
    static vec3 normal[NVERTMAX] = {0};
    static float constant_normal_shade = 1.;
    
    void glNormal3d (GLdouble nx, GLdouble ny, GLdouble nz)
    {
      assert (TinyFramebuffer);
      assert (nnormal < NVERTMAX);
      normal[nnormal++] = vec3_normalized ((vec3){nx, ny, nz});
      constant_normal_shade = normal_shade (normal[nnormal - 1]);
    }
    
    static inline
    void shaded_color (const TinyColor * color, float shade, TinyColor * frag_color)
    {
      unsigned char * f = &frag_color->r;
      const unsigned char * c = &color->r;
      for (int i = 0; i < 3; i++)
        f[i] = shade*c[i];
      f[3] = c[3]; // alpha channel
    }
    
    static
    int constant_normal_shader (const void * data, const vec3 bar, TinyColor * frag_color)
    {
      const TinyColor * color = data;
      shaded_color (color, constant_normal_shade, frag_color);
      return 0; // the pixel is not discarded
    }
    
    static
    int constant_normal_color_shader (const void * data, const vec3 bar, TinyColor * frag_color)
    {
      const mat3 * nc = data;
      vec3 bc = mat3_mul (mat3_transpose (nc[0]), bar); // per-vertex color interpolation
      TinyColor color = { bc.x*255, bc.y*255, bc.z*255, 255 };
      shaded_color (&color, constant_normal_shade, frag_color);
      return 0; // the pixel is not discarded
    }
    
    static
    int constant_normal_texture_shader (const void * data, const vec3 bar, TinyColor * frag_color)
    {
      const vec3 * t = data;
      real bt = vec3_scalar (*t, bar); // per-vertex texture interpolation
      int i = clamp (bt, 0, 1)*(TEXTURE_WIDTH - 1);
      TinyColor color = { 255*texture[3*i], 255*texture[3*i+1], 255*texture[3*i+2], 255 };
      shaded_color (&color, constant_normal_shade, frag_color);
      return 0; // the pixel is not discarded
    }
    
    static
    int vertex_normal_shader (const void * data, const vec3 bar, TinyColor * frag_color)
    {
      const mat3 * nm = data;
      vec3 bn = vec3_normalized (mat3_mul (mat3_transpose (nm[0]), bar)); // per-vertex normal interpolation
      shaded_color (&FgColor, normal_shade (bn), frag_color);
      return 0; // the pixel is not discarded
    }
    
    static
    int vertex_normal_color_shader (const void * data, const vec3 bar, TinyColor * frag_color)
    {
      const mat3 * nm = data;
      vec3 bn = vec3_normalized (mat3_mul (mat3_transpose (nm[0]), bar)); // per-vertex normal interpolation
      vec3 bc = mat3_mul (mat3_transpose (nm[1]), bar); // per-vertex color interpolation
      TinyColor color = { bc.x*255, bc.y*255, bc.z*255, 255 };
      shaded_color (&color, normal_shade (bn), frag_color);
      return 0; // the pixel is not discarded
    }
    
    static
    int vertex_normal_texture_shader (const void * data, const vec3 bar, TinyColor * frag_color)
    {
      const mat3 * nm = data;
      vec3 bn = vec3_normalized (mat3_mul (mat3_transpose (nm[0]), bar)); // per-vertex normal interpolation
      real bt = vec3_scalar (nm[1].x, bar); // per-vertex texture interpolation
      int i = clamp (bt, 0, 1)*(TEXTURE_WIDTH - 1);
      TinyColor color = { 255*texture[3*i], 255*texture[3*i+1], 255*texture[3*i+2], 255 };
      shaded_color (&color, normal_shade (bn), frag_color);
      return 0; // the pixel is not discarded
    }
    
    void glVertex3d (GLdouble x, GLdouble y, GLdouble z)
    {
      assert (TinyFramebuffer);
      assert (Mode >= 0);
      assert (nvertex < NVERTMAX);
      vertex[nvertex++] = mat4_mul (mat4_transpose (*((mat4 *)projection)),
    				mat4_mul (mat4_transpose (*((mat4 *)modelview)),
    					  (vec4){x, y, z, 1}));
    
      switch (Mode) {
    
      case GL_LINES:
        if (nvertex == 2) {
          tiny_line (vertex[0], vertex[1], &FgColor, LineWidth, TinyFramebuffer);
          reset_vertices();
        }
        break;
    
      case GL_LINE_STRIP: case GL_LINE_LOOP:
        if (nvertex == 1)
          startv = vertex[0];
        else {
          tiny_line (vertex[0], vertex[1], &FgColor, LineWidth, TinyFramebuffer);
          vertex[0] = vertex[1];
          nvertex = 1;
        }
        break;
    
      case GL_QUADS:
        if (nvertex == 4) {
          assert (nnormal == 0); // only constant shading is implemented
          assert (ntexture == 0); // textures are not implemented on quads
          tiny_triangle ((vec4[3]){vertex[0], vertex[1], vertex[3]},
    		     &FgColor, constant_normal_shader, Face, // fixme: swap NULL and constant_normal_shader
    		     TinyFramebuffer);
          tiny_triangle ((vec4[3]){vertex[1], vertex[2], vertex[3]},
    		     &FgColor, constant_normal_shader, Face,
    		     TinyFramebuffer);
          reset_vertices();
        }
        break;
    
      case GL_POINTS:
        tiny_point (vertex[0], &FgColor, PointSize, TinyFramebuffer);
        reset_vertices();
        break;
        
      case GL_POLYGON:
      case GL_TRIANGLE_FAN:
        break;
        
      default:
        assert (not_implemented);
      }
    }
    
    void glVertex3f (GLfloat x, GLfloat y, GLfloat z) {
      glVertex3d (x, y, z);
    }
    
    void glEnd (void)
    {
      assert (TinyFramebuffer && Mode >= 0); // fixme: replace assertions
      
      switch (Mode) {
    
      case GL_LINE_LOOP:
        tiny_line (vertex[0], startv, &FgColor, LineWidth, TinyFramebuffer);
        break;
    
      case GL_TRIANGLE_FAN:
      case GL_POLYGON:
        if (nnormal == 0) {
          if (ntexture == nvertex)
    	for (int i = 1; i < nvertex - 1; i++) {
    	  vec3 t = { texcoord[i], texcoord[i + 1], texcoord[0] };
    	  tiny_triangle ((vec4[3]){vertex[i], vertex[i + 1], vertex[0]},
    			 &t, constant_normal_texture_shader, Face, TinyFramebuffer);
    	}
          else if (ncolor == 0)
    	for (int i = 1; i < nvertex - 1; i++)
    	  tiny_triangle ((vec4[3]){vertex[i], vertex[i + 1], vertex[0]},
    			 &FgColor, constant_normal_shader, Face, TinyFramebuffer);
          else if (ncolor == nvertex)
    	for (int i = 1; i < nvertex - 1; i++) {
    	  mat3 mc = { color[i], color[i + 1], color[0] };
    	  tiny_triangle ((vec4[3]){vertex[i], vertex[i + 1], vertex[0]},
    			 &mc, constant_normal_color_shader, Face, TinyFramebuffer);
    	}
          else
    	fprintf (stderr, "%s:%d: warning: %d != %d\n", __FILE__, __LINE__, ncolor, nvertex);
        }
        else if (nnormal == nvertex) {
          if (ntexture == nvertex)
    	for (int i = 1; i < nvertex - 1; i++) {
    	  mat3 nm[2] = { { normal[i], normal[i + 1], normal[0] },
    			 { { texcoord[i], texcoord[i + 1], texcoord[0] } } };
    	  tiny_triangle ((vec4[3]){vertex[i], vertex[i + 1], vertex[0]},
    			 nm, vertex_normal_texture_shader, Face, TinyFramebuffer);
    	}
          else if (ncolor == 0)
    	for (int i = 1; i < nvertex - 1; i++) {
    	  mat3 nm = { normal[i], normal[i + 1], normal[0] };
    	  tiny_triangle ((vec4[3]){vertex[i], vertex[i + 1], vertex[0]},
    			 &nm, vertex_normal_shader, Face, TinyFramebuffer);
    	}
          else if (ncolor == nvertex)
    	for (int i = 1; i < nvertex - 1; i++) {
    	  mat3 nm[2] = { { normal[i], normal[i + 1], normal[0] },
    			 { color[i],  color[i + 1],  color[0] } };
    	  tiny_triangle ((vec4[3]){vertex[i], vertex[i + 1], vertex[0]},
    			 nm, vertex_normal_color_shader, Face, TinyFramebuffer);
    	}
          else
    	fprintf (stderr, "%s:%d: warning: %d, %d, %d\n", __FILE__, __LINE__, ntexture, ncolor, nvertex);
        }
        else
          fprintf (stderr, "%s:%d: warning: %d != %d\n", __FILE__, __LINE__, nnormal, nvertex);
        break;
        
      }
      Mode = -1;
      reset_vertices();
    }