使用WebGL绘制一只平面小鸡

这学期选修了计算机图形学这门课,最近刚开始接触WebGL,目前只学到二维的部分,尝试绘制一只平面的小鸡。

效果图

首先上效果图:
可爱的小鸡

下面直接进入代码部分:

drawCircle.html

<!DOCTYPE html>
<html>
    <head lang="en">
        <meta charset="UTF-8">
        <title>DrawCircle</title>

        <script id="vertex-shader" type="x-shader/x-vertex">
            attribute vec4 vPosition;
            uniform mat4 u_transMat;
            void main(){
              gl_Position = u_transMat * vPosition;
            }
        </script>
        <script id="fragment-shader" type="x-shader/x-fragment">
            precision mediump float;
            uniform vec4 u_FragColor;
            void main(){
                gl_FragColor = u_FragColor;
            }
        </script>

        <script type="text/javascript" src="../Common/webgl-utils.js"></script>
        <script type="text/javascript" src="../Common/initShaders.js"></script>
        <script type="text/javascript" src="drawCircle.js"></script>

    </head>
    <body>
        <canvas id="gl-canvas" width="512" height="512">
            Oops ... your browser doesn't support the HTML5 canvas element
        </canvas>
    </body>
</html>

在html文件中,声明了两个着色器。
顶点着色器内,设置了attribute变量vPosition用于记录顶点的属性,uniform变量u_transMat为变换矩阵,用于对绘制出的图案进行旋转平移操作。
片元着色器内,首先设置精度为中低精度,uniform变量u_FragColor用于记录片元颜色。
body中,声明一个canvas画布,所有的绘制都在画布上进行。
需要注意的是,gl_Position的值是用变换矩阵 * 顶点坐标向量得到的,变换矩阵和顶点坐标向量的位置不能交换,因为我们最终需要得到的gl_Position是一个四阶行向量。

drawCircle.js

js代码部分比较长,让我分解为多个部分。
通过对小鸡的分析,不难得知,组成该图形的图形为三角形、圆形、线段。

1. 我们首先来介绍绘制圆的函数getCircleVertex。

// 画圆
// 半径r 面数m 度数c
function getCircleVertex(r, m, c) {
    var arr = [];
    var addAng = c / m;
    var angle = 0;
    for (var i = 0; i < m; i++) {
        arr.push(Math.sin(Math.PI / 180 * angle) * r, Math.cos(Math.PI / 180 * angle) * r, 0, 1.0);
        arr.push(0.0, 0.0, 0.0, 1.0);
        angle = angle + addAng;
        arr.push(Math.sin(Math.PI / 180 * angle) * r, Math.cos(Math.PI / 180 * angle) * r, 0, 1.0);
    }
    return arr;
}

由于WebGL没有提供圆这个基本图形,于是我们把圆细分,通过绘制多个三角形来达到圆的效果。该函数它有三个参数:r表示圆的半径,m表示绘制该圆需要的三角形面数(即个数,数字越大,圆的效果越好),c表示需要绘制的度数(c取360时为整圆,取180时为半圆,以此类推),函数返回值为存储了构成圆的每个三角形的三个顶点的数组(有点绕)。

观察for循环内部,我们使用JavaScript内置的Math.sin()和Math.cos()来计算三角形(可看做扇形)圆心角的正余弦,但这两个方法必须接收弧度值,所以要先把角度转换为弧度值。
由于最后我们使用gl.TRIANGLES来绘制三角形,因此每一次for循环就将三角形的三个角的坐标加入到arr数组中。

2. 接着进入js主体代码部分

	window.onload = function init () {
    var canvas = document.getElementById( "gl-canvas" );

    var gl = WebGLUtils.setupWebGL( canvas );
    if ( !gl ) { alert( "WebGL isn't available" ); }

    // 设置窗口大小
    gl.viewport( 0, 0, canvas.width, canvas.height );
    gl.clearColor( 0.0, 0.0, 0.0, 1.0 );

    // 初始化着色器
    var program = initShaders( gl, "vertex-shader", "fragment-shader" );
    gl.useProgram( program );

    // 获取vPosition变量的存储位置
    var vPosition = gl.getAttribLocation(program, "vPosition");
    if (vPosition < 0) {
        console.log('Failed to get the storage location of vPosition');
        return;
    }

    // 获取u_transMat变量的存储位置
    var u_transMat = gl.getUniformLocation(program, "u_transMat");
    if (u_transMat < 0) {
        console.log('Failed to get the storage location of u_transMat');
        return;
    }

    // 获取u_FragColor变量的存储位置
    var u_FragColor = gl.getUniformLocation(program, 'u_FragColor');
    if (!u_FragColor) {
        console.log('Failed to get the storage location of u_FragColor');
        return;
    }

    var colors = [
        [1.0, 0.843, 0.0, 1.0], //金黄色
        [1.0, 0.647, 0.0, 1.0], //橙色
        [0.824, 0.412, 0.118, 1.0], //巧克力色
        [0.0, 0.0, 0.0, 1.0] //黑色
    ];

以上完成了基本的初始化操作。我们首先获取到canvas画布,然后得到绘图上下文gl,设置清空画布的颜色为黑色,并初始化着色器。获取我们之前设置在着色器内attribute和uniform变量的存储位置,这里为了提高代码的稳健性,我们对获取到的变量进行了非空校验。最后定义了一个color数组,用于存储绘制中需要用到的四种颜色。

3. 下面,步入真正的绘制阶段。

注意:需要按照一定的顺序来进行绘制,因为当绘制部分有重叠时,后绘制的部分会遮挡先绘制的部分。
所有绘制点的坐标需要在纸上提前设计、演算好,这里只做对绘制过程的大致解释。

* 首先绘制鸡的嘴巴(三角形):
    // 画鸡嘴(三角形)
    vertices = [
        -0.392, -0.04, 0.0, 1.0,
        -0.42, -0.08, 0.0, 1.0,
        -0.36, -0.08, 0.0, 1.0
    ];
    mat4 = new Float32Array([
        1.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0
    ]);
    // 创建缓存
    var buffer = gl.createBuffer(); // 为顶点创建的缓存
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer); // 绑定缓冲区
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(vPosition);

    gl.uniform4f(u_FragColor, colors[1][0], colors[1][1], colors[1][2], colors[1][3]);
    gl.uniformMatrix4fv(u_transMat, false, mat4);

    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES, 0, 3);

鸡的嘴巴是一个三角形,我们直接将计算出的三角形的三个顶点赋值给vertices数组,此处不需要对三角形进行平移或旋转操作,因此直接将旋转矩阵的值给为单位矩阵即可。由于是第一次绘制,我们首先需要为顶点创建出缓冲区,然后就是常规的操作了,将缓冲区和顶点绑定,向缓冲区内写入已创建好的顶点数据vertices,将点的位置传递到attribute变量中,激活变量,向uniform变量写入数据(分别写入了片元颜色和变换矩阵的数据)。最后,在第一次绘制前,我们需要清空画布,然后再开始绘制。至此,鸡嘴就完成了。

* 下面进入鸡头(圆形)的绘制:
// 画鸡头(圆)
    var ms = 180; // 组成圆的划分三角形个数
    var vertices = getCircleVertex(0.1, ms, 360);

    var Tx = -0.3;
    var Ty = 0.0;
    var Tz = 0.0;
    var mat4 = new Float32Array([
        1.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        Tx, Ty, Tz, 1.0
    ]);

    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // 向缓冲区写入顶点数据
    gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);
    gl.uniform4f(u_FragColor, colors[0][0], colors[0][1], colors[0][2], colors[0][3]);
    gl.uniformMatrix4fv(u_transMat, false, mat4);
    gl.drawArrays(gl.TRIANGLES, 0, ms*3);

定义一个ms变量,用于表示划分圆所使用的划分三角形个数。利用我们已经写好的getCircleVertex函数,传入相应参数,此处头的半径给为0.1,就可以得到所有的划分三角形的顶点数组了。利用这些三角形数据,作出的圆的圆心是位于原点处的,我们还需要为变换矩阵进行赋值,以使圆平移到正确的位置。mat4矩阵是一个平移矩阵,Tx、Ty、Tz分别表示需要在x、y、z轴上平移的距离,由于我们是作的二维图形,Tz直接赋为0就好,Tx、Ty根据自己的设计进行赋值,此处我将它们分别赋值为-0.3, 0.0,负数表示向负方向平移。
然后的操作就和第一步绘制鸡嘴巴的类似了,只不过这次不用再清空画布,否则第一次绘制的图像会消失,只需要将相应的数据再次绑定到缓冲区,赋值给相应的变量即可,最后,19行这里调用画图函数时要注意,将第三个参数更改为ms*3,因为我们一共绘制了ms数量个三角形。

* 其余部分的绘制:

剩下的绘制部分就大同小异了,不再一一赘述。只对某些部分进行一定的解释:

  1. 对于需要同时进行旋转和平移操作的图形,要在草稿纸上计算好旋转矩阵和平移矩阵的积,将这个结果赋值给变换矩阵。
  2. 在绘制鸡的身体和尾巴时,它们都不是整圆,在给getCircleVertex函数传入参数时,第三个参数要相应修改为扇形的圆心角的角度值。
  3. 鸡的左腿和右腿是用线段来绘制的,由于单条线段的宽度过小(此处我在网上搜索过一些改变线的宽度的例子,但我实际使用后反应无效,无奈只能想出了以下办法),我在代码中同时传入了两条邻近线段的顶点值,并使用gl.LINE_LOOP的绘制方式,让这两条线段绘制的位置贴近,视觉上达到加粗的效果。
// 画鸡内侧翅膀(0.1半圆)
    vertices = getCircleVertex(0.1, ms, 180);
    mat4 = new Float32Array([
        0.866, -0.5, 0.0, 0.0,
        0.5, 0.866, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        -0.05, 0.02, 0.0, 1.0
    ]);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);
    gl.uniform4f(u_FragColor, colors[1][0], colors[1][1], colors[1][2], colors[1][3]);
    gl.uniformMatrix4fv(u_transMat, false, mat4);
    gl.drawArrays(gl.TRIANGLES, 0, ms*3);

    // 画鸡身体(半圆)
    vertices = getCircleVertex(0.2, ms, 180);
    mat4 = new Float32Array([
        0.0, -1.0, 0.0, 0.0,
        1.0, 0.0, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0
    ]);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);
    gl.uniform4f(u_FragColor, colors[0][0], colors[0][1], colors[0][2], colors[0][3]);
    gl.uniformMatrix4fv(u_transMat, false, mat4);
    gl.drawArrays(gl.TRIANGLES, 0, ms*3);

    // 画鸡尾巴(60°扇形)
    vertices = getCircleVertex(0.08, ms, 60);
    mat4 = new Float32Array([
        0.5, -0.866, 0.0, 0.0,
        0.866, 0.5, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.2, 0.0, 0.0, 1.0
    ]);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);
    gl.uniformMatrix4fv(u_transMat, false, mat4);
    gl.drawArrays(gl.TRIANGLES, 0, ms*3);

    // 画鸡外侧翅膀(0.15半圆)
    vertices = getCircleVertex(0.15, ms, 180);
    mat4 = new Float32Array([
        0.707, -0.707, 0.0, 0.0,
        0.707, 0.707, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0
    ]);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);
    gl.uniform4f(u_FragColor, colors[1][0], colors[1][1], colors[1][2], colors[1][3]);
    gl.uniformMatrix4fv(u_transMat, false, mat4);
    gl.drawArrays(gl.TRIANGLES, 0, ms*3);

    // 画鸡左腿(直线)
    vertices = [
        -0.05, -0.19, 0.0, 1.0,
        -0.05, -0.25, 0.0, 1.0,
        -0.045, -0.195, 0.0, 1.0,
        -0.045, -0.25, 0.0, 1.0
    ];
    mat4 = new Float32Array([
        1.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0
    ]);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);
    gl.uniform4f(u_FragColor, colors[2][0], colors[2][1], colors[2][2], colors[2][3]);
    gl.uniformMatrix4fv(u_transMat, false, mat4);
    gl.drawArrays(gl.LINE_LOOP, 0, 4);

    // 画鸡右腿(直线)
    vertices = [
        0.05, -0.19, 0.0, 1.0,
        0.05, -0.25, 0.0, 1.0,
        0.045, -0.195, 0.0, 1.0,
        0.045, -0.25, 0.0, 1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);
    gl.uniformMatrix4fv(u_transMat, false, mat4);
    gl.drawArrays(gl.LINE_LOOP, 0, 4);

    // 画鸡左脚(三角形)
    vertices = [
        -0.045, -0.25, 0.0, 1.0,
        -0.04, -0.28, 0.0, 1.0,
        -0.09, -0.28, 0.0, 1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);
    gl.uniformMatrix4fv(u_transMat, false, mat4);
    gl.drawArrays(gl.TRIANGLES, 0, 3);

    // 画鸡右脚(三角形)
    vertices = [
        0.05, -0.25, 0.0, 1.0,
        0.0, -0.28, 0.0, 1.0,
        0.055, -0.28, 0.0, 1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);
    gl.uniformMatrix4fv(u_transMat, false, mat4);
    gl.drawArrays(gl.TRIANGLES, 0, 3);

    // 画鸡眼睛(圆)
    var vertices = getCircleVertex(0.01, ms, 360);

    var Tx = -0.35;
    var Ty = -0.015;
    var Tz = 0.0;
    var mat4 = new Float32Array([
        1.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        Tx, Ty, Tz, 1.0
    ]);

    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // 向缓冲区写入顶点数据
    gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);
    gl.uniform4f(u_FragColor, colors[3][0], colors[3][1], colors[3][2], colors[3][3]);
    gl.uniformMatrix4fv(u_transMat, false, mat4);
    gl.drawArrays(gl.TRIANGLES, 0, ms*3);
    };

至此,一个小鸡就完成了。

猜你喜欢

转载自blog.csdn.net/qq_40121502/article/details/83099254