WebGL入门教程第1篇——六色立方

WebGL,一项允许开发人员在浏览器里操纵GPU来显示图形的技术。让我们一起走进WebGL的世界。


读者对象


本系列适合具有基础JavaScript知识的开发人员。


准备工作


我们应该在本地搭建好web服务器,或者安装了具有预览功能的IDE。如果你安装了Visual Studio,Nivk童鞋为我们开发了WebGL代码提示功能,你可以通过以下步骤使Visual Studio支持WebGL代码提示:打开Visual Studio——点击工具——点击选项——展开文本编辑器——展开JavaScript——展开IntelliSense——点击引用——切换引用组为Implicit(Web)——将http://www.nivkgames.com/WebGL-vs-doc.js引用添加到当前组。需要注意的是现在这个引用还有点瑕疵,Nivk童鞋将很快修复它。

此外我们还应该安装了支持WebGL的浏览器,本系列将全部以Chrome为例。

本文的完整示例可以在这里下载(访问密码f6b0)。


本文目标


我们将一边学习WebGL基础知识一边利用所学知识绘制一个六面有着不同颜色的立方体。最后我们将得到一个类似下图的立方体:


WebGL入门教程第1篇——六色立方


理想实验


我们应该如何绘制立方体呢?我们可以在三维空间定义六个顶点,定义顶点间的连接顺序,然后命令GPU按我们给定的点和顺序把图形画出来,并在不同的面涂上不同的颜色。

那我们应该如何定义顶点呢?使用坐标系来定义点已经是常识了。接下来我们来看看如何建立标准的笛卡尔三维坐标系。


WebGL坐标系统


WebGL入门教程第1篇——六色立方


习惯上,我们建立右手坐标系,坐标原点位于画布*,X轴单位长度为原点到画布右边缘的长度,Y轴单位长度类似。我们会发现X轴的单位长度和Y轴的不一致,这个问题将有之后介绍的投影矩阵来解决。先假设坐标各轴的单位长度相同,我们来定义一下立方体的六个顶点。


顶点坐标


WebGL入门教程第1篇——六色立方


如果立方体中心位于坐标原点并假设边长为2,我们可以得到V0=(1.0,1.0,1.0),V1=(-1.0,1.0,1.0),V2=(-1.0,-1.0,1.0),V3=(1.0,-1.0,1.0),V4=(1.0,1.0,-1.0),V5=(-1.0,1.0,-1.0),V6=(-1.0,-1.0,-1.0),V7=(1.0,-1.0,-1.0)。


如何绘制面


因为WebGL只能绘制三角形(还有点和线),所以为了绘制面V0-V1-V2-V3,我们可以通过绘制三角形V0-V1-V2和三角形V0-V2-V3来拼成面V0-V1-V2-V3。其他面类似。


缠绕顺序


对于三角形V0-V1-V2,我们通过连接V0和V1然后连接V1和V2最后连接V2和V0这种逆时针的缠绕顺序来绘制,也可以通过连接V0和V2然后连接V2和V1最后连接V1和V0这种顺时针的缠绕顺序来绘制。习惯上,我们采用逆时针的缠绕顺序。

如果一个三角形正面各顶点的缠绕顺序为逆时针,那么如果我们站在这个三角形的背面就会感觉各顶点的缠绕顺序为顺时针。

WebGL通过三角形的缠绕顺序来判断正反面。如果我们采用逆时针的缠绕顺序,那么一个面各顶点的缠绕顺序为逆时针为正面,反之为背面。背面是看不到的面,我们可以通过激活WebGL的面剔除功能来剔除背面三角形。


绘制顺序


下面我们来看看这个立方体的各个顶点的绘制顺序,需要注意的是绘制的所有三角形都是逆时针的缠绕顺序。

(从现在开始,三角形V0-V1-V2表示是通过连接V0和V1然后连接V1和V2最后连接V2和V0这种逆时针的缠绕顺序绘制的。)

面V0-V1-V2-V3可通过三角形V0-V1-V2和三角形V0-V2-V3拼成。

面V4-V5-V6-V7可通过三角形V4-V6-V5和三角形V4-V7-V6拼成。(这里要注意一下,面V4-V5-V6-V7的正面是朝向Z轴负方向的,这时候逆时针到底是怎么绕的不易判断。但是这个面的背面是朝向Z轴正方向,也就是说我们站在立方体前望向Z轴负方向,刚好看到的是这个面的背面,所以我们按顺时针的顺序来缠绕,就保证这个面的正面的缠绕顺序为逆时针)

面V4-V5-V1-V0可通过三角形V4-V5-V1和三角形V4-V1-V0拼成。

面V7-V6-V2-V3可通过三角形V7-V2-V6和三角形V7-V3-V2拼成。

面V4-V0-V3-V7可通过三角形V4-V0-V3和三角形V4-V3-V7拼成。

面V5-V1-V2-V6可通过三角形V5-V2-V1和三角形V5-V6-V2拼成。


GPU处理方式


CPU 由专为顺序串行处理而优化的几个核心组成。基于CPU遍历一个数组,一般我们处理完第一个元素才能处理第二个。而GPU 由数以千计的更小、更高效的核心组成,可以高效地处理并行任务。基于GPU遍历一个数组,我们可以同时处理所有元素。我们将所有顶点信息存入一个数组,然后传递给GPU处理。

WebGL渲染管线

那GPU接收到数组后是怎么处理的呢?

来看个简化的模型:


WebGL入门教程第1篇——六色立方


顶点数组:包含我们要提交给GPU的顶点信息。

顶点着色器:处理顶点的程序。GPU将并行地在每个顶点上运行顶点着色器。顶点着色器的作用之一是得到顶点的位置信息。

图元装配:经过顶点着色器我们得到了顶点的位置,图元装配阶段将顶点连接成三角形(或连接成线段,或描述为点),然后考察新图形是否位于画布可见的区域内。可见区域内的图形进入下个步骤,其他的删除。

光栅化:经过图元装配我们得到了三角形的外形,光栅化阶段将用像素来填充三角形。经过光栅化,我们得到了由像素描述的三角形,而非由顶点描述的三角形。

片段着色器:处理像素的程序。GPU将并行地在光栅化得到的每个像素上运行片段着色器。片段着色器的作用之一是指定每个像素的颜色。

深度测试:测试像素的前后关系。被其他像素遮挡的像素是无法被看到的,将在测试里被丢弃。

帧缓冲区:到达帧缓冲区的像素将被显示到屏幕上。

其中图元装配,光栅化,深度测试都是自动完成的,我们真正要关心的是顶点着色器和片段着色器。


顶点着色器


我们先来看一段完整的顶点着色器代码:

attribute vec3 aVertexPosition;
attribute vec3 aVertexColor;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying vec4 vColor;

void main()
{
  gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aVertexPosition, 1.0);
  vColor = vec4(aVertexColor, 1.0);
}
首先我们会明显发现的是这不是JavaScript代码。这是专门用于编写OpenGL着色器的GLSL语言。不同于JavaScript,GLSL是一门强类型和编译型的语言。

从第一行开始考察,attribute是存储限定符,vec3是数据类型,aVertexPosition是变量名。接下来四行也是一样的道理。

那么attribute,uniform,varying是什么意思呢?

attribute表示当顶点着色器在每个顶点运行时它修饰的变量每次都不一样。

uniform表示当顶点着色器在每个顶点运行时它修饰的变量每次都一样。

varying表示当顶点着色器在每个顶点运行时它修饰的变量的值最终都需要传递给片段着色器。因为三个顶点可以定义一个三角形,而很多像素才能装配一个三角形,所以varying修饰的变量的值将经过插值后分配给片段着色器。

那vec3,mat4代表什么呢?

vec3表示所修饰的变量的数据类型为三维向量。

mat4表示所修饰的变量的数据类型为4*4矩阵。

那么这些变量将用来做什么呢?

aVertexPosition将用来存放各个顶点的位置坐标。因为顶点坐标只需要三个分量XYZ,所以我们把aVertexPosition定义为vec3类型。因为在顶点着色器运行的时候顶点的位置一般都不一样,所以我们把它定义为attribute类型。

aVertexColor将用来存放各个顶点的颜色信息。因为颜色信息只需要三个分量RGB(假设物体完全不透明),所以和aVertexPosition的情况基本一样。

uModelViewMatrix将用来存放模型视图矩阵,uProjectionMatrix将用来存放投影矩阵。因为对图形进行3D变换需要用到4*4矩阵,所以我们把它们的类型定义为mat4。因为我们一般是对整个3D物体进行变换的,所以对于每个顶点这些矩阵为不变量,所以我们把它们定义为uniform变量。

vColor将用于存放颜色信息。颜色信息从顶点数组途经顶点着色器和插值运算最终传递到片段着色器中。我们要给立方体的每个面定义不同的颜色,这些颜色信息需要传递给片段着色器,要给片段着色器传递这种会变化的值,只能使用varying变量。

接下来我们会看到main函数,使用过C或之类语言的童鞋会很熟悉这是程序的入口。void表示这个函数没有返回值。

下一个映入眼帘的是gl_Position。我们并没有定义过这个变量,它是顶点着色器内置的变量,用于向GPU传递顶点的位置坐标。我们通过vec4(aVertexPosition, 1.0)来得到一个四维向量,1.0表示它的第四维为1.0。为什么需要一个四维向量呢?因为4*4矩阵只能和四维向量点乘。uProjectionMatrix * uModelViewMatrix * vec4(aVertexPosition, 1.0)对原始的顶点坐标进行矩阵变换,然后存入gl_Position。为什么要进行矩阵变换呢?因为我们用三维坐标来定义图形,使用矩阵变换将方便我们对图形进行平移旋转缩放等操作,并最终能把三维的坐标投影到二维的屏幕上去。

接下来的是vColor = vec4(aVertexColor, 1.0),我们通过vec4(aVertexColor, 1.0)来得到一个四维向量,第四维表示透明度,1.0为完全不透明,0.0为完全透明。我们把vec4(aVertexColor, 1.0)的值赋给vColor,值经过插值最终将传递给片段着色器。


片段着色器


precision highp float;
varying vec4 vColor;

void main()
{
  gl_FragColor = vColor;
}
第一行表示片段着色器要采用高精度的浮点值。我们可以把highp换成mediump(中等精度)或lowp(低精度)来改变浮点值的精度。选择什么精度取决于你的需求,精度越高计算越慢越占内存越耗电。

第二行和顶点着色器的第五行一致,vColor的值来自于顶点着色器,但是经过了插值(自动完成)。

gl_FragColor是片段着色器内置的变量,用于向GPU传递每个像素的颜色值。这里我们简单的把vColor的值赋给gl_FragColor。

需要注意的是在片段着色器里是不能定义attribute变量的。


编译链接


GPU不能直接理解GLSL语言,我们需要编译着色器源代码并链接到着色器程序才能供GPU使用。方便的是我们可以在浏览器里调用JavaScript API完成编译链接工作。


身体力行


现在到了实战的时候了。我们边读代码边讲解细节问题。

先看看我们的HTML结构:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>六色立方</title>
    <!--<script src="promise-0.1.1.js"></script>-->
    <script src="gl-matrix.js"></script>
    <script>
        window.onload = function () {
            
        };
    </script>
</head>
<body>
    <canvas id="webgl" width="960" height="540"></canvas>
</body>
</html>
因为JavaScript并不能直接支持矩阵和向量的运算,所以我们引用了gl-matrix库,来帮助我们简化这类运算。gl-matrix的github地址为https://github.com/toji/gl-matrix。

如果你使用Chrome浏览器,注释掉promise-0.1.1.js,这个文件只是为了让其他浏览器完整支持Promise API。(现在是学习Promise API的大好时机,Chrome完整支持,Firefox接近完整支持,微软很早之前就为WinJS引入Promise,相信IE也即将支持。一个不错的Promise API教程在这里


获取3D绘图上下文


var gl = webgl.getContext("webgl");

这个和获取2D绘图上下文的方法差不多,只是参数从2d变成了webgl。


初始化着色器


着色器源代码只是字符串而已,我们定义一个JS变量vertexSource用来存储顶点着色器的源代码,定义一个JS变量fragmentSource用来存储片段着色器的源代码。最简单的方法是我们直接把着色器源代码的字符串赋值给这俩变量。这种方法很好,除了不易读。这里我们采用另外一种方法,通过AJAX加载着色器源代码的文本文件,然后提取出字符串。

function get(url) {
    return new Promise(function (resolve) {
        var xhr = new XMLHttpRequest();
        xhr.onload = function () {
            resolve(this.responseText);
        };
        xhr.open("get", url);
        xhr.send();
    });
}
Promise.all([get("source.vert"), get("source.frag")]).then(function (sources) {
    var vertexSource = sources[0];
    var fragmentSource = sources[1];
});
source.vert和source.frag是我们的两个文本文件,如果你使用Visual Studio,启动调试时会发现这俩文件加载失败了,我们需要让web服务器允许加载vert和frag这两种格式,所以可以把Web.config改成这样:

<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.5.1"/>
    <httpRuntime targetFramework="4.5.1"/>
  </system.web>
  <system.webServer>
    <staticContent>
      <mimeMap fileExtension=".vert" mimeType="text/plain" />
      <mimeMap fileExtension=".frag" mimeType="text/plain" />
    </staticContent >
  </system.webServer>
</configuration>

得到源代码的字符串后,现在我们要来编译它了,对于顶点着色器:

var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexSource);
gl.compileShader(vertexShader);
我们通过创建了一个顶点着色器对象,然后指定它的源代码,最后进行了编译。

片段着色器也是一样的流程:

var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentSource);
gl.compileShader(fragmentShader);
接下来我们要把它们链接到着色器程序:

var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);

我们创建了着色器程序,然后将顶点着色器和片段着色器包含进程序,接着链接,最后通知WebGL我们要使用的着色器程序就是它了。


为顶点着色器传入矩阵


我们的屏幕是二维的,却要它能展示出图形的立体感。就像绘画里的透视法,投影矩阵就是我们的透视法,帮助完成三维到二维的变换。我们真正要关心的是怎么搭建和搭建怎样的一个3D场景,而不用去在意要怎么把场景变换到平面里。gl-matrix为我们完成了所有脏活。

我们知道观察事物的角度不同得出的结论也往往不同。要描述某个3D场景,我们还需要指定观察者的角度位置。

var modelViewMatrix = mat4.create();
mat4.lookAt(modelViewMatrix, [4, 4, 8], [0, 0, 0], [0, 1, 0]);
我们创建了一个4*4单位矩阵,它将作为我们的模型视图矩阵,用于描述物体的变换和观察者的观察方式。因为我们不打算对立方体进行变换操作,所以这里我们只需调用lookAt函数来指定我们的观察者站在(4,4,8)坐标位置,眼睛望向(0,0,0)坐标位置,头顶朝向(0,1,0)坐标位置。需要指出的是这里JS上下文中的mat4是gl-matrix引入的全局变量,不同于着色器源代码里的mat4。

还记得我们在顶点着色器里定义的uModelViewMatrix么?我们已经得到了模型变换矩阵modelViewMatrix,现在需要把它传递给顶点着色器的uModelViewMatrix。WebGL API并没有提供直接给着色器里的变量赋值的方法,所以我们需要先获得变量在内存里的地址,再在相应的内存里写入值。

var uModelViewMatrix = gl.getUniformLocation(program, "uModelViewMatrix");
gl.uniformMatrix4fv(uModelViewMatrix, false, modelViewMatrix);

我们先调用了getUniformLocation获取uniform变量uModelViewMatrix的地址,然后赋值给JS变量uModelViewMatrix。uniformMatrix4fv用于将模型视图矩阵modelViewMatrix写入相应的内存。false参数表示不需要转置(行变列,列变行)这个矩阵,WebGL要求这个参数必须设置为false。

接下来我们来设置投影矩阵。

var projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, Math.PI / 6, webgl.width / webgl.height, 0.1, 100);

我们创建了一个4*4单位矩阵,它将作为我们的投影矩阵。Math.PI / 6表示视角为30°,webgl.width / webgl.height表示视口的宽高比,0.1表示视锥近截面到观察点的距离,100表示视锥远截面到观察点的距离。在近截面和远截面所截的视锥外的图形在图元装配阶段将被舍弃。


WebGL入门教程第1篇——六色立方


图中两红线的夹角为视角,蓝点为观察点,橙色截面为近截面,紫色截面为远截面。

设置完投影矩阵,我们需要把它传递给顶点着色器的uniform变量uProjectionMatrix。

var uProjectionMatrix = gl.getUniformLocation(program, "uProjectionMatrix");
gl.uniformMatrix4fv(uProjectionMatrix, false, projectionMatrix);

这部分和之前传递模型视图矩阵的做法是相同的。


初始化缓冲区


现在开始传递立方体的顶点坐标和颜色值了,还记得顶点着色器里的两个attribute变量aVertexPosition和aVertexColor么?aVertexPosition用来接收顶点坐标,aVertexColor用来接收顶点颜色。

我们将通过绘制两个三角形来合成一个立方体的面,六个面我们需要绘制十二个三角形。

先定义一个数组vertices,用来存放所有的顶点信息。

var vertices = [
    //前
    1.0, 1.0, 1.0, 0.0, 0.8, 0.0,
    -1.0, 1.0, 1.0, 0.0, 0.8, 0.0,
    -1.0, -1.0, 1.0, 0.0, 0.8, 0.0,
    1.0, -1.0, 1.0, 0.0, 0.8, 0.0,
    //后
    1.0, 1.0, -1.0, 0.6, 0.9, 0.0,
    -1.0, 1.0, -1.0, 0.6, 0.9, 0.0,
    -1.0, -1.0, -1.0, 0.6, 0.9, 0.0,
    1.0, -1.0, -1.0, 0.6, 0.9, 0.0,
    //上
    1.0, 1.0, -1.0, 1.0, 1.0, 0.0,
    -1.0, 1.0, -1.0, 1.0, 1.0, 0.0,
    -1.0, 1.0, 1.0, 1.0, 1.0, 0.0,
    1.0, 1.0, 1.0, 1.0, 1.0, 0.0,
    //下
    1.0, -1.0, -1.0, 1.0, 0.5, 0.0,
    -1.0, -1.0, -1.0, 1.0, 0.5, 0.0,
    -1.0, -1.0, 1.0, 1.0, 0.5, 0.0,
    1.0, -1.0, 1.0, 1.0, 0.5, 0.0,
    //右
    1.0, 1.0, -1.0, 0.9, 0.0, 0.2,
    1.0, 1.0, 1.0, 0.9, 0.0, 0.2,
    1.0, -1.0, 1.0, 0.9, 0.0, 0.2,
    1.0, -1.0, -1.0, 0.9, 0.0, 0.2,
    //左
    -1.0, 1.0, -1.0, 0.6, 0.0, 0.6,
    -1.0, 1.0, 1.0, 0.6, 0.0, 0.6,
    -1.0, -1.0, 1.0, 0.6, 0.0, 0.6,
    -1.0, -1.0, -1.0, 0.6, 0.0, 0.6
];
每行表示一个顶点,前三个元素表示该顶点的坐标XYZ,后三个元素表示该顶点的颜色RGB。我们会发现WebGL中很多数值都是介于0.0~1.0(或-1.0~1.0),统一的取值范围便于运算优化。对于立方体的一个面,四个顶点都是相同的颜色,但是四个顶点的位置坐标却各不相同。对于立方体的一个顶点,与它接壤的三个面里这个顶点的位置坐标都相同,但是因为三个面的颜色都不同导致这个顶点的颜色值在三个面里都不相同。所以虽然理想中只需要6个顶点6种颜色,但是实际上我们不得不为每个面定义四个不同的顶点。

WebGL入门教程第1篇——六色立方

图中括号里表示的是三个颜色分量。与其把一个顶点当做一个三态叠加的顶点,不如就把它当做是三个重叠的不同顶点。

vertices只是一个数组,WebGL并不能直接操作JS数组,我们需要把它转换成类型化数组然后载入缓冲区。

var vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
我们创建了一个缓冲区,然后绑定为gl.ARRAY_BUFFER,之后的缓冲区操作都将基于当前绑定的缓冲区。这和2D绘图上下文中的fillStyle是相似的,给fillStyle绑定某种颜色后,之后的填充操作都将使用这种颜色,直到fillStyle再次被改变为止。

bufferData方法用于向当前缓冲区传递数据,这里我们把vertices包装成Float32Array三十二位浮点类型化数组然后传入。gl.STATIC_DRAW表示我们的数据只加载一次,然后在之后绘图中多次使用。

在我们把顶点信息载入缓冲区后,WebGL已可以直接操作它。是时候把顶点信息传入顶点着色器了。

var aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 24, 0);
gl.enableVertexAttribArray(aVertexPosition);

我们通过getAttribLocation获取了aVertexPosition的地址,这一步和我们获取uModelViewMatrix的地址的方法是相似的。aVertexPosition需要接收顶点的位置信息,但是我们的顶点数组包含了位置和颜色两种信息,所以需要调用vertexAttribPointer把位置信息提取出来。实参3表示顶点数组中我们是用3个浮点值表示一个顶点,gl.FLOAT表示我们的数据为浮点类型的值,false表示我们的数据已经是介于-1.0~1.0的gl.FLOAT类型,不需要规范化。24表示的是从同一种类型的元素到下一个同类型元素的跨度,顶点数组的第一行第一个元素是位置坐标,第二行第一个元素也是位置坐标,它们间跨了6个元素,因为我们的顶点数组是用Float32Array类型存储的,所以一个元素占了4字节,6个元素需要跨24字节。0表示每个跨度里我们的位置坐标类型的第一个元素的偏移量,因为我们先排位置信息再排颜色信息,所以它的偏移量为0。

enableVertexAttribArray告诉WebGL我们要使用顶点数组。默认情况下是使用常量顶点数据,但是因为我们的每个顶点都有各自不同的位置信息,所以不能使用常量顶点数据。

传入顶点位置信息后我们再传入顶点颜色信息:

var aVertexColor = gl.getAttribLocation(program, "aVertexColor");
gl.vertexAttribPointer(aVertexColor, 3, gl.FLOAT, false, 24, 12);
gl.enableVertexAttribArray(aVertexColor);
这和之前传入顶点位置信息的操作是很相似的。我们的颜色信息是用0.0~1.0范围的值表示的,如果我们用0~255范围的值来表示颜色信息的话,那么就需要vertexAttribPointer的gl.FLOAT实参改为gl.UNSIGNED_BYTE,false实参修改为true,WebGL内部将自动为我们把颜色信息的值映射到0.0~1.0的范围。因为在一个跨度中前三个是位置信息,所以偏移量设置为3*4字节=12字节。

顶点信息传递完后,现在该传递绘制顺序的信息了。

var indices = [
    0, 1, 2, 0, 2, 3,
    4, 6, 5, 4, 7, 6,
    8, 9, 10, 8, 10, 11,
    12, 14, 13, 12, 15, 14,
    16, 17, 18, 16, 18, 19,
    20, 22, 21, 20, 23, 22
];
我们定义了一个数组indices,元素的值i表示顶点数组vertices所表示的第i个顶点(从零开始计数)。数组indices的每三个元素表示一个三角形的绘制顺序。所以0,1,2表示绘制三角形V0-V1-V2的顺序,0,2,3表示绘制三角形V0-V2-V3的顺序。数组indices的每行表示使用两个三角形合成的一个面。

指定绘制顺序的数组我们称之为索引数组。

接着,我们把索引数组载入缓冲区:

var indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array(indices), gl.STATIC_DRAW);

这和把顶点信息载入缓冲区的步骤是相似的。顶点数组我们绑定为gl.ARRAY_BUFFER,而索引数组则需要绑定为gl.ELEMENT_ARRAY_BUFFER。因为我们的索引数组的元素都是很小的正整数,所以这里把数组indices包装成8位无符号类型化数组。


绘制


到现在,我们所需各自信息都齐全了,只差把它绘制出来。

gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_BYTE, 0);
我们启用了深度测试和面剔除功能。然后我们把清除颜色设置为黑色,接着我们清除了前一次绘图遗留的颜色缓存和深度缓存。虽然这里我们只绘制一帧,不清除也一样,但是每次绘图前清除缓存是个好习惯。

最后我们调用drawElements来绘制图形。drawElements基于WebGL当前绑定的索引数组来绘制图形。第一个参数表示要用什么图元渲染,这里指定为三角形;第二个参数表示要使用多少个索引数组元素来渲染图形,这里我们指定为indices.length表示我们需要所有的索引数组元素来渲染;第三个参数表示索引数组元素的类型,因为我们把数组indices包装成Uint8Array,所以元素的类型指定为gl.UNSIGNED_BYTE,如果我们是用Uint16Array来包装,则这个参数指定为gl.UNSIGNED_SHORT;最后一个参数我们要从索引数组的第几个元素开始渲染,所有元素都需要用来渲染,所以这个参数指定为0。


结束语


经过这些步骤,终于我们得到了一个六色立方体。你可以把示例加载到浏览器中查看,然后试试修改观察者的位置角度或顶点数组的位置颜色,最后刷新浏览器看看图形发生了什么变化。

WebGL更多细节本文并没有详细展开,比如光照纹理等,将在WebGL系列的后续文章继续介绍。

欢迎留言交流。

WebGL入门教程第1篇——六色立方,布布扣,bubuko.com

WebGL入门教程第1篇——六色立方

上一篇:解决异构系统之间交互——Web Service


下一篇:给大家推荐一款高大上的代码高亮插件(sublime,github风格)——highlight.js