搭建WebGL开发环境

前言

本篇文章介绍如何搭建WebGL开发环境

WebGL

WebGL的技术规范继承自免费和开源的OpenGL ES标准,从某种意义上说,WebGL就是Web版的OpenGL ES,而OpenGL ES是从OpenGL中派生出来的。他们的应用环境有区别,一般来说:

  • OpenGL:一般应用于桌面级别的三维图形渲染,同类的还有微软的DirectX和苹果公司的Metal技术
  • OpenGL ES:OpenGL的子集,删除了很多用处不大的接口和特性,但仍然能进行三维渲染,应用于智能手机、嵌入式计算机、家用游戏机等
  • WebGL:从OpenGL ES派生出来的。需要浏览器内核的支持。WebGL广泛应用于所有的网页渲染。因为需要运行在浏览器上,所以浏览器在背后做了很多工作,包括跨平台的特性。

    版本

    既然OpenGL、OpenGL ES和WebGL存在继承关系,那必然涉及到版本信息

    先看一下OpenGL的版本信息:

    • OpenGL 1.5:这个版本的OpenGL不支持着色器编程,采用固定渲染管线
    • OpenGL 2.0:这个版本加入GLSL着色器编程语言
    • OpenGL 3.3:2010年发布
    • OpenGL 4.3:2013年发布

      看一下OpenGL ES的版本信息:

      • OpenGL ES 1.1:OpenGL 1.5的子集,应用范围有限
      • OpenGL ES 2.0:OpenGL 2.0的子集,应用最广泛的版本
      • OpenGL ES 3.0:OpenGL 3.3的子集,增加了很多新功能,包括:
        • 遮挡查询
        • 变缓反馈(Transform Feedback)
        • 实例渲染(Instanced Rendering)
        • 四个或更多渲染目标支持
        • 着色语言全面支持整数和32位浮点操作
        • 纹理功能大幅增强,支持浮点纹理、3D纹理、深度纹理、顶点纹理、NPOT纹理、R/RG单双通道纹理、不可变纹理、2D阵列纹理、无二次幂限制纹理、阴影对比、调配(swizzle)、LOD与mip level clamps、无缝立方体贴图、采样对象、纹理MSAA抗锯齿渲染器
        • 一系列广泛的精确尺寸纹理和渲染缓冲格式

          看一下WebGL的版本信息:

          • WebGL 1.0:基于OpenGL ES 2.0,并提供了3D图形的API,它使用HTML5Canvas并允许利用文档对象模型接口。
          • WebGL 2.0:WebGL 2.0基于OpenGL ES 3.0,确保了提供许多选择性的WebGL 1.0扩展,并引入新的API。可利用部分Javascript实现自动存储器管理

            WebGL程序组成

            一个最简单的WebGL程序之需要一个html文件和几个js文件就可以了,下面详细介绍

            html部分

            html主要是为了放置canvas元素,因为WebGL的渲染都是在canvas进行的。

            下面是一个html部分:

              wgl render ins  当前浏览器不支持canvas 

            这个简单的不能再简单的html就已经够用了。

            上面的html我们命名为main.html。从上面的html中,可以发现我们需要一个main函数,这个函数一般来说在js中实现,这也是我们WebGL程序正式开始的入口函数,我们添加一个js文件sk_init.js,并实现该函数,当然我们也需要在html中加入引用js的代码,为了保持代码的简洁,已经在上面的html中加入了该代码。

            js部分

            下面是sk_init.js的代码:

            var gl;
            function main()
            {
                var canvas = document.getElementById("wglCanvas");
                gl = create3DContext(canvas);
                if(!gl)
                {
                    console.log("获取webgl渲染上下文失败");
                    return;
                }
                gl.clearColor(0.0,0.0,0.0,1.0);
                gl.clear(gl.COLOR_BUFFER_BIT);
            }
            var create3DContext = function(canvas) {
            	var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
            	var context = null;
            	for (var ii = 0; ii < names.length; ++ii) {
            		try {
            			context = canvas.getContext(names[ii]);
            		} catch(e) {}
            		if (context) {
            			break;
            		}
            	}
            	return context;
            }
            

            上面这个代码分为几部分:

            • 获取canvas对象
            • 获取webgl上下文
            • 设置canvas画布背景色
            • 使用背景色清空绘图区

              虽然代码很简单,但是这几步是所有webgl程序都需要做的事情,当前main.html已经可以运行了。不过只会显示一个黑色背景。下面我们开始加入些东西

              shader

              想编写一个真正可以使用的webgl程序,首先需要写shader,由于本篇文章我们着重介绍WebGL框架,所以我们只是简单说明一下,关于shader语法,会有专门文章介绍。

              • WebGL程序的shader一般有两个,顶点着色器和片元着色器
              • 着色器是以字符串的形式嵌入到js文件中的

                下面是两个简单的着色器代码,我们创建一个sk_shader.js存放这部分代码:

                顶点着色器:

                var VERTEX_SHADER=`
                attribute vec4 a_Position;
                uniform float u_PointSize;
                void main() {
                  gl_Position = a_Position;
                  gl_PointSize = u_PointSize;
                }`;
                

                片元着色器:

                var FRAG_SHADER=`
                void main() {
                  gl_FragColor = vec4(1.0,0.0,0.0,1.0);
                }`;
                

                初始化着色器

                创建好了着色器以后,下一步就要初始化着色器了

                加载着色器代码

                我们新建一个sk_util.js文件,里面封装一些公共的方法,首先我们要添加的方法就是加载着色器代码的方法:

                // gl: webgl上下文
                // type: 着色器类型
                // source: 着色器代码
                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;
                }
                

                加载一个着色器需要以下步骤:

                • 创建着色器对象:使用gl.createShader接口,参数为着色器类型,着色器类型一般有两类:
                  • gl.VERTEX_SHADER:顶点着色器
                  • gl.FRAGMENT_SHADER:片元着色器
                  • 加载着色器代码:使用gl.shaderSource接口,第一个参数为第一步创建的着色器对象,第二个参数为着色器的字符串表示
                  • 编译着色器代码:使用gl.compileShader接口,参数为第一步创建的着色器对象
                  • 获取着色器编译状态,如果编译失败,获取错误信息

                    创建着色器程序

                    下面是创建着色器程序的代码:

                    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;
                    }
                    

                    创建着色器程序的步骤如下:

                    • 创建着色器程序:使用gl.createProgram接口,返回着色器程序对象
                    • 关联着色器对象:使用gl.attachShader接口,第一个参数是第一步创建的着色器程序对象,第二个参数是关联的着色器对象
                    • 链接着色器程序:使用gl.linkProgram接口链接程序,参数是第一步创建的着色器程序对象
                    • 获取着色器程序链接状态,如果链接失败,获取错误信息

                      使用着色器对象

                      • 使用gl.useProgram接口使用我们上一步创建的着色器程序对象。
                      • 将着色器程序对象赋给gl.program,方便我们后面传递数据的时候使用

                        代码如下:

                        function initShaders(gl, vshader, fshader) {
                            var program = createProgram(gl, vshader, fshader);
                            if (!program) {
                              console.log('Failed to create program');
                              return false;
                            }
                            gl.useProgram(program);
                            gl.program = program;
                            return true;
                        }
                        

                        经过这三步以后,着色器程序已经初始化完成了,接下来就是向着色器传递数据

                        传递数据

                        所谓的传递数据就是将内存的数据传递给显存。

                        一般情况下shader中使用的数据有三种:

                        • 使用attribute标记的数据,这个也叫做顶点数据,就是每个顶点都会有自己的数值
                        • 使用uniform标记的数据,这个也叫做统一数据,就是所有顶点共用的数据
                        • 使用varying标记的数据,这个是在着色器之间传递的数据

                          获取shader的数据标记

                          • 使用gl.getAttribLocation接口获取attribute数据的标记,第一个参数是着色器程序对象,第二个参数是数据在shader中的名称的字符串表示
                          • 使用gl.getUniformLocation接口获取uniform数据的标记,第一个参数是着色器程序对象,第二个参数是数据在shader中的名称的字符串表示

                            下面我们获取前面着色器程序的a_Position和u_PointSize的标记:

                            var u_PointSize = gl.getUniformLocation(gl.program, "u_PointSize");
                            if (u_PointSize < 0) {
                            	console.log('Failed to get the storage location of u_PointSize');
                            	return -1;
                            }
                            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 -1;
                            }
                            

                            从内存传递attribute数据到显存

                            • 创建缓冲区,使用gl.createBuffer()创建缓冲区
                            • 绑定缓冲区,使用gl.bindBuffer接口绑定缓冲区,第一个参数是缓冲区类型,这里我们使用gl.ARRAY_BUFFER,第二个参数是上一步创建的缓冲区
                            • 传递数据,使用gl.bufferData接口传递数据,第一参数是缓冲区类型,这里我们使用gl.ARRAY_BUFFER,第二个参数是内存数据指针,第三个参数是数据的使用方式,这里我们使用gl.STATIC_DRAW。

                              代码如下:

                              var verticesTexCoords = new Float32Array([
                                  -0.5, 0.5, 0.0, 1.0,
                                  -0.5, -0.5, 0.0, 1.0,
                                  0.5, 0.5, 0.0, 1.0,
                                  0.5, -0.5, 0.0, 1.0,    
                                ]);
                              var FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;
                              var vertexTexCoordBuffer = gl.createBuffer();
                              if (!vertexTexCoordBuffer) {
                              	console.log('Failed to create the buffer object');
                              	return -1;
                              }
                              gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
                              gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);
                              

                              传递uniform数据到显存

                              使用gl.uniform1f接口传递单个float数值到显存,在当前程序中用来传递u_PointSize的值

                              代码如下:

                              gl.uniform1f(u_PointSize, 10);
                              

                              绑定缓冲区与attribute顶点属性

                              • 使用gl.vertexAttribPointer接口绑定缓冲区与attribute顶点属性
                              • 使用gl.enableVertexAttribArray接口开启顶点属性数组,如果不开启的话,默认会使用静态属性,就是所有顶点数据都使用一个默认的值。

                                代码如下:

                                gl.vertexAttribPointer(a_Position, 4, gl.FLOAT, false, FSIZE * 4, 0);
                                gl.enableVertexAttribArray(a_Position);
                                

                                绘制

                                当前程序使用gl.drawArrays来开始绘制操作

                                gl.drawArrays的第一个参数是绘制的类型,我们当前程序就使用gl.POINTS方式

                                gl.drawArrays的第二个参数是绘制顶点的偏移,当前为0

                                gl.drawArrays的第三个参数是绘制顶点的数量,当前为4

                                代码如下:

                                gl.clearColor(0.0,0.0,0.0,1.0);
                                gl.clear(gl.COLOR_BUFFER_BIT);
                                gl.drawArrays(gl.POINTS, 0, 4);
                                

                                代码结构调整

                                • 我们把创建WebGL上下文和初始化着色器的方法放进sk_utils.js
                                • 我们把shader字符串放进sk_shader.js
                                • 我们把传递数据的部分放进sk_init.js
                                • main.html中需要添加对这3个js的引用,我们已经添加好了

                                  完整的代码已经上传gitlab:

                                  https://gitlab.com/lingyanTools/sk_webgl.git

                                  该例子绘制了四个顶点