Search code examples
glslshaderlightopenframeworks

How to get the viewing angle relative to the normal in a GLSL fragment shader?


I'm just getting started with shaders in openframeworks, and am trying to write a fragment shader that changes the color of the fragment based on what angle its viewed from. For example, given a rectangle, if viewed from head on (camera is parallel to normal) it would be red, but if viewed from a side, it turns blue.

For a sphere, similarly, its apparent middle would be red (as the camera is perpendicular to those faces), while the apparent edges should be blue.

I figured I could set the fragment color based on what the viewing angle relative to the normal is, however, I'm having trouble finding any GLSL input variables that gives me the requisite information. gl_FragCoord doesn't seem to work, as it only gives position relative to the window, and gl_PointCoord gives the position relative to the model itself, but not from where its viewed.

What would be the straightforward way to get a viewing angle/ accomplish this effect?

I'm using GLSL version 1.2, and loading the shaders with openframeworks 0.9.8.


Solution

  • For a sphere, similarly, its apparent middle would be red (as the camera is perpendicular to those faces), while the apparent edges should be blue.

    To achieve what you want I recommend to calculate the intensity of a perfect lambertian diffuse light, which lits the scene from the front. It is easy to do this calcualtions in viewspace.

    Lambertian reflection is often used as a model for diffuse reflection. This technique causes all closed polygons (such as a triangle within a 3D mesh) to reflect light equally in all directions when rendered. The diffusion coefficient is calculated from the angle between the normal vector and the light vector.

    f_Lambertian = max( 0.0, dot( N, L ) )
    

    where N is the normal vector of the surface, and L is the vector towards to the light source.
    See How does this faking the light work on aerotwist? for detailed information.

    In the view space of a Right-handed system, Z-axis points out of the view (Note in a right hand system the Z-Axis is the cross product of the X-Axis and the Y-Axis).

    enter image description here

    This means the light intensity can be calculated by the dot product of the normal vector in view space and the "light" vector in view space, which is the view space Z-axis (0, 0, 1).

    The following shader colors the the fragments which have a normal vector that points to the view, in red. The fragments whose normal vector point sidewards ar tint blue:

    Vertex shader:

    in vec3 inPos;
    in vec3 inNV;
    
    out vec3 viewNV;
    
    uniform mat4 u_projectionMat44;
    uniform mat4 u_viewMat44;
    uniform mat4 u_modelMat44;
    
    void main()
    {   
        viewNV      = mat3(u_viewMat44 * u_modelMat44) * inNV;
    
        vec4 pos    = u_viewMat44 * u_modelMat44 * vec4( inPos, 1.0 );
        gl_Position = u_projectionMat44 * pos;
    }
    

    Fragment shader:

    in vec3 viewNV;
    
    void main()
    {
        vec3  N      = normalize(viewNV);
        vec3  L      = vec3(0.0, 0.0, 1.0);
        float NdotL  = dot(N, L);
    
        vec3  color  = vec3(NdotL, 0.0, 1.0-NdotL);
    
        gl_FragColor = vec4( color.rgb, 1.0 );
    } 
    

    See the WebGL example, which demonstrates the effect on a rotating cube:

    (function loadscene() {
      
      var gl, progDraw, vp_size;
      var bufCube = {};
      
      function render(delteMS){
          Camera.create();
          Camera.vp = vp_size;
              
          gl.viewport( 0, 0, vp_size[0], vp_size[1] );
          gl.enable( gl.DEPTH_TEST );
          gl.clearColor( 0.0, 0.0, 0.0, 1.0 );
          gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );
    
          // set up draw shader
          ShaderProgram.Use( progDraw );
          ShaderProgram.SetUniformM44( progDraw, "u_projectionMat44", Camera.Perspective() );
          ShaderProgram.SetUniformM44( progDraw, "u_viewMat44", Camera.LookAt() );
          var modelMat = IdentityMat44()
          modelMat = RotateAxis( modelMat, CalcAng( delteMS, 13.0 ), 0 );
          modelMat = RotateAxis( modelMat, CalcAng( delteMS, 17.0 ), 1 );
          ShaderProgram.SetUniformM44( progDraw, "u_modelMat44", modelMat );
          
          // draw scene
          VertexBuffer.Draw( bufCube );
    
          requestAnimationFrame(render);
      }
      
      function resize() {
          //vp_size = [gl.drawingBufferWidth, gl.drawingBufferHeight];
          vp_size = [window.innerWidth, window.innerHeight]
          canvas.width = vp_size[0];
          canvas.height = vp_size[1];
      }
      
      function initScene() {
      
          canvas = document.getElementById( "canvas");
          gl = canvas.getContext( "experimental-webgl" );
          if ( !gl )
            return null;
          
          progDraw = ShaderProgram.Create( 
            [ { source : "draw-shader-vs", stage : gl.VERTEX_SHADER },
              { source : "draw-shader-fs", stage : gl.FRAGMENT_SHADER }
            ] );
          if ( !progDraw.progObj )
              return null;
          progDraw.inPos = ShaderProgram.AttributeIndex( progDraw, "inPos" );
          progDraw.inNV  = ShaderProgram.AttributeIndex( progDraw, "inNV" );
         // create cube
         var cubePos = [
            -1.0, -1.0,  1.0,  1.0, -1.0,  1.0,  1.0,  1.0,  1.0, -1.0,  1.0,  1.0,
            -1.0, -1.0, -1.0,  1.0, -1.0, -1.0,  1.0,  1.0, -1.0, -1.0,  1.0, -1.0 ];
          var cubeCol = [ 1.0, 0.0, 0.0, 1.0, 0.5, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 ];
          var cubeHlpInx = [ 0, 1, 2, 3, 1, 5, 6, 2, 5, 4, 7, 6, 4, 0, 3, 7, 3, 2, 6, 7, 1, 0, 4, 5 ];  
          var cubePosData = [];
          for ( var i = 0; i < cubeHlpInx.length; ++ i ) {
            cubePosData.push( cubePos[cubeHlpInx[i]*3], cubePos[cubeHlpInx[i]*3+1], cubePos[cubeHlpInx[i]*3+2] );
          }
          var cubeNVData = [];
          for ( var i1 = 0; i1 < cubeHlpInx.length; i1 += 4 ) {
          var nv = [0, 0, 0];
          for ( i2 = 0; i2 < 4; ++ i2 ) {
              var i = i1 + i2;
              nv[0] += cubePosData[i*3]; nv[1] += cubePosData[i*3+1]; nv[2] += cubePosData[i*3+2];
          }
          for ( i2 = 0; i2 < 4; ++ i2 )
            cubeNVData.push( nv[0], nv[1], nv[2] );
          }
          var cubeColData = [];
          for ( var is = 0; is < 6; ++ is ) {
            for ( var ip = 0; ip < 4; ++ ip ) {
             cubeColData.push( cubeCol[is*3], cubeCol[is*3+1], cubeCol[is*3+2] ); 
            }
          }
          var cubeInxData = [];
          for ( var i = 0; i < cubeHlpInx.length; i += 4 ) {
            cubeInxData.push( i, i+1, i+2, i, i+2, i+3 );   
          }
          bufCube = VertexBuffer.Create(
          [ { data : cubePosData, attrSize : 3, attrLoc : progDraw.inPos },
            { data : cubeNVData,  attrSize : 3, attrLoc : progDraw.inNV } ],
            cubeInxData );
            
          window.onresize = resize;
          resize();
          requestAnimationFrame(render);
      }
      
      function Fract( val ) { 
          return val - Math.trunc( val );
      }
      function CalcAng( deltaTime, intervall ) {
          return Fract( deltaTime / (1000*intervall) ) * 2.0 * Math.PI;
      }
      function CalcMove( deltaTime, intervall, range ) {
          var pos = self.Fract( deltaTime / (1000*intervall) ) * 2.0
          var pos = pos < 1.0 ? pos : (2.0-pos)
          return range[0] + (range[1] - range[0]) * pos;
      }    
      function EllipticalPosition( a, b, angRag ) {
          var a_b = a * a - b * b
          var ea = (a_b <= 0) ? 0 : Math.sqrt( a_b );
          var eb = (a_b >= 0) ? 0 : Math.sqrt( -a_b );
          return [ a * Math.sin( angRag ) - ea, b * Math.cos( angRag ) - eb, 0 ];
      }
      
      glArrayType = typeof Float32Array !="undefined" ? Float32Array : ( typeof WebGLFloatArray != "undefined" ? WebGLFloatArray : Array );
      
      function IdentityMat44() {
        var m = new glArrayType(16);
        m[0]  = 1; m[1]  = 0; m[2]  = 0; m[3]  = 0;
        m[4]  = 0; m[5]  = 1; m[6]  = 0; m[7]  = 0;
        m[8]  = 0; m[9]  = 0; m[10] = 1; m[11] = 0;
        m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1;
        return m;
      };
      
      function RotateAxis(matA, angRad, axis) {
          var aMap = [ [1, 2], [2, 0], [0, 1] ];
          var a0 = aMap[axis][0], a1 = aMap[axis][1]; 
          var sinAng = Math.sin(angRad), cosAng = Math.cos(angRad);
          var matB = new glArrayType(16);
          for ( var i = 0; i < 16; ++ i ) matB[i] = matA[i];
          for ( var i = 0; i < 3; ++ i ) {
              matB[a0*4+i] = matA[a0*4+i] * cosAng + matA[a1*4+i] * sinAng;
              matB[a1*4+i] = matA[a0*4+i] * -sinAng + matA[a1*4+i] * cosAng;
          }
          return matB;
      }
      
      function Cross( a, b ) { return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0], 0.0 ]; }
      function Dot( a, b ) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; }
      function Normalize( v ) {
          var len = Math.sqrt( v[0] * v[0] + v[1] * v[1] + v[2] * v[2] );
          return [ v[0] / len, v[1] / len, v[2] / len ];
      }
      
      var Camera = {};
      Camera.create = function() {
          this.pos    = [0, 3.0, 0.0];
          this.target = [0, 0, 0];
          this.up     = [0, 0, 1];
          this.fov_y  = 90;
          this.vp     = [800, 600];
          this.near   = 0.5;
          this.far    = 100.0;
      }
      Camera.Perspective = function() {
          var fn = this.far + this.near;
          var f_n = this.far - this.near;
          var r = this.vp[0] / this.vp[1];
          var t = 1 / Math.tan( Math.PI * this.fov_y / 360 );
          var m = IdentityMat44();
          m[0]  = t/r; m[1]  = 0; m[2]  =  0;                              m[3]  = 0;
          m[4]  = 0;   m[5]  = t; m[6]  =  0;                              m[7]  = 0;
          m[8]  = 0;   m[9]  = 0; m[10] = -fn / f_n;                       m[11] = -1;
          m[12] = 0;   m[13] = 0; m[14] = -2 * this.far * this.near / f_n; m[15] =  0;
          return m;
      }
      Camera.LookAt = function() {
          var mz = Normalize( [ this.pos[0]-this.target[0], this.pos[1]-this.target[1], this.pos[2]-this.target[2] ] );
          var mx = Normalize( Cross( this.up, mz ) );
          var my = Normalize( Cross( mz, mx ) );
          var tx = Dot( mx, this.pos );
          var ty = Dot( my, this.pos );
          var tz = Dot( [-mz[0], -mz[1], -mz[2]], this.pos ); 
          var m = IdentityMat44();
          m[0]  = mx[0]; m[1]  = my[0]; m[2]  = mz[0]; m[3]  = 0;
          m[4]  = mx[1]; m[5]  = my[1]; m[6]  = mz[1]; m[7]  = 0;
          m[8]  = mx[2]; m[9]  = my[2]; m[10] = mz[2]; m[11] = 0;
          m[12] = tx;    m[13] = ty;    m[14] = tz;    m[15] = 1; 
          return m;
      } 
      
      var ShaderProgram = {};
      ShaderProgram.Create = function( shaderList ) {
          var shaderObjs = [];
          for ( var i_sh = 0; i_sh < shaderList.length; ++ i_sh ) {
              var shderObj = this.CompileShader( shaderList[i_sh].source, shaderList[i_sh].stage );
              if ( shderObj == 0 )
                  return 0;
              shaderObjs.push( shderObj );
          }
          var prog = {}
          prog.progObj = this.LinkProgram( shaderObjs )
          if ( prog.progObj ) {
              prog.attribIndex = {};
              var noOfAttributes = gl.getProgramParameter( prog.progObj, gl.ACTIVE_ATTRIBUTES );
              for ( var i_n = 0; i_n < noOfAttributes; ++ i_n ) {
                  var name = gl.getActiveAttrib( prog.progObj, i_n ).name;
                  prog.attribIndex[name] = gl.getAttribLocation( prog.progObj, name );
              }
              prog.unifomLocation = {};
              var noOfUniforms = gl.getProgramParameter( prog.progObj, gl.ACTIVE_UNIFORMS );
              for ( var i_n = 0; i_n < noOfUniforms; ++ i_n ) {
                  var name = gl.getActiveUniform( prog.progObj, i_n ).name;
                  prog.unifomLocation[name] = gl.getUniformLocation( prog.progObj, name );
              }
          }
          return prog;
      }
      ShaderProgram.AttributeIndex = function( prog, name ) { return prog.attribIndex[name]; } 
      ShaderProgram.UniformLocation = function( prog, name ) { return prog.unifomLocation[name]; } 
      ShaderProgram.Use = function( prog ) { gl.useProgram( prog.progObj ); } 
      ShaderProgram.SetUniformI1  = function( prog, name, val ) { if(prog.unifomLocation[name]) gl.uniform1i( prog.unifomLocation[name], val ); }
      ShaderProgram.SetUniformF1  = function( prog, name, val ) { if(prog.unifomLocation[name]) gl.uniform1f( prog.unifomLocation[name], val ); }
      ShaderProgram.SetUniformF2  = function( prog, name, arr ) { if(prog.unifomLocation[name]) gl.uniform2fv( prog.unifomLocation[name], arr ); }
      ShaderProgram.SetUniformF3  = function( prog, name, arr ) { if(prog.unifomLocation[name]) gl.uniform3fv( prog.unifomLocation[name], arr ); }
      ShaderProgram.SetUniformF4  = function( prog, name, arr ) { if(prog.unifomLocation[name]) gl.uniform4fv( prog.unifomLocation[name], arr ); }
      ShaderProgram.SetUniformM33 = function( prog, name, mat ) { if(prog.unifomLocation[name]) gl.uniformMatrix3fv( prog.unifomLocation[name], false, mat ); }
      ShaderProgram.SetUniformM44 = function( prog, name, mat ) { if(prog.unifomLocation[name]) gl.uniformMatrix4fv( prog.unifomLocation[name], false, mat ); }
      ShaderProgram.CompileShader = function( source, shaderStage ) {
          var shaderScript = document.getElementById(source);
          if (shaderScript)
            source = shaderScript.text;
          var shaderObj = gl.createShader( shaderStage );
          gl.shaderSource( shaderObj, source );
          gl.compileShader( shaderObj );
          var status = gl.getShaderParameter( shaderObj, gl.COMPILE_STATUS );
          if ( !status ) alert(gl.getShaderInfoLog(shaderObj));
          return status ? shaderObj : null;
      } 
      ShaderProgram.LinkProgram = function( shaderObjs ) {
          var prog = gl.createProgram();
          for ( var i_sh = 0; i_sh < shaderObjs.length; ++ i_sh )
              gl.attachShader( prog, shaderObjs[i_sh] );
          gl.linkProgram( prog );
          status = gl.getProgramParameter( prog, gl.LINK_STATUS );
          if ( !status ) alert("Could not initialise shaders");
          gl.useProgram( null );
          return status ? prog : null;
      }
      
      var VertexBuffer = {};
      VertexBuffer.Create = function( attributes, indices ) {
          var buffer = {};
          buffer.buf = [];
          buffer.attr = []
          for ( var i = 0; i < attributes.length; ++ i ) {
              buffer.buf.push( gl.createBuffer() );
              buffer.attr.push( { size : attributes[i].attrSize, loc : attributes[i].attrLoc } );
              gl.bindBuffer( gl.ARRAY_BUFFER, buffer.buf[i] );
              gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( attributes[i].data ), gl.STATIC_DRAW );
          }
          buffer.inx = gl.createBuffer();
          gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, buffer.inx );
          gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, new Uint16Array( indices ), gl.STATIC_DRAW );
          buffer.inxLen = indices.length;
          gl.bindBuffer( gl.ARRAY_BUFFER, null );
          gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, null );
          return buffer;
      }
      VertexBuffer.Draw = function( bufObj ) {
        for ( var i = 0; i < bufObj.buf.length; ++ i ) {
              gl.bindBuffer( gl.ARRAY_BUFFER, bufObj.buf[i] );
              gl.vertexAttribPointer( bufObj.attr[i].loc, bufObj.attr[i].size, gl.FLOAT, false, 0, 0 );
              gl.enableVertexAttribArray( bufObj.attr[i].loc );
          }
          gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, bufObj.inx );
          gl.drawElements( gl.TRIANGLES, bufObj.inxLen, gl.UNSIGNED_SHORT, 0 );
          for ( var i = 0; i < bufObj.buf.length; ++ i )
             gl.disableVertexAttribArray( bufObj.attr[i].loc );
          gl.bindBuffer( gl.ARRAY_BUFFER, null );
          gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, null );
      }
      
      initScene();
      
      })();
    <script id="draw-shader-vs" type="x-shader/x-vertex">
        precision highp float;
        
        attribute vec3 inPos;
        attribute vec3 inNV;
      
        varying vec3 viewNV;
        
        uniform mat4 u_projectionMat44;
        uniform mat4 u_viewMat44;
        uniform mat4 u_modelMat44;
        
        void main()
        {   
            viewNV   = mat3(u_viewMat44 * u_modelMat44) * inNV;
            
            vec4 pos  = u_viewMat44 * u_modelMat44 * vec4( inPos, 1.0 );
            gl_Position   = u_projectionMat44 * pos;
        }
    </script>
      
    <script id="draw-shader-fs" type="x-shader/x-fragment">
        precision mediump float;
    
        varying vec3 viewNV;
        
        void main()
        {
            vec3 N = normalize(viewNV);
            vec3 Z = vec3(0.0, 0.0, 1.0);
            float NdotZ = dot(N, Z);
    
            vec3 color = vec3(NdotZ, 0.0, 1.0-NdotZ);
            
            gl_FragColor = vec4( color.rgb, 1.0 );
        } 
    </script>
    
    <canvas id="canvas" style="border: none;" width="100%" height="100%"></canvas>