OpenGLWebGL Canvas 图形学让前端飞

WebGL学习(1) - 三角形

2017-12-01  本文已影响39人  jeffzhong

  原文地址:WebGL学习(1) - 三角形
  还记得第一次看到canvas的粒子特效的时候,真的把我给惊艳到了,原来在浏览器也能做出这么棒的效果。结合《HTML5 Canvas核心技术》和网上的教程,经过半年断断续续的学习,对canvas的学习终于完结,对常用的canvas特效基本能做到信手拈来的。canvas特效请看:样例列表

  众所周知,canvas是2D绘图技术,虽然可以通过坐标变换,位置计算也能做到3D的效果。但3D场景数据量毕竟比2D要高一个数量级的,纯粹用canvas的话,不管是性能和开发的复杂度会成为一个瓶颈。

  这也是webGL出现的原因,解决web端3D渲染的场景。webGL会调用到GPU,处理大量重复的3D场景数据时,性能非常有优势。同时webGL是基于openGL ES 2.0, 因此它处理3D场景是非常成熟的。但为什么不直接学习three.js呢?因为本人对图形学感兴趣,只是希望做一些自己喜欢的效果的同时深入了解计算机图形学,没指望通过它做商业项目。

  为了让学习更有动力和目的性,我们以实例为导向学习webGL,再从中展开到需要学习哪些知识点。这次我们来实现如下的动画,该教程参考了《WebGL编程指南》

实际效果请看:旋转的三角形

triangle

webGL渲染流程

  webGL的渲染流程如下,其中第2,3,4步是重点,里面细节比较多。接着我们就按这个流程一步一步解决问题

  1. 获取webGL绘图上下文
  2. 初始化着色器
  3. 创建、绑定缓冲区对象
  4. 向顶点着色器和片元着色器写入数据
  5. 设置canvas背景色,清空canvas
  6. 绘制

webGL绘图上下文

  webGL是canvas基础之上的3D绘图技术,只是上下文不同,get3DContext函数作用就是依次降级获取上下文。

    var canvas=document.getElementById('canvas'),
        gl=get3DContext(canvas,true);
    function get3DContext(canvas, opt) {
      var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
      var context = null;
      for (var i = 0, len=names.length; i < len; i++) {
        try {
          context = canvas.getContext(names[i], opt);
        } catch(e) {}
        if (context) {
          break;
        }
      }
      return context;
    }

着色器

  着色器就是嵌入到js中的webGL代码,是由GLSL语言编写的,可以把着色器看成是js代码连接webGL的中间件。顶点着色器和片元着色器分别用于操作顶点和颜色光照,《WebGL编程指南》中是把着色器写成字符串,但从可维护性考虑,还是写在script标签中比较好。GLSL语言与C语言非常像,只要熟悉了GLSL特有的部分,其实还是比较简单的。

限定符
  限定符只能用于全局变量,有3种类型:attribute,uniform,varying,目前只用到前两种
attribute用于表示顶点信息
uniform用于表示除顶点外的其他信息,可以是除结构体和数组之外的任意类型
varying用于顶点着色器向片元着色器传输数据

GLSL特有的数据类型
向量:
vec2, vec3, vec4 //表示有2,3,4个浮点数的向量
ivec2, ivec3, ivec4 //表示有2,3,4个整形的向量
bvec2, bvec3, bvec4 //表示有2,3,4个布尔值的向量
矩阵:
mat2, mat3, mat4 //表示有2x2,3x3,4x4的浮点数的矩阵

顶点着色器

    <script type="x-shader/x-vertex" id="vs">
      attribute vec4 a_Position; //顶点,4个浮点的矢量,attribute变量传输与顶点有关的数据,表示逐顶点的信息
      uniform mat4 u_xformMatrix; //变换矩阵,4*4浮点矩阵, uniform变量传输的是所有顶点都相同的数据
      void main() { 
        gl_Position=u_xformMatrix*a_Position;
      } 
    </script>

片元着色器

    <script type="x-shader/x-fragment" id="fs">
      precision mediump float; // 精度限定
      uniform vec4 u_FragColor;  // 颜色
      void main() {
        gl_FragColor = u_FragColor;
      }
    </script>

  接着就是创建着色器了,首先从页面script标签取出着色器代码,初始化着色器;接着创建程序对象,最后连接程序对象。中间的步骤其实非常的啰嗦,已经把这几个步骤封装,我们只需要调用createShaders就可以了。

    /**
     * 根据script id创建着色器
     * @param  {Object} gl  context
     * @param  {String} vid script id
     * @param  {String} fid script id
     * @return {Boolen} 
     */
    function createShaders(gl,vid,fid){
        var vshader,fshader,element,program;

        [vid,fid].forEach(function(id){
            element= document.getElementById(id);
            if(element){
                switch(element.type){  
                    // 顶点着色器的时候  
                    case 'x-shader/x-vertex': vshader = element.text; break;
                    // 片段着色器的时候  
                    case 'x-shader/x-fragment': fshader = element.text; break;
                    default : break;
                }
            }
        });
        if(!vshader){
            console.log('VERTEX_SHADER String not exist');
            return false;
        }
        if(!fshader){
            console.log('FRAGMENT_SHADER String not exist');
            return false;
        }
        program = createProgram(gl, vshader, fshader);
        if (!program) {
            console.log('Failed to create program');
            return false;
        }

        gl.useProgram(program);
        gl.program = program;
        return true;
    }

    /**
     * 创建连接程序对象
     * @param  {Object} gl       上下文
     * @param  {String} vshader  顶点着色器代码
     * @param  {String} fshader  片元着色器代码
     * @return {Object}         
     */
    function createProgram(gl, vshader, fshader) {
      // 创建着色器对象
      var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
      var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
      if (!vertexShader || !fragmentShader) {
        return null;
      }

      // 创建程序对象
      var program = gl.createProgram();
      if (!program) {
        return null;
      }

      // 连接着色器对象
      gl.attachShader(program, vertexShader);
      gl.attachShader(program, fragmentShader);

      // 连接程序对象
      gl.linkProgram(program);

      // 检查连接结果
      var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
      if (!linked) {
        var error = gl.getProgramInfoLog(program);
        console.log('Failed to link program: ' + error);
        gl.deleteProgram(program);
        gl.deleteShader(fragmentShader);
        gl.deleteShader(vertexShader);
        return null;
      }
      return program;
    }

    /**
     * 加载着色器
     * @param  {Object} gl     上下文
     * @param  {Object} type   类型
     * @param  {String} source 代码字符串
     * @return {Object}       
     */
    function loadShader(gl, type, source) {
      // 创建着色器对象
      var shader = gl.createShader(type);
      if (shader == null) {
        console.log('unable to create shader');
        return null;
      }

      // 设置着色器程序
      gl.shaderSource(shader, source);

      // 编译着色器
      gl.compileShader(shader);

      // 检查编译结果
      var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
      if (!compiled) {
        var error = gl.getShaderInfoLog(shader);
        console.log('Failed to compile shader: ' + error);
        gl.deleteShader(shader);
        return null;
      }

      return shader;
    }

缓冲区

  创建好缓冲区对象后,需要把它分配给变量,然后使它生效。注意顶点数组使用的是类型化数组Float32Array,这样更加高效。vertexAttribPointer方法这里指定了每个顶点分量的个数为2,因为我们目前只定义x,y坐标,z坐标使用系统默认。

    /**
     * 创建缓冲区
     * @param  {Array} data
     * @param  {Object} bufferType
     * @return {Object}     
     */
    function createBuffer(data,bufferType){  
      // 生成缓存对象  
      var buffer = gl.createBuffer();  
      if (!buffer) {
        console.log('Failed to create the buffer object');
        return null;
      }
      // 绑定缓存(gl.ARRAY_BUFFER<顶点>||gl.ELEMENT_ARRAY_BUFFER<顶点索引>) 
      gl.bindBuffer(bufferType||gl.ARRAY_BUFFER, buffer);  
        
      // 向缓存中写入数据  
      gl.bufferData(bufferType||gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);  
        
      // 将绑定的缓存设为无效  
      // gl.bindBuffer(gl.ARRAY_BUFFER, null);  
        
      // 返回生成的buffer  
      return buffer;
    } 

    // 创建缓冲区并传人顶点
    var vertices=new Float32Array([-0.5, 0.5,   -0.5, -0.5,   0.5, 0.5, 0.5, -0.5 ])
    if(!createBuffer(vertices)){
        return;
    }

    // 分配缓冲区对象给a_Position变量
    // (地址,每个顶点分量的个数<1-4>,数据类型<整形,符点等>,是否归一化,指定相邻两个顶点间字节数<默认0>,指定缓冲区对象偏移量<默认0>)
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

    // 启动
    gl.enableVertexAttribArray(a_Position);

写入数据

  首先要获取变量的地址,然后再给变量赋值,感觉挺麻烦的。attribute标记的变量使用getAttribLocation获取,同理uniform标记的变量使用getUniformLocation获取。

  我们的动画要使图形绕坐标原点旋转,那么这就需要用到矩阵的变换,矩阵相关的知识就不详细说明了。要注意webGL使用的是列主序的矩阵,计算好变换矩阵后,把值赋予变量就ok。

    // 获取 u_FragColor变量的存储地址并赋值
    var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
    if (!u_FragColor) {
        return;
    }
    //颜色模式为rgba,值范围0~1
    gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);

    // 绕z轴旋转
    var deg=Math.PI/180*(angle++),
        cos=Math.cos(deg),
        sin=Math.sin(deg);

    //  webgl中是按列主序 旋转加位移
    var xformMatrix=new Float32Array([
        cos,sin,0.0,0.0,
        -sin,cos,0.0,0.0,
        0.0,0.0,1.0,0.0,
        0.3,0.0,0.0,1.0
    ]);

    // v表示可以向着色器传输多个数值(地址变量,webgl中必须false,矩阵)
    gl.uniformMatrix4fv(u_xformMatrix,false,xformMatrix);

背景操作

  每次执行动画前进行清屏,和canvas中的设置fillStyle,执行clearRect,效果一样。

    // 设置清屏颜色
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    // 清屏
    gl.clear(gl.COLOR_BUFFER_BIT);

绘制

  最后渲染图形,注意第一个参数,指定不同的值,它就渲染为不同的图形,大家可以用不同的值试试效果。
POINTS //点
LINES //线段
LINE_STRIP //线条
LINE_LOOP //回路
TRIANGLES //三角形
TRIANGLE_STRIP //三角带
TRIANGLE_FAN //三角扇

    // (基本图形,第几个顶点,执行几次),修改基本图形项可以生成点,线,三角形,矩形,扇形等
    gl.drawArrays(gl.TRIANGLES, 0, 3);

最后主体代码如下:

    var canvas=document.getElementById('canvas'),
        gl=get3DContext(canvas,true);

    function main() {
        if (!gl) {
          console.log('Failed to get the rendering context for WebGL');
          return;
        }

        if (!createShaders(gl, 'fs', 'vs')) {
          console.log('Failed to intialize shaders.');
          return;
        }

        // 创建缓冲区并传人顶点
        var vertices=new Float32Array([-0.5, 0.5,   -0.5, -0.5,   0.5, 0.5, 0.5, -0.5 ])
        if(!createBuffer(vertices)){
          console.log('Failed to create the buffer object');
          return;
        }

        // 获取顶点位置
        var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
        if (a_Position < 0) {
          console.log('Failed to get the storage location of a_Position');
          return;
        }

        // 分配缓冲区对象给a_Position变量
        gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(a_Position);


        // 获取 u_FragColor变量的存储地址并赋值
        var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
        if (!u_FragColor) {
          console.log('Failed to get the storage location of u_FragColor');
          return;
        }
        gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);


        // 获取矩阵变量
        var u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix');
        if (!u_xformMatrix) {
          console.log('Failed to get the storage location of u_xformMatrix');
          return;
        }

        var xformMatrix,angle=0;
        // 设置清屏颜色
        gl.clearColor(0.0, 0.0, 0.0, 1.0);

        // 执行动画
        (function animate(){
          var deg=Math.PI/180*(angle++),
              cos=Math.cos(deg),
              sin=Math.sin(deg);

            // 旋转加位移
            xformMatrix=new Float32Array([
              cos,sin,0.0,0.0,
              -sin,cos,0.0,0.0,
              0.0,0.0,1.0,0.0,
              0.3,0.0,0.0,1.0
            ]);

            // v表示可以向着色器传输多个数值(地址变量,webgl中必须false,矩阵)
            gl.uniformMatrix4fv(u_xformMatrix,false,xformMatrix);

            gl.clear(gl.COLOR_BUFFER_BIT);

            // (基本图形,第几个顶点,执行几次),修改基本图形项可以生成点,线,三角形,矩形,扇形等
            gl.drawArrays(gl.TRIANGLES, 0, 3);

            requestAnimationFrame(animate);
        }());
    }

    main();

总结

  相比canvas,webGL的api要原始得多,涉及到很多底层的openGL细节,但经过封装后,我们可以把那部分细节看成一个黑箱。大部分的操作都是基于矩阵变换,尽管有很多方便的第三方矩阵库,但有牢固的线性代数基础还是大有裨益的。GLSL编程语言也是一样需要熟练掌握。

上一篇 下一篇

猜你喜欢

热点阅读