10.1 着色频率

  • 上一节我们说到了一个简单的着色模型,这些着色模型是应用在一个点上的,我们叫做着色点,但不同的应用对象也会有不同的效果。

10.1.1 Flat着色

  • 这种着色方式所对应的着色‘点’,实际上是一个面,我们在blender里新建一个球体,如果面数很低的话,它就会呈现出下面这种样子,可以看到着色是分块进行的,过度非常不自然,也被称为平直着色。

  • image.png
    10.1.1 Flat着色下的球体
  • 说回到原因,其本质是因为,我们着色时是需要法线这个重要参数的,而Flat着色,每个面只有一个法线信息,也就是该面对应的法线,因此我们只能得到这样的效果。

10.1.2 Gouraud着色

  • 这种着色方式相比之前的Flat着色是逐顶点的,也就是说计算每一个顶点的法线数据,以此来进行着色计算,这种计算方式要比Flat方式更加平滑,其实就是根据顶点附近的几个面的法线信息进行平均计算,最后得到这个顶点的法线值。

  • image.png
    10.1.2 Gouraud着色下的球体
  • 可以看到有很明显的区别,着色明显更平滑光滑了,但我们依旧可以继续提升它,尽管此着色方式的效果已经很不错了。

10.1.3 Phong着色

  • Phong着色是逐像素的着色方式,这种着色方式应用了逐像素的插值,将每一个点的着色信息全部计算,是最光滑最均匀的着色方式,因为它已经精确到了像素。这种计算方式需要一个全新的知识——重心坐标,我们同样会讲到,现在来看看它的表现。

  • image.png
    10.1.3 Phong着色下的球体

  • 可以看到相比上一幅图,它的明暗过度更为细腻丝滑,这是因为每一个呈现在屏幕上的像素都会被计算到,因此过度的非常自然,当然代价就是,它的计算量很大,会很消耗性能,因为在编写着色器的时候,这部分内容将被放在片元着色器中,这使得其会消耗更多的性能,本节我们将重新认识渲染管线,相信经过着色部分的学习,你应该对渲染管线有了更加深入地理解。

10.1.4 添加更多的面

  • 最后的效果除了着色频率带来的影响,模型本身同样不可忽视,在相同的着色状态下,一个低面数的模型效果往往比高面数的模型效果差。

  • image.png
    10.1.4 两个同样运用Flat着色下的不同精度的球体
  • 但它的计算消耗也是巨大的,如果你在场景中添加过多的高面数模型,一样会消耗性能,这时就需要去均衡考虑效果,来决定使用不同的方案。

10.2 再谈渲染管线

  • 直至目前,我们已经完成了将物体渲染到屏幕上的最基本操作,我们得到了一个有着基本亮暗面的模型,下面我们来回顾一下我们都做了些什么。

10.2.1 顶点变换部分

  • 首先,我们在一开始从点入手,了解了顶点是如何经过一系列变换最终变换到屏幕视口坐标下的,这为我们的一切提供了基础。这是管线的第一部分。
  • image.png
    10.2.1 渲染管线顶点变换部分

10.2.2 三角形部分

  • 随后,我们将对应的顶点运用算法,将其绘制为三角形片元,作为一切的基础绘制,之后我们开始了光栅化阶段,我们运用了深度缓冲等技术,解决了物体映射到屏幕上的先后顺序,成功绘制了一个带面的模型(如果你跟随实践的话)。
  • image.png
    10.2.2 渲染管线三角形光栅化阶段

10.2.3 逐片元操作部分

  • 随后便是我们的着色部分,这一部分我们真正赋予了三角形模型灵魂,我们给予了它明暗面的变化,并且了解了着色模型的着色方式,知道了一个很简单的着色模型,布林冯模型,并将其应用在了我们的模型之上。

  • image.png
    10.2.3 基本的渲染管线概览

10.2.4 概述

  • 至此我们从头至尾大概了解了什么是渲染管线(渲染流水线),并跟随实践了其中一些内容,完成了一个最简单的软光栅渲染器(它真的很简单)。我们了解了每个步骤都做了些什么,正如我之前所说,其中有些部分是可配置的,有些则是可编程的。用于编程他们的语言就是所谓Shader语言,着色语言,比如大家从基础课一开始认识坐标系时就了解到的OPenGL的GLSL,DX的HLSL以及CG,都是作为编程语言编写Shader使用的。
  • 我们用它来对渲染管线中我们希望定义的部分进行编写。这就催生了两个最常用且应该是必备的着色器阶段。它们分别是VertexShader(顶点着色器),FragmentShader(片元着色器)。
  • 讲到这里,我们终于可以来看一下我们的代码了,相信我,很想跟大家分享这部分,我们可以开始干活了!

10.2.5 一个简单的顶点着色器

  • 后续会有专题文章分享着色器语言的编程学习,这里只作为认识,介绍一下着色器是什么。
  • 首先一个基本的着色器需要有基本的输入,这也叫做参数,在我最常写的CG语言之上的ShaderLab中,你需要在Shader中声明你传入的变量,它可以是浮点型(注意Shader中一般只有浮点型作为数字变量类型),2D纹理,颜色值等等。
1
2
3
_Color("MainColor", Color) = (1,1,1,1)
_Float1("Float_1",Float) = 0.1
_Float2("Float_2",Range(0,1)) =0.5
  • 这样就声明了一个变量,当然在GLSL中你得这么写
1
2
uniform float Float_1;
uniform vec4 Color;
  • 随后,你就可以在你的着色器中计算了。

  • 在着色器计算部分,你可以将其理解为一个函数体,它有参数也有返回值,不同的语言定义的着色器都不同,我们这里还是展示两种最主流语言的书写方式。
1
2
3
4
5
6
7
8
9
struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
...
};
struct v2f{
float4 pos : SV_POSITION;
float3 texcoord : TEXCOORD;
};

  • 首先,在Shaderlab中的CG风格语法中,我们首先需要定义结构体,这个结构体正如其名,是用来在两个着色器之间传递参数的。

  • 随后我们就可以定义着色器代码了,顶点着色器需要使用v2f类型,这是因为其返回值应为传递给片元着色器的返回值类型,而其参数则为a2v 对象。
1
2
3
4
5
v2f vert(a2v v)
{
v2f o;
...
}

  • GLSL中则一般片元着色器和顶点着色器分开,并定义统一的传入传出标识符。
1
2
3
4
5
6
7
out vec2 fragUV;
void main()
{
gl_Position = vec4(pos,1.0);
fragUV = vec2(uv.x,1.0-uv.y);
}


  • 片元着色器部分与顶点着色器类似,只不过这部分是真正向外输出的部分,因此返回的就不是结构体类型,而是颜色了。之后会单独出详细的文章分享,接下来让我们关注另一个重要的问题,我们该如何处理逐像素的颜色?换种说法,我们该如何呈现纹理?

10.3 纹理UV与重心坐标

  • 经过前文的介绍,在着色时,有一项参数我们其实并没有说清楚,那就是漫反射系数,在实际的游戏,场景之中,漫反射系数在每一个材质的每一个点上其实都是不一样的,而定义它的方式,实际上就是用纹理贴图来定义,接下来我们来看纹理贴图的概念。

10.3.1 纹理贴图基础概念

  • 为什么会出现纹理贴图,这是由于对画面表达的丰富度与便捷性的结合而催生出的产物,纹理贴图可以快速的记录一组参数数据,并以直观的图像形式呈现,便于复用扩展修改

  • 纹理贴图实际上就是一张图片,其本身通过一系列操作映射到物体表面。纹理贴图有很多种类,包括但不限于漫反射贴图,法线贴图,粗糙度贴图,环境光遮蔽贴图,高光贴图,视差贴图等等。
  • image.png
    10.3.1 一组纹理图像
  • 那么纹理图像是如何映射到模型上的呢?这就需要提到其中的操作,便是纹理映射,纹理映射自然需要坐标去描述,这就是我们常常听到的UV坐标。

  • UV坐标是一组二维坐标,其数值范围是0-1,不论纹理图像本身大小如何,我们都会用这个区间去描述它,相当于你可以理解为纹理图像是面料,但最后穿在身上的才是衣服,而如何穿着在身上就纹理映射的过程。
  • image.png
    10.3.1.2 纹理坐标图

  • 从这张图可以看到,颜色对应的是绿色和红色,为什么是这两种颜色呢?前面我们提到RGB颜色中我们将其表示为0-1之间的数值,所以三个值分别对应的就是红绿蓝。因此在这个范围内,我们可以发现它便是红绿两色的混合值。
  • 我们用这个坐标进行纹理的映射,但这个坐标是建立在模型空间之上的,我们利用一些方法去将模型空间坐标对应在这张纹理坐标上,然后去寻找该点在纹理图像上对应的纹理值

  • image.png
    10.3.1.3 纹理映射的过程
  • 因此实际上纹理坐标只是便于平铺纹理的工具,我们会在很多DCC软件中捡到纹理坐标的应用,也就是我们所说的展UV的过程,实际上就是将贴图与模型上的点进行对应,中间的桥梁叫做纹理坐标。讲到这里各位应该大概明白是什么意思了吧,很不直观,不过现在如果再去软件中操作一下,应该会有不一样的体会

  • image.png
    10.3.1.4 Blender中的UV编辑
  • 大家可以看到上图之中,实际上我们选择的是苹果上表面的顶点,其在左侧UV坐标中对应的区域就是我们现在所看到的样子,很直观的可以体会到实际上物体的顶点是与材质如此一一对应的。

10.3.2 重心坐标

  • 重心坐标是用三角形三个顶点来描述三角形内任意一点的方式。
  • 三角形内任意一点都可以用重心坐标来表示,重心坐标由三角形三点乘以三个系数来表示,且这三个系数和为1
  • image.png
    10.3.2 重心坐标

  • 那么如何求这三个系数呢?我们可以利用三角形面积关系来表示。
  • 三个系数所对应的面积是其对边所成三角形的面积,用这个面积作为分子与整个分母面积相除,得到的就是系数。
  • 而且我们也可以只计算两个值,由定义求出第三个值。
  • image.png
    10.3.2.1 重心坐标系数表示方法

10.3.3 重心坐标的应用——插值

  • 先前我们提到,我们想通过一些手段得到一个片元上每一个像素的颜色值,这就需要我们使用纹理。而纹理覆盖在每一个片元上的方式,就是插值。重心坐标就是一种插值手段。
  • image.png
    10.3.3 重心坐标的应用
  • 但是重心坐标同样存在问题,那就是可能在投影变化下出错,因为在三维空间中使用二维的重心坐标容易出现错误,所以我们应该在三维空间下插值后再去映射到二维坐标。

10.3.4 纹理的放大与优化

  • 纹素(Texel——texture pixel)也被成为纹理元素,其是纹理空间中的基本单位,也是我们在查找纹理颜色时搜索的基本单位。
  • 在纹理放大时我们就需要通过在纹素上做文章来尽量减少因为多个纹素同时映射在同一个像素内而导致的图片模糊问题,由此我们得出了我们的优化方法——双线性差值法。

10.3.5 双线性差值(Bilinear Interpolation)

  • 在了解双线性插值前,我们首先要先了解线性插值,线性插值是什么呢?如果从数学上定义很像我们画一条直线的方法。画一条直线的时候我们需要知道斜率,而如果知道斜率实际上就等于我们知道了这条线的走向,就是说我们可以定义这条线上任意一点的值。
  • image.png
    10.3.5 线性插值

  • 但在图形学应用中实际上我们可以将其抽象为占比表示,这也是很多真实应用时的方法,我们定义
  • lerp(x,v0,v1)= v0 + x(v1 - v0)
  • 这样我们就可以表示v0到v1这条线段上任意一点了。
  • 正如前文所说,我们应用线性插值的目的在于解决在低像素情况下任意一点所对应纹素值的问题。因此我们就可以使用这个插值方法,先找到临近的四个像素中心,随后对其进行插值,因为t,s长度均为1,实际上就是一个百分比计算
  • image.png
    10.3.6 双线性差值——1

  • 接着,由上述操作我们可以得到u1,u2,它们分别是对上下两组像素坐标进行插值得到的结果。随后就要体现我们的‘双’线性插值了。我们将再对这两个值在竖直方向上做插值,最后就能得到对应的我们想要的任何一点的值了。
  • image.png
    10.3.7 双线性差值——2


10.3.5 过大的纹理带来的问题

  • 前文我们讨论了如果一个纹理比对应的像素小,该如何优化其显示,接下来我们来看反之的情况,若过大的问题映射在小像素中呢?
  • 下面这幅图像,右图我们可以看到,远处得到纹理出现了摩尔纹,希望你还记得摩尔纹的概念,摩尔纹就是走样的一种形式,说明我们的图像出现了走样,可为何会出现走样呢?
  • image.png
    10.3.5 大纹理映射在屏幕上出现了摩尔纹

  • 这就要回顾我们之前提到的走样的概念,走样就是高频信息的叠加,就是采样频率慢于纹理变化频率。解决方法我们当时也提出了,那就是增加采样率的诸多方法,其中最有代表性的就是MSAA。希望你还记得。
  • 分析之后我们就可以得出一个简单的解决方案,那就是使用算法进行超采样。但换来的却是庞大的数据开销,会严重影响性能。所以我们还要讨论别的解决方案,这就引出了一个非常重要的概念,那就是范围查询。
  • 我们先前讨论的都是点查询,何谓点查询,其实就是聚焦于每一个点上的属性,我们之前聚焦的是采样点,而现在,我们需要快速得出一片纹理范围内对应的像素属性,这时候,就需要使用范围查询优化性能了,其中最具代表性的方式就是MIPMAP,也叫多级纹理。

10.3.6 MIPMAP

  • MipMap这个概念其实在引擎使用之中或多或少大家可能都看见过,比如如果你在Unity打开一个纹理对象,它的面板中就会有这一选项。
  • image.png
    10.3.6 Unity面板中的MIPMAP视窗

  • MipMap顾名思义,就是小纹理的意思。为了解决范围查询问题,我们将一张纹理上任意相邻像素求其平均并缩放到新一级纹理的一个纹素上,这样。最终我们一定会得到一张单个像素的纹理值(如果其符合方形并且是2的指数次方的像素数的话)
  • MIPMAP会由引擎自行生成,而其所占内存总量仅为原本原图的4/3倍,这是个级数求和问题,最后得出仅仅会多占1/3的空间,是一个非常好的算法。
  • 而我们该如何将像素对应到纹素并进行查询呢?

  • 首先,我们先在对应片元上寻找我们感兴趣的点,接着,需要将其映射到Uv坐标下,这个过程其实是在求对应像素的偏导值,在此之后我们就能在对应的UV坐标下获取这个片元像素对应的范围了。
  • image.png
    10.3.6.1 像素映射UV坐标的过程

  • 接着,我们该如何获取这个区域的颜色呢?这就需要我们找到对应的MIPMAP,现在需要一点数学知识。我们知道每一级是上一级对应的两个像素之和,因此共有log2N级MIPMAP,我们只需要去找对应的矩形范围在哪一级变为一个像素范围即可,因此可以得出以下公式:
  • image.png
    10.3.6.2 MIPMAP层级对应公式
  • 现在我们就可以查找任意像素对应的颜色范围了,因为最终都会对应到一张MIPMAP只需要获取对应的颜色即可。

  • 我们还能继续优化吗?当然可以,现在我们只有10张图,肯定无法满足所有情况下的查找,如果像素覆盖范围在两张图之间,我们该如何取舍?这时候就要继续用到我们先前所学的概念,那就是插值。
  • 这次我们要对两张MIPMAP进行插值。
  • image.png
    10.3.6.3 三线性插值

10.3.7 各向异性过滤

  • 前文所提到了三线性插值,虽然能够获得一个较为平滑的过度,但仍然无法解决一些问题,比如如果一个区域被拉伸为长方形而不是正方向,就无法查询了。这时就会诞生出一个新的解决方法,那就是各向异性过滤。
  • 各向异性过滤在游戏中有广泛的应用,它的原理实际上很简单,就是通过按不同比率压缩图像,实际上这也叫做RipMap
  • image.png
    10.3.7 各向异性过滤RIpMAP

  • 而在引擎中,实际上我们并不是这样做的,因为这样做会消耗很大的内存空间,我们会仍然复用MIPMAP,但这时我们会用同区域的最短边区域查找对应的MIPMAP层级,并沿着各向异性的方向多次采样,这里调节的采样倍数,就是采样次数,这样我们就可以以同样的内存代价获得与RIpMap一样的结果
  • image.png
    10.3.7.1 引擎中的采样方案

10.3.8 其他的查询方法

  • 积分图 利用积分生成各个区域的积分值,并利用算法查找对应的平均值,SAT
  • image.png
    10.3.8 SAT 积分图

10.3.9 纹理的优化区间

  • 在业界,我们优化的目标是尽量降低DrawCall,希望你还记得它是CPU向GPU发送指令的方式。
  • 我们会将纹理合并为纹理图集,通过使用不同的采样坐标来采样对应的图片,同样我们也可以将纹理放入一个数组之中,这一样可以减少DRAWCALL降低开销。

10.4 一些常用纹理

  • 这部分是简单扼要的介绍,主要介绍一下一些在美术和引擎中常用的纹理。

10.4.1 反射率or漫反射纹理

  • 这种纹理是提供颜色的基础纹理,我们可以用这种纹理为图像提供基本的颜色。
  • image.png
    10.4.1 反射率or漫反射纹理

10.4.2 凹凸纹理与法线纹理

  • 凹凸纹理只记录了每个像素突出的大小,而且这种计算并不能真正改变顶点高度,只能让顶点看起来这样,这是因为它改变了纹理着色的数值,因此会使得不同区域呈现出不同的质感。

  • 我们可以在一条线身上来看这个问题,原本的这条直线的法线方向是竖直向上的,而如果增加了凹凸贴图,计算时就会改变该点的法线方向。
  • image.png
    10.4.2 bump贴图改变表面法线的原理
  • 这样表面法线的数据就被改变了,因此根据我们之前计算漫反射的方法,物体表面就会出现凹凸感。

  • 法线纹理则实质上使得纹理向不同方向进行突出,不过它仍然无法改变突出大小,但我们可以利用它改变顶点位置。
  • image.png
    10.4.2.1 加入了基础的凹凸纹理

10.4.3 遮蔽纹理

  • 这类纹理一般是用来定制一些特殊效果,比如高光遮蔽纹理,环境光遮蔽纹理等,都是用来提供不同的质感。
  • image.png
    10.4.3 加入了基础的高光遮蔽纹理

10.4.4 环境光

  • 我们之前一直回避的一个问题,那就是环境光。环境光虽然看起来很简单,不就是环境给物体的映射吗,实际上环境光的计算并不简单,反而是光栅化中最困难的部分,因为我们要获取整个场景的反射,这就是环境光照,动态环境光技术也是图形学前沿一直在努力的话题。现在我们来看一下我们如何巧妙地得到环境光。

  • 光栅化中,我们尽量减少计算,因此我们更希望通过内存来换取性能的节省,因此,我们往往使用一张环境光贴图来为所有物体提供光照采样。比如Unity中的天空盒子以及UE中的天空光照,实际上就是干的这件事。
  • 10.4.4Ambient.png
    10.4.4 Ue天空光照开启与关闭对比
  • 可以看到开启环境光之后,整个环境明显变亮很多,细节更加丰富了。

10.4.5 HDR与HDRI

  • 首先我们要区分HDR和HDRI的区别。HDR叫做高动态范围,什么是动态范围?
  • 动态范围是指一张图片中最亮处与最暗处之比。亮部和暗部范围可以非常大。之前我们提到我们使用256种不同的亮度来表现环境。而HDR可以让我们很方便的尽可能记录更多的光照变化。
  • 用NVIDIA的总结来说:HDR可以让亮部更亮,暗部更暗
  • 我们可以用HDRI图像来模拟环境光照,往往能得到更加真实的结果。
  • indoor.png
    10.4.5 blender中HDRI贴图转模型的实例

  • 至此为止,我们基本上介绍了在光栅化中着色的各类技术,但这只是部分,如果你想进一步了解,欢迎浏览下方的参考资料与网站,我们下一节将来看看渲染中的几何部分。

参考资料