WebGL编程指南 - 绘制和变换三角形

  • 三角形在三维图形学中的重要地位,以及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可以绘制的基本图形如下:

  1. 点的顺序对图形的呈现有重要影响;
  2. 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 变量传递矩阵

平移矩阵

缩放矩阵:

参考:【《WebGL编程指南》读书笔记-绘制和变换三角形】_webgl绘制三角形并进行平移缩放旋转-CSDN博客

相关推荐
zaizai10078 天前
WebGL编程指南 - 颜色与纹理续
图形学
zaizai100712 天前
WebGL编程指南 - 入门续
图形学
闲人编程1 个月前
使用Python实现图形学的阴影贴图算法
python·算法·图形学·贴图·阴影贴图
闲人编程1 个月前
使用Python实现图形学的纹理映射算法
开发语言·python·算法·图形学·纹理映射
闲人编程1 个月前
使用Python实现图形学的环境映射算法
开发语言·python·算法·图形学·环境映射
闲人编程1 个月前
Python实现图形学曲线和曲面的Bezier曲线算法
开发语言·python·算法·图形学·曲线·曲面·bezier
CaptainHarryChen1 个月前
从 Affine Particle-In-Cell (APIC) 到 Material Point Method (MPM 物质点法)
图形学·物理仿真·mpm·仿真算法
Jozky862 个月前
图形学论文笔记
笔记·图形学
赵青青3 个月前
DirectX9(D3D9)游戏开发:高光时刻录制和共享纹理的踩坑
图形学