前端建模基础

认识计算机图形学(CG)

计算机图形在前端中最常见的应用就是数据统计了吧,echartG2等等插件提供了多样的数据图,这些插件通过将大量的数据转化成可视形式,其数据趋势与线性关系得以更容易的展示出来,使得数据在前端的显示更为直观与优雅

另外,新兴起的VR(virtual-reality)技术,也在不少的前端页面中出现,VR交互中,用户可与三维场景中的对象进行交互。此类展现方式可以让用户更好的与设计者沟通,使用户体验得到增强

CG是以建模,绘制,交互,动画,影像为核心内容并服务于各个领域的一门科学。
对于前端工程师来说,建模就是指使用前端库,通过数据来表达物体和场景并构建对应的数据化三维模型的过程。
绘制则是指将这些三维模型通过一些计算和变换,最终在屏幕上渲染出来的过程,也是从几何模型到像素的转化。
所以,工程师使用计算机语言输入的是三维模型,而通过计算机模拟人类的视觉功能,对三维模型进行加工处理,最终在屏幕上输出的是二维图像。虽然输出的是二维图像,但是通过计算机的处理,图像中的物体之间的关系符合人类对现实世界的认知,从而使大脑形成三维空间的感知。

基础概念

图形是由点,线,面等几何元素组成而成的,这些元素统称为图元。简单的图元可以组成一些立体图元,从而又组合成简单的几何模型,复杂的模型又通过这些简单的模型组合而成,通过模型变换整合坐标系最终形成复杂场景。在threejs中的场景图就是这样一层一层的构建的。

正如上述,创建的三维模型最终要在显示器中显示,而显示器是基于像素的二维图像显示,所以需要将图形经过一系列的几何变换映射成屏幕坐标中的二维图像,确定图元对应的图像坐标位置,其中涉及各种物理坐标的转换,最后还要经过光栅化处理(确定指定分辨率下哪些像素被覆盖,以何种颜色进行色彩显示)。最后才显示在显示器中。
这一系列的过程也称做GPU的图形流水线(也称渲染流水线或渲染管线),实际的应用中,图形流水线是十分复杂的,以OpenGL为例,按照流水线操作的对象大体可分为两类操作:

🔺坐标的几何变换

几何变换包括了:

  • 模型变换:将物体的局部坐标系变换到世界坐标系,比如three中的场景图将一个模型自身的坐标系融合进sence中,否则模型的动画计算量将无可估量
  • 视点变换:将场景中的物体模型转化到以人眼为中心的坐标系下表示,方便人眼视网膜投影成像
  • 投影变换:将眼睛坐标系通过投影产生二维图像,这就像是three中的camera,使用正交投影或者透视投影减少一个维度,生成二维图像
  • 窗口变换:最后将二维图像进一步像素化,映射到屏幕窗口上,显示出来

几种变换的原理都是矩阵变换,几何操作中使用矩阵变换与canvas的矩阵变换类似,因为使用齐次坐标系,所以使用四阶矩阵表示线性关系

1const point = vec(x, y, z, 1)
2const matrix = [
3  a, d, g, j,
4  b, e, h, k,
5  c, f, i, l,
6  0001
7]
8// j k l 表示x y z的平移
9// a e i 表示x y z的缩放
10// ehfi, agci, adbe (cos⌀ - sin⌀ sin⌀  cos⌀)表示x y z的旋转

🔺光栅化

光栅化处理包括了:

  • 简单图元生成
    复杂的图形是由基本的图元组合而成的,因此首先需要使用算法将图元变为像素集合。例如:

    图片摘自《现代计算机图形学基础》
  • 多边形填充
    确定屏幕上哪些像素位于多边形内部
  • 剔除
    将不会或不需要在屏幕上显示的图元排除,减少图形绘制的计算量
    包括视域剔除,小物体剔除,背面剔除,退化剔除等等
  • 可见性判断
    深度测试,判断模型是否被遮挡
    深度缓存算法(z-buffer):按照越在后面的物体深度z值越大的原则,每个像素只填充z值最小的图元数据
    但是深度问题一直是3D场景中难以处理的问题,就像和平精英这样的游戏中也避免不了类似的bug,远处观察时可以穿透建筑或是树看到后面的人,这就是因为显示器分辨不出哪个在前哪个在后的问题
  • 其他(纹理贴图,透明,反走样,阴影,雾化,模糊)

🔺数字几何

几何模型是从数学形状的角度描述物体的外观。通常建模使用多边形网格表示。
由顶点、边和多边形面组成的集合来表示的几何形状称为网格(Mesh)。其中由三角形面片组成的三角形网格更加的稳定与灵活,成为前端建模的首选。

通过对网格的编辑,操纵和修改基础几何模型,可以实现对复杂模型的快速建模

🔺图形硬件

显卡的功能是将计算机需要显示的数据信息转换为显示器信息,并驱动其显示的计算机硬件。在GPU出现之前,图形流水线的操作大多由CPU来完成,显卡上只能处理一些简单的算法。而随着图形显示功能的需求发展,CPU的串联工作模式已经不能满足数以亿计的像素计算,(一台1920*1080的显示器,一帧就有约200万的像素需要计算,60Hz的刷新率,一秒就有上亿的计算量)。所以以并联工作方式为基础的GPU得以迅速发展,GPU是由数以千计的更小更高效的核心组成的大规模并行计算构建,专门为同时处理多重任务而设计的。
支持可编程GPU(Graphic Processing Unit)图形处理器的出现,取代了部分原本由CPU执行的工作(主要是几何变换和光栅化),GPU更适合处理大量并行而相单一的逻辑,这样就减轻了CPU的负担,并提升了绘制效率。

随着开发者需求的增长,上述固定渲染流水线已经满足不了开发者的需求,所以一些用来代替固定渲染管线的可高度编程程序算法出现了,这就是着色器。通过着色器,开发者可以自己编写显卡渲染的相关算法,可以实现各种各样的图像效果而不用受显卡的固定渲染管线限制。

WebGL渲染管线

创建顶点数组。这些数组包含顶点属性,以及有关顶点的纹理、颜色或光照(垂直顶点)如何影响顶点的信息。

然后,通过将顶点数组中的数据发送到顶点缓冲区,以便GPU快速读取。我们还提供指向顶点数组元素的附加索引数组。它们控制顶点稍后如何组合成三角形。

GPU 首先从顶点缓冲区中读取每个选定的顶点,并通过顶点着色器运行它。顶点着色器将一组顶点属性作为输入并输出一组新的属性。

然后,GPU 连接投影的顶点以形成三角形。它通过按 indexs 数组指定的顺序获取顶点。

光栅器获取每个三角形,对其进行裁剪,丢弃屏幕外部的部分,并将剩余的可见部分分解为像素大小的碎片。顶点着色器其他顶点属性的输出也会在每个三角形的栅格化表面上进行插值,从而为每个片元分配一个平滑的值渐变。

然后,生成的像素大小的片元通过片元着色器。片元着色器输出每个像素的颜色和深度值,然后将其绘制到帧缓冲区中。

帧缓冲器是渲染作业输出的最终目标。帧缓存包括颜色、scissor、alpha、stencil、depth这些缓存,所以帧缓存不是一片缓存,而是所有这些缓存的组合,帧缓冲器还可以具有深度缓冲区和/或模具缓冲区,这两者都可以选择在片元绘制到帧缓冲器之前对其进行过滤。
深度测试会丢弃位于已绘制对象后面的对象中的片元,而模具测试使用绘制到模具缓冲区中的形状来约束帧缓冲器的可绘制部分,从而“模具化”渲染作业。在这两个滤镜中幸存下来的片元的颜色值 alpha 与它们覆盖的颜色值混合在一起。最终的颜色、深度和模具值将绘制到相应的缓冲区中。缓冲区的输出还可以用作其他渲染作业的纹理输入。

GLSL ES

专门用来为着色器编程的编程语言就是着色器语言。区别于大多数运行在CPU中的语言,着色器语言运行在GPU中,与OpenGL相配合的着色器语言是GLSL,应用在客户端,而与WebGL配合的着色器语言是GLSL ES,应用在浏览器平台。
WebGL着色器代码分为顶点着色器代码和片元着色器代码两部分,通过WebGL编译处理后,最终在GPU的顶点着色器单元与片元着色器单元上执行。

首先,与OpenGL配合的GLSL使用C语言作为基础高阶着色语言,避免了使用汇编语言或硬件规格语言的复杂性。GLSL ES语言是则是在GLSL着色器语言的基础上,删除和简化一部分功能后形成的,其语法与C语言的较为类似。

🔺基本数据类型

着色器语言GLSL的基本数据类型和C语言一样具有常见的整型数int、浮点数float和布尔值bool类型数据。

🔺声明一个变量/常量

着色器语言ES GLSL和C语言一样属于强类型语言,声明一个变量需要定义变量的数据类型

1// 着色器语言定义一个整形常量
2int count = 10;
3// 定义一个浮点数变量num,并赋值10.0
4float num = 10.0;
5// 声明一个数据类型是布尔值的变量,并赋值为true
6bool lightBool = true;
7
8// 着色器语言定义一个整形常量
9const int count = 10;

🔺向量

1vec3 dirction = vec3(1.00.00.0)
2dirction.x = 1.0
3dirction.y = 0.0 + 2.0
4// dirction === vec3(1.0, 2.0, 0.0)
5
6vec2 v2 = v3.xy
7// v2 === vec2(1.0, 2.0)

🔺矩阵

1// 对角矩阵可以简写
2// 2.0 0.0 0.0 0.0
3// 0.0 2.0 0.0 0.0
4// 0.0 0.0 2.0 0.0
5// 0.0 0.0 0.0 2.0
6mat4 matrix = mat4(2.0)
7// 需要表示的矩阵
8// 1.1 1.2 1.3 1.4
9// 2.1 2.2 2.3 2.4
10// 3.1 3.2 3.3 3.4
11// 4.1 4.2 4.3 4.4
12// 注意行列对应关系,按照列的先后顺序,从上到下依次传入mat构造函数参数中
13mat4 matrix4 = mat4(
141.1,2.1,3.1,4.1,
151.2,2.2,3.2,4.2,
161.3,2.3,3.3,4.3,
171.4,2.4,3.4,4.4
18);
19
20// 访问矩阵matrix4的第二列
21vec4 v4 = matrix4[1];//返回值vec4(1.2,2.2,3.2,4.2)
22// 访问矩阵matrix4的第三列第四行对应的元素
23float f = matrix4[2][3];//返回4.3

🔺数组

和javascript语言、C语言一样 可以声明数组类型变量,不过WebGL着色器的数据仅仅支持一维数组,不支持多维数组。

🔺条件语句/fors语句

关于if语句、for语句的使用,和javascript语言逻辑规则一致

🔺函数

函数有返回值时,函数计算后需要返回的值通过关键字return返回,注意声明函数时候,函数名称前需要声明return返回值的数据类型。

1int add(int x,int y){
2  return ...
3}

自定义函数无返回值的时候,和主函数main一样,使用关键字void声明函数。

🔺结构体

结构体主要功能就是利用WebGL着色器已经提供的常见数据类型,自定义一个新的数据类型。

1struct newVar {
2  int a;
3  float b;
4};
5
6uniform newVar myVar;
7
8myVar.a = 8
9myVar.b = 8.0

🔺内置变量

🔺attribute、uniform 和 varying

🔺discard

舍弃片元,可用于光栅化中的剔除操作

WebGL

WebGL是一个JavaScript API,它允许我们直接在浏览器中实现交互式3D图形。WebGL API多数与GPU硬件相关,GPU硬件(渲染管线)=>显卡驱动=>操作系统=>浏览器=>WebGL API为上一层提供接口,开发者调用WebGL API控制图形处理单元

几乎整个WebGL API都是关于如何设置着色器的状态值以及运行它们。
对于想要绘制的每一个对象,都需要先设置一系列状态值,然后通过调用 gl.drawArrays 或 gl.drawElements 运行一个着色方法对(一个着色程序),使得你的着色器对能够在GPU上运行。

这边不详述WebGL的各个API,只通过下述例子简单实践管线渲染的流程,从而更好的理解前文内容,更多的请自行查阅官方文档
实际上学习WebGL是为了学习Web3D知识,而通过原生WebGL直接编写程序,会比较麻烦,实际开发中一般会使用Three.js这样或那样的库

1<!-- html -->
2<!DOCTYPE html>
3<html lang="en">
4  <head>
5    <meta charset="UTF-8" />
6    <title>WebGL</title>
7  </head>
8  <body>
9    <!--canvas标签创建一个宽高均为500像素,背景为蓝色的矩形画布-->
10    <canvas id="c"></canvas>
11    <script>
12      // js (内置vue3(Vue),jQuery($))
13      var canvas = document.querySelector("#c");
14      var gl = canvas.getContext("webgl");
15
16      // 着色器代码
17      var vertexShaderSource = `
18        attribute vec4 a_position;
19        void main() {
20          gl_Position = a_position;
21        }
22      `
;
23      var fragmentShaderSource = `
24        precision mediump float;
25        void main() {
26          gl_FragColor = vec4(1, 0, 0.5, 1); // return redish-purple
27        }
28      `
;
29
30      //创建顶点着色器对象
31      var vertexShader = gl.createShader(gl.VERTEX_SHADER);
32      //创建片元着色器对象
33      var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
34      //引入顶点、片元着色器源代码
35      gl.shaderSource(vertexShader,vertexShaderSource);
36      gl.shaderSource(fragmentShader,fragmentShaderSource);
37      //编译顶点、片元着色器
38      gl.compileShader(vertexShader);
39      gl.compileShader(fragmentShader);
40      //创建程序对象program
41      var program = gl.createProgram();
42      //附着顶点着色器和片元着色器到program
43      gl.attachShader(program,vertexShader);
44      gl.attachShader(program,fragmentShader);
45      //链接program
46      gl.linkProgram(program);
47      //使用program
48      gl.useProgram(program);
49
50      // 从创建的着色程序中找到属性【a_position】值所在的位置
51      var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
52      // 属性值从缓冲中获取数据,所以我们创建一个缓冲
53      var positionBuffer = gl.createBuffer();
54      // 绑定一个数据源到绑定点,然后可以引用绑定点指向该数据源
55      gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
56      // 创建顶点数据
57      var positions = [
58        00,
59        00.5,
60        0.70,
61      ];
62      // 通过绑定点向缓冲中存放数据
63      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
64
65      // 设置背景色
66      gl.clearColor(0001);
67      gl.clear(gl.COLOR_BUFFER_BIT);
68
69      // 允许顶点着色器读取GPU(服务器端)数据
70      gl.enableVertexAttribArray(positionAttributeLocation);
71      // 需要告诉WebGL怎么从我们之前准备的缓冲中获取数据给着色器中的属性
72      var size = 2;
73      var type = gl.FLOAT;
74      var normalize = false;
75      var stride = 0;
76      var offset = 0;
77      gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);
78
79      // 设置primitiveType(图元类型)为 gl.TRIANGLES(三角形)
80      var primitiveType = gl.TRIANGLES;
81      // 着色器将运行三次
82      var count = 3;
83      gl.drawArrays(primitiveType, 0, count);
84    
</script>
85  </body>
86</html>

04-11 21:52