欢迎来到我的第二课WebGL教程!这个时间我们来学习怎样向场景里面添加颜色。它是以Nehe的OpenGL教程第三课为基础的。
这儿是这节课的代码在支持WebGL的浏览器运行看起来的效果:
一个小的忠告:这些教程所针对的人群是具有一定的编程知识,但是没有真正的3D图形编程经验;目的是使你入门并且知道这些代码是怎样运行的,以至于你能够尽快开始制作自己的3D网页。如果你还没有学习第一课,在你学习这一课之前最好去学习一下第一课——这里我将只解释这节课与上节课不同的代码。
依然,在这节教程中可能有bugs和错误的地方。如果你指出了一些错误,请在评论里让我知道,我将会尽快更正它。
有两种方法你能够得到这个示例的代码:当你看现场演示的时候,查看源代码或是如果你使用GitHub,你能够复制它(还有以后的教程)从那个代码库。两者选一,一旦你有了代码,加载到你最喜欢的文本编辑器里看一看。
大部分代码和第一课是非常相似的。从头至尾,我们:
定义顶点渲染器和片段渲染器,使用含有"x-shader/x-vertex"和"x-shader/x-fragment"的<script>标签;
通过initGL函数初始化WebGL文本;
使用getShader和initShaders函数将渲染器导入到WebGL program中;
定义模型视图矩阵mvMatrix函数,并通过实用函数调用loadIdentity、multMatrix、mvTranslate函数来操纵它;
定义投影矩阵pMatrix函数,用一个透视实用函数perspective来操纵它;
定义setMatrixUniforms函数来将模型视图和投影矩阵推至JavaScript/WebGL设备上以至于渲染器能够识别;
使用initBuffers函数加载包含场景对象信息的缓冲;
使用drawScene函数绘制场景;
定义函数webGLStart设置好一切;
最后,我们提供要求展示图形的最少的HTML代码。
与第一课代码改变的地方是渲染器、initBuffers函数和drawScene函数。为了解释它们运行的改变,你需要知道一点关于WebGL的渲染管道。这儿是原理图:
这个简易图显示,在drawScene函数中传入JavaScript函数的数据是怎样转变为在屏幕中WebGL canvas里显示的像素的。它只是显示了在这节课中需要解释的步骤;我们将会在以后的教程中关注更加详细的信息。
在最高的层次上,工作过程是这样的:你每一次调用像drawArray这样的函数,WebGL就会处理你之前以属性的形式(像在第一课中设置的顶点缓存)和以一致变量的形式(我们用来组建投影和模型视图矩阵)给它的数据,并且将这些数据传到顶点渲染器。
在这个过程中,为每一个顶点调用顶点渲染器,为每一个顶点设置合适的属性,一致变量也被传入,但是正如它的名字所表示的,一致变量被调用的过程中它们没有改变。顶点渲染器用这些数据来填充图形——在第一课,顶点渲染器使用投影和模型视图矩阵来使得这些顶点都能形成透视的效果,使得这些顶点根据我们的模型视图状态来移动——并且将这些数据转化为易变变量。顶点渲染器能够输出许多种类型的易变变量;有一种特殊的类型是必须输出的,gl_positon,它包含顶点的坐标当渲染器处理完后。
当顶点渲染器完成任务后,WebGL要求将易变变量所代表的3D图像变为2D图像,然后WebGL为图像里的每一个像素调用一次片段渲染器(在3D图形系统里,你也能听到将片段渲染器称为像素渲染器,就是这个原因)。当然,这也意味着在图像里面不含有顶点的像素也会被片段渲染器处理。因此,WebGL在填充这样的像素时采用了一种方法,叫做线性插值——对于组成三角形顶点的位置,这个过程是填充被顶点划定的空间,这样形成了一个可见的三角形。片段渲染器的目的是返回每一个插值点的颜色,并且是在叫gl_FragColor的易变变量中完成的。
一旦片段渲染器完成了这些,它的结果被WebGL处理了(我们将会在以后的课程中讨论)并且它们被导入到帧缓存(frame buffer)里面,帧缓存就是最终在屏幕上显示的东西。
如果顺利的话,到现在为止这节课所教的怎样让顶点获得颜色从JavaScript代码一直到片段渲染器的重要技巧是非常清楚了,尽管现在还没有分析具体的代码。
我们使用的方法是利用这个事实:我们能够在顶点渲染器外面传入一些易变变量,不仅仅是顶点位置,然后再从片段渲染器中取回它们。因此,我们将颜色传入顶点渲染器,然后直接转化为片段渲染器可拾取的易变变量。
很方便,我们可以直接得到颜色的线性变化。顶点渲染器计算顶点之间的片段时,所有的易变变量按线性插值的方法计算,不仅仅是位置。两点之间的颜色线性插值是一个平滑的变化效果,就像你看到的上面那张图片里的三角形的效果。
让我们来看看代码;我们来看看与第一课不同的地方。首先,是顶点渲染器。它改变了很多,这儿是新代码:
attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
varying vec4 vColor;
void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
vColor = aVertexColor;
}
这些代码的意思是说我们有两个属性——不同顶点的输入值——分别叫做aVertexPosition和aVertexColor,有两个一致变量叫做uMVMatrix和uPMatrix,还有一个以易变变量的形式的输出值叫做vColor。
在渲染器的主体部分,我们计算gl_Position(被定义为每一个顶点渲染器的易变变量)实际上和第一课的方法相同,我们所做的是将颜色从输入的属性值转化为输出的易变变量。
一旦每一个顶点都被这样运行出来,插值用来运算之间的片段,这些都被传入到片段渲染器:
#ifdef GL_ES
precision highp float;
#endif
varying vec4 vColor;
void main(void) {
gl_FragColor = vColor;
}
这里,在设置浮点精确度的模板代码之后,我们获取包含经过线性插值运算后的平滑混合颜色数据的易变变量vColor。
这是这节课与上节课在渲染器里所有的不同。还有两个其它的不同。第一个非常小;在initShaders函数中我们获得两个属性的参数而不是一个;额外的一行代码用红色标记着:
var shaderProgram;
function initShaders() {
var fragmentShader = getShader(gl, "shader-fs");
var vertexShader = getShader(gl, "shader-vs");
shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert("Could not initialise shaders");
}
gl.useProgram(shaderProgram);
shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute);
shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}
这个获得属性的代码,在上一课中在某种程度上我已经解释过,现在应该是十分清楚了:这是我们获得想要传入顶点渲染器为每个顶点设置属性的参数的办法。在第一课,我们只是获得顶点位置属性。现在,足够明显,我们也得到了颜色属性。
这一课剩下的改变是initBuffers函数,现在需要为顶点位置和顶点颜色设置缓存了,还有drawScene函数,需要把它们两个都传入WebGL。
首先来看看initBuffers函数,我们定义新的全局变量来为三角形和四边形存储颜色缓存:
var triangleVertexPositionBuffer;
var triangleVertexColorBuffer;
var squareVertexPositionBuffer;
var squareVertexColorBuffer;
然后,我们创建三角形顶点位置缓存之后,我们指明它的顶点颜色:
function initBuffers() {
triangleVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
var vertices = [
0.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
triangleVertexPositionBuffer.itemSize = 3;
triangleVertexPositionBuffer.numItems = 3;
triangleVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
var colors = [
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
triangleVertexColorBuffer.itemSize = 4;
triangleVertexColorBuffer.numItems = 3;
我们提供的颜色数据是一个数列,每一个值对应一个点,就像位置。然而,在这两个数组缓存中有一个有趣的不同:一个顶点的位置可以被三个数指明,X、Y和Z坐标,而每个顶点的颜色被四个元素所指明——红、绿、蓝和alpha值。Alpha,如果你对它不熟悉,是不透明度的一个度量值(0是透明的,1完全不透明),在后面的教程中是非常有用的。缓存中每项元素的改变导致需要改变相应的itemSize。
下一步,我们为四边形写同样的代码;此时,我们给每一面设置相同的颜色,所以我们用循环来生成缓存数据:
squareVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
vertices = [
1.0, 1.0, 0.0,
-1.0, 1.0, 0.0,
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
squareVertexPositionBuffer.itemSize = 3;
squareVertexPositionBuffer.numItems = 4;
squareVertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
colors = []
for (var i=0; i < 4; i++) {
colors = colors.concat([0.5, 0.5, 1.0, 1.0]);
}
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
squareVertexColorBuffer.itemSize = 4;
squareVertexColorBuffer.numItems = 4;
现在我们有了图形的所有的数据在四个缓存中,下一个不同的地方是使得drawScene函数使用新的数据。新的代码仍然用红色标识,应该很容易理解:
function drawScene() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0);
loadIdentity();
mvTranslate([-1.5, 0.0, -7.0])
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, triangleVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
setMatrixUniforms();
gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
mvTranslate([3.0, 0.0, 0.0])
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);
setMatrixUniforms();
gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);
}
下一个不同的地方……等一下,没有下一个不同的地方!这就是在我们的WebGL场景里添加颜色所必须有的,如果顺利的话,你现在轻松的掌握了渲染器的基础知识和它们之间数据的传输方法。
这就是这一课所有的内容——它比上一课要容易!如果你有任何的问题,评论,或者改正,请给我留言。
下一课,我们将在屏幕中添加点运动,旋转三角形和四边形。