GPU精粹与Shader编程(一):关于基础物理渲染

作者:毛星云编译 2019-07-08
第一部分·主核心内容提炼总结

一、用物理模型进行高效的水模拟(Effective Water Simulation from Physical Models)

【内容概览】

本章介绍了在GPU中模拟和渲染大型水体的一些方法,并且提出了改进反射的一些有用技巧。

文章由计算简单的正弦函数之和来对水面进行近似模拟开始,逐步扩展到更复杂的函数如Gerstner波,也扩展到像素着色器。主要思路是使用周期波的加和,来创建动态的平铺(tiling)凹凸贴图,从而获得优质的水面细节。

这章也集中解释了水体渲染与模拟系统中常用参数的物理意义,说明了用正弦波之和等方法来近似水面的要点。

图基于文中水体技术渲染的Uru:Ages Beyond Myst中的场景

【核心内容提炼】

1.1背景与范围

《GPU Gems 1》出版于2004年,在这几年间,实时渲染技术渐渐从离线渲染领域中分离,自成一派。

而《GPU Gems 1》中收录的这篇文章问世期间,快速傅里叶变换(Fast Fourier

Transform,FFT)库已经能用于顶点和像素着色器中。同时,运行于GPU上的水体模拟的模型也得到了改进。Isidoro等人在2002年提出了在一个顶点着色器中加和4个正弦波以计算水面的高度和方位的思路。另外,Laeuchi在2002年也发表了一个使用3个Gerstner波计算水面高度的着色器。

图基于快速傅里叶变换的水体渲染

1.2水体渲染的思路

文中对水体渲染的思路,运行了两个表面模拟:一个用于表面网格的几何波动,另一个是网格上法线图的扰动。这两个模拟本质上是相同的。而水面高度由简单的周期波叠加表示。

正弦函数叠加后得到了一个连续的函数,这个函数描述了水面上所有点的高度和方向。在处理顶点时,基于每个顶点的水平位置对函数取样,使得网格细分形成连续水面。在几何分辨率之下,将该技术继续应用于纹理空间。通过对近似正弦叠加的法线取样,用简单像素着色器渲染到渲染目标纹理(render target texture),从而产生表面的法线图。对每帧渲染法线图,允许有限数量的正弦波组相互独立地运动,这大大提高了渲染的逼真度。

而直接叠加正弦波产生的波浪有太多的“簸荡(roll)”,而真实的波峰比较尖,波谷比较宽。事实证明,正弦函数有一个简单的变体,可以很好地控制这个效果。

1.2.1波的选择

对于每个波的组成,有如下几个参数需要选择:

•波长Wavelength(L):世界空间中波峰到波峰之间的距离。波长L与角频率ω的关系为

ω=2π/L。

•振幅Amplitude(A):从水平面到波峰的高度。

•速度Speed(S):每秒种波峰移动的距离。为了方便,把速度表示成相位常数φ=S x

2π/L。

•方向Direction(D):垂直于波峰沿波前进方向的水平矢量。

波的状态定义为水平位置(x,y)和时间(t)的函数:


图单个波函数的参数

而包括所有的波i的总表面是:


为了提供场景动力学的变量,我们将在约束中随机产生这些波的参数,随着时间的变化,我们会不断将某个波淡出,然后再以一组不同的参数将其淡入。且此过程的这些参数是相关联的,必须仔细地产生一套完整的参数组,才能使各个波以可信的方式进行组合。


1.2.2法线与切线

因为我们的表面有定义明确的函数,所以可以直接计算任意给定点处的曲面方向,而不是依赖于有限差分技术。

副法线(Binormal)B和正切矢量T分别是x和y方向上的偏导数。

对于2D水平面中的任意点(x,y),表面上的三维位置P为:


求副法线(Binormal)B方向,即对上式对x方向求偏导。而求正切矢量T方向,即对上式对y方向求偏导。

而法线N由副法线B和切线T的叉积给出:


1.3波的几何特征

首先文中将几何波限制为4个,因为添加更多的波并不能增加新的概念,只不过增加更多相同的顶点Shader处理指令和常数而已。

1.3.1方向波或圆形波的选择

需要对下图所示的方向波或圆形波进行选择。

图方向波和圆形波

对于两种类型的波,视觉特性和复杂性都是由干涉条纹引起的。

方向波需要的顶点shader处理指令较少,但是究竟选择何种波需要取决于模拟的场景。对于大的水体,方向波往往更好,因为它们是风吹动产生的波较好的模型。对于较小的池塘的水,产生波的原因不是由于风,而是诸如例如瀑布,水中的鱼,圆形波则更好一些。对于方向波,波的方向是在风向的一定范围内任意绘制的;对于圆形波,波中心是在某些限定的范围内任意绘制的。

1.3.2 Gerstner波

正弦波看起来圆滑,用于渲染平静的,田园诗般的池塘很合适。而对于粗犷的海洋,需要形成较尖的浪头和较宽的浪槽,则可以选择Gerstner波。

Gerstner波早在计算机图形学出现之前就已经被研发了出来,用于物理学基础上为海水建模。Gerstner波可以提供一些表面的微妙运动,虽然不是很明显但是却很可信(具体可见[Tessendorf 2001])。

另外,Gerstner波有一种经常被忽略的性质:它将顶点朝着每个浪头顶部移动,从而形成更尖锐的波峰。因为波峰是我们水表面上最锐利的(即最高频率,最主要)特征,所以我们正希望顶点可以集中在此处。

图Gerstner波

图基于Gerstner渲染出的水面 Unreal Engine 4


1.3.3波长等参数的选择

波长等参数的选择方法:

•波长的选择,要点是不要追求波在真实世界中的分布,而是要使用现在的少数几个波达到最大效果。对波长相似的波进行叠加可以突显水面的活力。于是文中选择中等的波长,然后从它的1/2至两倍之间产生任意波长。

•波的速度,通过波长,基于公式即可计算得出。

•振幅方面,主要是在Shader中指定一个系数,由美术同学对波长指定对应的合适振幅。

•波的方向,运动方向与其他参数完全独立,因此可以自由选择。

1.4波的纹理特征

加和到纹理中的波也像上文说到的顶点一样需要参数化,但是其具有不同的约束条件。首先,在纹理中得到宽频谱更为重要。其次,在纹理中更容易形成不像天然波纹的图案。第三,对给定波长只有某些波方向能保证全部纹理的平铺(tiling)。也就是说,不像在世界空间中仅仅需要注意距离,在纹素(texel)中要注意所有的量。

文中的思路是在2到4个通道中,使用15个频率和方位不同的波进行处理。虽然4个通道听起来有点多,但是它们是进行256 x 256分辨率的渲染目标纹理的处理,而不是处理主帧的帧缓冲。实际上,生成法线贴图的填充率所造成的影响小到可以忽略不计。

1.5关于深度

首先,把在顶点上的水深度作为一个输入参数,这样,在着色器碰到岸边这样的微妙区域时,便可以自动进行校正。

因为水的高度需要计算,所以顶点位置的z分量就没什么用了。虽然我们可以利用这点来压缩顶点的数据量,但是选择把水深度编码在z分量中,是一个更好的选择。

更确切地说,就是把水体底部的高度放在顶点的z分量中,作为常数带入水的高度表中,这样通过相减,即可得到水深度。而同样,这里假定了一个恒定高度的水位表(constant-height

water table)。

我们也使用水深度来控制水的不透明度、反射强度和几何波振幅。简单来说,即水浅的地方颜色浅,水深的地方颜色深。有了适当的水深度,也就可以去光的传播效果进行更完善的建模。

图真实感水体渲染效果图 @Unreal Engine 4

【核心要点总结】

文中提出的水体渲染方法,总结起来有三个要点:

1)使用周期波(正弦波、Gerstner波)的加和

2)创建动态的平铺(tiling)贴图

3)使用凹凸环境映射(Bump-Environment Mapping)

【本章配套代码汇总表】

文中并没有贴出相关代码,但原书配套CD提供了完整的源代码和项目工程,具体代码和工程可以查看:

QianMo/GPU-Gems-CD-Contentgithub.com

【关键词提炼】

水的模拟(Water Simulation)

水的渲染(Water Rendering)

正弦函数近似加和(Sum of Sines Approximation)

Gerstner波(Gerstner Waves)

凹凸环境映射(Bump Environment Mapping)

二、Dawn Demo中的皮肤渲染(Skin in the Dawn Demo)

十年技术变迁:NVIDIA Dawn Demo

最初的Dawn Demo由NVIDIA于2002年发布,而十年之后的2012年,NVIDIA新发布了“A New Dawn”技术Demo。

图A New Dawn Demo截图

以下是一张新老Demo的对比效果图。

图Dawn Demo(2002年)

图A New Dawn Demo(2012年)

图技术指标的对比

【章节概览】

这章详细介绍了NVIDIA出品的Dawn Demo中对精灵人物的着色技术,主要是皮肤的着色技巧。在当时(2002年)NVIDIA创造的此demo的品质,已经成为照片级真实感渲染和实时渲染的代表。

图Dawn Demo截图

【核心内容提炼】

2.1关于皮肤着色

基于多种原因,在计算机图形中模拟皮肤十分困难。在当时,即使是在电影中用高端产品模拟出来的仿真角色,通常也经不起近距离的观察。因为,人类可以从中获得大量非语言来表达的信息,如重心的移动,走动的特别习惯,面部的表情,甚至有些人的皮肤泛红等等。

虽然很少有人能理解像“次表面散射(Subsurface Scattering)”、“轮廓照明(Rim

Lighting)”这些词汇,但是当把它们渲染错了的时候,几乎任何人都可以指出来。而且除了着色问题外,有时人们会因为皮肤渲染的问题,说皮肤看起来像是塑料做的。

2.2皮肤如何对光进行响应

皮肤不像大多数在计算机渲染中建模的表面,因为它是由半透明的表皮、真皮和皮下组织等数层构成的。这可以用次表面散射来模拟。这种现象很普遍,当在太阳面前向上举起手,就能看到穿过皮肤的桔红色的光。

图次表面散射-穿过皮肤的桔红色的光

皮肤下的散射在所有的角度上显现皮肤形态,使它具有了柔软的、与众不同的特征。

在这之前有一些小组尝试使用多层纹理贴图来模仿皮肤的复杂性,但一般而言,这个方法比较难管理,美术同学很难通过预想,混合出最终符合预期的效果。

相反,文中使用单张彩色贴图,通过着色程序来增加色彩的变化。

图Dawn头部的前半边的漫反射贴图

另外,皮肤具有一些极细微的变化,会影响其反射特性。这对皮肤外观有微妙的影响,特别是当光线直接与相机位置相反时,皮肤的表现则是存在边缘(Edge)与轮廓光照(Rim Lighting),这时,需要皮肤轮廓边缘的光照,或给皮肤边缘加上光晕。

真正的皮肤具有一些细微的特征,比如汗毛和毛孔能捕捉光线。尽管这些细节用于显式地建模是太不明显了,但我们还是希望得到一个合适、整体更逼真的皮肤渲染外观。在特写时,可以增加凹凸贴图,提供一些额外的细节,特别是一些小的皱纹。但需要注意,我们想要的是柔软的皮肤外观,而不是光闪闪的油腻的塑料。另外,凹凸贴图通常只需静距离特写时才可见。

我们可以通过建模来近似这两个着色属性,建模可以是基于表面法线的简单公式,或者是基于光线或视线矢量的简单公式。

通过认识,我们可以将上述两种渲染特性(次表面散射和边缘光照),建模为基于表面法线和照明或观察向量的简单公式,从而近似出两种着色属性。尤其是沿着Dawn的轮廓边缘,对她身后的光线取样,按照观察向量的索引,让“穿过”Dawn的光与她的基础皮肤色调混合,从而创建次表面散射和边缘光照的着色效果。尤其是背景图中更加明亮的区域。如下图。

图Dawn的头部前面的切线空间法线贴图(凹凸贴图)

2.3场景的照明

Dawn Demo中场景的照明使用了基于图像的光照(Image Based Lighting,

IBL),创建高动态范围(High-Dynamic Range,HDR))的全景,使用环境映射贴图(Environment Maps)进行场景的照明。

图立方体环境反射贴图

漫反射环境贴图(Diffuse Environment Map)也是一个立方体映射贴图,它使用网格表面的法线作为索引。每个像素存储了相应法线与入射光夹角的余弦加权平均值。

图漫反射环境贴图(Diffuse Environment Map)

镜面高光环境贴图(Specular Environment Map)同样也是一个立方体映射贴图,使用反射矢量作为索引(类似于立方体映射)。把此镜面高光环境贴图基于粗糙因子进行模糊,目的是模拟对任何表面任何给定点上的法线的改变。

图镜面高光环境贴图(Specular Environment Map)

存在的问题是,漫反射环境贴图(Diffuse Environment Map)和镜面高光环境贴图(Specular Environment Map)考虑了来自环境的入射光,但不包含由物体引起的阴影。

要解决这个问题,可以生成一个遮挡项,用来近似表达在每个顶点上半球辐射光中,有多大比率场景中其他物体所遮挡。

2.4实现

Dawn Demo中,毫无悬念地使用顶点着色器和像素着色器进行光照处理,顶点shader的主要功能是将坐标转换到投影空间,并执行那些不能在像素着色器中执行的数学运算。

采用单通道(one-pass)的光照解决方案,不需要另外其他的通道渲染,或alpha混合来创建皮肤表面。

文中提供了完整的顶点Shader和像素Shader的源代码,这里因为篇幅原因不再赘述,具体可以参考原文(PS:上文有贴出Web版的英文全书原文的链接)。

【核心要点总结】

文中采用的皮肤渲染方法,总结起来有三个要点:

1)基于图像的光照(Image Based Lighting,IBL),采用高动态范围(High-Dynamic-Range,HDR)光照环境映射贴图

2)次表面散射(Subsurface Scattering)

3)对皮肤边缘加上光晕,即轮廓照明/边缘光照(Rim Lighting)

【本章配套代码汇总表】

Example 3-1.从CPU应用程序接收的每个顶点数据示例代码(The Per-Vertex Data

Received from the CPU Application)

Example 3-2.输出顶点的数据结构示例代码(The Data Structure of the Output

Vertices)

Example 3-3.Dawn脸部的皮肤渲染顶点着色器示例代码(A Sample Vertex Shader for

Dawn's Face)

Example 3-4.Dawn脸部的皮肤渲染片元着色器代码(The Fragment Shader for Dawn's

Face)

【关键词提炼】

皮肤渲染(Skin Rendering)

次表面散射(Subsurface Scattering)

轮廓照明(Rim Lighting)

基于图像的光照(Image Based Lighting,IBL)

高动态范围(High-Dynamic-Range,HDR)

环境映射贴图(Environment Maps)

三、无尽波动的草地叶片的渲染(Rendering Countless Blades of Waving Grass)

【章节概览】

这章关于巨量自然元素的渲染,特别是对于无尽波动的草地叶片的渲染。作者对Codecreatures demo中首次成形的技术进行了扩展,使其能够高性能的渲染,以更好地适应游戏引擎的需要。

图Realistic Grass Field Giovanni Baer

【核心内容提炼】

3.1概述

首先,需要意识到,对单个草叶的细节建模意义不大,因为那样大片草地需要的多边形数目会太多。

所以,我们必须建立一个符合以下条件的简单而有用的替代方案:

•许多草的叶片必须由少数多边形表示。

•草地必须从不同的视线看起来显得密集。

而要做到让场景不依赖于摄像机的位置和方向,可以把一些草叶组合起来,表示在一个纹理中,并将多个纹理组合起来,且在结果中单个的多边形不应该引起注意。当观察者四处活动时,通过将草体加入混合操作或者移除混合操作,以在距离范围内增加或删去草体,来保证整个草地的渲染效果具有稳定的视觉质量。

3.2草的纹理

草的纹理,应该是一些一簇一簇聚集丛生的草,否则,会出现大片的透明区域。

需在透明的alpha通道上画实体草茎。在彩色通道中,用深浅不同的绿色和黄色,来较好地区别各个单独的叶片,也应该模拟不同情况的草叶:长得好的和长得差的、老的和嫩的,甚至区别叶片的前面与后面。

下图是一个草地纹理的示例。

图草地纹理的示意图

3.3草体

这一部分将探讨总结如何对多边形进行组合,并用上文提到的草地纹理进行映射,以模拟出茂密的草地,并且不凸显个别多边形。此技术也保证了单个多边形不可见。

因为用户能自由地在场景中游玩,下图所示的结构便不能产生令人信服的效果。

图线性排布

对于线性排布,如果从垂直于多边形的方向观看场景,就会立刻穿帮,看出草地多边形的结构是线性排布的。另外这种情况下草地会看起来非常稀疏。只有在摄像机自动导航,或者渲染无法到达的远距离草地时,才会考虑这样的排布。

为了保证独立于当前视线的良好视觉质量,我们必须交叉地排布草地多边形。已证明,使用星型结构是非常好的。下图给出了“草体”可能的两种变体。

他们由3个相交的方块构成。我们必须禁用背面剔除来渲染多边形,以保证双面都可见。为了得到合适的照明度,应该让所有顶点的法线方向与多边形的垂直边平行。这保证了位于斜坡上的所有草体都可以得到正确的光照,不会因为地形的亮度而出现差异。

图草体的交叉排布

如果把这些草地物体彼此相当靠近地设置在一个大的区域里,如下图。在运行期间把它们从后向前进行排序,使用alpha混合,并启用Draw

Call中的z-testing/writing,那么就会得到自然而茂密的草地渲染效果。

图草地的扩展


3.4草地的动画

关于草地的动画,基本思想是以三角函数(尤其是正弦和余弦)为基础进行计算,且计算应该考虑到移动的位置和当前时间、风向和强度。

以基本思想为基础,实现起来有几种方法:

1)每草丛草体的动画(Animation per Cluster of Grass Objects)

2)每顶点的动画(Animation per Vertex)

3)每草体的动画(Animation per Grass Object)

三种方法各有优缺点,而文中都给出了具体算法步骤和实现的Shader源码,这里因为篇幅原因,便不展开分析了,具体可以参阅原文。

最终可以实现的渲染效果。

图Realistic Grass

【核心要点总结】

1)草的纹理,应选取一簇一簇聚集丛生的草。在透明的alpha通道上画实体草茎。在彩色通道中,用深浅不同的绿色和黄色,区别各个单独的叶片。

2)草体的渲染,适合进行交叉排布,从后向前进行排序,使用alpha混合,并启用Draw

Call中的z-testing/writing,便能得到自然而茂密的草地渲染效果。

3)草地的动画,以三角函数(尤其是正弦和余弦)为基础,且应该考虑到移动的位置和当前时间、风向和强度。实现起来有三种方法:

1.每草丛草体的动画(Animation per Cluster of Grass Objects)

2.每顶点的动画(Animation per Vertex)

3.每草体的动画(Animation per Grass Object)

【本章配套代码汇总表】

Example 7-1.顶点着色器框架(Framework in the Vertex Shader)

Example 7-2.对每草丛草体的动画的实现Shader代码(Code for Animation per Cluster

of Grass Objects)

Example 7-3.每顶点动画实现Shader代码(Code for Animation per Vertex)

Example 7-4.每草体的动画实现Shader代码(Code for Animation per Grass Object)

【关键词提炼】

草地渲染(Grass Rendering)

草地动画(Grass Animation)

草体(Grass Objects)

本文节选自GPU精粹三部曲“11本书中的第一本《GPU Gems 1》

《GPU Gems 1》其书

《GPU Gems 1》英文原版出版于2004年4月,中文版《GPU精粹1》出版于2006年1月。需要说明的是,书中很多内容放到今天,并不过时,仍然很有研究、学习、运用、实践的价值。尤其是水体渲染,皮肤渲染,次表面散射、阴影渲染、后处理相关的章节。

图《GPU Gems 1》封面

图全书内容概览图

•原书配套源代码、工程与资源下载:

http://http.download.nvidia.com/ ... CD_Image/Index.html

我维护了的一个名为“GPU-Gems-CD-Content”的GitHub仓库,以备份这些珍贵的资源,也方便直接在GitHub Web端查看大牛们写的代码:https://link.zhihu.com/?target=h ... GPU-Gems-CD-Content

作者:毛星云编译
专栏地址:https://zhuanlan.zhihu.com/p/35974789

最新评论
暂无评论
参与评论

商务合作 查看更多

编辑推荐 查看更多