【笔记】《WebGL编程指南》学习-第7章进入三维世界(2-可视范围)

在上一节的最后一个示例程序中,当视点在极左或极右的位置时,三角形会缺少一部分:

这里写图片描述

三角形缺了一角的原因是,我们没有指定可视范围,即实际观察得到的区域边界。如前一章所属,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 变量。运行示例程序,然后像之前那样移动视点,你会发现三角形再也不会被裁剪了。

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值