WebGL编程指南笔记四 第六章第七章 进入三维世界

第六章 OpenGL ES着色器语言(GLSL ES)主要介绍了语法细节,和C语言基本一致,建议有问题时再做查找。

第七章 进入三维世界 建议了解基本的图形学知识再来阅读,本笔记不再详细记录。

参考
【《WebGL编程指南》读书笔记-进入三维世界(上)】
【《WebGL编程指南》读书笔记-进入三维世界(下)】

一、相机

相关概念,建议先阅读一下
图形学笔记四 MVP
UnityShader精要笔记二 数学基础 MVP实例

1.相机的观察点
image.png

为了确定观察者的状态,我们需要获取三项信息:

  • 视点(eye point):观察者所在的三维空间中位置,视线的起点。此处使用(eyeX, eyeY, eyeZ)表示,OpenGL中常称作相机。
  • 观察目标点(look-at point):被观察目标所在的点。视线从视点出发,穿过观察目标点并继续延伸。观察目标点是一个点而不是视线方向,只有同时知道观察目标点和视点,才能算出视线方向。观察目标点用(atX, atY, atZ)表示
  • 上方向(up direction):最终绘制在屏幕上的影像中的向上的方向。试想,如果仅仅确定了视点和观察点,观察者还是可能以视线为轴旋转,如图,头部偏移会导致观察到的场景也偏移了。所以,为了将观察者固定住,我们还需要指定上方向,上方向是具有三个分量的 矢量,用(upX,upY,upZ)表示。

在WebGL中,我们可以用上述三个矢量创建一个视图矩阵(view matrix),然后将矩阵传给顶点着色器

视图矩阵可以表示观察者的状态,含有观察者的视点、观察目标点、上方向等信息,最终影响显示在屏幕上的视图,也就是观察者观察到的场景。

在WebGL中,观察者的默认状态如下:

  • 视点位于坐标系统原点(0,0,0)
  • 视线为Z轴负方向,观察点为(0,0,-1),上方向为Y轴正方向,即(0,1,0)
2.观察空间的矩阵转换

引用UnityShader精要笔记二 数学基础 MVP实例中的理解:

这里说一下自己的理解,以我的眼睛举例,当我向前方进行移动时,会发现所有物体都在向后方移动。那么在游戏中,以世界坐标系为参考,一个摄像机的坐标发生变化时,就模拟了我的眼睛在移动,此时在我的眼睛坐标系内,所有物体的顶点坐标全部反向移动了。也就是,要考虑摄像机和物体的相对关系,以摄像机为参考系,把物体的顶点坐标转换到摄像机坐标系内。这个移动的矩阵如何计算出来呢?正常的思路就是,把世界坐标系的基转换到相机坐标系即可,但是这样计算却比较繁琐,因为已知条件里,更容易得到的是摄像机在世界空间中的坐标和其它属性。其实这里就有个简化的转换方式,就是把相机的坐标和其它属性,还原到世界坐标系与其重合,然后把所有物体的顶点也这么操作一遍,显然它们的相对关系是不变的。而这个还原过程,正是我们要求出的移动矩阵,还原计算非常简单,之前摄像机怎么变换的,现在逆回去就行了。

回到我们的农场游戏。现在我们需要把妞妞的鼻子从世界空间变换到观察空间中。为此我们需要知道世界坐标系下摄像机的变换信息。这同样可以通过摄像机面板中的Transform组件得到:(1,1,1)的缩放,(30,0,0)的旋转,(0,10,-10)的平移。

为了把摄像机重新移回到初始状态(这里指摄像机原点位于世界坐标原点、坐标轴与世界空间中的坐标轴重合),我们需要进行逆向变换,即先按(0,-10,10)进行平移,以便摄像机回到原点,再按(-30,0,0)进行旋转,以便让坐标轴重合。因此变换矩阵就是:


image.png
3.视图矩阵的应用-示例程序LookAtTriangels.js
// LookAtTriangles.js
// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_ViewMatrix;\n' +
  'varying vec4 v_Color;\n' +
  'void main(){\n' +
  ' gl_Position = u_ViewMatrix * a_Position;\n' +
  ' v_Color = a_Color;\n' +
  '}\n'
// 片元着色器
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'varying vec4 v_Color;\n' +
  'void main(){\n' +
  ' gl_FragColor = v_Color;\n' +
  '}\n'
// 主函数
function main() {
  // 获取canvas元素
  let canvas = document.getElementById('webgl')
  // 获取webgl上下文
  let 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
  }
  // 设置顶点坐标和颜色
  let n = initVertexBuffers(gl)
  if (n < 0) {
    console.log('Failed to set the positions of the vertices')
    return
  }
  // 获取u_ViewMatrix存储地址
  let u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix')
  if (!u_ViewMatrix) {
    console.log('Failed to get the storage loaction of u_ViewMatrix')
    return
  }
  // 设置视点、视线和上方向
  let viewMatrix = new Matrix4()
  viewMatrix.setLookAt(0.25, 0.25, 0.25, 0, 0, 0, 0, 1, 0)
  // viewMatrix.setLookAt(0.25, 0.25, 0.25, 0, 0, 0, -0.25, 0.75, -0.25) // 视线和上方向可以不垂直么
  // 将视图矩阵传递给u_ViewMatrix
  gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements)
  // 绘制三角形
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.drawArrays(gl.TRIANGLES, 0, n)
}
// 设置顶点坐标和颜色
function initVertexBuffers(gl) {
  // 准备数据
  let 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,
  ])
  let n = 9
  // 创建缓冲区对象
  let vertexColorbuffer = gl.createBuffer()
  if (!vertexColorbuffer) {
    console.log('Failed to create the buffer object')
    return -1
  }
  // 绑定缓冲区对象
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorbuffer)
  // 向缓冲区对象传输数据
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW)

  let FSIZE = verticesColors.BYTES_PER_ELEMENT
  // a_Position配置
  let 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)
  // a_Color配置
  let 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)

  return n
}
4. viewMatrix.setLookAt(0.25, 0.25, 0.25, 0, 0, 0, 0, 1, 0)

本示例中使用cuon-matrix.js库中提供的Matrix4.setLookAt()函数,根据三项信息创建视图矩阵:

Matrix.setLookAt(eyeX, eyeY, eyeZ, atX, atY, atZ, upX, upY, upZ)
根据视点(eyeX, eyeY, eyeZ)、观察点(atX, atY, atZ)、上方向(upX, upY, upZ)创建视图矩阵。视图矩阵的类型是Matrix4,其观察点映射到<canvas>的中心点。

我们看到viewMatrix.setLookAt()传入的参数,视线方向是(0.25, 0.25, 0.25),和上方向(0,1,0)并不垂直。但是按照定义,摄像机坐标系应该是垂直的:


image.png

实际上,viewMatrix.setLookAt()函数对这种情况进行了处理,此处进行简单介绍,详细过程可见源码。

首先,关于矢量计算有一个计算规则:两个矢量叉乘获得的新矢量垂直于两个矢量构成的平面。viewMatrix.setLookAt()函数对视线和上方向两个矢量及其运算结果进行了两次叉乘运算。

可以这样理解,look-at vector不足以确定摄像机的画面,因为还能绕着look-at vector旋转。up vector 和look-at vector共同定义的那个平面,其实就已经确认了摄像机的画面。只不过为了后续的计算,up vector需要变成这个定义的平面上,与look-at vector垂直的那个向量。要找到这个向量,两次叉乘是最方便的算法。

5.setLookAt计算出的矩阵是什么?

这个在原书中讲的有点绕,根据上面的结论,应该是摄像机在世界坐标系属性,回到原点的逆矩阵:

而这个还原过程,正是我们要求出的移动矩阵,还原计算非常简单,之前摄像机怎么变换的,现在逆回去就行了。

现在看原书举的例子:
举个例子,默认情况下视点在原点,视线沿着Z轴负方向进行观察。假如我们将点移动到(0, 0, 1),如下图所示。这时,视点与被观察的三角形在Z轴上的距离增加了 1.0 个单位。实际上,如果我们使三角形沿着Z轴负方向1.0个单位,也可以达到同样的效果,因为观察者看上去是一样的。

image.png

事实上,上述过程就发生在示例程序 LookAtTriangles.js 中。根据视点、观察点和上方向参数,setLookAt()方法计算出的视图矩阵恰恰就是“沿着Z轴负方向移动1.0个单位”的变换矩阵。所以,把这个矩阵与顶点坐标相乘,就相当于获得了“将视点设置在(0.0, 0.0, 1.0)”的效果。视点移动的方向与被观察对象移动的方向正好相反。对于视点的旋转,也可以采用类似的方式。

可以确认的是,摄像机坐标是(0,0,1),setLookAt返回的是沿着Z轴负方向移动1.0个单位的变换矩阵。

6.MVP中的V已经解决了,现在要加上M这部分

上一个示例展示了视图矩阵的添加方式,如果我们在变换视角的同时也需要对图形进行旋转平移等变换,如何处理视图矩阵和模型矩阵的顺序,就是此处讨论的问题。答案也比较简单:

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

原书关于MVP的讲解不够细致,可以参考
图形学笔记四 MVP
UnityShader精要笔记二 数学基础 MVP实例

  // 获取u_ModelViewMatrix存储地址
  let u_ModelViewMatrix = gl.getUniformLocation(gl.program, 'u_ModelViewMatrix')
  if (!u_ModelViewMatrix) {
    console.log('Failed to get the storage loaction of u_ModelViewMatrix')
    return
  }
  // 设置视点、视线和上方向
  let viewMatrix = new Matrix4()
  viewMatrix.setLookAt(0.25, 0.25, 0.25, 0, 0, 0, 0, 1, 0)

  // 计算旋转矩阵
  let modelMatrix = new Matrix4()
  modelMatrix.setRotate(-90, 0, 0, 1)
  // 两个矩阵相乘
  let modelViewMatrix = viewMatrix.multiply(modelMatrix)
  
  // 直接使用如下方法,不计算旋转矩阵和相乘
  // let modelViewMatrix = viewMatrix.rotate(-90, 0, 0, 1)
  
  // 将模型视图矩阵传递给u_ViewMatrix
  gl.uniformMatrix4fv(u_ModelViewMatrix, false, modelViewMatrix.elements)
7.利用键盘改变视点

LookAtTrianglesWithKeys.js在之前视图矩阵示例LookAtTriangles.js的基础上,加入了键盘响应函数,通过左右方向键控制视点沿X轴移动。效果如下:


image.png
// 键盘响应事件
var g_eyeX = 0.2,
  g_eyeY = 0.25,
  g_eyeZ = 0.25
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)
}
// 绘制函数
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)
}
二、指定可视范围

WebGL绘制物体时存在可视范围的概念,只有物体处于可视范围内,WebGL才会绘制它。这种设计一方面符合人眼观察物体的方式,一方面也降低了程序开销。比如上一个键盘响应的示例,当视点处于极右或者极左时,三角形会少一个角,这一个角就超出了可视范围:


image.png

后面的概念就是在讲MVP的P部分了,概念可以参考
图形学笔记四 MVP
UnityShader精要笔记二 数学基础 MVP实例

1.正交投影
// 绘制函数
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)
  // 显示当前的near值和far值
  nf.innerHTML =
    'near:' +
    Math.round(g_near * 100) / 100 +
    ',far:' +
    Math.round(g_far * 100) / 100
  // 绘制三角形
  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.drawArrays(gl.TRIANGLES, 0, n)
}

可视空间采用投影的方法进行定义,投影同样采用矩阵的方式呈现,称为投影矩阵,对于盒状可视空间,需要采用正射投影矩阵(orthographic projection)进行变换。在cuon-matrix,js中设置正射投影矩阵的方法如下:

Matrix4.setOrtho(left, right, bottom, top, near, far)

通过各参数计算正射投影矩阵,将其存储在Matrix4中。注意,left不一定与right相等,bottom不一定与top相等,near与far不相等。

参数:

  • left, right: 指定近裁剪面(也是可视空间的,下同)的左边界和右边界
  • bottom, top: 指定近裁剪面的上边界和下边界
  • near, far: 指定近裁剪面和远裁剪面的位置,即可视空间的近边界和远边界
  • 返回值: 无
image.png
2.可视空间长宽比与<canvas>不一致造成的变形

在前文有过说明:如果可视空间近裁剪面的宽高比与<canvas>不一致,显示出来的物体就会被压缩变形。这一部分很好理解,下面以OrthoView.js为例,展示相关效果。

不做任何修改时,可视空间设置如下:

  projMatrix.setOrtho(-1, 1, -1, 1, 0.0, 0.5)

显示效果如下:


image.png

如果把可视空间xy等比例减小:

projMatrix.setOrtho(-0.5, 0.5, -0.5, 0.5, 0.0, 0.5)

显示效果如下:


image.png

如果可视空间xy非等比例减小:

  projMatrix.setOrtho(-0.3, 0.3, -1.0, 1.0, 0.0, 0.5)
image.png
2.透视投影

Matrix4对象通过setPerspective()方法来生成透视投影矩阵(perspective projection matrix)来定义透视投影可视空间,函数规范如下:

Matrix4.setPerspective(fov, aspect, near, far)

通过各参数计算透视投影矩阵,将其存储在Matrix4中。注意,near的值必须小于far。
参数:

  • fov: 指定垂直视角,即可视空间顶面和底面间的夹角,必须大于0(degree,单位为度)
  • aspect: 指定近裁剪面的宽高比(宽度/高度)
  • near, far: 指定近裁剪面和远裁剪面的位置,即可视空间的近边界和远边界(near和far必须都大于0)
  • 返回值: 无
  // 可视空间操作
  let u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix')
  if (!u_ProjMatrix) {
    console.log('Failed to get the storage loaction of u_ProjMatrix')
    return
  }
  let projMatrix = new Matrix4()
  projMatrix.setPerspective(30, canvas.width / canvas.clientHeight, 1, 100)
  gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements)
三、MVP
    var modelMatrix = new Matrix4(); //模型矩阵
    var viewMatrix = new Matrix4(); //视图矩阵
    var projMatrix = new Matrix4(); //投影矩阵

    modelMatrix.setTranslate(0.75, 0, 0);
    viewMatrix.setLookAt(0, 0, 5, 0, 0, -100, 0, 1, 0);
    projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);

    gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);
    gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);
    gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);

合成一个:

  //模型矩阵
  var modelMatrix = new Matrix4();
  modelMatrix.setTranslate(0.75, 0, 0);
  //视图矩阵
  var viewMatrix = new Matrix4();  // View matrix
  viewMatrix.setLookAt(0, 0, 5, 0, 0, -100, 0, 1, 0);
  //投影矩阵
  var projMatrix = new Matrix4();  // Projection matrix
  projMatrix.setPerspective(30, canvas.width / canvas.height, 1, 100);
  //MVP矩阵
  var mvpMatrix = new Matrix4();
  mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
  //将MVP矩阵传输到着色器的uniform变量u_MvpMatrix
  gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
四、正确处理前后关系

关于深度测试,可以参考
图形学笔记六 Shading 渲染管线
UnityShader精要笔记四 渲染流水线
UnityShader精要笔记七 透明效果 附RenderType

1.gl.enable(gl.DEPTH_TEST)

开启cap表示的功能(capability)。
参数:

  • cap: 指定需要开启的功能,有可能是以下几个(更多参数参阅OpenGL Programming Guide一书)
    • gl.DEPTH_TEST: 深度检测
    • gl.BLEND: 混合(参见“层次模型”章节)
    • gl.POLYGON_OFFSET_FILL: 多边形位移(见下一节)等
  • 返回值: 无
  • 错误:
    INVALID_ENUM: cap的值无效

与gl.enable()函数对应,gl.disable()函数用于关闭功能,函数规范如下:

gl.disable(cap)
关闭cap表示的功能(capability)。
参数:

  • cap: 与gl.enable()相同
  • 返回值: 无
  • 错误:
    INVALID_ENUM: cap的值无效
2.gl.clear(gl.DEPTH_BUFFER_BIT)

和颜色缓冲区一样,开启深度检测之后,在绘制每一帧之前必须清除深度缓冲区,否则会出现错误的结果,清除深度缓冲区的方法如下:

gl.clear(gl.DEPTH_BUFFER_BIT)

我们也可以使用按位或符号(|)同时连接两个缓冲区同时清除:

gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT)
3.示例程序DepthBuffer.js

示例程序在PerspectiveView_mvp.js的基础上加入隐藏面消除的代码,其中顶点顺序改为从近到远。程序运行效果与PerspectiveView_mvp.js一致,部分重要代码展示如下:

  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  gl.enable(gl.DEPTH_TEST)
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
  // 绘制右侧三角形
  gl.drawArrays(gl.TRIANGLES, 0, n)

开启深度检测比较简单,只需要在第一次绘制操作之前开启消除隐藏面并清空深度缓冲区即可,与颜色缓冲区的设置较类似。

书中指出:

  • 在任何三维场景中,都应该开启隐藏面消除,并在适当的时刻清空深度缓冲区(通常是在绘制每一帧之前)。
  • 隐藏面消除的前提是正确设置可视空间。
4.深度冲突
image.png

之所以会产生深度冲突,是因为两个表面过于接近,深度缓冲区有限的精度已经不能区分哪个在前,哪个在后了。严格地说,如果创建三维模型阶段就对顶点的深度值加以注意,是能够避免深度冲突的。但是,当场景中有多个运动者的物体时,实现这一点几乎是不可能的。

WebGL 提供一种称为多边形偏移的的机制来解决这个问题。该机制将自动在Z值加上一个偏移量,偏移量的值由物体表面相对与观察者视线的角度来确定。启用该机制只需两个代码。

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

gl.polygonOffset(factor, units)

  • 指定加到每个顶点绘制后Z值上的偏移量,偏移量按照公式mfactor+runits计算,其中m表示顶点所在表面相对于观察者的视线的角度,r表示硬件能够区分两个z值之差的最小值。
  • 返回值: 无
  • 错误: 无
五、绘制立方体

目前,我们都是调用 gl.drawArrays()方法来进行绘制操作的。考虑以下,如何用该函数绘制出一个立方体呢。我们只能使用 gl.TRIANGLES、gl.TRIANGLE_STRIP 或者 gl.TRIANGLE_FAN 模型来绘制三角形,那么最简单也就最直接的方法就是,通过绘制两个三角形来拼成立方体的一个矩形表面。换句话说,为了绘制四个顶点(v0, v1, v2, v3)组成的矩形表面,你可以分别绘制三角形(v0, v1, v2)和三角形(v0, v2, v3)。对立方体的所有表面都这样做就绘制出了整个立方体。

立方体的每一面由两个三角形组成,每个三角形由三个顶点,所以每个面需要用6个顶点。立方体共有6个面,一共需要36个顶点。将36个顶点的数据写入缓冲区,再调用 gl.drawArrays(gl.TRIANGLES, 0, 36) 就可以绘制处立方体。问题是,立方体实际只有8个顶点,而我们却定义了36个之多,这是因为每个顶点会被多个三角形公用。

或者,你也可以使用 gl.TRIANGLE_FAN 模式来绘制立方体。在 gl.TRIANGLE_FAN 模式下,用4个顶点(v0, v1, v2, v3)就可以绘制出一个四方形,所以你只需要4x6=24个顶点。但是,如果这样做你就必须为立方体的每个面调用一次 gl.drawArrays(),一共需要6次调用。所以,两种绘制模式各有优缺点,没有一种是完美的。

1.gl.drawElements

如你所愿,WebGL 确实提供了一种完美的方案:gl.drawElements()。使用该函数替代 gl.drawArryas()函数进行绘制,能够避免重复定义顶点,保持顶点数量最小。为此,你需要知道模型的每一个顶点的坐标,这些顶点坐标描述了整个模型。

我们将立方体拆成顶点和三角形。立方体被拆成6个面:前、后、左、右、上、下,每个面都由两个三角形组成,与三角形列表中的两个三角形相关联。每个三角形都由3个顶点,与顶点列表的3个顶点相关联。三角形列表中的数字表示该三角形的3个顶点在顶点列表中的索引值。顶点列表共有8个顶点,索引值从0到7。


image.png

gl.drawElements(mode, count, type, offset)
执行着色器,按照mode参数指定的方式,根据绑定到gl.ELEMENT_ARRAY_BUFFER的缓冲区中的顶点索引值绘制图形。
参数:

  • mode: 指定的绘制方式,与gl.drawArrays()相同
  • count: 指定绘制顶点的个数(整型数)
  • type: 指定索引值数据类型:gl.UNSIGNED_BYTE或gl.UNSIGNED_SHORT
  • offset: 指定索引数据中开始绘制的位置,以字节为单位
  • 返回值: 无
  • 错误:
    INVALID_ENUM: 传入的mode参数不是前述参数之一
    INVALID_VALUE: 参数count或offset是负数

在使用gl.drawElements()之前,除了需要将顶点数据写入缓冲区绑定到gl.ARRAY_BUFFER之外,还需要把顶点索引数据写入缓冲区绑定到gl.ELEMENT_ARRAY_BUFFER。

2.示例程序HelloCube.js
image.png

initVertexBuffers()函数部分,顶点坐标和颜色如下:

  let verticesColors = new Float32Array([
    // 顶点坐标和颜色
    1.0,    1.0,    1.0,    1.0,    1.0,    1.0, // v0 White
    -1.0,    1.0,    1.0,    1.0,    0.0,    1.0, // v1 Magenta
    -1.0,    -1.0,    1.0,    1.0,    0.0,    0.0, // v2 Red
    1.0,    -1.0,    1.0,    1.0,    1.0,    0.0, // v3 Yellow
    1.0,    -1.0,    -1.0,    0.0,    1.0,    0.0, // v4 Green
    1.0,    1.0,    -1.0,    0.0,    1.0,    1.0, // v5 Cyan
    -1.0,    1.0,    -1.0,    0.0,    0.0,    1.0, // v6 Blue
    -1.0,    -1.0,    -1.0,    0.0,    0.0,    0.0, // v7 Black
  ])

initVertexBuffers()函数部分,顶点索引定义如下:

  // 顶点索引
  let indices = new Uint8Array([
    0,    1,    2,    0,    2,    3, // 前
    0,    3,    4,    0,    4,    5, // 右
    0,    5,    6,    0,    6,    1, // 上
    1,    6,    7,    1,    7,    2, // 左
    7,    4,    3,    7,    3,    2, // 下
    4,    7,    6,    4,    6,    5, // 后
  ])

initVertexBuffers()函数,在配置顶点坐标和颜色之后,需要创建顶点索引的缓冲区对象、绑定缓冲区、存储数据,函数返回值是共需绘制多少个点:

  // 写入顶点索引数据
  let indexBuffer = gl.createBuffer()
  if (!indexBuffer) {
    console.log('Failed to create indexBuffer')
    return -1
  }
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW)

  return indices.length

最后,我们调用 gl.drawElements(),就绘制出了立方体。

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

gl.drawElements()时,WebGL 首先从绑定到 gl.ELEMENT_ARRAY_BUFFER 的缓冲区中获取顶点的索引值,然后根据该索引值,从绑定到 gl.ARRAY_BUFFER 的缓冲区中获取顶点的坐标、颜色等信息,然后传递给 attribute 变量并执行顶点着色器。对每个索引值都这样做,最后就绘制出了整个立方体,而此时你只调用了一次 gl.drawElements()。这种方式通过索引来访问顶点数据,从而循环利用顶点信息,控制内存的开销,但代价是你需要通过索引来间接地访问顶点,在某种程度上使程序复杂化了。所以,gl.drawElements()和 gl.drawArrays()各有优劣,具体用哪一个取决于具体的系统需求。

虽然我们已经证明了 gl.drawElements()是高效的绘制三维图形的方式,但还是漏了关键的一点:我们无法通过将颜色定义在索引值上,颜色仍然是依赖于顶点的。

考虑这样的情况:我们希望立方体的每个表面都是不同的单一颜色(而非颜色渐变效果)或者纹理图像。我们需要把每个面的颜色或纹理信息写入三角形、索引和顶点数据中。

3.为立方体的每个表面指定颜色
image.png

我们知道,顶点着色器进行的是逐顶点的计算,接受的是逐顶点的信息。这说明,如果你想指定表面的颜色,你也需要将颜色定义为逐顶点的信息,并传给顶点着色器。举个例子,你想把立方体的前表面涂成蓝色,前表面由顶点v0、v1、v2、v3组成,那么你就需要将这4个顶点都指定为蓝色。

但是你会发现,顶点v0 不仅在前表面上,也在右表面上和上表面上,如果你将 v0 指定为蓝色,那么它在另外两个表面上也会是蓝色,这不是我们想要的而结果。为了解决这个问题,我们需要创建多个具有相同顶点坐标的点,如下图所示。如果这样做,你就必须把那些具有相同坐标的顶点分开处理。


image.png

此时三角形列表,也就是顶点索引值序列,对每个面都指向一组不同的顶点,不再有前表面和上表面共享一个顶点的情况,这样一来,就可以实现前述的结果,为每个表面涂上不同的单色。我们也可以使用类似的方法为立方体的每个表面贴上不同的纹理,只需将上图的颜色值换成纹理坐标即可。

4.示例程序ColoredCube.js

相比于HelloCube.js,本例在initVertexBuffers()函数中有一定改动:

  • 将顶点坐标和颜色分别存储在两个缓冲区中。(这种方式与存储在一个缓冲区的方式各有利弊,利处在于更加灵活,弊端在于编写繁琐。)
  • 顶点、颜色和索引数据有所更改。
  • 定义了函数initArrayBuffer()封装缓冲区对象的创建、绑定、数据写入和开启操作。
//顶点着色器程序
var VSHADER_SOURCE =
    'attribute vec4 a_Position;'+
    'attribute vec4 a_Color;'+
    'uniform mat4 u_MvpMatrix;'+
    'varying vec4 v_Color;'+
    'void main(){'+
    'gl_Position = u_MvpMatrix * 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);
    gl.enable(gl.DEPTH_TEST);

    //获取 u_ViewMatrix 、u_ModelMatrix和 u_ProjMatrix 变量的存储位置
    var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
    if(u_MvpMatrix < 0){
        console.log("Failed to get the storage location of u_MvpMatrix");
        return;
    }

    var mvpMatrix = new Matrix4();
    mvpMatrix.setPerspective(30, 1, 1, 100);
    mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);


    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);
}

function initVertexBuffers(gl) {
    //    v6----- v5
    //   /|      /|
    //  v1------v0|
    //  | |     | |
    //  | |v7---|-|v4
    //  |/      |/
    //  v2------v3

    var vertices = new Float32Array([   //顶点坐标
        1.0, 1.0, 1.0,  -1.0, 1.0, 1.0,  -1.0,-1.0, 1.0,   1.0,-1.0, 1.0,  // v0-v1-v2-v3
        1.0, 1.0, 1.0,   1.0,-1.0, 1.0,   1.0,-1.0,-1.0,   1.0, 1.0,-1.0,  // v0-v3-v4-v5
        1.0, 1.0, 1.0,   1.0, 1.0,-1.0,  -1.0, 1.0,-1.0,  -1.0, 1.0, 1.0,  // v0-v5-v6-v1
        -1.0, 1.0, 1.0,  -1.0, 1.0,-1.0,  -1.0,-1.0,-1.0,  -1.0,-1.0, 1.0,  // v1-v6-v7-v2
        -1.0,-1.0,-1.0,   1.0,-1.0,-1.0,   1.0,-1.0, 1.0,  -1.0,-1.0, 1.0,  // v7-v4-v3-v2
        1.0,-1.0,-1.0,  -1.0,-1.0,-1.0,  -1.0, 1.0,-1.0,   1.0, 1.0,-1.0   // v4-v7-v6-v5
    ]);

    var colors = new Float32Array([     // 颜色
        0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  0.4, 0.4, 1.0,
        0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  0.4, 1.0, 0.4,
        1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  1.0, 0.4, 0.4,
        1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  1.0, 1.0, 0.4,
        1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  1.0, 1.0, 1.0,
        0.4, 1.0, 1.0,  0.4, 1.0, 1.0,  0.4, 1.0, 1.0,  0.4, 1.0, 1.0
    ]);

    var indices = new Uint8Array([       // 顶点索引
        0, 1, 2,   0, 2, 3,
        4, 5, 6,   4, 6, 7,
        8, 9,10,   8,10,11,
        12,13,14,  12,14,15,
        16,17,18,  16,18,19,
        20,21,22,  20,22,23
    ]);


    //创建缓冲区对象
    var indexBuffer = gl.createBuffer();
    if(!indexBuffer){
        console.log("Failed to create thie buffer object");
        return -1;
    }

    if (!initArrayBuffer(gl, vertices, 3, gl.FLOAT, 'a_Position'))
        return -1;

    if (!initArrayBuffer(gl, colors, 3, gl.FLOAT, 'a_Color'))
        return -1;


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

    return indices.length;
}

function initArrayBuffer(gl, data, num, type, attribute) {
    var buffer = gl.createBuffer();
    if(!buffer){
        console.log("Failed to create thie buffer object");
        return -1;
    }

    //将缓冲区对象保存到目标上
    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("Failed to get the storage location of " + attribute);
        return -1;
    }

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

    return true;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,639评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,277评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,221评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,474评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,570评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,816评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,957评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,718评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,176评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,511评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,646评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,322评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,934评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,755评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,987评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,358评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,514评论 2 348

推荐阅读更多精彩内容