在之前的文章中,我想大家已经对WebGL有了一个大体的了解,不过为了凑字数,我在这篇文章的开头再稍微回顾一下,如果我们需要使用WebGL来绘制图像需要走完以下这五步:
1、从canvas元素中获取webgl context
2、利用GLSL ES语言,编写顶点着色器和片元着色器,并成对应的着色器程序
3、准备好你想要绘制的图像的顶点数据,并写入缓冲区
4、把着色器中的变量与载有顶点数据的缓冲区对应起来
5、最后执行着色器程序,并在canvas上绘制出图形
当然,并不是说所有的WebGL程序都必须按这样的逻辑进行,这里只是让大家对WebGL有一个基本的概念,而那些项目中使用到的真正的WebGL程序要比这复杂得多。
为了使得我们能集中精力去编写那些酷炫的WebGL程序,我把上面这些基本的步骤封装在几个工具类中,大家只要在页面里引入附件中的gl-core-min.js即可。在gl-core文件中,第一个提供给大家使用的是Program类,用于创建和管理着色器程序,具体使用如下:
<div class="km_insert_code" style="text-align: left;">
var program = new Program(gl);
// 载入顶点着色器
program.attach(VERTEX_SHADER_SOURCE, gl.VERTEX_SHADER);
// 载入片元着色器
program.attach(FRAGMENT_SHADER_SOURCE, gl.FRAGMENT_SHADER);
program.link();
program.bind();
</div>
第二个提供给大家使用的是Buffer类,用于创建和管理数据缓冲区,具体使用如下:
<div class="km_insert_code" style="text-align: left;">
// 创建顶点数据缓冲区
var vertexBuffer = new Buffer(gl, gl.ARRAY_BUFFER);
// 向缓存区写入数据
vertexBuffer.write(modelObject.vertex, gl.STATIC_DRAW);
// 是缓冲区中的数据和着色器中的a_VertexPosition变量对应起来
vertexBuffer.bind(programAttribs.a_VertexPosition, 3, gl.FLOAT);
</div>
除此以外,由于三维图形绘制还会涉及到大量的矩阵或向量运算,我们需要一些数学工具辅助我们的开发,这里给大家介绍一个库——gl-matrix(http://glmatrix.net/)。同样,大家只要在页面里引入附件中的gl-matrix-min.js即可。这里给大家演示一下gl-matrix的基本用法,详细的可以参考官网上的文档:
<div class="km_insert_code" style="text-align: left;">
// 创建4x4矩阵
var viewMatrix = mat4.create();
// 对矩阵进行转置
mat4.transpose(viewMatrix, viewMatrix);
// 对矩阵进行求反
mat4.invert(viewMatrix, viewMatrix);
// 创建1x3向量
var positionVector = vec3.create();
// 对向量进行规范化
vec3.normalize(positionVector, positionVector);
</div>
有了这些工具,我们就可以开始讲解今天的主题了,使用WebGL绘制三维图形。
在之前的例子中,我只是给大家演示了如何绘制一个二维的矩形,但WebGL真正强大的地方,在于它为我们提供了三维图像的绘制能力。当然这主要的得益于WebGL的计算速度,要知道,绘制三维图形,我们需要进行大量的(逐顶点甚至是逐片元)的矩阵运算,而且这些运算都必须在16ms内完成,才能保证画面的流畅。如果是直接使用Canvas 2D Api绘制三维图像,所有这些运算,只能在CPU中完成。而通过WebGL,这些耗时的运算就可以直接交给GPU,通过GPU中一些专用的硬件,使得运算的过程得到优化(管线,并行)。
说了那么多,那到底我们怎样才能绘制出一个三维图形呢?要绘制出三维图像,我们首先要知道三维图像和二维图像有那些区别。
从上面这幅图,我们可以比较直观的看到一个正方形到立方体的演变过程,主要经历了以下四步:
1、给图像加入深度信息,也就是让这个正方形有了厚度,从一个“面”成为一个“体”
2、换一个角度去观察我们的场景(45度俯瞰),这使得深度信息在视觉上得到了体现
3、光有深度信息还不够,我们观察三维图像一般是带透视的,这样图像更有立体感
4、最后我们需要在场景中加入灯光,灯光下的立方体更加真实
这样我们就有了一个大致的方向,为了绘制三维图形,我们需要一下四部分信息:
<div class="km_insert_code" style="text-align: left;">
// 获取立方体的顶点数据,包含深度信息(即z轴坐标)
var modelObject = getCubeVertexData();
// 获取视图变换矩阵,用来模拟摄像机在不同角度的拍摄
var viewMatrix = getCameraMatrix();
// 获取透视变换矩阵,用来模拟现实中近大远小的透视效果
var projectMatrix = getPerspectiveMatrix();
// 获取灯光数据,这里得到的是一个点光源,类似白炽灯
var pointLight = getPositionalLight();
</div>
接下来,我就带着大家一步一步来完成各个方法的功能开发,首先是getCubeVertexData方法:
<div class="km_insert_code" style="text-align: left;">
function getCubeVertexData() {
var width = 4,
height = 4,
depth = 4;
var w = width / 2,
h = height / 2,
d = depth / 2;
// 顶点数据,三个坐标(x, y, z)组成一个顶点
// 每个面有4个点,总共是4x6,24个顶点坐标
var vertices = new Float32Array([
-w, h, d, w, h, d, -w,-h, d, w,-h, d, // 0-1-2-3 // front
w, h,-d, -w, h,-d, w,-h,-d, -w,-h,-d, // 4-5-6-7 // back
-w, h,-d, -w, h, d, -w,-h,-d, -w,-h, d, // 8-9-10-11 // left
w, h, d, w, h,-d, w,-h, d, w,-h,-d, // 12-13-14-15 // right
-w, h,-d, w, h,-d, -w, h, d, w, h, d, // 16-17-18-19 // top
-w,-h, d, w,-h, d, -w,-h,-d, w,-h,-d // 20-21-22-23 // bottom
]);
// 法线方向,三个坐标(x, y, z)组成一条法线
// 例如 [0, 0, 1] 代表指向z轴正方向,长度为1的法线
var normals = new Float32Array([
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // 0-1-2-3 // front
0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, // 4-5-6-7 // back
-1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, // 8-9-10-11 // left
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // 12-13-14-15 // right
0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, // 16-17-18-19 // top
0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0 // 20-21-22-23 // bottom
]);
// 顶点下标,WebGL中只能绘制三角形
// 因此绘制一个面(矩形),需要两个三角形
//
// 0┌──────────┐1
// | |
// │ │
// 2└──────────┘3
//
// 其中 [0, 1, 2] 三个顶点构成第一个三角形
// 而 [1, 3, 2] 三个顶点构成了第二个三角形
var indices = new Uint16Array([
0, 1, 2, 1, 3, 2, // front
4, 5, 6, 5, 7, 6, // back
8, 9, 10, 9, 11, 10, // left
12, 13, 14, 13, 15, 14, // right
16, 17, 18, 17, 19, 18, // top
20, 21, 22, 21, 23, 22 // bottom
]);
return { vertices: vertices, normals: normals, indices: indices, size: indices.length };
}
</div>
从getCubeVertexData方法中,我们获得了与立方体相关的顶点数据,其中包括了顶点坐标vertices,顶点法线方向normals,还有顶点下标indices。其中大家可以注意到,顶点的坐标是包含了z轴坐标,也就是包含了深度信息。有了这些信息,我们就可以进行下一步了,但在继续讲解前,我需要给大家普及一些基本的计算机图形学姿势——矩阵变换。
PG 以下内容涉及三角函数和线性代数,敬请家长注意
1、旋转变换
从上图,已知坐标(x, y),求出绕(0, 0)点旋转弧度b后的坐标(x, y
)
我们可以使用矩阵来表示:
2、拉伸变换
已知坐标(x, y),求出绕(0, 0)点向x拉伸s倍,向y方向拉伸t倍后的坐标(x, y
)
同样,我们可以使用矩阵来表示:
3、平移变换
已知坐标(x, y),求出向x方向平移s,向y方向平移t后的坐标(x, y
)
似乎我们没有办法把它转换成矩阵表示?Too Naive,只要我们引入齐次坐标即可:
通过以上的演示,我们得到了这样一个结论,所有的几何变换都可以转换成这种向量左乘矩阵的形式表示,而且多种变换作用于同一坐标点,只需依次与对应的矩阵左乘即可。
有了以上这些数学姿势,我们就可以继续我们的编码了。
我们先看getCameraMatrix方法,用于获取视图变换矩阵。WebGL绘图都是眼睛(摄像机)在z轴上,向z轴负方向(即屏幕方向)看去产生的视图:
我们并不能改变这个视线的方向。因此,为了能从别的角度来观察场景,我们只能对场景本身进行操作,例如我们要把眼睛从(0, 0, 0)点移动到(0, 0, 10)点,实质上可以通过,把整个场景向z轴负方向移动10来到达相同的效果。也就是说,我们可以通过对场景进行反向的几何变换来模拟眼睛的移动。但如果每一步都需要我们手动的进行反向操作,也未免太麻烦了,为此gl-matrix中提供了mat4.lookAt这样一个方法来辅助我们进行计算:
<div class="km_insert_code" style="text-align: left;">
function getCameraMatrix() {
var viewMatrix = mat4.create();
// 眼睛的位置
var eye = vec3.fromValues(10, 10, 10);
// 眼睛所看目标位置
var center = vec3.fromValues(0, 0, 0);
// 眼睛向上的方向
var up = vec3.fromValues(-1, 1, -1);
mat4.lookAt(viewMatrix, eye, center, up);
return viewMatrix;
}
</div>
利用mat4.lookAt方法,我们可以快速的得到眼睛从(10, 10, 10)点,望向(0, 0, 0)点的所需要的反向几何变换矩阵,我们把所得到的矩阵称为视图变换矩阵。
接下来我们看看getPerspectiveMatrix方法,用于获取透视变换矩阵。WebGL绘图的空间,实际为一个1x1x1的单位立方体,而我们眼睛所看到的真实的视觉空间,则是一个四方台:
我们要在WebGL中模拟这种透视,实际就是把这个四方台变换到WebGL的单位立方体上,只要是变换,我们都可以使用矩阵来表示,当然这其中的数学推导我(wo)就(ye)不(bu)说(dong)了,gl-mtraix给我们提供了mat4.perspective方法,来帮助我们计算出这个透视变换矩阵:
<div class="km_insert_code" style="text-align: left;">
function getPerspectiveMatrix() {
var projectMatrix = mat4.create();
// 视场角45度,宽高比为1,最近的平面为z等于0.1,最远的平面为z等于1000
mat4.perspective(projectMatrix, 45, 1, 0.1, 1000);
return projectMatrix;
}
</div>
最后,调用getPositionalLight,来得到灯光信息。这里我们模拟的是一个点光源,与点光源相关的信息,只有两个,第一个是光源的颜色,另一个是光源的位置:
<div class="km_insert_code" style="text-align: left;">
function getDirectionalLight() {
// 点光源的颜色,白色,带点灰
var color = vec3.fromValues(0.7, 0.7, 0.7);
// 点光源的位置,坐标(-10, 5, 10)
var position = vec3.fromValues(-10, 5, 10);
return { color: color, position: position };
}
</div>
来到这里我们终于集齐7颗龙珠,可以召唤出神龙了!!!!!
好吧,其实还不可以,我们只是拥有了用于绘制的数据,但怎么绘制我们还不知道(神龙:怪我咯)
我们之前说过,要教会WebGL绘图,我们需要着色器。着色器其实是GPU提供给我们的可编程接口,用于对GPU的管线进行编程,使得GPU能完成我们所需的渲染需求。着色器程序写得好,你就等于成为了上帝,上帝说要有水,给你(http://www.html5tricks.com/demo/webgl-water/index.html),上帝说要有风,拿去(http://www.bongiovi.tw/experiments/webgl/blossom/)
我们不需要弄这么复杂,只要有钱,不,只要一个立方体就可以了:
顶点着色器
<div class="km_insert_code">
attribute vec4 a_VertexPosition;
attribute vec4 a_VertexNormal;
uniform mat4 u_ProjectMatrix;
uniform mat4 u_ViewMatrix;
uniform mat4 u_NormalMatrix;
varying vec3 v_VertexPosition;
varying vec3 v_VertexNormal;
void main() {
vec4 vertexPosition = u_ViewMatrix * a_VertexPosition;
vec4 vertexNormal = u_NormalMatrix * a_VertexNormal;
v_VertexPosition = vertexPosition.xyz;
v_VertexNormal = vertexNormal.xyz;
gl_Position = u_ProjectMatrix * vertexPosition;
}
</div>
片元着色器
<div class="km_insert_code">
precision mediump float;
varying vec3 v_VertexPosition;
varying vec3 v_VertexNormal;
uniform vec3 u_Color;
uniform vec3 u_LightColor;
uniform vec3 u_LightPosition;
void main() {
vec3 direction = normalize(u_LightPosition - v_VertexPosition);
vec3 normal = normalize(v_VertexNormal);
vec3 color = u_LightColor * u_Color * max(dot(direction, normal), 0.0);
gl_FragColor = vec4(color, 1.0);
}
</div>
鉴于篇幅的关系,这次我就先不解析这两个着色器的意思,有兴趣的同学可以rtx我。
有了着色器,我们就利用gl-core提供给我们的方法,生成着色器程序,把数据写于缓冲区,最终绘制我们想要的立方体:
<div class="km_insert_code">
// 创建着色器程序
var program = new Program(gl);
// 绑定顶点着色器
program.attach(VERTEX_SHADER_SOURCE, gl.VERTEX_SHADER);
// 绑定片元着色器
program.attach(FRAGMENT_SHADER_SOURCE, gl.FRAGMENT_SHADER);
program.link();
program.bind();
// 创建顶点坐标缓存区
var vertexBuffer = new Buffer(gl, gl.ARRAY_BUFFER);
// 写入顶点坐标数据
vertexBuffer.write(modelObject.vertices, gl.STATIC_DRAW);
vertexBuffer.bind(program.attribs.a_VertexPosition, 3, gl.FLOAT);
// 创建顶点法线缓冲区
var normalBuffer = new Buffer(gl, gl.ARRAY_BUFFER);
// 写入顶点法线数据
normalBuffer.write(modelObject.normals, gl.STATIC_DRAW);
normalBuffer.bind(program.attribs.a_VertexNormal, 3, gl.FLOAT);
// 创建顶点下标缓存区
var indexBuffer = new Buffer(gl, gl.ELEMENT_ARRAY_BUFFER);
// 写入顶点下标数据
indexBuffer.write(modelObject.indices, gl.STATIC_DRAW);
indexBuffer.bind();
// 写入透视变换矩阵
gl.uniformMatrix4fv(program.uniforms.u_ProjectMatrix, false, projectMatrix);
// 写入视图变换矩阵
gl.uniformMatrix4fv(program.uniforms.u_ViewMatrix, false, viewMatrix);
// 写入法线变换矩阵
gl.uniformMatrix4fv(program.uniforms.u_NormalMatrix, false, normalMatrix);
// 写入立方体颜色
gl.uniform3f(program.uniforms.u_Color, 1.0, 0.0, 0.0);
// 写入灯光颜色
gl.uniform3fv(program.uniforms.u_LightColor, pointLight.color);
// 写入灯光位置
gl.uniform3fv(program.uniforms.u_LightPosition, pointLight.position);
// 清除color buffer和depth buffer中的数据
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 绘制
gl.drawElements(gl.TRIANGLES, modelObject.size, gl.UNSIGNED_SHORT, 0);
</div>
大功告成!!!!
以上!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。