WebGL - 可视空间,深度处理,顶点索引

研究内容大概如下:

  • 以用户视角进入三围世界
  • 控制三围可视空间
  • 裁剪
  • 处理物体的前后关系
  • 绘制三围的立方体

上面会讲到如何使用模型矩阵、视图矩阵、以及该投影矩阵的使用,并且最后会绘制一个三维的立方体;

1、立方体由三角形构成

在这里插入图片描述

如图所示,三维物体都是由三角面构成的,因此逐个的去绘制三角形就能绘制出三维物体。但是三维跟二维有一个显著的区别,绘制二维只需要考虑x,y轴,而绘制三维物体的时候,需要考虑物体的 深度信息,还需要定义三维世界的观察者,在什么地方,朝哪里看,视野有多宽,能够看多远;

2、视点和视线

三维物体具有深度,深度也就是z轴,因此需要考虑一下观察者的两点问题:

  • 观察方向,即观察者自己再什么位置,在看场景的那一部分?
  • 可视距离,即观察者能够看多远?

视点:观察者所处的位置

视线:从视点出发沿观察方向的射线

webgl系统中,默认情况下视点处于(0,0,0)原点,视线时z轴负半轴;

3、视点、观察目标点和上方向

为了确保观察者的状态也就是相机的属性,需要确定两项信息

  • 视点:观察者所在的三维空间的位置,视线的起点
  • 观察目标点:被观察目标所在的点
  • 上方向:最终绘制在屏幕上的影像中的向上的方向

如图所示以上三个条件确定一个观察者(相机)
在这里插入图片描述

webgl中可以通过三个矢量,创建一个视图矩阵,然后将矩阵传给顶点着色器,矩阵表示观察者的视点,观察目标点,上方向等信息

  • 视图矩阵:影响显示在屏幕上的视图

通过《webgl编程指南》这本书提供的矩阵方法库,可以设置视图矩阵

Matrix4.setLookAt(eyeX,eyeY,eyeZ,atX,atY,atZ,upX,upY,upZ)

// 视点:(eyeX,eyeY,eyeZ)
// 观察点:(atX,atY,atZ)
// 上方向:(upX,upY,upZ)

// 示例
var viewMatrix = new Matrix4();

viewMatrix.setLookAt(0, 0, 0, 0, 0, -1, 0, 1, 0);

// 视点矢量:vec3(0, 0, 0) 视点为原点 webgl 默认
// 观察点矢量:vec3(0, 0, -1) 即z轴负方向
// 上方向矢量:vec3(0, 1, 0) 以y轴为上方向 

1、设置视图矩阵

顶点着色器

attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_ViewMatrix;
varying vec4 v_Color;
void main(){
    gl_Position = u_ViewMatrix * a_Position;
    v_Color = a_Color;
}

片元着色器仅仅是接收varying变量,此处不再写出;

javaScript将视图矩阵传递给顶点着色器

// 获取u_ViewMatrix变量的存储地址
var u_ViewMatrix = gl.getUniformLocation(gl.program,'u_ViewMatrix');

// 设置视点,视线,和上方向
var viewMatrix = new Matrix4();
viewMatrix.setLookAt(0.20, 0.25, 0.25, 0, 0, 0, 0, 1, 0);

// 将视图矩阵传给u_Matrix变量
gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);

示例:设置观察者状态

设置了观察者的状态,可以调整看物体的角度以及远近距离,但是如果我们不通过调正观察者的状态,而对三维对象进行平移、旋转等变换,也是可以视线上面的行为,因为两者是相对的;

改变观察者的状态对整个世界进行平移和旋转变换本质上是一样的,都可以使用矩阵来描述

4、指定视点观察旋转后的三角形

矩阵乘以顶点坐标,得到的结果是顶点经过旋转变换之后的新坐标,也就是说用旋转矩阵乘以顶点坐标,就可以得到旋转后的顶点坐标;

<旋转后的顶点坐标> = <旋转矩阵>*原始矩阵

因此可以可以得到以下组合

<从视点看上去的旋转后的顶点坐标> = <视图矩阵>*<旋转矩阵>*<原始顶点坐标>

除了旋转矩阵,还可以使用 平移、缩放等基本变换或他们的组合此时的矩阵被称为 模型矩阵

所以可以写成这样

<视图矩阵>*<模型矩阵>*<原始顶点坐标>

着色器程序可以这样写

attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_ViewMatrix; // 视图矩阵
uniform mat4 u_ModeMatrix; // 模型矩阵
varying vec4 v_Color;
void main(){
	gl_Position = u_ViewMatrix * u_ModelMatrix * a_Position;
	v_Color = a_Color;
}

其实视图和模型矩阵可以写到一起,简称模型视图矩阵

<模型视图矩阵> = <视图矩阵>*<模型矩阵>

<模型视图矩阵> * <顶点坐标>

着色器代码如下

...
uniform mat4 u_ModelViewMatrix; 
void mina(){
    gl_Position = u_ModelVIewMatrix * a_Position;
    ...
}
    

两个矩阵相乘

var modelViewMatrix = viewMatrix.multipy(modelMatrix);//两个矩阵相乘

显示效果和之前一样

示例:<模型视图矩阵>

5、可视范围

虽然我们可以将三维物体放到三维空间中的任何地方,但只有当它在可视范围内时webgl才会绘制它,不绘制可视范围外的对象,是基本的降低程序开销的手段,因为绘制可视范围外的东西是没有意义的,即使绘制出来也不会在屏幕上显示;

除了水平和垂直范围的限制,webgl还限制观察者的 可视深度,就是能看多远,包括水平视角、垂直视角;

1、可视空间

可视空间:水平视角、垂直视角和可视深度定义了可视空间
可视空间由两种:

  • 长方体可视空间,盒装空间,由 正射投影产生;
  • 四棱锥/金字塔可视空间,由 透视投影产生;

透视投影下产生的三维场景看上去更有深度感觉,我们平时观察的真世界用的也是透视投影;
而正射投影与物体的远近无关,在机械建模以及建筑类等技术中应用广泛;

1、盒装空间(正射投影)

在这里插入图片描述

盒装可视空间,由前后两个矩形表面来确定,分别称为 近裁剪面远裁剪面
<canvas>上显示的就是可视空间中物体在近裁剪面上的投影;

通过《webgl编程指南》这本书作者视线工具类,cuon-matrix.js提供的Matrix4.setOrtho()可以用来设置投影矩阵,定义盒装可视空间

Matrix4.prototype.setOrtho = function (left, right, bottom, top, near, far){
    // ...
}

// left, right -- 指定远近裁剪面的左边界和有边界
// bottom,top -- 指定远近裁剪面的上边界和下边界
// near,top -- 指定近裁剪面和远裁剪面的位置,即可视空间的近远边界

这个矩阵被叫做 正射投影矩阵

加入正射投影矩阵示例如下:

顶点着色器部分

...
uniform mat4 u_ProjMatrix;
..
void main(){
	gl_Position = u_ProjMatrix * a_Position;
}

然后通过js代码设置正射投影的参数

var projMatrix = new Matrix4();
projMatrix.setOrtho(-1, 1, 01, 1, 0, 100);

示例:<可视区域>

通过键盘左右键可以调整视角

2、可视空间(透视投影)

在正射投影的可视空间中,不管三角形与视点的距离是近是远,它由多么大就会画出来多么大,但是场景没有深度感,因此想要模拟显示的观察效果就需要使用 透视投影,来定义可视空间;

定义透视投影可视空间
在这里插入图片描述

透视投影可视空间也有视点、视线、近裁剪面和远裁剪面,同样也使用投影矩阵来表示;

Matrix4.prototype.setPerspective = function (fov, aspect, near, far){
    //...
}
// fov -- 指定垂直视角,即可视空间顶面和地面间的夹角,必须大于0
// aspect -- 指定近裁剪面的宽高比(宽度/高度)
// near,far -- 指定近裁剪面和远裁剪面的位置,即可视空间的近边界和远边界(near和far都必须大于0)

这个矩阵叫做 透视投影矩阵

示例程序如下:

...
uniform mat4 u_ViewMatrix;
uniform mat4 u_ProjMatrix;
...
void main(){
	gl_Position = u_ProjMatrix * u_ViewMatrix * a_Position;
	...
}

js代码如下

...
var viewMatrix = new Matrix4();// 视图矩阵
var projMatrix = new Matrix4();// 投影矩阵

// 计算视图矩阵和投影矩阵
viewMatrix.setLookAt(0, 0, 5, 0, 0, -100, 0, 1, 0);// 设置视点,目标点和上方向
projMatrix.setPrespective(30, canvas.width/canvas.height, 1, 100);

2、模型、视图和投影矩阵

通过以下式子可以得出

<投影矩阵> * <视图矩阵> * <模型矩阵> * <顶点坐标>

如果投影矩阵和模型矩阵都是单位阵那么由上面式子可得

<模型视图投影矩阵> * <顶点坐标>

上面三个矩阵变成一个矩阵即 模型视图投影矩阵

因此示例如下:

顶点着色器部分

...
uniform mat4 u_MvpMatrix;
...
void main(){
    gl_Position = u_MvpMatrix * a_Position;
    ...
}

js代码部分

var modelMatrix = new Matrix4(); // 模型矩阵
var viewMatrix = new Matrix4();  // 视图矩阵
var projMatrix = new Matrix4();  // 投影矩阵
var mvpMatrix = new Matrix4();   // 模型视图投影矩阵

...

// 计算模型视图投影矩阵
mvpMatrix.set(projMatrix).multipy(viewMatrix).multipy(modelMatrix);
// 然后将模型视图投影矩阵传递给着色器即可

带有set前缀的方法,就使得矩阵相乘的结果又写入到 mvpMatrix中;

6、对象前后关系

真实世界中,如果两个盒子一前一后放到桌子上,前面的盒子会挡住后面的盒子,之前绘制三角形的示例貌似webgl可以处理遮挡关系,其实webgl为了加速绘制操作,是按照顶点在缓冲区的顺序来处理他们的,之前的示例的顶点是故意定义成那样的;

webgl在默认情况下,会按照缓冲区中的顺序去绘制图形,而且后绘制的图形覆盖先绘制的图形;

如果场景中的对象不发生运动,观察者的状态也是唯一的,那么这种做法没有问题,但是如果视点移动,从不同角度看物体的画,那么就不能不事先决定对象出现的顺序;

1、隐藏面消除

因此webgl提供了 隐藏面消除的功能,这个特性会帮助我们为您消除那些被遮挡的隐藏面,因此我们可以放心的绘制场景而不必担心物体在缓冲区的顺序,这个功能已经被内嵌在 webgl中了,只需要开启就行;

1、开启隐藏面

遵循以下两步

  • 开启隐藏面消除功能

    gl.enable(gl.DEPTH_TEST)

  • 在绘制之前,清除深度缓冲区

    gl.clear(gl.DEPTH_BUFFER_BIT)

gl.enable(cap)方法的使用

可以去查看MDN接口文档介绍 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/enable

方法作用是:让webgl开启某种特性,具体参数如下

参数介绍
gl.BLEND激活片元的颜色融合计算,参见WebGLRenderingContext.blendFunc()
gl.CULL_FACE激活多边形正反面剔除,参见WebGLRenderingContext.cullFace()
gl.DEPTH_TEST激活深度检测,需要更新深度缓冲区,参见WebGLRenderingContext.depthFunc()
gl.DITHER激活在写入颜色缓冲区之前,抖动颜色成分
gl.POLYGON_OFFSET_FILL激活添加多边形片段的深度偏移值,参见WebGLRendering.polygonOffset()
gl.SAMPLE_ALPHA_TO_COVERAGE激活通过alpha值决定的临时覆盖之计算(抗锯齿)
gl.SAMP_COVERAGE激活使用临时覆盖值,位和运算片段的覆盖值,参见WebGLRenderingContext.sampleCoverage()
gl.SCISSON_TEST激活裁剪检测,即丢弃剪裁矩形范围外的片段,参见WebGLRenderingContext.scisson()
gl.STENCIL_TEST激活模板测试并且更新模板缓冲区,参见WebGLRenderingContext.stenciFunc()

使用之前可以进行检测该参数是否已经开启,使用以下方法

WebGLRenderingContext.isEnabled()

gl.enable(gl.DITHER);//开启

gl.isEnabled(gl.DITHER);// true 该属性已经开启
2、清空深度缓冲区

深度缓冲区是一个隐藏对象,其作用就是帮助webgl进行隐藏面消除

webgl在颜色缓冲区中,绘制几何图形,绘制完成之后将颜色缓冲区显示到canvas上,要将隐藏面消除就必须知道几何图形的深度信息,而深度缓冲区就是用来存储深度信息的,由于深度方向通常是z轴方向,所以有时候也曾为z缓冲区;

在绘制每一帧之前,都必须清除深度缓冲区,以消除绘制上一帧时在其中留下的痕迹;

gl.clear(gl.DEPTH_BUFFER_BIT)

当然还需要清除颜色缓冲区,因此可以这样写

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 类似的同时清除两个缓冲区都可以使用按位或符号

示例程序如下

// 开启隐藏面消除,也就是深度检测
gl.enable(gl.DEPTH_TEST);
// 清空颜色和深度缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

示例:<深度检测消除隐藏面>

在任何三维场景中,都应该开启隐藏面消除,并在适当的时候清空深度缓冲区,通常是在绘制每一帧之前;

2、深度冲突

消除隐藏面时webgl的一项复杂而又强大的特性,然而当两个几何体的表面极为接近时,就会出现新的问题,使得表面看上去斑斑驳驳的,如下图所示,这种现象称为 深度冲突

在这里插入图片描述

之所以会产生深度冲突,是因为两个表面极为接近,深度缓冲区有限的精度已经不能区分,哪个在前哪个在后,严格的来说在创建三维模型的时候是可以避免深度冲突,但是在运动着的物体,是很难避免的;

webgl提供一种被称为 多边形偏移的机制来解决这个问题,该机制会自动在z值上加一个偏移量,偏移量的值,由物体表面相对于观察者视线的角度来确定;

1、启用多边形偏移

只需要两步

  • gl.enable(gl.POLYGON_OFFSET_FILL) 启用多边形偏移
  • gl.polygonOffset(1.0, 1.0) 在绘制之前用来计算偏移量的参数

激活参数gl.POLYGON_OFFSET_FILL启用多边形偏移,然后通过设置偏移量参数;

gl.polygonOffset(factor,units)

指定加到每个顶点绘制z值上的偏移量,偏移量按照公式m * factor + r * units计算,其中m表示顶点所在表面相对于观察者的视线的角度,而r表示硬件能够区分两个z值之差的最小值

启用多边形偏移消除深度冲突后的效果

示例:<多边形偏移消除深度冲突>

7、绘制立方体

立方体是三维模型,但它是由多个二维三角面组成的,下面就开始绘制有八个顶点组成的立方体,每个顶点定义颜色后,立方体表面的颜色会根据顶点颜色内插出来,形成一种光滑的渐变效果;

立方体的每一个面都有两个三角形组成,每个三角形有3个顶点,所以每个面都要用到6个顶点,立方体6个面,因此一共需要36个顶点;

但问题是立方体实际上只有八个顶点,而我们却定义了36个,这是因为每个顶点都会被多个三角形所共用;

webgl提供了一种通过顶点索引的方式,来解决顶点复用的问题,通过gl.drawELements()代替gl.drawArrays()进行绘制,就能够避免重复定义顶点;

1、顶点索引

之前都是使用gl.drawArrays()进行绘制,现在使用gl.drawElements()来进行绘制,这种绘制模式需要在gl.ELEMENT_ARRAY_BUFFER中,指定顶点索引值,gl.ARRAY_BUFFERgl.ELEMENT_ARRAY_BUFFER两者最终要的区别就是,gl.ELEMENT_ARRAY_BUFFER管理着具有索引结构的三维模型数据;

gl.drawElements(mode,count,type,offset)

执行着色器,按照mode参数指定的方式,根据绑定到gl.ELEMENT_ARRAY_BUFFER的缓冲区只能的索引值绘制图形;

  • mode:指定绘制的方式
  • count:指定绘制顶点的个数(整形数)
  • offset:指定索引数组中开始绘制的位置,以字节为单位

因此,需要将顶点索引,写入到缓冲区,并绑定到gl.ELEMENT_ARRAY_BUFFER上;

此处把创建缓冲区对象的过程封装一个方法

// 初始化缓冲区对象
function initArrayBuffer(gl, data, num, type, attribute) {

    // 创建缓冲区
    var buffer = gl.createBuffer();

    if (!buffer) {

        console.log('创建缓冲区失败!');
        return false;

    }

    // 写入数据到缓冲区
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

    // 将缓冲区对象分配给属性变量
    var a_attribute = gl.getAttribLocation(gl.program, attribute);

    if (a_attribute < 0) {

        console.log('获取变量存储地址失败' + attribute);
        return false;

    }

    gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);

    // 启用缓冲区属性变量
    gl.enableVertexAttribArray(a_attribute);

    return true;

}

然后首先定义顶点数组,再定义顶点索引数组

var indices = new Uint8Array([       // Indices of the vertices
    0, 1, 2, 0, 2, 3,    // front
    4, 5, 6, 4, 6, 7,    // right
    8, 9, 10, 8, 10, 11,    // up
    12, 13, 14, 12, 14, 15,    // left
    16, 17, 18, 16, 18, 19,    // down
    20, 21, 22, 20, 22, 23     // back
]);

创建顶点索引缓冲区

var indexBuffer = gl.createBuffer();
if (!indexBuffer) return -1;

然后向顶点索引缓冲区中绑定数据

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

示例效果如下:

<通过顶点索引绘制立方体>

打开示例后你会发现立方体在按照y做自转运动,这是因为我们在代码中循环渲染,来改变它自身的旋转角度,重要代码如下:

// 循环渲染 旋转立方体
function animate() {

    requestAnimationFrame(animate);

    mvpMatrix.rotate(-1, 0, 1, 0);// 绕y轴进行顺时针旋转,每次旋转-1度(角度制)

    gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

    // 每一帧都需要清空颜色和深度缓冲区
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);

}

animate();

2、每个面指定颜色

最然已经证明了gl.drawElements()是高校的绘制三维图形的方式,但是我们无法通过将颜色定义在索引值上,颜色仍然是依赖于顶点的;

我们希望每个立方体的表面都是不同的单一颜色,而非渐变色效果,或者是纹理图像,因此需要把每个面的颜色或纹理信息写入三角形列表、索引和顶点数据中;

顶点着色器是逐顶点的计算,接收的是逐顶点的信息,因此如果想要指定表面的颜色,也需要将颜色定义为逐顶点的信息;

此时需要单独创建一个颜色缓冲区,用来放颜色值的信息;

var colors = new Float32Array([
    ......
]);
// 然后将颜色数据写入缓冲区
var color_num = initArrayBuffer(gl, color, 3, gl.FLOAT,'a_Color');

效果案例如下:

<指定颜色信息的立方体>

此时如果我们把所有的颜色值都变成1.0的话,立方体的每个面都是白色的;

<纯白色立方体>

此时显示的效果是看不到棱角分明的,但显示世界中白色的立方体是可以看到棱角分明的效果的,这是因为此时的立方体没有灯光信息的计算,所以都是白色的,下一章会讲解灯光在三维世界中的重要作用;

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值