第2课 添加颜色
这节课介绍的内容是:
- 如何在顶点数据中包含颜色信息。
- 使用gl.drawArrays()和gl.drawElements()这两个绘图方法绘制gl.TRIANGLE_STRIP和gl.TRIANGLES类型的图元。
- 如何在<script>标记中插入顶点着色器和片段着色器代码,并使用DOM API检索着色器。
最终效果的截图如下:

你可以打开新网页Lesson2.html观看效果。
这节课介绍的内容基于第1课,下面的说明只包含与第1课不同的部分。
创建顶点缓冲
使用drawArrays()方法和TRIANGLE_STRIP图元绘制左侧矩形
setupBuffer()函数的前半部分创建了左边的矩形的顶点坐标缓冲和顶点颜色缓冲:
// 创建左边矩形的顶点坐标缓冲
leftSquareVertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, leftSquareVertexBuffer);
var leftSquareVertices = [
-0.75, 0.25, 0.0, //v0
-0.75, -0.25, 0.0, //v1
-0.25, 0.25, 0.0, //v2
-0.25, -0.25, 0.0, //v3
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(leftSquareVertices), gl.STATIC_DRAW);
leftSquareVertexBuffer.itemSize = 3;
leftSquareVertexBuffer.numberOfItems = 4;
左边的矩形是使用gl.drawArrays()方法进行绘制的,使用的图元类型为三角形带(TRIANGLE_STRIP),因为如果像第1课那样使用三角形列表(TRIANGLES)图元类型的话,画一个矩形需要定义6个顶点数据,额外多出2个。
有关图元类型的介绍可参见http://msdn.microsoft.com/en-us/library/bb147291(v=vs.85).aspx,虽然是DirectX,但原理是相同的,有关使用三角形带的优点可参见使用Triangle Strips提高性能,虽然是XNA代码,但原理是相同的。
gl.drawArrays()方法根据启用的WebGLBuffer对象中的顶点数据,绘制由第1个参数定义的图元。启用的webGLBuffer对象绑定到gl.ARRAY_BUFFER目标上。这意味着在调用gl.drawArrays()之前,必须执行以下操作:
- 用gl.createBuffer()建立一个webGLBuffer对象。
- 用gl.bindBuffer()方法把WebGLBuffer对象绑定到gl.ARRAY_BUFFER目标。
- 用gl.bufferData()方法把顶点数据载入到缓冲中。
- 用gl.enableVertexAttribArray()方法激活通用顶点属性。
- 调用gl.vertexAttribPointer()方法把顶点着色器的属性连接到webGLBuffer对象中的正确数据。
gl.drawArrays()http://msdn.microsoft.com/en-us/library/ie/dn302395(v=vs.85).aspx方法的原型如下:
void drawArrays(GLenum mode,Glint first,GLsizei count);
下面介绍这些参数的意义:
- mode定义了所要渲染的图元的类型。它可以取gl.POINTS、gl.LINES、gl.LINE_LOOP、 gl.LINE_STRIP、gl.TRIANGLES 、gl.TRIANGLE_STRIP、gl.TRIANGLE_FAN
- first参数定义顶点数据数组中的哪个索引用作第一个索引。
- count定义了需耍使用的顶点数。
归纳起来,这个方法的mode参数定义了要绘制的图元类型,count参数定义连续顶点的个数,用first参数定义了第一个顶点在数组中的索引位置。
gl.drawArrays()方法的设计要求表示图元的顶点必须按正确的顺序进行绘制。此矩形顶点的绘制顺序为V0 V1 V2 V3,TRIANGLE_STRIP绘制的第一个三角形应该是逆时针绕行的(这是因为OpenGL为右手坐标系,因此顺时针绕行的三角形会因背面剔除而不显示),下一个三角形的前两个顶点即上一个三角形的后两个顶点,依此原理,如果以V3V2V1V0的顺序绘制也能正确显示。顶点的位置如下图所示:

在第1课中,顶点数据只包含顶点位置信息。然而,在实际的webGL应用程序中,顶点数据通常还包含更多的信息。除了顶点位置信息,顶点数据还包括顶点法线、顶点颜色和纹理坐标。当顶点包含多种数据时,可以采用以下两种方法组织这些数据:
- 把每类顶点数据保存在WebGLBuffer对象的单独数组中,这意味着,除了顶点位置数组外,可能还有其他数组,如法线数组、颜色数组等。这通常称为数组结构。
- 把所有类型的数据都保存在WebGLBuffer对象的一个数组中。这意味着,需要把不同类型的数据交叉保存在同一个数组中。这通常称为结构数组。
一般来说,第一个方法(数组结构)是建立缓冲并把数据载入缓冲的最简单方法。每类顶点数据都有自己的顶点数组。下图是当顶点数据包含了位置信息(用坐标x、y、z)和颜色信息(RGBA格式,具有4个颜色分量且每个分量的类型为无符号字节)时的数组结构。

本教程使用的是第一种方法,第二种方法会在下一个教程中介绍。代码如下:
// 创建矩形的顶点颜色缓冲,右边的矩形共用这个颜色缓冲
squareVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
var squareColors = [
1.0, 0.0, 0.0, 1.0, //v0
0.0, 1.0, 0.0, 1.0, //v1
0.0, 0.0, 1.0, 1.0, //v2
1.0, 1.0, 0.0, 1.0 //v3
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(squareColors), gl.STATIC_DRAW);
squareVertexColorBuffer.itemSize = 4;
squareVertexColorBuffer.numberOfItems = 4;
在draw()方法中我们使用如下代码绘制左边的矩形:
// 绑定包含左边矩形顶点位置的缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, leftSquareVertexBuffer);
// 指定顶点位置在顶点数组的组织形式
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,
leftSquareVertexBuffer.itemSize, gl.FLOAT, false, 0, 0);
// 绑定矩形顶点颜色的缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
// 指定顶点颜色在顶点数组的组织形式
gl.vertexAttribPointer(shaderProgram.vertexColorAttribute,
squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
// 绘制左边的矩形
gl.drawArrays(gl.TRIANGLE_STRIP, 0, leftSquareVertexBuffer.numberOfItems);
使用drawElements()方法和TRIANGLES图元绘制右侧矩形
前面已经提到:如果使用gl.drawArrays方法和TRIANGLES图元绘制右侧的矩形,则需要定义6个顶点,有2个是重复的。这里我们可以使用gl.drawElements()方法和TRIANGLES图元,这样只需定义4个顶点即可,但需要额外定义一个元素数组缓冲。
drawArrays()和drawElements()的概念对应的就是DirectX中的Draw()和DrawIndexed()方法,因此drawElements()有时也被称为索引绘图,元素数组缓冲又被称为索引缓冲。在XNA中的相同概念可参见5.3 使用索引移除冗余顶点和5.4 使用顶点缓冲和索引缓冲将顶点和索引保存在显存中。基于以上说明,setup()函数中建立右侧矩形缓冲的代码如下:
// 创建右边矩形的顶点坐标缓冲
rightSquareVertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, rightSquareVertexBuffer);
var rightSquareVertices = [
0.25, 0.25, 0.0, //v0
0.25, -0.25, 0.0, //v1
0.75, 0.25, 0.0, //v2
0.75, -0.25, 0.0, //v3
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(rightSquareVertices), gl.STATIC_DRAW);
rightSquareVertexBuffer.itemSize = 3;
rightSquareVertexBuffer.numberOfItems = 4;
// 创建右边矩形的顶点索引缓冲
rightSquareElementBuffer=gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, rightSquareElementBuffer);
var indices = [0, 1, 2, 2, 1, 3];
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
rightSquareElementBuffer.numberOfItems = 6;
你可以参见上面的矩形图像理解索引的排列顺序,只要两个三角形是逆时针绕行的即可,因此按[2,0,1, 3, 2, 1]的顺序也能正常显示。
gl.drawElements()http://msdn.microsoft.com/en-us/library/ie/dn302396(v=vs.85).aspx方法的原型如下:
void drawElements (Glenum mode, GLsizei count, GLenum type,Glintptr offset)
该方法的各个参数意义如下:
- mode定义了要渲染的图元的类型。
- count定义了绑定到gl.ELEMENT_ARRAY_BUFFER目标上的缓冲中的索引数。
- type定义了元素索引的类型,元素索引存储在绑定到gl.ELEMENT_ARRAY_BUFFER目标上的缓冲中。可以指定的类型为gl.UNSIGNED_BYTE或者gI.UNSIGNED_SHORT。
- offset定义绑定到gl.ELEMENT_ARRAY_BUFFER目标的缓冲中的偏移量,索引从此处开始。
在draw()方法中我们使用如下代码绘制右边的矩形:
// 绑定包含右边矩形顶点位置的缓冲
gl.bindBuffer(gl.ARRAY_BUFFER,rightSquareVertexBuffer);
// 指定顶点位置在顶点数组的组织形式
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,
rightSquareVertexBuffer.itemSize, gl.FLOAT, false, 0, 0);
// 由于右边矩形共用顶点颜色的缓冲,所以这步可以省略
// gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
// gl.vertexAttribPointer(shaderProgram.vertexColorAttribute,
//squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
// 绑定右边矩形的索引缓冲
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, rightSquareElementBuffer);
// 绘制右边的矩形
gl.drawElements(gl.TRIANGLES, rightSquareElementBuffer.numberOfItems, gl.UNSIGNED_SHORT, 0);
加载Shader
在第1课中,顶点着色器和片段着色器都是以字符串的形式插入到JavaScript代码中,并用“+”运算符把字符串合并成着色器代码,如下所示:
var vertexShaderSource =
"attribute vec3 aVertexPosition; \n" +
"void main() { \n" +
" gl_Position = vec4(aVertexPosition, 1.0); \n" +
"} \n";
var fragmentShaderSource =
"precision mediump float; \n" +
"void main() { \n" +
" gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); \n" +
"} \n";
在这节课中,我们使用一个更简单的方法。下面是相同的顶点着色器代码,但是它插入<script>标记中,但并没有使用JavaScript的“+”运算符合并字符串。
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;
varying vec4 vColor;
void main() {
vColor = aVertexColor;
gl_Position = vec4(aVertexPosition, 1.0);
}
</script>
<script id="shader-fs" type="x-shader/x-fragment">
precision mediump float;
varying vec4 vColor;
void main() {
gl_FragColor = vColor;
}
</script>
对于这个小示例,这两种方法可能没有多大区别。但是,当我们遇到一个源代码很长的大型着色器时,则使用第二种方法会更加简洁,而且可读性会更好。
此外,由于WebGL的着色器是用OpenGL ES着色语言编写的,因此Internet上和很多文献中有很多的着色器示例都是用OpenGL ES2.0编写的,它们也可以用于webGL。也有一些着色器开发工具,如AMD公司的RenderMonkey,它可以输出OpenGL ES着色语言。如果使用第二个方法,则很容易在自己的webGL应用程序中,在<script>示记之间插入这些着色器。因此,在以后的课程中,我都将使用这种方法加载shader代码。
当在<script>标记之间包含着色器源代码而不是把它赋给一个JavaScript变量时,需要用一种方法检索它,并把它合并到一个Javascript字符串中,然后通过webGL API传入,代码如下:
function loadShaderFromDOM(id) {
var shaderScript = document.getElementById(id);
// 若未找到指定id的元素则退出
if (!shaderScript) {
return null;
}
// 遍历DOM元素的子节点,将shader代码重新构建为一个字符串
var shaderSource = "";
var currentChild = shaderScript.firstChild;
while (currentChild) {
if (currentChild.nodeType == 3) { // 3对应TEXT_NODE
shaderSource += currentChild.textContent;
}
currentChild = currentChild.nextSibling;
}
var shader;
if (shaderScript.type == "x-shader/x-fragment") {
shader = gl.createShader(gl.FRAGMENT_SHADER);
} else if (shaderScript.type == "x-shader/x-vertex") {
shader = gl.createShader(gl.VERTEX_SHADER);
} else {
return null;
}
gl.shaderSource(shader, shaderSource);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
函数loadshaderFromDOM()需要接收一个id属性参数,它利用这个id属性在DOM树中找到相应的元素。例如,如果我们给插入着色器源代码的<script>标记设置id为shader-vs,则需要把shader-vs作为参数传给loadshaderFromDOM()函数。
loadshaderFromDOM()使用DOM API函数并根据传入的id找到相应的元素,然后遍历这个元素的全部子元素,生成一个表示源代码的文本字符串。
最后,这个函数检查所找到元素的类型属性,并根据类型创建一个顶点着色器或片段着色器。它把着色器的源代码载入创建的着色器对象中,并对它进行编译。如果编译成功,把这个着色器对象返回给调用这个函数的程序。
WebGL渲染管线
以下内容来自于Lesson2添加颜色http://www.hiwebgl.com/?p=133。
在shader代码中还有一个与第1课不同地方:多了“varying vec4 vColor;”。为了理解这些改变的代码的原理,你需要先了解一下WebGL的渲染管线。这里有个流程图:

这个流程图用简单的形式表现出数据从Javascript的draw()函数中转换为像素显示在WebGL Canvas中的流程。这个流程图只给出了我们这节课需要用到的步骤,我们在以后的课程中还会为这个流程图添加更多细节。
从最高层开始,处理的过程是这样的:每次你调用类似于drawArrays的函数时,WebGL会处理你之前传递给它的数据,这些数据都是以属性(Attribute)(比如第1课中用到的顶点位置数组)和Uniform变量(我们用它来储存模型视图矩阵和投影矩阵,会在下一节课涉及)的形式存在的,然后WebGL会把这些数据传递给顶点着色器。每次当相应顶点的属性建立完成后,都会调用一次顶点着色器;而Uniform变量,就像它的名字一样,在调用过程中并不发生任何改变,顶点着色器只是需要这些数据。然后顶点着色器把处理的结果储存在称为“Varying变量”(Varying Variable)的变量中。顶点着色器通常会输出一系列的Varying变量,其中有个特别的也是必须的变量,那就是gl_Position,它储存着经过顶点着色器处理过的顶点坐标。
在顶点着色器处理完成之后,WebGL将这些Varying变量中描述的3D图形转换为2D图片,然后为图片中的每个像素调用片元着色器(这就是为什么在有些3D图形系统中你会听到他们把片元着色器称为“像素着色器”的原因了)。当然,确切的说是为那些非顶点位置的像素调用片元着色器,而在那些顶点位置上的像素则已经建立好了顶点。这个过程“填充”了各顶点间限定的空间,从而显示出一个可见的形状。片元着色器的作用是返回每个内插点的颜色,并储存在称为gl_FragColor的Varying变量中。
当片元处理器工作完毕后,WebGL会再处理一下它输出的结果(我们在以后的课程中会讲到这个“再处理一下”的详细内容),然后放到Frame Buffer(帧缓冲)中,也就是最终显示在屏幕上的东西。
我们可以从顶点着色器中输出包括位置信息在内的一系列Varying变量,然后在片元着色器中重新获得他们。所以,我们要把颜色信息传递给顶点着色器,然后再直接输出为Varying变量,使片元着色器可以获取到它们。
这种方式下我们可以很容易填充渐变色。因为所有顶点着色器输出的Varying变量都在顶点间产生片元的时候被进行了线性插值,注意是所有Varying变量而不仅仅是位置信息。顶点间颜色信息的线性插值可以让我们制作平滑的渐变。
完整js代码
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;
varying vec4 vColor;
void main() {
vColor = aVertexColor;
gl_Position = vec4(aVertexPosition, 1.0);
}
</script>
<script id="shader-fs" type="x-shader/x-fragment">
precision mediump float;
varying vec4 vColor;
void main() {
gl_FragColor = vColor;
}
</script>
<script type="text/javascript">
var gl;
var canvas;
var shaderProgram;
var leftSquareVertexBuffer;
var rightSquareVertexBuffer;
var rightSquareElementBuffer;
var squareVertexColorBuffer;
function createGLContext(canvas) {
var names = ["webgl", "experimental-webgl"];
var context = null;
for (var i = 0; i < names.length; i++) {
try {
context = canvas.getContext(names[i]);
} catch (e) { }
if (context) {
break;
}
}
if (context) {
context.viewportWidth = canvas.width;
context.viewportHeight = canvas.height;
} else {
alert("无法创建WebGL上下文!");
}
return context;
}
function loadShaderFromDOM(id) {
var shaderScript = document.getElementById(id);
// 若未找到指定id的元素则退出
if (!shaderScript) {
return null;
}
// 遍历DOM元素的子节点,将shader代码重新构建为一个字符串
var shaderSource = "";
var currentChild = shaderScript.firstChild;
while (currentChild) {
if (currentChild.nodeType == 3) { // 3对应TEXT_NODE
shaderSource += currentChild.textContent;
}
currentChild = currentChild.nextSibling;
}
var shader;
if (shaderScript.type == "x-shader/x-fragment") {
shader = gl.createShader(gl.FRAGMENT_SHADER);
} else if (shaderScript.type == "x-shader/x-vertex") {
shader = gl.createShader(gl.VERTEX_SHADER);
} else {
return null;
}
gl.shaderSource(shader, shaderSource);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
function setupShaders() {
vertexShader = loadShaderFromDOM("shader-vs");
fragmentShader = loadShaderFromDOM("shader-fs");
shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert("创建shader失败");
}
gl.useProgram(shaderProgram);
shaderProgram.vertexPositionAttribute =
gl.getAttribLocation(shaderProgram, "aVertexPosition");
shaderProgram.vertexColorAttribute =
gl.getAttribLocation(shaderProgram, "aVertexColor");
// 激活顶点着色器中的位置和颜色属性
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute);
}
function setupBuffers() {
// 创建左边矩形的顶点坐标缓冲
leftSquareVertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, leftSquareVertexBuffer);
var leftSquareVertices = [
-0.75, 0.25, 0.0, //v0
-0.75, -0.25, 0.0, //v1
-0.25, 0.25, 0.0, //v2
-0.25, -0.25, 0.0, //v3
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(leftSquareVertices), gl.STATIC_DRAW);
leftSquareVertexBuffer.itemSize = 3;
leftSquareVertexBuffer.numberOfItems = 4;
// 创建矩形的顶点颜色缓冲,右边的矩形共用这个颜色缓冲
squareVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
var squareColors = [
1.0, 0.0, 0.0, 1.0, //v0
0.0, 1.0, 0.0, 1.0, //v1
0.0, 0.0, 1.0, 1.0, //v2
1.0, 1.0, 0.0, 1.0 //v3
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(squareColors), gl.STATIC_DRAW);
squareVertexColorBuffer.itemSize = 4;
squareVertexColorBuffer.numberOfItems = 4;
// 创建右边矩形的顶点坐标缓冲
rightSquareVertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, rightSquareVertexBuffer);
var rightSquareVertices = [
0.25, 0.25, 0.0, //v0
0.25, -0.25, 0.0, //v1
0.75, 0.25, 0.0, //v2
0.75, -0.25, 0.0, //v3
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(rightSquareVertices), gl.STATIC_DRAW);
rightSquareVertexBuffer.itemSize = 3;
rightSquareVertexBuffer.numberOfItems = 4;
// 创建右边矩形的顶点索引缓冲
rightSquareElementBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, rightSquareElementBuffer);
var indices = [0, 1, 2, 2, 1, 3];
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
rightSquareElementBuffer.numberOfItems = 6;
}
function draw() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.clear(gl.COLOR_BUFFER_BIT);
// 绑定包含左边矩形顶点位置的缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, leftSquareVertexBuffer);
// 指定顶点位置在顶点数组的组织形式
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,
leftSquareVertexBuffer.itemSize, gl.FLOAT, false, 0, 0);
// 绑定矩形顶点颜色的缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
// 指定顶点颜色在顶点数组的组织形式
gl.vertexAttribPointer(shaderProgram.vertexColorAttribute,
squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
// 绘制左边的矩形
gl.drawArrays(gl.TRIANGLE_STRIP, 0, leftSquareVertexBuffer.numberOfItems);
// 绑定包含右边矩形顶点位置的缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, rightSquareVertexBuffer);
// 指定顶点位置在顶点数组的组织形式
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,
rightSquareVertexBuffer.itemSize, gl.FLOAT, false, 0, 0);
// 由于右边矩形共用顶点颜色的缓冲,所以这步可以省略
// gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
// gl.vertexAttribPointer(shaderProgram.vertexColorAttribute,
//squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
// 绑定右边矩形的索引缓冲
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, rightSquareElementBuffer);
// 绘制右边的矩形
gl.drawElements(gl.TRIANGLES, rightSquareElementBuffer.numberOfItems, gl.UNSIGNED_SHORT, 0);
}
function startup() {
canvas = document.getElementById("Lesson2");
gl = createGLContext(canvas);
setupBuffers();
setupShaders();
gl.clearColor(0.0, 0.125, 0.3, 1.0);
draw();
}
</script>
文件下载(已下载 2497 次)
发布时间:2013/10/5 下午10:30:16 阅读次数:8548
