变换矩阵

872

线性变换在图形学、webGL中有着广泛的用途,通过线性变换可以操作向量所在的空间。在阅读本文之前,还得了解一下向量、矩阵、webGL的相关知识。

线性变换

定义:

  1. 直线在变换后任然保持直线,不能有所弯曲
  2. 变换后原点必须保持固定
  3. 变换后网格保持等比分布

那么我们常见的一些变换中旋转和缩放就属于线性变换,而平移不属于线性变换(坐标原点会变化,属于仿射变换)。

你可能会感到疑惑为什么平移一个物体会导致坐标原点的变换。首先我们需要明白两个概念:

  1. 线性变换其实是对基向量的操作,线性变换完全由它所在空间的基向量的作用决定。
  2. 坐标系是由基向量和坐标原点组成

所以在线性变换中操作的是空间的基向量而不是某个向量(由基向量组成的)本身,但是基向量发生变化又基向量组成的向量也会发生变化,所以平移变换时实际上是平移的基向量,那么原点就会发生变化,所以平移不是线性变换。

线性变换其实就是一种映射函数,可以把空间里的一个向量映射到另一个空间里的另一个向量,所以线性变换可以理解为输入一个向量然后出另一个向量的变换函数

我们在初始基向量构成的坐标系中新建一个向量,并将基向量作用逆时针旋转90°的线性变换:

在旋转后得到了一个全新的坐标系,由于向量所在空间的基向量坐标发生了改变它自己的坐标也发生了改变。

我们发现在旋转后向量和两个基向量的关系没有变,都等于(感兴趣可以试试其他任意的线性变换,你会发现变换前后向量和基向量的关系都没有变化)。这是线性变换十分重要的一个属性:如果向量 ,和单位向量具有某种数学关系(线性组合),那么在线性变换后任然具有该数学关系。这是一个非常重要的属性,意味着我们只需要记录线性变换之前某个向量和基向量的关系,以及线性变换后基向量的坐标,不管变化是怎样的都可以推算出变化之后向量的坐标

比如上面的旋转变换中向量和基向量的关系,由于两个基向量也是单位向量,所以我们使用矩阵的方式来表示向量 = 。在旋转变换之后两个基向量相对与变换之前的坐标使用一个2x2矩阵来表示,并按照矩阵的第一列是变换之后水平基向量的坐标,第二列是变换之后垂直基向量的坐标的方式:

上面我们说到了线性变换其实就是一种映射函数,所以将它们用函数调用的方式写在一起:,我们再具象化一点就可以写成两个矩阵相乘:

我们在之前说过,线性变换前后向量和基向量的数学关系是不会变的,任然满足关系,由于第一列是变换之后水平基向量的坐标,第二列是变换之后垂直基向量的坐标。所以上面的等式可以写成:

然后再使用向量的数乘和加法公式可以得到:

这样我们将矩阵的乘法转化为了向量的数乘和加法运算,你也可以试试直接使用矩阵的乘法看看计算结果是否相同。

线性变换是操作空间的一种手段,他保持网格平行等距分布,并且保持原点不变。我们只需要使用四个数字就能表示线性变换,即变换之后的两个基向量的坐标位置,将这四个数字使用矩阵的方式来表示,那么这个矩阵就被称之为变换矩阵

这是在二维空间的线性变换,在三维空间也同样适用,但是三维空间中变换矩阵往往是4x4的矩阵,在webGL中三维空间点是由四元坐标构成,这样的坐标被称之为齐次坐标,齐次坐标在表示观测物体和观测者之间的距离非常实用。

变换矩阵

旋转变换矩阵

假设在二维坐标系中有一个向量,它的坐标表示为(a, b),现在我们需要将该向量逆时针旋转α角度,我们应该得到旋转后的向量坐标?

我们可以通过旋转前的坐标和选中角度,根据三角函数来计算旋转后的坐标。通过观察我们可以发现,在对向量进行旋转操作的前后两个向量的模并没有发生变换,即圆的半径。那么我们使用这个不变的量来表示旋转前后向量的坐标,假设圆的半径为r

旋转前的坐标:

旋转后的坐标:

封装一个Vector并实现rotate方法来计算旋转之后的向量坐标:

class Vector {
  x: number
  y: number
  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }

  /**
   * 旋转向量
   * @param rad 
   * @returns {Vector}
   */
  rotate(rad: number) {
    const s = Math.sin(rad)
    const c = Math.cos(rad)

    const { x, y } = this

    this.x = x * c - y * s
    this.y = x * s + y * c

    return this
  }
}

现在我们可以直接使用rotate方法来计算任意角度的向量旋转了

const v1 = new Vector(1, 1)

const rotateV1 = Vector.ratate(Math.PI * 30 / 180)

上面我们通过数学表达式来计算旋转之后的向量坐标 ,这对于简单的变换非常适用,但是当变换变得十分复杂的时候我们再使用数学表达式的方式来计算就会变得十分的繁琐了,比如我们需要旋转之后再平移某个向量,剪切某个向量的坐标。而在webGL中直接提供了矩阵类型和向量的变量,所以内置支持矩阵的乘法,故使用变换矩阵来对向量进行变换是更好的做法,省去了我们自己去进行复杂的数学计算。

最开始我们已经说过线性变换和变换矩阵了,我们假设旋转变换的变换矩阵是,让旋转变换矩阵作用于向量

在上面数学计算中我们知道旋转之后x、y轴的坐标分别为xcosβ - ysinβxsinβ + ycosβ,带入上面的矩阵可以得到:。那么旋转矩阵就是:

第一节我们知道变换矩阵的第一列数值表示变换之后水平基向量的坐标,第二列数值表示变换之后垂直基向量的坐标,通过计算发现:将基向量逆时针旋转β角度后,垂直基向量的坐标是(cosβ, sinβ)、水平基向量的坐标是(-sinβ, cosβ),所以在验证之后变换矩阵是正确的。

缩放变换矩阵

假设水平方向和垂直方向基向量的缩放比例分别是SxSy,那么在缩放后水平方向的基向量坐标为,那么缩放变换的变换矩阵为:

当然还有其他很多线性变换,你可以尝试写出其变换矩阵。

变换矩阵应用

在webGL中,默认内置支持矩阵类型的变量,所以我们只需要将变换矩阵传递给webGL程序中,让webGL自己处理即可,但是变换矩阵任然需要从JavaScript程序中将变量赋值给webGL变量所在的地址,然而在JavaScript中是没有矩阵类型的变量的,所以在JavaScript中是使用类型数组在存储矩阵数据的:

const matrix = new Float32Array([
	0, 1, 2, 1
])

上面就表示一个2x2的矩阵,但问题是数组表示是一维的,矩阵表示是二维的,webGL怎么知道如何将数组中每个元素和矩阵中的每个元素一一对应起来的?这里我们可以按照列主序和行主序来排序存储在数组中的矩阵元素,在webGL中,矩阵元素是按照列主序存储在类型数组中的。

列主序排列:

行主序排列:

在webGL使用旋转变换矩阵来旋转一个三角形的例子:

const canvas = document.querySelector('canvas')
const gl = canvas.getContext('webgl')

// x' = x * cosa - y * sina
// y' = x * sina + y * cosa
var vertex_shader =`
  attribute vec4 a_Position;
  uniform mat4 u_xformMatrix;
  void main() {
    gl_Position = u_xformMatrix * a_Position;
  }
`

const fragement_shader = `
  void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
`
const ANGLE = 60.0

function main() {
  const init = initShaders(gl, vertex_shader, fragement_shader)
  const n = initVertexBuffers()

  const radian = Math.PI * ANGLE / 180
  const cosB = Math.cos(radian)
  const sinB = Math.sin(radian)

  const 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,
  ])

  const u_xformMatrix = gl.getUniformLocation(gl.program, '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 - 0)
}

function initVertexBuffers() {
  const vertices = new Float32Array([
    0.0, 0.5,   -0.5, -0.5,   0.5, -0.5
  ])
  const n = 3
  // create a buffer object
  const vertexBuffer = gl.createBuffer()

  // bind the buffer object to target
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
  
  // write data into the buffer object
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)

  const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)

  gl.enableVertexAttribArray(a_Position)

  return n
}

function initShaders(gl, vShaderSource, fShaderSource) {
  const vShader = gl.createShader(gl.VERTEX_SHADER)
  gl.shaderSource(vShader, vShaderSource)
  gl.compileShader(vShader)
  
  const fShader = gl.createShader(gl.FRAGMENT_SHADER)
  gl.shaderSource(fShader, fShaderSource)
  gl.compileShader(fShader)
  
  const program = gl.createProgram()
  gl.attachShader(program, vShader)
  gl.attachShader(program, fShader)
  gl.linkProgram(program)
  gl.useProgram(program)
  
  gl.program = program
}

main()

最后

以上就是线性变换、变换矩阵及其在webGL中使用变换矩阵,后续将会继续介绍webGL中的图形学相关知识。