Search code examples
javascriptglslwebgllinear-algebravertex-shader

How to preserve rotation and scaling transforms in a model view projection billboard vertex shader?


I am looking for a GLSL billboard vertex shader solution. I am rendering a quad with a texture on it. I currently have a vertex shader that looks like:

precision mediump float;

attribute vec3 position;
attribute vec2 uvs;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

varying vec2 uv;

void main() {
  uv = uvs;

  gl_Position = projection * view * model * vec4(position, 1);
}

My model matrix is constructed elsewhere from a rotation, translation, and scale transformation matrices.

A few of the solutions I have tried, have worked and provided a billboard (face the camera) effect. Unfortunately, they discard the original rotation, and scaling transformations to the original model matrix. The closest solution I have tried is from : http://www.geeks3d.com/20140807/billboarding-vertex-shader-glsl/

UPDATE:

Here is a MVCE of the current setup http://requirebin.com/?gist=9491aa294f11b31af639910cfeff7140

There is a camera rotating a quad. The quad has scaling and rotation applied to it. The texture says 'player name'. The quad should always face the camera as a billboard saying 'player name' without discarding the scale, or x rotation.


Solution

  • To orientate the object to the viewport, you have to omit the orientation of the view matrix. The orientation is the normalized upper left 3*3 of the matrix. Since your model is flipped, you have to compensate this by inverting the X-axis:

    precision mediump float;
    attribute vec3 position;
    attribute vec2 uvs;
    
    uniform mat4 projection;
    uniform mat4 view;
    uniform mat4 model;
    
    varying vec2 uv;
    
    void main() {
        uv = uvs;
    
        mat4 bbView = mat4(
            vec4(-1.0,0.0,0.0,0.0),
            vec4(0.0,1.0,0.0,0.0),
            vec4(0.0,0.0,1.0,0.0),
            view[3] );
    
        gl_Position = projection * bbView * model * vec4(position, 1);
    }
    

    Note, a transformation matrix (model and view matrix) looks like this:

    ( X-axis.x, X-axis.y, X-axis.z, 0 )
    ( Y-axis.x, Y-axis.y, Y-axis.z, 0 )
    ( Z-axis.x, Z-axis.y, Z-axis.z, 0 )
    ( trans.x,  trans.y,  trans.z,  1 ) 
    

    The billboard matrix bbView uses the view (camera) position, but it omits the rotation provided by the view matrix. This causes the object to look like it is being viewed from the front.


    At perspective projection the size of an object changes linearly with the distance to the camera.

    This means if you want that the object keeps its size and its place on the viewport, then you have to scale the object by the distance to the camera:

    precision mediump float;
    attribute vec3 position;
    attribute vec2 uvs;
    
    uniform mat4 projection;
    uniform mat4 view;
    uniform mat4 model;
    
    varying vec2 uv;
    
    void main() {
        uv = uvs;
    
        float scale = length(view[3].xyz);
        mat4 scaleMat = mat4(
            vec4(scale,0.0,0.0,0.0),
            vec4(0.0,scale,0.0,0.0),
            vec4(0.0,0.0,1.0,0.0),
            vec4(0.0,0.0,0.0,1.0) );
    
        mat4 bbView = mat4(
            vec4(-1.0,0.0,0.0,0.0),
            vec4(0.0,1.0,0.0,0.0),
            vec4(0.0,0.0,1.0,0.0),
            view[3] );
    
        gl_Position = projection * bbView * model * scaleMat * vec4(position, 1);
    }
    

    See the code snippet:

    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];
        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 progObj = this.LinkProgram( shaderObjs )
        if ( progObj != 0 ) {
            progObj.attribIndex = {};
            var noOfAttributes = gl.getProgramParameter( progObj, gl.ACTIVE_ATTRIBUTES );
            for ( var i_n = 0; i_n < noOfAttributes; ++ i_n ) {
                var name = gl.getActiveAttrib( progObj, i_n ).name;
                progObj.attribIndex[name] = gl.getAttribLocation( progObj, name );
            }
            progObj.unifomLocation = {};
            var noOfUniforms = gl.getProgramParameter( progObj, gl.ACTIVE_UNIFORMS );
            for ( var i_n = 0; i_n < noOfUniforms; ++ i_n ) {
                var name = gl.getActiveUniform( progObj, i_n ).name;
                progObj.unifomLocation[name] = gl.getUniformLocation( progObj, name );
            }
        }
        return progObj;
    }
    ShaderProgram.AttributeIndex = function( progObj, name ) { return progObj.attribIndex[name]; } 
    ShaderProgram.UniformLocation = function( progObj, name ) { return progObj.unifomLocation[name]; } 
    ShaderProgram.Use = function( progObj ) { gl.useProgram( progObj ); } 
    ShaderProgram.SetUniformI1  = function( progObj, name, val ) { if(progObj.unifomLocation[name]) gl.uniform1i( progObj.unifomLocation[name], val ); }
    ShaderProgram.SetUniformF1  = function( progObj, name, val ) { if(progObj.unifomLocation[name]) gl.uniform1f( progObj.unifomLocation[name], val ); }
    ShaderProgram.SetUniformM44 = function( progObj, name, mat ) { if(progObj.unifomLocation[name]) gl.uniformMatrix4fv( progObj.unifomLocation[name], false, mat ); }
    ShaderProgram.CompileShader = function( source, shaderStage ) {
        var shaderScript = document.getElementById(source);
        if (shaderScript) {
          source = "";
          var node = shaderScript.firstChild;
          while (node) {
            if (node.nodeType == 3) source += node.textContent;
            node = node.nextSibling;
          }
        }
        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 : 0;
    } 
    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 : 0;
    }
    
    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 );
    }
            
    function drawScene(){
    
        var canvas = document.getElementById( "billboard-canvas" );
        Camera.create();
        Camera.vp = [canvas.width, canvas.height];
        var currentTime = Date.now();   
        var deltaMS = currentTime - startTime;
        
        var texUnit = 0;
        gl.activeTexture( gl.TEXTURE0 + texUnit );
        gl.bindTexture( gl.TEXTURE_2D, textureObj );
    
        gl.viewport( 0, 0, canvas.width, canvas.height );
        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 );
        
        var rotMat = IdentityMat44();
        rotMat = RotateAxis( rotMat, CalcAng( currentTime, 13.0 ), 0 );
        rotMat = RotateAxis( rotMat, CalcAng( currentTime, 17.0 ), 1 );
        var d = 1.0 + 0.7*Math.sin( CalcAng( currentTime, 25.0 ) )
        Camera.pos = [d*rotMat[0], d*rotMat[1], d*rotMat[2]];
        var viewMat = Camera.LookAt();
    
        // set up draw shader
        ShaderProgram.Use( progDraw );
        ShaderProgram.SetUniformM44( progDraw, "u_projectionMat44", Camera.Perspective() );
        ShaderProgram.SetUniformM44( progDraw, "u_viewMat44", viewMat );
        ShaderProgram.SetUniformI1( progDraw, "u_texture", texUnit );
        var modelMat = IdentityMat44();
        modelMat[0] = 0.5; modelMat[5] = 0.5;
        modelMat[12] = -0.55;
        ShaderProgram.SetUniformM44( progDraw, "u_modelMat44", modelMat );
        ShaderProgram.SetUniformF1( progDraw, "u_billboard", 0.0 );
        VertexBuffer.Draw( bufPlane );
    
        modelMat[12] = 0.55
        ShaderProgram.SetUniformM44( progDraw, "u_modelMat44", modelMat );
        ShaderProgram.SetUniformF1( progDraw, "u_billboard", 1.0 );
        VertexBuffer.Draw( bufPlane );
    }
    
    var Texture = {};
    Texture.HandleLoadedTexture2D = function( image, texture, flipY ) {
        gl.activeTexture( gl.TEXTURE0 );
        gl.bindTexture( gl.TEXTURE_2D, texture );
        gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image );
        if ( flipY != undefined && flipY == true )
          gl.pixelStorei( gl.UNPACK_FLIP_Y_WEBGL, true );
        gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR );
        gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR );
        gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT );
      	gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT );
        gl.bindTexture( gl.TEXTURE_2D, null );
        return texture;
    }
    Texture.LoadTexture2D = function( name ) {
        var texture = gl.createTexture();
        texture.image = new Image();
        texture.image.setAttribute('crossorigin', 'anonymous');
        texture.image.onload = function () {
            Texture.HandleLoadedTexture2D( texture.image, texture, true )
        }
        texture.image.src = name;
        return texture;
    }
    
    var startTime;
    function Fract( val ) { 
        return val - Math.trunc( val );
    }
    function CalcAng( currentTime, intervall ) {
        return Fract( (currentTime - startTime) / (1000*intervall) ) * 2.0 * Math.PI;
    }
    function CalcMove( currentTime, intervall, range ) {
        var pos = self.Fract( (currentTime - startTime) / (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 ];
    }
    
    var sliderScale = 100.0
    var gl;
    var progDraw;
    var bufCube = {};
    function sceneStart() {
    
        var canvas = document.getElementById( "billboard-canvas");
        var vp = [canvas.width, canvas.height];
        gl = canvas.getContext( "experimental-webgl" );
        if ( !gl )
          return;
    
        progDraw = ShaderProgram.Create( 
          [ { source : "draw-shader-vs", stage : gl.VERTEX_SHADER },
            { source : "draw-shader-fs", stage : gl.FRAGMENT_SHADER }
          ] );
        progDraw.inPos = gl.getAttribLocation( progDraw, "inPos" );
        progDraw.inTex = gl.getAttribLocation( progDraw, "inTex" );
        if ( progDraw == 0 )
            return;
    
        var planPosData = [-1.0, -1.0, 0.0,     1.0, -1.0, 0.0,     1.0,  1.0,  0.0,    -1.0, 1.0, 0.0];
        var planTexData = [ 0.0,  1.0,          1.0,  1.0,          1.0,  0.0,          0.0, 0.0     ];
        var planInxData = [0,1,2,0,2,3];
        bufPlane = VertexBuffer.Create(
        [ { data : planPosData, attrSize : 3, attrLoc : progDraw.inPos },
          { data : planTexData, attrSize : 2, attrLoc : progDraw.inTex } ],
          planInxData );
    
        textureObj = Texture.LoadTexture2D( "https://raw.githubusercontent.com/Rabbid76/graphics-snippets/master/resource/texture/tree.jpg" );
        
        startTime = Date.now();
        setInterval(drawScene, 50);
    }
    <script id="draw-shader-vs" type="x-shader/x-vertex">
    precision mediump float;
    
    attribute vec3 inPos;
    attribute vec2 inTex;
    
    varying vec2 vertTex;
    
    uniform mat4 u_projectionMat44;
    uniform mat4 u_viewMat44;
    uniform mat4 u_modelMat44;
    
    uniform float u_billboard;
    
    void main()
    {
        vertTex       = inTex;
    
        float scale = u_billboard > 0.5 ? length(u_viewMat44[3].xyz) : 1.0;
        mat4 scaleMat = mat4(
            vec4(scale,0.0,0.0,0.0),
            vec4(0.0,scale,0.0,0.0),
            vec4(0.0,0.0,1.0,0.0),
            vec4(0.0,0.0,0.0,1.0) );
    
        mat4 bbView = mat4(
            vec4(1.0,0.0,0.0,0.0),
            vec4(0.0,1.0,0.0,0.0),
            vec4(0.0,0.0,1.0,0.0),
            u_viewMat44[3] );
    
        mat4 view = u_billboard > 0.5 ? bbView : u_viewMat44;
    
        gl_Position  = u_projectionMat44 * view * scaleMat * u_modelMat44 * vec4( inPos, 1.0 );
    }
    </script>
    
    <script id="draw-shader-fs" type="x-shader/x-fragment">
    precision mediump float;
    
    varying vec2 vertTex;
    
    uniform sampler2D u_texture;
    uniform sampler2D u_normalMap;
    
    void main()
    {
        vec3 texColor = texture2D( u_texture, vertTex.st ).rgb;
        gl_FragColor = vec4( texColor.rgb, 1.0 );
    }
    </script>
    
    <body onload="sceneStart();">
        <canvas id="billboard-canvas" style="border: none;" width="512" height="512"></canvas>
    </body>