这篇文章总结一下图形学在游戏开发工程师面试时常考(可能会考)的问题。虽然图形学在面试中的比重比较小,但是还是要复习一下的。图形学的考点分为三部分,一是渲染管线,二是数学尤其是线性代数和三维几何。
一、渲染管线
1.1 有那几个坐标系(空间)?如何在空间之间转换?
有五个坐标系,分别是:
物体坐标系(本地坐标系)Local Space 或 Model Space
世界坐标系 World Space
观察者坐标系(摄像机坐标系)View Space
裁剪空间 Clipping Space
屏幕空间 Screen Space
其中前四个矩阵之间需要通过model, view , projection矩阵变换,裁剪空间到屏幕空间通过视口变换进行;前三个是三维空间,后两个是二维空间。
图1.1 坐标系(空间)的转换
所有的变换都发生在顶点着色器,经过顶点着色器,所有顶点都变成了屏幕上的二维坐标,下一步进行图元装配后进入几何着色器。
1.2 三个重要的空间变换矩阵?作用?
需要注意这三个矩阵都在顶点着色器中应用。
- Model matrix 模型矩阵。进行从物体坐标到世界坐标的转换。控制了物体的平移、旋转、缩放。在3D建模软件中为模型坐标,导入游戏后使用model matrix 进行大小、位置、角度的设置
- View matrix 观察矩阵。将世界坐标系变换到观察者坐标系,通过一些列平移、旋转的组合来移动整个场景(而不是移动摄像机,摄像机 是一个虚拟概念,事实上代码中并没有摄像机camera,而是用view matrix来表示摄像机,然后把view matrix附加到每一个物体,来模拟摄像机),用来模拟一个摄像机
- projection matrix 投影矩阵。将观察者坐标系转换到裁剪坐标系。将3D坐标投影到2D屏幕上,裁剪空间外的顶点会被裁剪掉,投影矩阵指定了坐标的范围。
1.3 视口变换是什么?
视口变换发生在投影到2D屏幕后,将投影之后归一化的点映射到屏幕上指定的一块区域。在OpenGL中,通过glViewPort指定。
1.4 渲染管线的流程?
下面的图更形象
其中,gl_Position在从顶点着色器输出后,会由OpenGL自己进行归一化和视口变换:
而片段处理程序的input是顶点处理程序的output经过了插值以后得到的值。
1.5 三种着色器分别有什么用?完成了什么过程?
- 顶点着色器。计算顶点的位置,并将顶点投影在二维屏幕上。
- 几何着色器。将形状(图元)划分为更多的形状(图元),影响后序的插值结果。
- 片段着色器。根据顶点着色器和几何着色器的输出插值,计算每一个片元的颜色。之后进行测试和混合后生成最终的像素。
1.6 什么是光栅化?
光栅(栅格化或者像素化)化负责的是整个渲染过程中的几何成像环节,把几何图元(点、线,面)投影到成像平面并确定哪些像素或采样点被图元覆盖。举例:
- 输入,一个三角形的三个顶点,(x0,y0,z0,w0) (x1,y1,z1,w1) (x2,y2,z2,w2)
- 输出,这个三角形会覆盖屏幕上哪些像素,可以认为是 Point2d [ ]
1.7 OpenGL中有哪几种缓冲?都有什么用?
- 帧缓冲Frame Buffer, 用于创建零时的渲染上下文,帧缓冲是一些二维数组和OpenG所使用的存储区的集合:颜色缓存、深度缓存、模板缓存和累计缓存。默认情况下,OpenGL将帧缓冲区作为渲染最终目的地。此帧缓冲区完全由window系统生成和管理。这个默认的帧缓存被称作“window系统生成”(window-system-provided)的帧缓冲区。
- 颜色缓冲 Color Buffer, 包含每个象素的颜色信息。颜色信息可以是颜色索引值(在颜色索引方式下),也可以是颜色的红、绿、蓝3个分量(在RGBA方式下),还可以存放表示物体透明程度的Alpha值。
- 深度缓冲 Depth Buffer, 包含每个象素的深度值。深度值与z坐标有关,描述物体上某点距离观察点的远近,也可以称它为Z缓存(Z Buffer) 。
- 模板缓冲 Stencil Buffer, 包含物体的模板值。模板值具有屏蔽作用,用于控制绘制的区域,使屏幕上某些区域可画,某些区域不可画。
- 累积缓存(Accumulation Buffer) 包含颜色信息。其可以合成一系列的绘制结果,实现某些特殊效果。
- 顶点缓冲 Vertex Buffer, 用于缓存顶点数据
- 元素缓冲 Element Buffer,用于缓存顶点序号数据
1.8 Alpha 混合的几种方式?
通用公式:
Color = Src *Srcfactor + Dst * Dstfactor
其中Color是混合结果,Src是源颜色向量也就是纹理本来的颜色,Dst是目标颜色向量也就是储存在颜色缓冲中当前位置的颜色向量, srcfactor和dstfactor分别是源因子和目标因子。先进入颜色缓冲区的是目标颜色,比如在红色方块上绘制绿色方块,则红色是Dst,绿色是Src。不同的 factor 导致了不同的混合方式:
注意,颜色常数向量可以用glBlendColor函数分开来设置。OpenGL中使用 void glBlendFunc(GLenum sfactor, GLenum dfactor)设置混合方式,接收两个参数,来设置源(source)和目标(destination)因子。OpenGL为我们定义了很多选项,我们把最常用的列在下面。注意,颜色常数向量[Math Processing Error]C¯constant可以用glBlendColor函数分开来设置。在使用alpha 混合前要开启 glEnable(GL_BLEND);
最常用的混合方式是 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1.9 颜色向量的计算
颜色向量(归一化的)有两种计算
- 数乘,n*color, n越大,结果越亮
- 点乘,colorA *colorB , 是A与B的混合,越大,越亮
1.10 GLSL 着色器程序的创建
1.11 GLSL数据传递有几种方式?
- uniform变量, uniform变量是外部application程序传递给(vertex和fragment)shader的变量。因此它是application通过函数glUniform**()函数赋值的。在(vertex和fragment)shader程序内部,uniform变量就像是C语言里面的常量(const ),它不能被shader程序修改。
- attribute变量, attribute变量是只能在vertex shader中使用的变量。(它不能在fragment shader中声明attribute变量,也不能被fragment shader中使用)。一般用attribute变量来表示一些顶点的数据,如:顶点坐标,法线,纹理坐标,顶点颜色等。在application中,一般用函数glBindAttribLocation()来绑定每个attribute变量的位置,然后用函数glVertexAttribPointer()为每个attribute变量赋值。
- varying(in/out)变量,varying变量是vertex和fragment shader之间做数据传递用的。一般vertex shader修改varying变量的值,然后fragment shader使用该varying变量的值。因此varying变量在vertex和fragment shader二者之间的声明必须是一致的。application不能使用此变量。
1.12 为什么要用齐次坐标系
- 方便进行平移变换
- 能够简化透视投影的计算
1.13 冯氏光照模型由哪三部分构成?
http://learnopengl-cn.readthedocs.io/zh/latest/02%20Lighting/02%20Basic%20Lighting/
光照要处理的就是光源颜色向量和物体颜色向量的点积。三种光照可以组合使用即 (ambient + diffuse + specular) * objectColor。
- 环境光 ambient .控制因素是 ambient strength环境光强度,和lightcolor数乘得到ambient环境光。然后在用ambient和物体颜色objectColor点乘。ambient strength由程序员指定。
- 漫反射 diffuse , 控制因素是 diff 散射因子,也是和lightcolor数乘得到diffuse漫反射光,然后在用diffuse河objectColor点乘。diff散射因子由法线与光线的夹角(点积)得到,漫反射光使物体上与光线排布越近的片段越能从光源处获得更多的亮度。为了更好的理解漫反射光照:
- θ越大,光对片段颜色的影响越小,反过来光线越靠近法线,对物体颜色的影响就越大。
- float diff = max(dot(norm, lightDir), 0.0); 两个向量之间的角度越大,散射因子就会越小:
3.镜面反射 specular,控制因素是 spec 反射强度。和环境光照一样,镜面光照(Specular Lighting)同样依据光的方向向量和物体的法向量,但是这次它也会依据观察方向,例如玩家是从什么方向看着这个片段的。镜面光照根据光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。
通过反射法向量周围光的方向计算反射向量。然后我们计算反射向量和视线方向的角度,如果之间的角度越小,那么镜面光的作用就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。
1.14 旋转的三种方法
- 旋转矩阵 4x4
- 欧拉角 yaw pitch roll
- 四元数
1.15 四元数 Quaternion 的概念和作用
四元数本质上是一种高阶复数(听不懂了吧。。。),是一个四维空间,相对于复数的二维空间。我们高中的时候应该都学过复数,一个复数由实部和虚部组成,即x = a + bi,i是虚数单位,如果你还记得的话应该知道i^2 = -1。而四元数其实和我们学到的这种是类似的,不同的是,它的虚部包含了三个虚数单位,i、j、k,即一个四元数可以表示为x = a + bi + cj + dk。那么,它和旋转为什么会有关系呢?
在Unity里,tranform组件有一个变量名为rotation,它的类型就是四元数。很多初学者会直接取rotation的x、y、z,认为它们分别对应了Transform面板里R的各个分量。当然很快我们就会发现这是完全不对的。实际上,四元数的x、y、z和R的那三个值从直观上来讲没什么关系,当然会存在一个表达式可以转换。
1.16 四元数、欧拉角、旋转矩阵的优点和缺点
矩阵旋转
优点:
旋转轴可以是任意向量;
缺点:
旋转其实只需要知道一个向量+一个角度,一共4个值的信息,但矩阵法却使用了16个元素;
而且在做乘法操作时也会增加计算量,造成了空间和时间上的一些浪费;
欧拉旋转
优点:
很容易理解,形象直观;
表示更方便,只需要3个值(分别对应x、y、z轴的旋转角度);但按我的理解,它还是转换到了3个3*3的矩阵做变换,效率不如四元数;
缺点:
之前提到过这种方法是要按照一个固定的坐标轴的顺序旋转的,因此不同的顺序会造成不同的结果;
会造成万向节锁(Gimbal Lock)的现象。这种现象的发生就是由于上述固定坐标轴旋转顺序造成的。理论上,欧拉旋转可以靠这种顺序让一个物体指到任何一个想要的方向,但如果在旋转中不幸让某些坐标轴重合了就会发生万向节锁,这时就会丢失一个方向上的旋转能力,也就是说在这种状态下我们无论怎么旋转(当然还是要原先的顺序)都不可能得到某些想要的旋转效果,除非我们打破原先的旋转顺序或者同时旋转3个坐标轴。这里有个视频可以直观的理解下;
由于万向节锁的存在,欧拉旋转无法实现球面平滑插值;
四元数旋转
优点:
可以避免万向节锁现象;
只需要一个4维的四元数就可以执行绕任意过原点的向量的旋转,方便快捷,在某些实现下比旋转矩阵效率更高;
可以提供平滑插值;
缺点:
比欧拉旋转稍微复杂了一点点,因为多了一个维度;
理解更困难,不直观;
1.17 多级渐近纹理 mipmap?有什么优缺点?
为了加快渲染速度和减少图像锯齿,贴图被处理成由一系列被预先计算和优化过的图片组成的文件,这样的贴图被称为 MIP map 或者 mipmap
多级渐进纹理由一组分辨率逐渐降低的纹理序列组成,每一级纹理宽度和高度都是上一级纹理宽度和高度的一半。宽和高不一定相等,也就是说,这些纹理不一定都是正方形。
优点:提高渲染速度,减少图像锯齿
缺点:会增加额外的内存消耗
1.18 片段和像素的区别 ?
- 片段是渲染一个像素需要的全部信息,所有片段经过测试与混合后渲染成像素。
- 片段是三维顶点光栅化后的数据集合,还没有经过深度测试,而像素是片段经过深度测试、模板测试、alpha混合之后的结果
- 片段的个数远远多于像素,因为有的片段会在测试和混合阶段被丢弃,无法被渲染成像素。
1.19 深度缓存算法(zbuffer算法)?
- 需要一个空间保存每个像素的深度,绘制前初始化所有深度为无限远,绘制时当前片段如果比zbuffer中的值大(说明更远),则跳过此片段,保留原来的渲染结果;否则,绘制此片段,并更新zbuffer。
- 可以处理对透明物体的消除
- 算法可以并行
- 与画家算法不同,不需要对物体排序
二、数学基础
2.1 平面上N个点,每两个点都确定一条直线, 求出斜率最大的那条直线所通过的两个点
平面上N个点,每两个点都确定一条直线,
求出斜率最大的那条直线所通过的两个点(斜率不存在的情况不考虑)。时间效率越高越好。
平面上N个点,每两个点都确定一条直线,求出斜率最大的那条直线所通过的两个点(斜率不存在的情况不考虑)。时间效率越高越好。
关于这道题,网上已经给出了解答要点:
3个点A,B,C,把它们的按x坐标排序。假设排序后的顺序是ABC,那么有两种情况:
1.ABC共线,则k(AB)=k(BC)=k(AC)
2.ABC不共线,则ABC将形成一个三角形,那么k(AC)<max(k(AB), k(BC))
其中k()表示求斜率。
所以程序的基本步骤就是:
1.把N个点按x坐标排序。
2.遍历,求相邻的两个点的斜率,找最大值。
时间复杂度Nlog(N)。
先把这些点按x坐标从小到大排序,斜率最大的两点必然是挨一起的两个点,所以排序O(n* lg n),遍历一次O(n)就够了