在上一节的最后一个示例程序中,当视点在极左或极右的位置时,三角形会缺少一部分:
三角形缺了一角的原因是,我们没有指定可视范围,即实际观察得到的区域边界。如前一章所属,WebGL 只显示可视范围内的区域。例中当我们改变视点位置时,三角形的一部分到了可视范围外,所以图中的三角形就缺了一个角。
可视范围(正射类型)
虽然你可以将三维物体放在三维空间中的任何地方,但是只有当它在可视范围内时,WebGL 才会绘制它。事实上,不绘制可视范围外的而对象,是基本的降低程序开销的手段。绘制可视范围外的对象没有意义,即使把它们绘制出来也不会在屏幕上显示。从某种程序上来说,这样也模拟了人类观察物体的方式,如下图所示。我们人类也只能看到眼前的东西,水平视角大约200度左右。总之,WebGL 就是以类似的方式,只绘制可视范围内的三维对象。
除了水平和垂直范围内的限制,WebGL 还限制观察者的可视深度,即”能够看多远“。所有这些限制,包括水平视角、垂直视角和可视深度,定义了可视空间。由于我们没有显示地指定可视空间,默认的可视深度又不够远,所以三角形的一个角看上去就消失了。
可视空间
有两类常用的可视空间:
- 长方体可视空间,也称盒状空间,由正摄投影产生。
- 四棱锥/金字塔可视空间,由透视投影产生。
在透视投影下,产生的三维常营看上去更是有深度感,更加自然,因为我们平时观察真实世界用的也是透视投影。在大多数情况下,比如三维射击类游戏中,我们都应当采用透视投影。相比之下,正射投影的好处是用户可以方便比较场景中物体的大小,这时因为物体看上去的大小与其所在的位置没有关系。在建筑平面图等技术绘图的相关场合,应当使用这种投影。
首先介绍基于正射投影的盒装可视空间的工作原理。
盒装可视空间的形状如下图所示。可视空间由前后两个矩形表面确定,分别称近裁剪面和远裁剪面,前者的四个顶点为(right, top,-near),(-left,top,-near),(-left,-bottom,-near),(right,-bottom,-near),而后者的四个顶点为(right, top,-far),(-left,top,-far),(-left,-bottom,-far),(right,-bottom,-far)。
<canvas> 上显示的就是可视空间中物体在近裁剪面上的投影。如果剪裁面的宽高比和 <canvas> 不一样,那么画面就会被按照 <canvas>的宽高比进行压缩,物体会被扭曲。近裁剪面与远裁剪面之间的盒装空间就是可视空间,只有在此空间内的物体会被显示出来。如果某个物体一部分在可视空间内,一部分在其外,那就只显示空间内的部分。
定义盒装可视空间
cuon-matrix.js 提供的 Matrix.setOrtho()方法可用来设置投影军阵,定义盒装可视空间。
我们在这里又用到了矩阵。这个矩阵被成为正射投影矩阵。示例程序 Ortho View 将使用这种矩阵定义盒装可视空间,并绘制3个与 LookAtRotatedTriangles 中一样的三角形,由此测试盒装可视空间的效果。LookAtRotatedTriangles 程序将视点放在一个指定的非原点位置上,但本例为方便,直接把视点置于原点处,视线为Z轴负方向。可视空间如下图所示,near = 0.0,far = 0.5,left = -1.0,right = 1.0,bottom = -1.0,top = 1.0,三角形处于Z轴 0.0 到 -0.4 区间上。
此外,示例程序还允许通过键盘按键修改可视空间的 near 和 far 值。这样我们就能直观地看到这两个值具体对可视空间由什么影响。下面列出了各按键的作用。
我们同时在网页上的画面下方显示这两个值。
来看一下示例程序代码。
示例程序(OrthoView)
示例程序子啊画面的下方显示 near 和 far 的值,所以需要对 HTML 文件略加修改,如:
OrthoView.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Draw Multiple Points</title>
</head>
<body onload="main()">
<canvas id="webgl" width="400" height="400">
Please use a browser that supports "canvas"
</canvas>
<p id = "nearFar">The near and far values are displayed here.</p>
<script src="../lib/webgl-utils.js"></script>
<script src="../lib/webgl-debug.js"></script>
<script src="../lib/cuon-utils.js"></script>
<script src="../lib/cuon-matrix.js"></script>
<script src="OrthoView.js"></script>
</body>
</html>
如你所见,HTML 文件中增加了一段说明文本“The near and far values are displayed here”。我们将用JS 重写文本内容,显示当前的 near 和 far 值。
OrthoView.js
//顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'attribute vec4 a_Color;'+
'uniform mat4 u_ProjMatrix;'+
'varying vec4 v_Color;'+
'void main(){'+
'gl_Position = u_ProjMatrix * a_Position;'+
'v_Color = a_Color;'+
'}';
//片元着色器程序
var FSHADER_SOURCE=
'#ifdef GL_ES\n' +
'precision mediump float;\n' +
'#endif\n' +
'varying vec4 v_Color;' +
'void main() {'+
'gl_FragColor = v_Color;'+
'}';
function main() {
//获取canvas元素
var canvas = document.getElementById("webgl");
if(!canvas){
console.log("Failed to retrieve the <canvas> element");
return;
}
//获取 nearFar 元素
var nf = document.getElementById("nearFar");
if(!nf){
console.log("Failed to retrieve the <nf> element");
return;
}
//获取WebGL绘图上下文
var gl = getWebGLContext(canvas);
if(!gl){
console.log("Failed to get the rendering context for WebGL");
return;
}
//初始化着色器
if(!initShaders(gl,VSHADER_SOURCE,FSHADER_SOURCE)){
console.log("Failed to initialize shaders.");
return;
}
//设置顶点位置
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}
//指定清空<canvas>颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
//获取 u_ViewMatrix 变量的存储位置
var u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix');
if(u_ProjMatrix < 0){
console.log("Failed to get the storage location of u_ProjMatrix");
return;
}
//设置视点、视线和上方向
var projMatrix = new Matrix4();
// 注册键盘事件响应函数
document.onkeydown = function(ev){
keydown(ev, gl, n, u_ProjMatrix, projMatrix, nf);
};
draw(gl, n, u_ProjMatrix, projMatrix, nf);
}
function initVertexBuffers(gl) {
var verticesColors = new Float32Array([
0.0, 0.6, -0.4, 0.4, 1.0, 0.4, //绿色三角形在最后面
-0.5, -0.4, -0.4, 0.4, 1.0, 0.4,
0.5, -0.4, -0.4, 1.0, 0.4, 0.4,
0.5, 0.4, -0.2, 1.0, 0.4, 0.4, //黄色三角形在中间
-0.5, 0.4, -0.2, 1.0, 1.0, 0.4,
0.0, -0.6, -0.2, 1.0, 1.0, 0.4,
0.0, 0.5, 0.0, 0.4, 0.4, 1.0, //蓝色三角形在最前面
-0.5, -0.5, 0.0, 0.4, 0.4, 1.0,
0.5, -0.5, 0.0, 1.0, 0.4, 0.4
]);
var n=9; //点的个数
//创建缓冲区对象
var vertexColorBuffer = gl.createBuffer();
if(!vertexColorBuffer){
console.log("Failed to create thie buffer object");
return -1;
}
//将缓冲区对象保存到目标上
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
//向缓存对象写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
var FSIZE = verticesColors.BYTES_PER_ELEMENT;
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;
}
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE*6, 0);
gl.enableVertexAttribArray(a_Position);
var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
if(a_Color < 0){
console.log("Failed to get the storage location of a_Color");
return -1;
}
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE*6, FSIZE*3);
gl.enableVertexAttribArray(a_Color);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
return n;
}
var g_near = 0.0, g_far = 0.5;
function draw(gl, n, u_ProjMatrix, projMatrix, nf) {
//使用矩阵设置可视空间
projMatrix.setOrtho(-1, 1, -1, 1, g_near, g_far);
//将投影矩阵传递给 u_ProjMatrix 变量
gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);
gl.clear(gl.COLOR_BUFFER_BIT);
nf.innerHTML = "near:" + Math.round(g_near * 100) / 100 + ",far:" + Math.round(g_far * 100) / 100;
gl.drawArrays(gl.TRIANGLES, 0, n);
}
function keydown(ev, gl, n, u_ProjMatrix, projMatrix, nf) {
switch (ev.keyCode){
case 39: g_near += 0.01; break; //右
case 37: g_near -= 0.01; break; //左
case 38: g_far += 0.01; break; //上
case 40: g_far -= 0.01; break; //下
default: return;
}
draw(gl, n, u_ProjMatrix, projMatrix, nf);
}
与 LookAtTrainglesWithKeys 类似,本例也定义了 keydown()函数,每当按下按键时,匿名的事件响应函数就会调用 keydown()函数。keydown()函数首先更新 near 和 far 的值,然后调用 draw()函数进行绘制。draw()函数将设置可视空间,更新页面上文本显示的 near 和 far 的值,并绘制3个三角形。最关键的事情是设置可视空间,就发生在 draw()函数中。但是在深入研究前,先来看一下JS如何修改页面上的文本。
JS 修改 HTML 元素
JS 修改 HTML 元素中内容的方法很简单。首先调用 getElementById()并传入元素的 id,获取待修改的HTML 元素。
在示例程序中,我们把下面这个 <p> 元素中的文本改成了“near:0.0, far:0.5”:
<p id = "nearFar">The near and far values are displayed here.</p>
在 OrthoView.js 中,我们调用 getElementById()并传入元素的 id 值 “nearfar” 以获取该元素。如下所示:
var nf = document.getElementById("nearFar");
一旦 nf 变量获取了 <p> 元素,就可以直接通过其 innerHTML 属性来进行修改,比如,如果你写下:
nf.innerHTML = 'Good Morning,Marusyje-san!'
在执行之后,”Good Morning,Marusyje-san!” 这段文本就显示了在页面上。你也可以在文本中加入 HTML 标签,比如‘Good Morning <b>Marisuke<b>-san!’,就会以突出显示“Marisuke”。
在OrthoView.js 中,可视空间的 near 和 far 的值会存储在全局变量 g_near 和 g_far 中。
顶点着色器的执行流程
本例中的顶点着色器与 LookAtTraingles.js 中的几乎一样,只是 uniform 变量变成了 u_ProjMatrix。该变量存储了可观空间的投影矩阵,我们将投影矩阵与顶点坐标相乘,再赋值给 gl_Position。
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'attribute vec4 a_Color;'+
'uniform mat4 u_ProjMatrix;'+
'varying vec4 v_Color;'+
'void main(){'+
'gl_Position = u_ProjMatrix * a_Position;'+
'v_Color = a_Color;'+
'}';
当键盘的上方向键被按下时,事件响应函数就会执行并调用 keydown()。注意我们将 nf 作为最后一个参数传入,这样 keydown()函数就能够访问并修改 <p> 元素了。keydown()函数最后调用了 draw()函数绘制三角形,这样每次按键后都会重绘整个图形。
document.onkeydown = function(ev){
keydown(ev, gl, n, u_ProjMatrix, projMatrix, nf);
};
keydown()函数首先检查哪个键被按下,然后根据按下的键,修改 g_near 和 g_far 的值,最后调用 draw()函数。注意,这里 g_near 和 g_far 是全局变量,不管是 keydown()还是 draw()函数都可以访问它。
function keydown(ev, gl, n, u_ProjMatrix, projMatrix, nf) {
switch (ev.keyCode){
case 39: g_near += 0.01; break; //右
case 37: g_near -= 0.01; break; //左
case 38: g_far += 0.01; break; //上
case 40: g_far -= 0.01; break; //下
default: return;
}
draw(gl, n, u_ProjMatrix, projMatrix, nf);
}
再看一下 draw()函数,它与 LookAtTriangle.js 中的几乎一样,唯一的区别是它修改了网页上的文本信息。
function draw(gl, n, u_ProjMatrix, projMatrix, nf) {
//使用矩阵设置可视空间
projMatrix.setOrtho(-1, 1, -1, 1, g_near, g_far);
//将投影矩阵传递给 u_ProjMatrix 变量
gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);
gl.clear(gl.COLOR_BUFFER_BIT);
nf.innerHTML = "near:" + Math.round(g_near * 100) / 100 + ",far:" + Math.round(g_far * 100) / 100;
gl.drawArrays(gl.TRIANGLES, 0, n);
}
draw()函数计算出可视空间对应的投影矩阵 projMatrix,将其传递给着色器中的 u_ProjMatrix 变量,接着在页面上更新 near 和 far 的值,最后绘制出三角形。
修改 near 和 far 值
运行程序,按下右方向键逐渐增加near 值,你会看到三角形逐个消失了。
默认情况下,near 值为 0.0,此时3个三角形都出现了。当我们首次按下右方向键时,将 near 值增加至 0.01 时,处在最前面的蓝色三角形消失了。这是因为,蓝色三角形就在 XY 平面上,近裁剪面越过了蓝色三角形,使其处在了可视空间外。
我们接着继续正大 near 值,当 near 值大于 0.2 时,近裁剪面越过了黄色三角形,使其处在可视空间外。黄色三角形也消失了,视野中只剩下绿色三角形。此时,如果你逐渐减小 near 值使其小于 0.2,黄色的三角形就会重新出现,而如果继续增大 near 值使其大于 0.4,绿色的三角形就会小时,视野中将空无一物,只剩下黑色的背景。
同意,如果你改变 far 的值,也会产生类似的效果。随着 far 值的逐渐减小,当值小于 0.4 时,绿色三角形会首先消失,小于 0.2 时,黄色三角形小时,最终只剩下蓝色三角形。
补上缺掉的角
在 LookAtTrianglesWithKeys 中,当你多次按左或右方向键,处于极左处或极右处观察三角形时,会发现三角形看上去缺了一个角。通过前一节的讨论,我们已经很明确地知道这时因为三角形的一部分处于可视区域之外,被裁剪掉了。这一节,我们就来修改程序,适当地设置可视空间,确保三角形不被裁剪。
从上图中可以看出,三角形中距离观点最远的角被裁剪了。显然,这时由远裁剪面国语接近视点导致,我们只需要将远裁剪面移到距离视点更远的地方。为此,我们可以按照以下的配置来修改可视空间:left = -1.0,right = 1.0,bottom = -1.0,top = 1.0,near = 0.0,far = 2.0。
程序涉及两个矩阵:关于可视空间的正射投影矩阵,以及关于视点与视线的试图矩阵。在顶点着色器中,我们需要用视图矩阵乘以顶点坐标,得到顶点在视图坐标系下的坐标,再左乘正射投影矩阵并赋值给 gl_Position。
<正射投影矩阵>x<视图矩阵>x<顶点坐标>
重点LookAtTrianglesWithKeys_ViewVolume.js
//顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'attribute vec4 a_Color;'+
'uniform mat4 u_ViewMatrix;'+
'uniform mat4 u_ProjMatrix;'+
'varying vec4 v_Color;'+
'void main(){'+
'gl_Position = u_ProjMatrix * u_ViewMatrix * a_Position;'+
'v_Color = a_Color;'+
'}';
//片元着色器程序
var FSHADER_SOURCE=
'#ifdef GL_ES\n' +
'precision mediump float;\n' +
'#endif\n' +
'varying vec4 v_Color;' +
'void main() {'+
'gl_FragColor = v_Color;'+
'}';
function main() {
//获取canvas元素
var canvas = document.getElementById("webgl");
if(!canvas){
console.log("Failed to retrieve the <canvas> element");
return;
}
//获取WebGL绘图上下文
var gl = getWebGLContext(canvas);
if(!gl){
console.log("Failed to get the rendering context for WebGL");
return;
}
//初始化着色器
if(!initShaders(gl,VSHADER_SOURCE,FSHADER_SOURCE)){
console.log("Failed to initialize shaders.");
return;
}
//设置顶点位置
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}
//指定清空<canvas>颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
//获取 u_ViewMatrix 和 u_ProjMatrix 变量的存储位置
var u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
if(u_ViewMatrix < 0){
console.log("Failed to get the storage location of u_ViewMatrix");
return;
}
var u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix');
if(u_ProjMatrix < 0){
console.log("Failed to get the storage location of u_ProjMatrix");
return;
}
//设置视点、视线和上方向
var viewMatrix = new Matrix4();
// 注册键盘事件响应函数
document.onkeydown = function(ev){
keydown(ev, gl, n, u_ViewMatrix, viewMatrix);
};
//设置视图矩阵
var projMatrix = new Matrix4();
projMatrix.setOrtho(-1.0, 1.0, -1.0, 1.0, 0.0, 2.0);
gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);
draw(gl, n, u_ViewMatrix, viewMatrix);
}
function initVertexBuffers(gl) {
var verticesColors = new Float32Array([
0.0, 0.5, -0.4, 0.4, 1.0, 0.4, //绿色三角形在最后面
-0.5, -0.5, -0.4, 0.4, 1.0, 0.4,
0.5, -0.5, -0.4, 1.0, 0.4, 0.4,
0.5, 0.4, -0.2, 1.0, 0.4, 0.4, //黄色三角形在中间
-0.5, 0.4, -0.2, 1.0, 1.0, 0.4,
0.0, -0.6, -0.2, 1.0, 1.0, 0.4,
0.0, 0.5, 0.0, 0.4, 0.4, 1.0, //蓝色三角形在最前面
-0.5, -0.5, 0.0, 0.4, 0.4, 1.0,
0.5, -0.5, 0.0, 1.0, 0.4, 0.4
]);
var n=9; //点的个数
//创建缓冲区对象
var vertexColorBuffer = gl.createBuffer();
if(!vertexColorBuffer){
console.log("Failed to create thie buffer object");
return -1;
}
//将缓冲区对象保存到目标上
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
//向缓存对象写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
var FSIZE = verticesColors.BYTES_PER_ELEMENT;
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;
}
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE*6, 0);
gl.enableVertexAttribArray(a_Position);
var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
if(a_Color < 0){
console.log("Failed to get the storage location of a_Color");
return -1;
}
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE*6, FSIZE*3);
gl.enableVertexAttribArray(a_Color);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
return n;
}
var g_eyeX = 0.20,g_eyeY = 0.25,g_eyeZ = 0.25;
function draw(gl, n, u_ViewMatrix, viewMatrix) {
//设置视点和视线
viewMatrix.setLookAt(g_eyeX, g_eyeY, g_eyeZ, 0, 0, 0, 0, 1, 0);
//将视图矩阵传递给 u_ViewMatrix 变量
gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, n);
}
function keydown(ev, gl, n, u_ViewMatrix, viewMatrix) {
if(ev.keyCode == 39){ //按下右键
g_eyeX += 0.01;
}else if(ev.keyCode == 37){ //按下左键
g_eyeX -= 0.01;
}else{
return;
}
draw(gl, n, u_ViewMatrix, viewMatrix);
}
在计算正射投影矩阵 projMatrix 时,我们将 far 的值从 1.0 改成 2.0,将结果传给了顶点着色器中的 u_ProjMatrix。投影矩阵与顶点无关,所以它是 uniform 变量。运行示例程序,然后像之前那样移动视点,你会发现三角形再也不会被裁剪了。