forthealllight / blog

📖我的博客,记录学习的一些笔记,如有喜欢,欢迎star
2.3k stars 225 forks source link

优雅的学习webgl(3)—webgl中的三维图形和矩阵变换 #52

Open forthealllight opened 5 years ago

forthealllight commented 5 years ago

优雅的学习webgl(3)—webgl中的矩阵变换和三维图形


    在前一章中我们了解了着色器和缓冲区,有了这两个我们可以绘制任意图形,本章我们在此基础上学习如何绘制三维图形以及如何让图形动起来.

  • 矩阵变换
  • 三维图形基础

这个系列的源码地址为:源码的地址为: https://github.com/forthealllight/webgl-demo

一、矩阵变换

    矩阵变换包含了平移、旋转和缩放,我们可以依次来看看如何实现这三种变换。以最简单的的三角形的平移为例,对于二维图形的平移就是x轴和y轴的坐标加上一定的分量:

const vsSource = `
    attribute vec4 a_Position;
    uniform vec4 u_Translation;
    void main() {
      gl_Position = a_Position + u_Translation;
    }
 `;

    上述就是顶点着色器平移的例子,只需要加上一个分量u_Translation,接着我们来看完整的实现三角形平移的例子:

  //顶点着色器
  const vsSource = `
    attribute vec4 a_Position;
    uniform vec4 u_Translation;
    void main() {
      gl_Position = a_Position + u_Translation;
    }
  `;
  //片元着色器
  const fsSource = `
    #ifdef GL_ES
    precision mediump float;
    #endif
    varying lowp vec4 vColor;
    uniform vec4 u_color;
    void main() {
      gl_FragColor = u_color;
    }
  `;
  const shaderProgram = initShaderProgram(gl, vsSource, fsSource)
  gl.clearColor(0.0, 0.0, 0.0, 1.0);  // Clear to black, fully opaque
  gl.clearDepth(1.0);                 // Clear everything
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
  gl.useProgram(shaderProgram);
  //开始绘制
  let vertices = new Float32Array([0.0,0.5,-0.5,-0.5,0.5,-0.5])
  //创建缓冲区
  let vertexBuffer = gl.createBuffer()
  //将缓冲区对象绑定到目标
  gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer)
  //向缓冲区对象写入数据
  gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW)
  let a_Position = gl.getAttribLocation(shaderProgram,'a_Position')
  //将缓冲区对象分配给a_Position
  gl.vertexAttribPointer(a_Position,2,gl.FLOAT,false,0,0)
  //开启attribute变
  gl.enableVertexAttribArray(a_Position);
  //设置三角形的颜色
  let u_color = gl.getUniformLocation(shaderProgram,'u_color');
  gl.uniform4f(u_color,1.0,1.0,1.0,1.0)
  gl.drawArrays(gl.TRIANGLES,0,3)
  //将平移距离传输给定点着色器
  let Tx = 0.5,Ty=0.5,Tz=0.0;
  let u_Translation = gl.getUniformLocation(shaderProgram,'u_Translation');
  gl.uniform4f(u_Translation,Tx,Ty,Tz,0.0)
  gl.uniform4f(u_color,0.5,0.5,0.5,1.0)
  gl.drawArrays(gl.TRIANGLES,0,3)

    做的事情其实也很简单,就是增加了一个u_Translation变量来表示平移量,且可以改变这个平移量来达到实现平移的效果,最后的结果如下图:

Lark20191210-203943

值得注意的在使用片元着色器的时候,片元着色器中float类型的变量必须制定精度:

precision mediump float;

这句话告诉片元着色器,它的浮点数的精度是中精度.

    上述的方法是完全通过原始的坐标相加减的方法来实现平移,复杂度不是很高,但如果涉及到旋转的话从纯坐标计算的方法会显得很复杂,我们知道三角形顶点的坐标等都是在欧拉坐标下的,我们可以用向量来表示三角形的坐标,用用矩阵来表示平移、旋转和缩放等变换,经过矩阵变换后得到的新的向量就是我们图形变换后的坐标。

    推理的过程就不详细描述,我们直接来看结果。假设一个图形,先经过平移,再旋转,最后缩放。那么模型矩阵应该是:

图形平移后的坐标 = (<伸缩变换矩阵> x <旋转矩阵> x <平移矩阵> )x 图形的初始坐标

这里的坐标就是用向量来表示,变换矩阵等都是用mat4矩阵来表示,一个4x4的矩阵可以完整的表述三维坐标下的任何变换。其中:

模型矩阵 = <伸缩变换矩阵> x <旋转矩阵> x <平移矩阵>

模型矩阵就是如上所示,所有变换的组合或者说复合就是模型矩阵。

    我们尝试用模型矩阵来模拟在上述三角形平移的例子:

//顶点着色器
const vsSource = `
    attribute vec4 a_Position;
    uniform mat4 u_xformMatrix;
    void main() {
      gl_Position = u_xformMatrix * a_Position;
    }
 `;

 //
 let u_xformMatrix = gl.getUniformLocation(shaderProgram,'u_xformMatrix');
 //将平移距离传输给定点着色器
  let Tx = 0.5,Ty=0.5,Tz=0.0;
  xformMatrix = new Float32Array([1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,Tx,Ty,Tz,1.0])
  gl.uniformMatrix4fv(u_xformMatrix,false,xformMatrix)

上述就用ELSE语言中的mat构建了一个平移变换矩阵来实现平移的。值得注意的是我们声明了一个数组xformMatrix = new Float32Array,将数组传入ELSE的mat矩阵变量是以列为主序的

上述三角形矩阵变换的例子可以看:https://github.com/forthealllight/webgl-demo/tree/master/demo4

从上述矩阵变换的例子中可以看出,如果用原生的数组来操作矩阵是在是太复杂了,在这里我们可以借助一些矩阵运算的库来实现(当然也可以自己封装一些矩阵运算的函数)。

接下来我们会借助:gl-matrix 来简化矩阵变换等操作.引入gl-matrix后,上述平移三角形的代码可以进一步的简化。

let translateMatrix = mat4.create(1);
//将平移距离传输给定点着色器
let Tx = 0.5,Ty=0.5,Tz=0.0;
mat4.translate(translateMatrix,translateMatrix,[Tx,Ty,Tz])
gl.uniformMatrix4fv(u_xformMatrix,false,translateMatrix)

引入gl-matrix会有全局的变量mat4,通过mat4来创建平移矩阵,十分方便。

上述引入gl-matrix的例子可以查看:

https://github.com/forthealllight/webgl-demo/tree/master/demo5

二、三维图形基础

    有了矩阵运算的基础知识之后,我们再来看,如何用webgl绘制三维图形。三维图形跟二维平面的图形区别很大,存在了视点、视线、投影、光照等等复杂的要素(当然二维图形中也存在这些要素,但是这些要素在二维平面中对渲染结果的影响不是很明显),我们先来介绍一下如何根据视点和视线确定一个视图矩阵。

视图矩阵

    我们前面提到所有的平移、旋转以及缩放等变换都可以用模型矩阵来表示,在三维场景下观察物体,我们可以用视觉矩阵来确定观察者视点和视线以及上方向的位置.

视点: 观察人所在位置在欧拉坐标系下的坐标

观察目标点:被观察的物体所在的点

视线:视点和观察目标点的连线

上方向:单一视点和视线还不能唯一确定所渲染的图形(可能上也可能下),上方向用于确定竖直方向。

总之:视点和观察点可以确定水平观测的方向和距离,上方向可以决定最后渲染的图形是正立还是倒立。

Lark20191213-171807

这里的视点、观测点和上方向都是三维的向量,因此我们可以用矩阵来表示,这个矩阵就是视图矩阵。在视图矩阵的基础上,我们可以修改上述图形坐标公式:

观测到的三维图形的坐标 = <视图矩阵> x 图形的原始坐标

如果进行了矩阵变换:

观测到的经过矩阵变换三维图形的坐标 = <视图矩阵> x <模型矩阵> 图形的原始坐标

结合上述的知识,我们来动手绘制一个三维的正方体。

着色器代码为:

//顶点着色器
attribute vec4 a_position;
uniform mat4 u_worldViewProjection;

void main() {
   gl_Position = u_worldViewProjection * a_position;
}
//颜色着色器

void main() {
   gl_FragColor = vec4(1,1,1,1);
}

    这里在顶点着色器中u_worldViewProjection变量就是与视图矩阵有关,但不完全等于视图矩阵,除了视图矩阵外,其实还有投影矩阵,视图矩阵在投影矩阵的部分是可视的。对于投影,我们下章会详细介绍。

决定u_worldViewProjection变量值的操作过程如下:

var worldViewProjectionLoc =
        gl.getUniformLocation(program, "u_worldViewProjection");

//投影矩阵
var fieldOfView = Math.PI * 0.25;
var aspect = canvas.clientWidth / canvas.clientHeight;
var projection = m.perspective(fieldOfView, aspect, 0.0001, 500);
var radius = 5;
var eye = [
  Math.sin(clock) * radius,
  1,
  Math.cos(clock) * radius,
];
var target = [0, 0, 0];
var up = [0, 1, 0];
//视图矩阵
var view = m.lookAt(eye, target, up);
//将视图矩阵乘投影矩阵就得到了可视区内的矩阵
var worldViewProjection = m.multiplyMatrix(view, projection);
gl.uniformMatrix4fv(worldViewProjectionLoc, false, worldViewProjection);

    这里的投影矩阵我们下一章会详细介绍,这里只要记住,在三维图形的渲染中,可视区由视觉矩阵和投影矩阵共同决定,视觉矩阵决定了人和物体的观测位置,而投影矩阵决定了如何成像。

完整的代码地址为:https://github.com/forthealllight/webgl-demo/tree/master/demo6

整个例子的最后渲染结果为:

Lark20191215-202310

    下一章我们会将投影,视觉矩阵和投影矩阵一起才能决定在画布上如何渲染一个完整的三维图形。通过本章我们知道了,图形的任何变换都可以用矩阵来表示,通过模型矩阵我们可以定义图形的变换,同时了解了三维图形在观测过程中的视图矩阵。