- 三角形在三维图形学中的重要地位,以及WebGL如何绘制三角形。
- 使用多个三角形绘制其它类型的基本图形。
- 利用简单的方程对三角形做基本的变换,如移动、旋转和缩放。
- 利用矩阵简化变换。
绘制多个点与缓冲区对象
相关内容 :缓冲区对象:创建缓冲区对象-绑定缓冲区对象-向缓冲区对象写入数据以及类型化数组-缓冲区对象分配给attribute变量-开启attribute变量;开始绘制及着色器运行过程
相关函数:gl.createBuffer(), gl.bindBuffer(), gl.bufferData(), new Float32Array()..., gl.vertexAttribPointer()(有自动补全), gl.enableVertexAttribArray(), gl.disableVertexAttribArray(), gl.drawArrays()
之前的示例都是逐个点进行绘制,本节将讨论一次性绘制多个点的方法,作为绘制多顶点图形的基础。
可以用WebGL缓冲区对象(buffer object),它可以一次性向着色器传入多个顶点的数据。缓冲区对象是WebGL系统中的一块内存区域,我们可以一次性地向缓冲区对象中填充大量的顶点数据,然后将这些数据保存其中,供顶点着色器使用。
javascript
// MultiPoint.js
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'void main(){\n' +
' gl_Position = a_Position;\n' +
' gl_PointSize = 10.0;\n' +
'}\n'
// 片元着色器
var FSHADER_SOURCE =
'void main() {\n' + ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\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
}
// 设置背景色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 清空canvas
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制
gl.drawArrays(gl.POINTS, 0, n)
}
function initVertexBuffers(gl) {
let vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5])
let n = 3 // 点的个数
// 创建缓冲区对象
let vertexBuffer = gl.createBuffer()
if (!vertexBuffer) {
console.log('Failed to create the buffer object')
return -1
}
// 将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
// 向缓冲区对象中写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
// 将缓冲区对象分配给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, 2, gl.FLOAT, false, 0, 0)
// 连接a_Position变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position)
return n
}
新加入的函数 **initVertexBuffers()**创建了顶点缓冲器对象,将多个顶点的数据保存在缓冲区中,最后将缓冲区传给顶点着色器。函数的返回值是待绘制顶点的数量,发生错误会返回 -1
使用缓冲区对象:
缓冲区对象是WebGL系统中的一块存储区,我们可以在缓冲区对象中保存想要绘制的所有顶点的数据。如下图所示:
使用缓冲区对象向顶点着色器传入多个顶点的数据,需要五个步骤:
- 创建缓冲区对象(gl.createBuffer())
- 绑定缓冲区对象 ( gl.bindBuffer() )
- 将数据写入缓冲区对象 ( gl.bufferData() )
- 将缓冲区对象分配给一个 attribute 变量 ( gl.vertexAttribPointer() )
- 开启 attribute 变量 ( gl.enableVertexAttrribArray() )
创建缓冲区对象( gl.createBuffer() )
javascript
// 创建缓冲区对象
let vertexBuffer = gl.createBuffer()
if (!vertexBuffer) {
console.log('Failed to create the buffer object')
return -1
}
绑定缓冲区 (gl.bindBuffer() )
将缓冲区对象绑定到WebGL系统中已经存在的目标上,这个目标表示缓冲区对象的用途,这样 WebGL 才能够正确处理其中的内容
javascript
// 将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
示例程序中,我们将创建的缓冲区对象绑定到**gl.ARRAY_BUFFER
**目标上,代码执行完毕后,WebGL系统内如下图所示:
向缓冲区对象写入数据(gl.bufferData() )
javascript
// 向缓冲区对象中写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
这一步将 vertices 中的数据写入到绑定在 gl.ARRAU_BUFFER目标中的缓冲区对象。此处不能直接向缓冲区写入数据,而实依据 target 写入数据
类型化数组
javascript
let vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5])
为了优化性能,WebGL 为每种基本数据类型引入了一种特输的数组**(类型化数组)**提前告诉浏览器数组中的数据类型,能够更有效率地处理数据,为绘制三维图形提供了大量便利。
javascript
let vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5])
let vertices = new Float32Array(4)
将缓冲区对象分配给一个 attribute 变量 (gl.vertexAttribPointer() )
缓冲区对象准备好之后,需要获取attribute变量地址,再向attribute变量传递参数。第二章中使用了gl.vertexAttrib[1234]f[v] 系列函数来传递数据,但此方法一次只能传递一个值,此时需要一次传递多个值,示例中采用**gl.vertexAttribPointer()**方法,它可以将缓冲区对象(实际上是缓冲区对象的引用或指针)分配给 attribute 变量
javascript
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
开启 attribute 变量 ( gl.enablevertexAttribArray() )
javascript
gl.enableVertexAttribArray(a_Position);
开始绘制
通过上面的函数,我们已经配置好了缓冲区和着色器,可以开始绘制:
javascript
// 设置背景色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 清空canvas
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制
gl.drawArrays(gl.POINTS, 0, n)
javascript
gl.drawArrays(mode, first, count)
在建立attribute变量和缓冲区联系时,函数**gl.vertexAttribPointer()**中的参数size为2,表示缓冲区每个顶点有2个分量值。所以每次着色器运行前,gl_Position都被提供了两个分量(通过attribute变量a_Position),其他值按照规则填充为0.0和1.0。
Hello Triangle
画一个三角形
javascript
// MultiPoint.js
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'void main(){\n' +
' gl_Position = a_Position;\n' +
'}\n'
// 片元着色器
var FSHADER_SOURCE =
'void main() {\n' + ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\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
}
// 设置背景色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 清空canvas
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制
gl.drawArrays(gl.TRIANGLES, 0, n)
}
function initVertexBuffers(gl) {
let vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5])
let n = 3 // 点的个数
// 创建缓冲区对象
let vertexBuffer = gl.createBuffer()
if (!vertexBuffer) {
console.log('Failed to create the buffer object')
return -1
}
// 将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
// 向缓冲区对象中写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
// 将缓冲区对象分配给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, 2, gl.FLOAT, false, 0, 0)
// 连接a_Position变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position)
return n
}
相对于上一个,有以下两处修改:
- 顶点着色器中删除了
'
gl_PointSize = 10.0;\n',该语句只在绘制单个点的时候才起作用; gl.drawArrays()
方法第一个参数改为了**gl.TRIANGLES
**(第37行)
基本图形与 gl.drawArrays() 方法
gl.drawArrays()方法强大又灵活,可以通过第1个参数mode指定不同的值来以7种不同的方式绘制图形。WebGL可以绘制的基本图形如下:
- 点的顺序对图形的呈现有重要影响;
- WebGL只能绘制三种图形:点、线段和三角形。当然,从球体到立方体,再到游戏中的三维角色,都可以由小的三角形组成,实际上,我们可以使用以上这些最基本的图形来绘制出任何东西。
用三角形绘制矩形(HelloQuad)
矩形可以由两个三角形组成,绘制方式可以采用gl.TRAINGLES、gl.TRIANGLE_STRIP、gl.TRIANGLE_FAN 三种方法,第一种方法需要用到6个顶点,后两种需要4个顶点,每种方法的顶点顺序都不相同。此处采用gl.TRIANGLE_STRIP方法进行绘制,相比于上个示例,此处改动如下:
javascript
let vertices = new Float32Array([-0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5]) # 正方形的四个顶点,点的顺序可见上一节gl.TRIANGLE_STRIP图形示例
let n = 4 // 点的个数
// 绘制
gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)
移动,旋转和缩放
相关内容 :1.表达式方式进行仿射变换;2.变换矩阵进行仿射变换;3.将矩阵传递给uniform变量;4.按列主序
相关函数:gl.uniformMatrix4fv()
本节将讨论如何移动(平移)、旋转和缩放三角形,这样的操作称为变换 (transformations)或仿射变换(affine transformations)。
百度百科中对仿射变换的定义如下:
仿射变换 ,又称仿射映射,是指在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间。显而易见,相关的变换通过矩阵可以简单获得。
该程序将上一节中的三角形向上向右移动了0.5哥单位
平移
- 显然,在WebGL系统中实现平移操作是一个逐顶点操作(per-vertex operation),我们需要在顶点着色器中为顶点坐标的每一个分量加上一个常量。
- 这一常量对于每个顶点都是一样的,故采用uniform变量即可。
该示例与 Hellotriangle 的差别有两处:
- 第一处在顶点着色器中,定义了uniform变量u_Translation作为平移量
javascript
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform vec4 u_Translation;\n' +
'void main(){\n' +
' gl_Position = a_Position + u_Translation;\n' +
'}\n'
- 第二处在main()函数中,增加了uniform变量传值的过程
javascript
// 在x,y,z方向上平移的距离
var Tx = 0.5,
Ty = 0.5,
Tz = 0.5
// 将平移距离传输给顶点着色器uniform变量
let u_Translation = gl.getUniformLocation(gl.program, 'u_Translation')
if (!u_Translation) {
console.log('Failed to get the storage location of u_Translation')
}
gl.uniform4f(u_Translation, Tx, Ty, Tz, 0.0)
完整代码:
javascript
// TranslatedTriangle.js
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform vec4 u_Translation;\n' +
'void main(){\n' +
' gl_Position = a_Position + u_Translation;\n' +
'}\n'
// 片元着色器
var FSHADER_SOURCE =
'void main(){\n' + ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + '}\n'
// 在x,y,z方向上平移的距离
var Tx = 0.5,
Ty = 0.5,
Tz = 0.5
// 主函数
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
}
// 将平移距离传输给顶点着色器uniform变量
let u_Translation = gl.getUniformLocation(gl.program, 'u_Translation')
if (!u_Translation) {
console.log('Failed to get the storage location of u_Translation')
}
gl.uniform4f(u_Translation, Tx, Ty, Tz, 0.0)
// 设置背景色
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 vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5])
let n = 3
// 创建缓冲区对象
let vertexBuffer = gl.createBuffer()
if (!vertexBuffer) {
console.log('Failed to create the buffer object')
return -1
}
// 绑定缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
// 缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STREAM_DRAW)
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
}
// 将缓冲区分配给attribute变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
// 开启attribute变量(连接)
gl.enableVertexAttribArray(a_Position)
return n
}
旋转
描述旋转,有以下三点需要指明:
- 旋转轴
- 旋转方向:逆时针或顺时针
- 旋转角度
书中这样表述旋转操作:绕Z轴,逆时针旋转了β角度。关于"逆时针"的约定是:如果β是正值 ,观察者在Z轴正半轴某处,视线沿着Z轴负方向进行观察,看到的物体是逆时针旋转 的,如下图所示。这种情况又可称作正旋转 (positive rotation),这是本书中WebGL程序的默认设定,当然,β小于零代表顺时针旋转。
呈现旋转效果的顶点着色器
- 顶点着色器部分进行改造,创建uniform变量u_CosB和u_SinB:(注意:vec4格式的数据与JavaScript中对象类似,每个数据都有索引)
javascript
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform float u_CosB, u_SinB;\n' +
'void main(){\n' +
' gl_Position.x = a_Position.x * u_CosB + a_Position.y * u_SinB;\n' +
' gl_Position.y = a_Position.x * u_SinB + a_Position.y * u_CosB;\n' +
' gl_Position.z = a_Position.z;\n' +
' gl_Position.w = 1.0;\n' +
'}\n'
- 根据旋转角度计算三角函数,传递参数给uniform变量:(注意:JavaScript中Math.sin()和Math.cos()方法的参数为弧度制的角度。)
javascript
// 将旋转图形所需数据传输给顶点着色器
let radian = (Math.PI * ANGLE) / 180.0 // 转换为弧度制
let cosB = Math.cos(radian)
let sinB = Math.sin(radian)
let u_CosB = gl.getUniformLocation(gl.program, 'u_CosB')
let u_SinB = gl.getUniformLocation(gl.program, 'u_SinB')
if (!u_CosB) {
console.log('Failed to get the storage location of u_CosB')
}
if (!u_SinB) {
console.log('Failed to get the storage location of u_SinB')
}
gl.uniform1f(u_CosB, cosB)
gl.uniform1f(u_SinB, sinB)
变换矩阵对表达式方式的改造:
旋转:该变换矩阵进行的变换是一次旋转,所以这个矩阵又可以称为旋转矩阵(rotation matrix)
平移的矩阵:
RotateTriangle_Matrix 的代码: (用变换矩阵实现效果)
javascript
// RotatedTriangle_Matrix.js
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform mat4 u_xformMatrix;\n' +
'void main(){\n' +
' gl_Position = u_xformMatrix * a_Position;\n' +
'}\n'
// 片元着色器
var FSHADER_SOURCE =
'void main(){\n' + ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + '}\n'
// 旋转角度
var ANGLE = 90.0
// 主函数
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
}
// 创建旋转矩阵
let radian = (Math.PI * ANGLE) / 180.0 // 转换为弧度制
let cosB = Math.cos(radian)
let sinB = Math.sin(radian)
// 注意WebGL中矩阵是列主序的
let xformMatrix = new Float32Array([
cosB, sinB, 0.0, 0.0,
-sinB, cosB, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
])
// 将旋转图形所需数据传输给顶点着色器
let u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix')
if (!u_xformMatrix) {
console.log('Failed to get the storage location of u_xformMatrix')
}
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix)
// 设置背景色
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 vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5])
let n = 3
// 创建缓冲区对象
let vertexBuffer = gl.createBuffer()
if (!vertexBuffer) {
console.log('Failed to create the buffer object')
return -1
}
// 绑定缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
// 缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STREAM_DRAW)
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
}
// 将缓冲区分配给attribute变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
// 开启attribute变量(连接)
gl.enableVertexAttribArray(a_Position)
return n
}
一般来说,我们有两种方式在数组中存储矩阵元素:按行主序 (row major order)和按列主序(column major order),如下图所示:
WebGL与OpenGL一样,矩阵元素是按列主序存储在数组中的。如图中的矩阵存储在数组中的顺序就是这样的:[a, e, i, m, b, f, j, n, c, g, k, o, d, h, l, p]。与我们平时写代码时使用的那种相反
javascript
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix)
向 uniform 变量传递矩阵
平移矩阵 :
缩放矩阵: