WebGL 3D开发实战详解(第2版)
上QQ阅读APP看书,第一时间看更新

2.2 初识WebGL 2.0应用

2.1节简单介绍了3D绘图标准WebGL 2.0,相信读者已经对WebGL 2.0的发展历程和作用有了一定的了解。上面的介绍偏重基础,读者无法深入学习具体编程技巧。为了使读者能够保持学习热情,在实践中提升编程能力,本章将给出一个WebGL 2.0的基础案例并对相关的运行步骤进行详细介绍。

2.2.1 WebGL 2.0应用案例部署步骤

由于WebGL 2.0应用程序是以浏览器为平台进行工作的,因此一般情况下开发完毕后都需要将应用部署到Web服务器上以供浏览器访问。在具体开发各个案例之前,本节先简要介绍一下如何在Tomcat上部署WebGL 2.0应用,具体步骤如下。

(1)登录Tomcat的官方网站下载Tomcat的压缩包(如“apache-tomcat-6.0.14.zip”),然后解压缩到本地磁盘。解压完成后对环境变量中的JAVA_HOME进行配置。

(2)找到解压后apache-tomcat-6.0.14目录下bin子目录中的startup.bat文件,双击此文件启动Tomcat服务器,如图2-3所示。

图2-3 打开Tomcat服务器

提示

Tomcat的配置过程难度较小。本书篇幅有限,对于Tomcat的配置过程不再赘述。不熟悉的读者可以参考其他的书籍或资料。

(3)将开发完毕的WebGL 2.0案例(如Sample2_1)复制到解压后的apache-tomcat-6.0.14目录下的webapps子目录中,如图2-4所示。

图2-4 WebGL 2.0案例位置

(4)查找并记录本机的IP地址,然后打开Firefox等可以支持WebGL 2.0的浏览器,在地址栏中输入指定网址,按下回车键即可。例如运行案例Sample2_1时输入的网址为:

http://10.16.189.15:8080/Sample2_1/Sample2_1.html

说明

读者只需找到“:8080”前的IP地址并替换为自己机器的IP地址即可在自己的计算机上成功运行本案例。

提示

作者运行本章案例时使用的都是Tomcat服务器,读者也可以选择其他的Web服务器。作者运行本章案例时使用的是Firefox浏览器,读者也可以使用其他支持WebGL 2.0的浏览器(如UC浏览器、淘宝浏览器等)。

2.2.2 初识WebGL 2.0应用程序

2.2.1节中简单地介绍了WebGL 2.0应用案例的运行步骤,读者可以参考给出的案例进行高效学习。接下来将通过一个旋转正方体的案例向读者介绍如何开发3D场景。具体运行效果如图2-5、图2-6所示。

图2-5 初始状态效果图

图2-6 绕y轴旋转大约120°的效果图

前面给出了本案例的运行效果图,有兴趣的读者可在自己的设备上运行本案例。下面将通过本案例的详细代码讲解具体开发步骤。

在开发与本案例直接相关的类之前,首先需要介绍在网络端读取着色器(shader)脚本的工具类。此类读取着色器的方法主要分为几步:发送一个打开指定URL的请求,接收文本并进行切分,新建着色器对象并加载。具体代码如下。

代码位置:随书源代码/第2章/Sample2_1/js目录下的LoadShaderUtil.js文件。

        1     function shaderObject(typeIn, textIn){     //声明shaderObject类
        2       this.type=typeIn;                        //初始化type成员变量
        3       this.text=textIn;                        //初始化text成员变量
        4     }
        5     var shaderStrArray=["a", "a"];              //存储着色器数组
        6     var shaderNumberCount=0;                   //数组索引值
        7     var shaderTypeName=["vertex", "fragment"]; //着色器名称数组
        8     function processLoadShader(req, index){    //处理着色器脚本内容的回调函数
        9       if (req.readyState == 4){                //数据接收
        10        var shaderStr = req.responseText;     //获取响应文本
        11        //根据不同的数组索引值创建不同的着色器,并存入着色器数组
        12        shaderStrArray[shaderNumberCount]=new
        13        shaderObject(shaderTypeName[shaderNumberCount], shaderStr);
        14        shaderNumberCount++;                   //数组索引值加1
        15        if(shaderNumberCount>1){               //如果两个着色器内容均不为空,则
        16          //加载着色器
        17          shaderProgArray[index]=loadShaderSerial(gl, shaderStrArray[0], shaderStrArray[1]);
        18        }
        19    }}
        20    function loadShaderFile(url, index){       //从服务器加载着色器脚本的函数
        21      var req = new XMLHttpRequest();         //创建XMLHttpRequest对象
        22      req.onreadystatechange = function ()    //设置响应回调函数
        23      { processLoadShader(req, index) };       //调用processLoadShader处理响应
        24      req.open("GET", url, true);              //用GET方式打开指定URL
        25      req.responseType = "text";               //设置响应类型
        26      req.send(null);                          //发送HTTP请求
        27    }

❑ 第1~4行声明了着色器类shaderObject,其中有type和text两个成员变量,分别为着色器的类型和其中的内容文本。着色器类型又分为两种,“vertex”表示顶点着色器,“fragment”表示片元着色器。

❑ 第5~19行为处理着色器脚本内容的回调函数。程序接收到文本和数值后,根据数组索引值新建着色器对象并创建着色器程序,同时存入单独的数组中以供使用。

❑ 第20~27行为从指定地址读取着色器脚本并加载函数。

上文已经将网络端读取着色器的工具类介绍完毕,接下来介绍的是用于初始化WebGL 2.0 Canvas的JavaScript脚本文件——GLUtil.js,首先给出的是其中的initWebGLCanvas方法,具体代码如下。

代码位置:随书源代码/第2章/Sample2_1/js目录下的GLUtil.js文件。

        1    function initWebGLCanvas(canvasName) {              //初始化WebGL Canvas的方法
        2      canvas = document.getElementById(canvasName);    //获取Canvas对象
        3      var context = canvas.getContext('webgl2', { antialias: true }); //获取GL上下文
        4      return context;                                    //返回GL上下文对象
        5    }

说明

在渲染WebGL 2.0之前,必须进行Canvas的初始化工作。在本方法中首先通过Canvas的名字找到对应的Canvas。然后获取GL上下文的操作,找到后返回上下文对象。

初始化WebGL 2.0 Canvas的方法已经介绍完毕。一般在较复杂的项目中为了实现更加酷炫的渲染效果需要多套着色器。为了使管理更加便捷,GLUtil.js中还开发了对着色器进行管理的方法,具体代码如下。

代码位置:随书源代码/第2章/Sample2_1/js目录下的GLUtil.js文件。

        1     function loadSingleShader(ctx, shaderScript){      //加载单个着色器的方法
        2       if (shaderScript.type == "vertex")                //若为顶点着色器
        3         var shaderType = ctx.VERTEX_SHADER;             //顶点着色器类型
        4       else if (shaderScript.type == "fragment")        //若为片元着色器
        5         var shaderType = ctx.FRAGMENT_SHADER;          //片元着色器类型
        6       else {                                             //否则打印错误信息
        7         console.log("*** Error: shader script of undefined type '"+shaderScript.type+"'");
        8         return null;
        9       }
        10      var shader = ctx.createShader(shaderType);       //根据类型创建着色器程序
        11      ctx.shaderSource(shader, shaderScript.text);     //加载着色器脚本
        12      ctx.compileShader(shader);                        //编译着色器
        13      var compiled = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS); //检查编译状态
        14      if (! compiled && ! ctx.isContextLost()){          //若编译出错
        15        var error = ctx.getShaderInfoLog(shader);      //获取错误信息
        16        console.log("*** Error compiling shader '"+shaderId+"':"+error); //打印错误信息
        17        ctx.deleteShader(shader);                       //删除着色器程序
        18        return null;                                     //返回空
        19      }
        20      return shader;                                     //返回着色器程序
        21    }
        22    function loadShaderSerial(gl, vshader, fshader){   //加载链接顶点、片元着色器的方法
        23      var vertexShader = loadSingleShader(gl, vshader); //加载顶点着色器
        24      var fragmentShader = loadSingleShader(gl, fshader);  //加载片元着色器
        25      var program = gl.createProgram();                 //创建着色器程序
        26      gl.attachShader (program, vertexShader);         //将顶点着色器添加到着色器程序中
        27      gl.attachShader (program, fragmentShader);       //将片元着色器添加到着色器程序中
        28      gl.linkProgram(program);                          //链接着色器程序
        29      var linked = gl.getProgramParameter(program, gl.LINK_STATUS); //检查链接是否成功
        30      if (! linked && ! gl.isContextLost()){              //若链接不成功
        31        var error = gl.getProgramInfoLog (program);    //获取错误信息
        32        console.log("Error in program linking:"+error); //打印错误信息
        33        gl.deleteProgram(program);                      //删除着色器程序
        34        gl.deleteProgram(fragmentShader);               //删除片元着色器
        35        gl.deleteProgram(vertexShader);                 //删除顶点着色器
        36        return null;                                     //返回空
        37      }                                                  //返回着色器程序
        38      gl.useProgram(program);                           //指明使用的着色器编号
        39      gl.enable(gl.DEPTH_TEST);                         //打开深度检测
        40      return program;                                    //返回着色器程序
        41    }

❑ 第1~21行为加载单个着色器的loadSingleShader方法。向此方法传入着色器类型,程序会新建一个同类型的着色器并加载着色器脚本。加载完成后,程序会对着色器进行编译。如果编译出错则打印错误信息。

❑ 第22~41行为对一套着色器进行加载和链接操作的loadShaderSerial方法。向此方法传入顶点着色器和片元着色器后,程序会分别加载顶点着色器和片元着色器并链接着色器程序。因为链接操作过程中可能会出现错误,所以此方法还进行链接检查。如果链接不成功则删除着色器及程序。如果链接成功则返回着色器程序。

上文已经详细介绍了着色器的操作、管理工具类和初始化上下文的相关方法,接下来将介绍经常使用的MatrixState工具类。此类的作用是对各种变换矩阵进行管理。在执行各种变换时,本类对矩阵进行计算,其具体代码如下。

代码位置:随书源代码/第2章/Sample2_1/js目录下的MatrixState.js文件。

        1     function MatrixState(){
        2       this.mProjMatrix = new Array(16);             //投影矩阵
        3       this.mVMatrix = new Array(16);                //摄像机矩阵
        4       this.currMatrix=new Array(16);                //基本变换矩阵
        5       this.mStack=new Array(100);                   //矩阵栈
        6       this.setInitStack=function(){                 //初始化矩阵的方法
        7         this.currMatrix=new Array(16);              //创建存储矩阵元素的数组
        8         setIdentityM(this.currMatrix,0);            //将元素填充为单位阵的元素值
        9       }
        10      this.pushMatrix=function(){                   //保护变换矩阵,当前矩阵入栈
        11        this.mStack.push(this.currMatrix.slice(0));
        12      }
        13      this.popMatrix=function(){                    //恢复变换矩阵,当前矩阵出栈
        14        this.currMatrix=this.mStack.pop();
        15      }
        16      this.translate=function(x, y, z){               //平移变换
        17        translateM(this.currMatrix, 0, x, y, z);   //将平移变换记录进矩阵
        18      }
        19      this.rotate=function(angle, x, y, z)    {       //旋转变换
        20        rotateM(this.currMatrix,0, angle, x, y, z);    //将旋转变换记录进矩阵
        21      }
        22      ……//此处省略了部分方法,读者可参见随书源代码
        23    }

❑ 第2~5行为对投影矩阵、摄像机矩阵、基本变换矩阵进行声明的相关代码。另外新建一个栈用来存储基本变换矩阵,在绘制时进行进栈和出栈的操作。

❑ 第6~21行为初始化矩阵、保护变换矩阵、恢复变换矩阵,以及进行平移旋转变换的相关代码。保护变换矩阵方法的实质是对当前变换矩阵执行入栈操作。恢复变换矩阵的方法的实质是对矩阵栈执行出栈操作,删除栈顶的矩阵。

提示

此类中还有对投影矩阵、摄像机矩阵及变换矩阵操作的相关方法。本书篇幅有限,不再赘述,有兴趣的读者可自行查阅随书源代码。

上文已经将项目开发中常用的几个工具类全部介绍完毕。下面介绍与本案例直接相关的三角形绘制脚本Triangle.js。此脚本中包含三角形顶点坐标信息和顶点颜色信息,在绘制时将这些信息传入渲染管线中。具体代码如下。

代码位置:随书源代码/第2章/Sample2_1/js目录下的Triangle.js文件。

        1     function Triangle(                                      //声明绘制物体对象所属类
        2       gl,                                                   //GL上下文
        3       programIn                                             //着色器程序id
        4     ){
        5         this.vertexData= [3.0,0.0,0.0,  0.0,0.0,0.0,  0.0,3.0,0.0];
                  //三角形顶点的xyz坐标
        6         this.vcount=this.vertexData.length/3;              //得到顶点数量
        7         this.vertexBuffer=gl.createBuffer();               //创建顶点坐标数据缓冲
        8         gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); //绑定顶点坐标数据缓冲
        9         //将顶点坐标数据送入缓冲
        10        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.vertexData), gl.STATIC_DRAW);
        11        this.colorsData=[1.0,1.0,1.0,1.0,  0.0,0.0,1.0,1.0,  0.0,1.0,0.0,1.0];
                  //初始化顶点颜色数据
        12        this.colorBuffer=gl.createBuffer();                //创建颜色数据缓冲
        13        gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);  //绑定颜色数据缓冲
        14        //将颜色数据送入缓冲
        15        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.colorsData), gl.STATIC_DRAW);
        16        this.program=programIn;                            //初始化着色器程序id
        17        this.drawSelf=function(ms) {                       //绘制三角形的方法
        18          gl.useProgram(this.program);                     //指定使用某套着色器程序
        19          //获取总变换矩阵引用id
        20          var uMVPMatrixHandle=gl.getUniformLocation(this.program, "uMVPMatrix");
        21          //将总变换矩阵送入渲染管线
        22          gl.uniformMatrix4fv(uMVPMatrixHandle, false, new Float32Array(ms.getFinalMatrix()));
        23          //启用顶点坐标数据
        24          gl.enableVertexAttribArray(gl.getAttribLocation(this.program, "aPosition"));
        25          gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);  //绑定顶点坐标数据缓冲
        26          //给管线指定顶点坐标数据
        27          gl.vertexAttribPointer(gl.getAttribLocation(this.program, "aPosition"),
                    3, gl.FLOAT, false,0, 0);
        28          //启用颜色坐标数据
        29          gl.enableVertexAttribArray(gl.getAttribLocation(this.program, "aColor"));
        30          gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);   //绑定颜色数据缓冲
        31          //给管线指定颜色数据
        32          gl.vertexAttribPointer(gl.getAttribLocation(this.program, "aColor"),
                    4, gl.FLOAT, false,0, 0);
        33          gl.drawArrays(gl.TRIANGLES, 0, this.vcount);        //用顶点法绘制物体
        34    }}

❑ 第5行为初始化三角形坐标。数组中每3个数为一组,分别代表顶点的xyz坐标。

❑ 第6~10行为创建并绑定顶点坐标数据缓冲。在绑定完成之后,将顶点坐标数据送入缓冲中,为之后的绘制做好准备。

❑ 第11行为初始化顶点颜色数据。数组与顶点坐标数组不同,每4个数为一组,分别代表每个顶点的RGBA数据。

❑ 第17~34行为绘制三角形。主要将总变换矩阵、顶点和颜色数据传给渲染管线。

上文已经详细介绍了三角形类的相关代码。可能读者看到绘制三角形方法中的uMVPMatrix、aPosition等变量会感到疑惑,实际上这些变量是着色器中定义的相关变量。接下来将会对本案例中与着色器相关的代码进行简要介绍,首先来看顶点着色器的相关代码。

代码位置:随书源代码/第2章/Sample2_1/shader目录下的vtrtex.bns文件。

        1    #version 300 es                                    //使用WebGL2.0着色器
        2    uniform mat4 uMVPMatrix;                          //总变换矩阵
        3    layout (location = 0) in vec3 aPosition;         //顶点位置
        4    layout (location = 1) in vec4 aColor;             //顶点颜色
        5    out  vec4 vColor;                                 //传递给片元着色器的变量
        6    void main(){
        7       gl_Position = uMVPMatrix * vec4(aPosition,1); //根据总变换矩阵计算此次绘制的顶点位置
        8       vColor = aColor;                               //将接收的颜色传递给片元着色器
        9    }

说明

第1~9行为顶点着色器的相关代码。顶点着色器的主要作用是执行顶点变换、纹理坐标变换等与顶点相关的操作。此顶点着色器只计算了绘制点的位置并将接收到的颜色数据传入片元着色器。

前面已经详细介绍了顶点着色器的相关代码。可以看到顶点着色器需要传值给片元着色器,下面来对片元着色器的相关代码进行简要介绍。

代码位置:随书源代码/第2章/Sample2_1/shader目录下的fragment.bns文件。

        1    #version 300 es                          //使用WebGL2.0着色器
        2    precision mediump float;                 //使用默认精度
        3    in vec4 vColor;                          //接收从顶点着色器传过来的参数
        4    out vec4 fragColor;                      //输出到片元颜色
        5    void main(){
        6       fragColor = vColor;                   //给此片元颜色赋值
        7    }

第1~7行为片元着色器的相关代码。片元着色器的作用为执行纹理访问、颜色汇总和雾效等操作。此片元着色器的作用是将接收到的颜色数据作为此顶点的颜色。

说明

可能有些读者这时对着色器的认识还是一头雾水。其实不必担心,后面的章节中会对着色语言以及着色器的逻辑和作用进行详细讲解。读者只需要对以上代码有基本的认识即可。

所有基础开发工作都已经结束,接下来将详细介绍呈现3D场景的网页的相关代码。此网页中的代码主要作用为初始化上下文及相关参数,为绘制工作做好准备,并且使用setInterval()函数对绘制方法进行定时调用。具体代码如下。

代码位置:随书源代码/第2章/Sample2_1目录下的Sample2_1.html文件。

        1     <html>
        2         <head>
        3         <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        4         <title>Triangle</title> <! --标题-->
        5         <script type="text/javascript" src="js/Matrix.js"></script>
        6         ……//此处省略了导入其他JavaScript脚本文件的代码,读者可自行查阅随书源代码
        7         <script>
        8              var gl;                               //GL上下文
        9              var ms=new MatrixState();              //变换矩阵管理类对象
        10             var ooTri;                            //要绘制的三角形
        11             var shaderProgArray=new Array();     //着色器程序列表,集中管理
        12             var currentAngle;                     //旋转角度
        13             var incAngle;                         //旋转角度增量
        14             var canvas;                           //图形容器
        15             function start(){                     //初始化的方法
        16               gl = initWebGLCanvas("bncanvas");  //获取GL上下文
        17               if (! gl){                           //若获取GL上下文失败
        18                 alert("创建GLES上下文失败,不支持WebGL2.0! ");    //显示错误提示信息
        19                 return;
        20               }
        21               gl.viewport(0, 0, canvas.width, canvas.height); //设置视口大小
        22               gl.clearColor(0.0,0.0,0.0,1.0);             //设置屏幕背景色 RGBA
        23               ms.setInitStack();                          //初始化变换矩阵
        24               ms.setCamera(0,0, -5,0,0,0,0,1,0);          //设置摄像机
        25               ms.setProjectFrustum(-1.5,1.5, -1,1,1,100); //设置投影参数
        26               gl.enable(gl.DEPTH_TEST);                   //开启深度检测
        27               loadShaderFile("shader/vtrtex.bns",0);     //加载顶点着色器程序
        28               loadShaderFile("shader/fragment.bns",0);   //加载片元着色器程序
        29               if(shaderProgArray[0]){                     //如果着色器已加载完毕
        30                 ooTri=new Triangle(gl, shaderProgArray[0]);    //则创建三角形绘制对象
        31               }else{
        32                 //休息10ms后再创建三角形绘制对象
        33                 setTimeout(function(){ooTri=new Triangle(gl, shaderProgArray[0]); },10);
        34               }
        35               currentAngle = 0;                       //初始化旋转角度
        36               incAngle = 0.4;                         //初始化角度步进值
        37               setInterval("drawFrame(); ",16.6);      //定时绘制画面
        38             }
        39             function drawFrame(){                     //绘制一帧画面的方法
        40               if(! ooTri){                             //如果三角形没有加载成功
        41                 console.log("加载未完成!");            //则提示信息
        42                 return;
        43               }
        44               //清除着色缓冲与深度缓冲
        45               gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        46               ms.pushMatrix();                        //保护现场
        47               ms.rotate(currentAngle,0,1,0);         //执行旋转
        48               ooTri.drawSelf(ms);                     //绘制物体
        49               ms.popMatrix();                         //恢复现场
        50               currentAngle += incAngle;               //修改旋转角度
        51               if (currentAngle > 360){currentAngle -= 360; } //保证角度范围不超过360°
        52             }
        53        </script>
        54        </head>
        55        <body onload="start(); ">
        56        <canvas height="800" width="1200" id="bncanvas">
        57             若看到这个文字,则说明浏览器不支持WebGL!
        58        </canvas>
        59        </body>
        60    </html>

❑ 第1~7行为常用的一些标签。它设置了本页面的名称、内容类型以及编码格式等。另外,第5行为引入外部脚本文件的代码,在此处将本案例需要的脚本文件都引入了操作。

❑ 第8~14行为声明多个全局变量的相关代码。其中包括GLES上下文、变换矩阵管理、三角形的绘制、着色器程序列表、旋转角度、旋转角度增量、图形容器。这些变量在后面的程序中会多次使用。

❑ 第15~38行为对绘制工作进行初始化的相关代码。首先获取上下文。如果获取上下文失败则显示错误信息。接下来设置视口大小、屏幕背景色、摄像机、投影参数等基本信息。最后进行着色器的加载并以定时回调的方式进行绘制。

❑ 第39~52行为绘制每一帧的方法。在进行绘制之前要先检查三角形的绘制是否完成。若没有加载完成,则弹出错误信息。接下来清除着色缓冲和深度缓冲。前面的工作完成后进行三角形的绘制。

项目中的Matrix.js文件是作者针对项目而开发的一个工具文件。此文件已经进行了加密操作,所以此处不进行深入介绍。MatrixState类中对矩阵执行操作的各种方法就是基于此文件进行开发的。