• WebGL学习(1)


    原文地址: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编程指南》

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

    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用于顶点着色器向片元着色器传输数据

    GLSL特有的数据类型

    1. 向量:

      vec2, vec3, vec4 : 表示有2,3,4个浮点数的向量
      ivec2, ivec3, ivec4 : 表示有2,3,4个整形的向量
      bvec2, bvec3, bvec4 : 表示有2,3,4个布尔值的向量

    2. 矩阵:
      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编程语言也是一样需要熟练掌握。

  • 相关阅读:
    神奇玻璃制品:鲁珀特之泪
    ReCaptcha——基于验证码的数据挖掘
    GCC 编译使用动态链接库和静态链接库
    转:Android View.post(Runnable )
    两个adb命令使用的问题
    转:android menu 实现动态修改menu
    Android Dialog自定义
    转:ActivityGroup + GridView 实现Tab分页标签
    TabHost与ActivityGroup整理
    转:Android之Tab分页标签的实现方法一TabActivity和TabHost的结合
  • 原文地址:https://www.cnblogs.com/edwardloveyou/p/7806258.html
Copyright © 2020-2023  润新知