UWA问答 | 博客 | 游戏及VR应用性能优化记录分享 | 侑虎科技 - 图形学
本文首先将会介绍光线追踪的类别族谱，介绍其公共部分，之后我们会分别对其中的每一个进行详细地剖析。 光线追踪（Ray tracing）是一个拥有历史感的词汇。图形学从业者从neutron transport、heat transfer和illumination engineering等领域引进其思想。由于这些概念被许多领域所研究，导致光线追踪的术语在学科之间和学科内部都进行着不断发展，有时也会产生分歧。经典的论文也可能会错误地使用术语，这就可能会造成混淆。所以，在介绍光线追踪的概念之前，我们需要分清楚这些术语的概念。 几乎所有的现代光线追踪器都是使用递归和蒙特卡罗方法；但是现在已经很少会有人把他们称为“递归的蒙特卡洛”方法。 一．光线追踪族谱 1.Ray Casting。1968年，Arthur Appel。 2.Whitted 光线追踪。1980年，Whitted，Kay和Greenberg提出了使用递归的光线追踪来描绘准确的折射和反射。 3.分布式光线追踪。1984年，Cook等人提出了分布式光线追踪（distributed or distribution ray tracing，DRT）。但是，这种方法通常被称为随机光线追踪（stochastic ray tracing），来避免与分布式处理（distributed processing）混淆。其和path tracing都使用了蒙特卡洛方法。 4.路径追踪。1986年引入了两个重要的算法。Kajiya将积分输运方程（integral transport equation）称为渲染方程（rendering equation）。他提出的渲染方程成了至今为止几乎所有的全局光照算法的数学依据，在同篇论文中他也提出了最原始路径追踪算法。 5.双向路径追踪（BidirectionalPath Tracing）由Lafortune和Willems提出，Veach则对双向路径追踪做了详细的描述。 6.Metropolis Light Transport。1997年Veach首次将最早应用在计算物理领域的Metropolis采样方法引入图形学。 7.Energy Redistribution Path Tracing 2005 8.Manifold Exploration 2012 二．Ray Casting 第一个用于渲染的光线投射算法最初由Arthur Appel在1968年引入。光线投射通过从观察点对每一个像素发射一条光线并找到在世界场景中阻挡光线路径的最近物体来渲染场景，Ray Casting只有两种射线，第一种是眼睛发射的eye射线，来寻找场景中的交点，另一个是从交点发到灯光的阴影射线，看自身是否是处于阴影当中。与传统扫描线渲染算法相比，光线投射的一个显着优点是能够处理不平整的表面和固体。电子世界争霸战（1982年电影Tron Series）的大部分动画都是使用光线投射技术渲染的。 早年间光栅化技术还未流行时，也会用于游戏当中。最著名的一款光线投射的游戏是Wolfenstein 3D。接下来将介绍不论在光线追踪还是光线投射算法中都通用的公共部分，避免之后的内容太过的庞杂。 2.1 光线投射基本算法 Render() for each pixel x,y color(pixel) = Trace(ray_through_pixel(x,y)) 首先做的就是创建光线，得到每一个点的光线数据结构，之后开始Trace，这里使用Trace这个函数名是为了和后面统一。是指在场景中追踪这个光线。 Trace(ray) object_point = Closest_intersection(ray) if object_point return Shade(object_point, ray) else return Background_ColorRender() for each pixel x,y color(pixel) = Trace(ray_through_pixel(x,y)) 我们在场景中寻找光线与场景中的物体是否有交点，并返回距离我们最近的那一个object的信息，如果找到，我们进行着色如果没找到我们返回设定的背景颜色。 Closest_intersection(ray) for each surface in scene calc_intersection(ray, surface) return the closest point of intersection to viewer 在光线投射的Closest_intersection中，我们并不需要额外的返回其他的复杂信息。 Shade(point, ray) to calculate contributions of each light source 光线投射的着色，你可以自由发挥，最初的实现中也只有阴影的划分。对于像Wolfenstein 3D这样的实时游戏中的场景，一般是使用了额外的场景信息，来提高性能。总之这里的shade并不重要，主要是理解RayCast这个思想框架，而之后的所有算法都是在这个框架中的发挥。 当然我们也可以进行简单的着色计算，这就和不进行递归的RayTracing是一致的，我们放在之后来做对比。 2.2 光线的表示和求交问题 不论对于哪种的光线渲染算法，第一步都是进行从相机进行光线投射，其最终的目的就是找到屏幕中的每一个像素对应的光线的方向。 在这里，对于局部、世界和相机空间的转换关系是和光栅化渲染中的空间转换的做法基本类似，所以在这里就不进行赘述。 大多数情况下，我们使用 p(t)=e+t(s-e) 的方法来进行光线的表示，e是我们设定的相机观察点，而s是画布上的点，通常我们渲染之前预先设定的长宽，然后我们通过参数t来表示光线的长度。 当t为0，表示原点 当t为正数代表正向我们的方向 当t为负数代表在原点后面 如果t1<t2，代表t1离我们更近 对于求交问题，网上非常多的资料，这里就不讲了，只给出思路和参考资料。 这里有一个对于物体来说是显示表示还是隐式表示法的问题。 三．Whitted RayTrace 光线投射渲染的进化发生在1979年，当时Turner Whitted通过引入反射，折射和阴影来延长光线投射过程从而形成自身的Whitted光线追踪。 在当时它得到的那张512x512的渲染图耗时74分钟，而今天则只需要几秒钟就可以完成。 大致上，Whitted RayTrace其主要把光线分为四种： 视角光线，和之前的一样，没什么好说的。 反射光线。在表面沿镜面反射方向继续照射。反射的颜色由反射光线与场景中的对象的交点决定。 折射光线，其创建与反射光线类似，只是它的方向是进入对象并最终可以退出对象。 阴影光线，是通过创建从交点到所有灯光的阴影光线来计算的。如果阴影光线在到达灯光之前与某个对象相交，则该交点将从该特定灯光中阴影显示。 Whitted光线追踪主要解决了场景中没有间接光的问题，但是它解决的也相当的一般，因为它所有的间接光都只来源于完美的镜面反射或者是折射，这种材质显然在现实世界中并不常见，对于大多数的间接光基本无法模拟，这也是由于它对每一个EyeRay的交点只发出一条的Reflected ray和Refracted ray。这个问题将在分布式光线追踪中有所改善。 3.1 光线追踪的类型 3.3.1 前向光线追踪 Forward Ray Tracing 前向光线追踪遵循光子从光源到物体。虽然前向光线可以最准确地确定每个物体的颜色，但效率非常低。这是因为来自光源的许多光线永远不会通过视平面并进入眼睛。追踪来自光源的每条光线意味着许多光线将被浪费掉，因为它们从未对从眼睛看。（这里的前向后向可能每个人理解不同而相反，这里指的是Light Ray Tracing。） 3.1.2 向后光线追踪 Backward Ray Tracing 为了使光线追踪更有效，引入了后向光线追踪方法。在后向光线中，在眼睛处产生眼睛光线；它通过视平面并进入世界。射线击中的第一个物体是从视平面的那个点可见的物体。 后向光线的缺点是它假设只有通过视平面并进入眼睛的光线对场景的最终图像有贡献。在某些情况下，这种假设是有缺陷的。例如，如果一个透镜被固定在桌子顶部的一个距离处，并且被正上方的光源照亮，那么在透镜下面会有一个具有大光浓度的焦点。如果反向光线追踪试图重新创建此图像，则会计算错误，因为向后发射光线只会确认光线通过透镜；反向光线无法识别通过透镜的正向光线弯曲。因此，如果只进行反向光线追踪，透镜下方将只有一个均匀的光斑，就像透镜是一块普通的玻璃。 如上所述，有效且最容易实现性能优化的一种方法是从眼睛向后发射光线，而不是从光源光发射线。通过这种方式，不会浪费计算能力来从未击中模型或相机的光线。 3.1.3 混合光线追踪 由于前向光线追踪和后向光线追踪都有其缺点，最近的研究试图开发出会影响速度和精度的混合解决方案。在这些混合解决方案中，仅执行某些级别的前向射线。算法记录数据，然后继续执行后向光线追踪。场景的最终着色将后向光线和前向光线计算都考虑在内。 Veach（1995）发明出了后向光线追踪+前向光线追踪+连接线（Bidirectional Path Tracing）的混合方法，这在后面会讲解。 综上，而我们的Whitted光线追踪一般而言都时使用的向后渲染的方式来进行的（从眼睛发射光线）。 3.2 光线追踪基本算法 我们在第二部分聊的Shade部分，在这里就有了用途。我们知道递归是光线追踪的最基本的特点，我们先来看没有递归的情况。其实没有递归的情况和RayCasting技术特点几乎是一模一样的。 之前的框架是一样的，所以这里就不都列出来了，只列出不一样的部分。 Shade(point, ray) calculate surface normal vector use Phong illumination formula to calculate contributions of each light source 首先如之前所列的年代关系，出现Whitted光线追踪时，并没有提出我们现在做全局光照的渲染方程，所以当时依旧是使用的基于经验的光照模型，就像这里的phong。当然你也可以用其他的。 我们来看递归的形式： Shade(point, ray) radiance = black; /*初始化 */ for each light source shadow_ray = calc_shadow_ray(point,light) if !in_shadow(shadow_ray,light) radiance += phong_illumination(point,ray,light) if material is specularly reflective radiance += spec_reflectance * Trace(reflected_ray(point,ray))) if material is specularly transmissive radiance += spec_transmittance * Trace(refracted_ray(point,ray))) return radiance 首先进行这个点的颜色初始化，如果不在阴影中，我们phong模型的着色，如果材质拥有镜面反射，最后的颜色要加上镜面反射光线的颜色贡献，如果材质拥有折射特性，加上折射的贡献。 当然，这里对于放射和折射的具体计算比较基础，就不表了。搞了这么多，我们先不进行其他的分析，先来看看效果上和RayCasting的区别： 递归两次 递归三次 还有一个比较重要的问题是什么时候结束这个罪恶的递归？有两种情况。第一种情况是，光线没有打到物体上，第二种是由于每一次反射或者折射，贡献值会逐渐的降低，我们预先设定一个阈值，当它小于这个阈值的时候，我们停止。当然，如果你只按照上面的做法来渲染一个图片出来是可行的，但是会有一些需要解决的问题。 3.3 锯齿 我们首要问的问题时，为什么会产生锯齿？由于之前的光线追踪算法对每一个像素值，都只创建了一个光线，都只采样了场景中的一个点，和那一个颜色，但是，对于一个像素而言，有可能包含了很多个不同的点，尤其是在物体边缘的情况下时，这些点不一定都有相同的颜色。而我们这种有规律的采样，就会导致这种锯齿。 3.3.1 超级采样 超级采样是为每个像素增加的光线数量的过程。这不能解决锯齿问题，但它会尝试减少它们对最终图像的影响。在以下示例中，从像素发出九条光线。六个是蓝色，三个是绿色。像素的最终颜色将是蓝色的三分之二和绿色的三分之一。 当然也可以有其他的组合方式，数量位置和比例上都可以自行设定。 3.3.2 自适应超级采样 自适应超级采样（也称为蒙特卡罗采样）是一种以更智能的方式进行超采样的尝试。首先发出固定数量的光线并比较它们的颜色。如果颜色相似，则程序假定像素正在查看同一个对象，并且光线的平均值被计算为该像素的颜色。如果光线颜色不同（由某个阈值定义）那么我们认为这个像素比较特殊需要进一步检查。在这种情况下，像素被细分为更小的区域，并且每个新区域被视为一个完整的像素。这个过程再次开始，同样的固定光线模式被射入每个新的部分。 遗憾的是，自适应超级采样仍然将像素划分为规则的光线模式，并且会受到常规像素细分可能出现的混叠的影响。例如对象和采样网格几乎是对齐的。总之有规律的方法都不得劲。 3.3.3 随机采样 Stochastic（随机）采样将固定数量的光线发送到像素中，但确保它们是随机分布的（但或多或少均匀地覆盖该区域）。此外，Stochastic射线试图解决在凹凸不平的表面上跟随入射光线的问题。这是分布式光线追踪中比较核心的概念，所以放在下面来说。 3.4 加速结构 加速结构用于限制要检查的对象数量以找到与谁相交的技术。例如，如果我们有一条射线，它将一个物体与数千个物体相交，我们想以某种方式智能地清除远离光线的物体。 3.4.1 包围盒（Bounding Volumes） 基于层次关系的划分方法中包围盒层次结构最有代表性。层次包围盒算法的基本思想是：用形状简单的包围盒（如球形、长方体等）将场景中的面片包围，相邻的包围盒被包含在一个更大的包围盒里，逐级扩大，生成一个层次的结构；在进行光线与物体相交测试之前，先进行光线与包围盒的测试，如果光线与包围盒相交，再与其包含的物体面片进行相交测试，提高效率；通过将包围盒按照有效的层次结构进行组织，减少进行相交测试的数目，降低复杂度，进一步得到效率上的提升。在包围盒的选取中，一方面应该选择形状简单的包围盒，降低光线与包围盒相交测试的代价；另一方面应该选取能够紧密包围物体面片的包围盒，提高光线与包围盒进行相交测试的有效性。 在实际应用中，最常用的是轴对称包围盒（Axis Aligned Bounding Box，AABB）。AABB轴对称包围盒简便，易于存储，易于计算，鲁棒性好，是对相交测试简便性和包围物体紧凑性的较好折中，效率较高。与基于空间分割的划分方法相比，基于层次关系的划分方法在动态场景中更有优势。因为，在动态场景中，场景的组织结构都要变化，每一帧都要重新构建，这无疑是巨大的开销，导致计算量激增。在动态场景中，基于层次关系的划分方法形成的数据结构，可以只更新相关包围盒的有关信息（如其位置和大小）。也就是说，BVH只需要进行数据结构的刷新，而不需要进行重建，这样在效率上有明显的提高。 快速拒绝：首先根据包围体检查光线 更快的拒绝：检查视锥体对象的包围体 一种组织结构是在将包围体分组到包含的包围体中，以创建自下而上的树层次结构，在命中包围体时，递归检查其子节点。 3.4.2 均匀网格 空间网格是把三维空间分别沿着三个方向轴以特定宽度划分，得到一定分辨率的网格，场景中的面片都被分配到相应的网格中。每个网格可以含有不同的面片，每个网格保存其所包含的场景面片的引用。最简单的便是均匀网格，把场景进行均匀划分。 生成均匀网格并创建一个结构，将每个网格位置与网格位置中的另一个对象链接起来。对于光线接触的每个网格，检查对象是否与光线相交。 这种方法的优点便是方便简洁，易于创建，并且可以快速将场景中的面片分配到相应的网格中。然而，在实际的场景中，面片的分布是比较不均匀的，比如大多数的面片集中在少数几个网格中，则当进行光线遍历时需要遍历很多面片，也有可能一次相交测试能够踢除的无关面片非常少，遍历过程很麻烦。所以，如果场景中面片分布不够均匀的时候，这种分割方法的遍历效率非常低。 3.4.3 KD-Tree BSP树是一种空间分割技术，在许多领域都有所应用，于20世纪90年代被引入到计算机图形学的各个研究领域和应用场合。KDTree可以看作是BSP树的一种特例，它是推广到多维空间下的树形结构。原理是将整个场景作为一个树，通过分割平面将当前树划分为两个空间得到两个子树，这两个子树又分别被各自的分割平面分割得到更小的子树，直到树的深度达到预定的阈值或者节点中含有的场景面片数小于预定的阈值。树的每个节点表示一个子空间，囊括所代表空间中包含的所有面片，树的根节点代表整个景空间。 以后单独讲这一块，下面的东西还非常的多，主线上不能丢下。 四．分布式光线追踪（distributed ray tracing，DRT） 我们终于即将触摸到现代的使用蒙特卡洛方法的光线追踪方法-分布式光线追踪。分布式光线追踪不是分布式系统上的光线追踪。分布式光线追踪是一种基于随机分布采样的光线追踪方法，用于减少渲染图像中的瑕疵。起初我们叫distributed ray tracing，后来叫它distribution ray tracing，为了区分parallel computing。我们根据特点也称其为stochastic ray tracing。为什么叫这个名字呢，看下去就知道了。那我们就有一个重要的问题出现了：我们为什么不满足Whitted光线追踪的效果，要不断地发明创造呢？ 观察上图，随便一个普通人都可以非常清楚地断定，这是一张计算机渲染出来的图像而不是相机照下来的图片。因为它拥有完美的反射，表面也是完美的颜色，并且有生硬的影子，难看的锯齿，没有景深。我们称这种现象为失真。甚至在Kajiya（1986）形式化渲染方程之前，Cook等人就认识到渲染只是解决一组嵌套积分的过程。这些积分没有可以在有限时间内计算的解析解，所以使用蒙特卡洛解决这些问题的技巧。在Cook的distributed ray tracing（1984）解决了这些问题，我们可以进行总结其特点： 使用非均匀（抖动，jittered）采样 使用噪点noise代替图像失真的情况 提供了一些特效，如Glossy reflections、Soft shadow、Motion Blur、Time等 分布式光线追踪的主要思想是，之前我们对每一个像素进行超采样可以进行求平均值的方式来进行抗锯齿的操作，那么也可以不光只在一开始eye Ray发出多个射线，也可以在每次反射折射时也发出多条射线，这能做到比whitted光线追踪的单条反射折射射线更多的事情和效果。 最值得我们关注的问题其实是，在1986年当渲染方程被归纳出来之前，1984年的Cook的论文缺乏真正推理它的数学框架，分布式光线追踪缺乏对全局照明效果的任何描述。所以你确实可以随意地发挥确保他符合LIT。 基本的直接光间接光的采样方式和path trace一样，在下一节介绍。 4.1 针对像素的采样 正如之前我们在Whitted中讲到的抗锯齿的方法一样，如果没有随机性的话，不管怎样都会出现特殊的情况从而导致错误。所以，我们分布式光线追踪中使用的就是随机采样的方式（stochastic sampling），这种方法避免了像网格抽样的那种规律性。 4.1.1 泊松圆盘采样（Poisson Disk Distribution） 一个采样位置不均匀分布的例子就是眼睛，眼睛有有限数量的光感受器，就像其它采样过程一样，应该是有一个奈奎斯特极限（Nyquist limit），但是眼睛正常情况下是不会发生锯齿现象的。在眼睛的中央窝（Fovea）中，六边形图案的细胞是紧密排列起来的，晶状体就扮演着低通滤波器的作用，这就避免了锯齿的发生。但是在中央窝外部，细胞的排列都很稀疏，所以采样率是很低的，然而在那里却也没有发生锯齿，原因就是通过细胞的不均匀分布来避免这个区域发生锯齿。 已经有人过研究眼睛中视锥细胞的分布，与人眼相似，光感受器在猴子眼睛的中央窝外部的分布如图所示，这样一个分布称之为泊松圆盘分布：采样点随机分布在一定的范围内且任意两个采样点之间的距离不小于某个值。距离的最小值能限制噪声的数量，举个例子，胶片颗粒（Film Grain）就是随机分布的，如下图所示，但是没有像泊松圆盘分布一样有最小距离的限制，而是采用纯随机分布。造成的结果就是一些样本点会集中一些区域而在其它的某些区域留下大量空白，所以胶片（Film）没有锯齿，有噪声（Noise）存在。 一种简单的实现泊松圆盘分布的方法是： （1）随机生成采样位置，如果随机生成的采样位置与已经选择过的距离小于一给定值，则丢弃它，至少采样区域满为止，采用这种方法可以创建一个查询表（Lookup Table）； （2）还需要计算滤波器的值，该值描述了每个采样点与周围的像素点的关系。位置信息和滤波器的值存储在一个查询表中，这种简单的方法确实能产生很好的图像效果，但是要求有一个非常大的查询表。因此这里引入另外一种技术：抖动（Jittered）。 4.1.2 抖动采样（Jitter Sampling） 抖动采样其实是分层采样（Stratified Sampling）的一种，也时随机采样的一种形式，是一种逼近泊松圆盘分布的技术。 抖动技术又有很多种类型，这里主要介绍规格网格的抖动技术，这种技术能产生较好的实验结果并且很适用于图像渲染算法。其具体原理是对于每一个像素进行分割，并对于每一块的中心区域增加一个随机的偏移，保证偏移在同一块像素里。当然这种的采样方式也可用在对区域光的采样当中。 抖动可以使得高频信号降低，但是降低的高频信号中的能量会出现在噪声中而不会消失，因此基本的光谱组合没有发生变化。与纯种的泊松圆盘分布技术相比，该技术可能会造成更多的噪声，而且可能会留下部分锯齿。 分布式光线追踪随机采样模式的一个有趣的副作用是它们实际上将噪点注入到解决方案中（略微更粗糙的图像）。这种噪点比失真的图像更容易接受吧。 举个抖动的例子，计算时间抖动（Time Jitter）的效果，第n个样本抖动ζn的量，所以会在nT+ζn的位置采样，T表示采样周期，如下图所示，就是时间抖动的效果。可以采用不同的模型来表示抖动量ζn，比如用方差是σ2的高斯分布函数，增益量就可以为频率μ的函数，如下等式所示： 把一个像素看成是一个网格，或者由多个子像素（subpixel）网格构成的大网格，这样就是一个二维的抖动。噪声随机的加到X方向上的位置或者Y方向上的位置，X、Y方向相互独立，就相当于是两个一维的抖动构成的，要求使得每个采样点发生在某个像素网格范围内的随机位置上。如果已知道哪些采样点是可见的，则通过重构过滤器（Reconstruction Filter）对那些采样点的值进行处理。 重构过滤器的实现方法是一个开放性的问题，最简单的重构过滤器是箱式滤波器（Box Filter）：取多个采样点的平均值。也可以采用加权重构滤波器，这种情况下，滤波器是一个采样位置与周围像素相关的加权值。每个像素是附近采样点的值乘以加权值的总和，这些滤波器可以提前计算好保存在一个查询表中。 抖动采样性能和方差的分析本篇文章篇幅问题暂时不予讨论，等有空的时候我们再一探究竟。 4.2 区域光和软阴影 Whitted光线追踪中的阴影是离散的。着色点时，检查每个光源以查看它是否可见。如果光源是可见的，则它对点的着色有贡献，否则它没有。光源本身由单个表示。 这对于距离很远的光源而言，这是相当准确的，但对于大型光源或近距离的光源来说，这个表示很差。这种离散决策的结果是阴影的边缘非常清晰。从点到可见光点到光源点都有明显的过渡。现实世界中的阴影要柔和得多。从完全阴影到部分阴影的过渡是渐进的。这是由于真实光源的有限区域和其他表面的光的散射。 分布式光线追踪尝试通过将光源表示为球和在球体表面的随机分布的点（区域光）来近似软阴影。当查看点是否处于阴影中时，围绕光源的投影区域投射一组光线。从光源传输到该点的光量可以通过击中光源的光线数量与投射的光线数量之比来近似。 区域光源会产生柔和的阴影（Soft shadow = umbra +penumbra）也就是完全阴影和完全光照之间的区域，软阴影可以通过向区域光源表面上的随机点发射阴影光线来计算。 4.3 高光反射 Whitted光线追踪可以很好地表示完美的反射表面，但却难以表现出高光或部分反射的表面。只有当曲面是完美的镜子时，它的反射才看起来与现实完全相同。更常见的是表面是高光的并且反映了场景的模糊图像。这是由于表面的光散射特性。而传统的Whitted追踪中的反射始终是锐利的。 为什么呢，主要是由于，传统的光线追踪的反射的贡献，都只来自于其单一的反射向量。 而分布式光线追踪的反射的贡献，不是沿反射方向投射单个光线，而是围绕反射方向发出一束光线，其是随机在锥体内对镜面角度周围进行多条光线采样。反射的实际贡献值可以通过取每个射线返回的值的统计平均值来得到。此方法还可用于通过使用区域光源生成镜面反射高光。如果从表面反射的光线撞击光源，它们将添加到表面照明的镜面反射分量。这可以取代phong模型的镜面反射分量。并且，反射强度随着镜面角度缩小迅速下降，该方向的取样概率应同样下降。 4.4 透明度 Transparency Whitte光线追踪可以很好地表现出完美透明的表面，但是在表现半透明的表面方面很差。半透明的真实表面通常传输其后面的场景的模糊图像。分布式光线追踪通过在传统光线追踪的折射光线的大致方向上投射随机分布的光线来实现这种类型的半透明表面。然后将从这些射线中的每一个计算的值平均以形成真正的半透明分量。 基本的Monte Carlo光线追踪方法效率不高，主要原因是会出现多次反射，折射却打不到光源的情况，特别是在光源面积比较小的地方非常明显。我们可以通过对光源进行采样来解决这个问题。在每次射线打到物体得到交点后，我们可以在面光源上采一个点，然后判断连接交点和光源点的阴影线是否被遮挡，如果未被遮挡，则可以计算光源对物体表面交点的直接光照。在这个过程中要通过使用多重重要性抽样（Multiple Importance Sampling）[4] 计算这次反射的pdf。 4.5 动态模糊（Motion Blur） 在摄像机中，快门速度表示每帧表示在相机快门打开期间场景的时间。如果场景中的物体相对于相机处于运动状态，则它们将在胶片上显得模糊。这个原理是常识性的，拍摄飞奔的运动员时，你需要较快的快门速度，否则由于曝光时间过长，图片会变的不清晰，因为最终的像素是这一时间曝光得到的平均值。当然如果你要的效果就是那种夜间川流不息的车辆的灯光流尾的效果，就需要比较长的快门速度，这还需要三脚架等固定工具，因为手抖会造成周围的景致的物体模糊。 分布式光线追踪可以通过在时间上和空间上分布光线来模拟这种模糊。在投射每条光线之前，将对象平移或旋转到该帧的正确位置。然后对光线进行平均后得到实际值。运动最多的对象在渲染图像中的模糊程度最高。这其实就是对时间的采样平均。 4.6 景深（depth of field） 人眼和相机都具有有限的镜头光圈，因此具有有限的景深。两个远处或两个近处的物体将显得不聚焦且模糊。但是几乎所有计算机图形渲染技术都使用针孔相机模型。在此模型中，无论距离如何，所有对象都处于完美的焦点。在许多方面，这是有利的，由于缺乏聚焦而导致的模糊通常在图像中是不希望的。然而，模拟景深可以产生更逼真的图像，因为它可以更准确地模拟真实的光学系统。 分布式光线追踪通过在视平面（Image plane）前放置人造透镜（Lens）来创建景深。再次使用随机分布的光线来模拟景深的模糊。第一次射线投射不会被镜头修改。假设透镜的焦点沿着该光线处于固定距离处。发送给同一像素的其余光线将散射在镜头表面周围。在镜头处，它们将弯曲以穿过焦点。场景中靠近镜头焦点的点将清晰对焦。更近或更远的点将变得模糊。 在所有在focal length里面的物体都是清晰的，其它的物体会变模糊。 4.7 分布式光线追踪的缺点 它的主要问题在于分布式光线追踪指数性质。你首先会发射光线，这通常是最重要和最重要的照明贡献。但是对于次级反弹，你必须发射越来越多的光线。1射线变成10射线，每射出10，你有100射线，每射出10，你有1000，依此类推。所以你的第3次反弹需要多1000倍的阴影，即使它可能占最终照明的1/100或更少。 如上图，如果像素抗锯齿+反射抗锯齿，想想都可怕。它本质上是低效率的，因为你花费最多的时间来计算图像中最不重要的部分。高级照明效果（如焦散）也很难使用分布式光线光线追踪进行渲染。 五．路径追踪 当Kajiya在1986年提出渲染方程时，他也给出了解决这个方程的方法。Kajiya将渲染方程转换为一个基于路径积分的形式。 路径追踪是分布式光线追踪的一种变体，其中每个点只发出一条反射和折射光线。这样可以避免光线数量的激增，但太简单的实现会导致拥有非常多噪点的图像。为了弥补这一点，每个像素都对多条光线进行追踪。 路径追踪的一个优点是，由于每个像素发出较多的可见性光线，因此可以以很少的额外成本合并景深和运动模糊等相机效果。另一方面，与分布式光线追踪相比，要确保反射光线（例如通过分层）的良好分布更为困难。简而言之，分布式光线追踪在光线树中发出的光线最多，而路径追踪追踪在初始时发出的光线最多。 5.1 路径追踪方程 渲染方程有两种形式，第一个就是我们经常使用的半球面形式，但是它还有一个重要的形式，那就是面积形式。Kajiya将渲染方程转换基于路径积分的显式方程，光照计算才能有效的进行蒙特卡洛计算。 根据立体角微元和面积微元间的关系： 这就是最终的面积积分形式，不停的进行左右替换，就得到了这样一个和式： 其中指长度为n的路径，是光源经n段传播过程后为最终结果作出的贡献： 这就是LTE的路径和形式。这是一个n-1维的高维积分，我们之后每一个采样样本都是一个n-1维的向量。我们称这个向量所在的空间为路径空间。 路径积分形式的好处是它把对整个场景的渲染问题转化成了对一个对由各种长度路径组成的路径空间的采样问题。在视点和光源之间随机生成若干条路径，估计它们的贡献值，并把贡献值加到该路径影响到的像素上，就可以得到场景的渲染图像。双向路径追踪和Metropolis光线追踪都很适合采用这种路径积分形式来描述和计算。 5.2 直接照明 5.2.1 只有一盏灯的照明 由于要计算灯光，所以 我们需要把半球积分变为面积积分： 其中 yi 是在灯光范围上的点, V（x，y）是阴影光线的可见函数，有遮挡物时为1，否则为0。 为了计算由于场景中的单个光源（引起的直接照明，我们需要在光源的区域上定义概率密度函数 p(y) ，在上面会产生阴影射线。我们假设可以构造这样的PDF，而不管光源的几何特性。我们使用蒙特卡洛积分进行估计。 directIllumination (x, theta)//x是表面的点 estimatedRadiance = 0; for all shadow rays generate point y on light source; estimatedRadiance +=Le(y, yx) * BRDF * radianceTransfer(x,y)/pdf(y); estimatedRadiance = estimatedRadiance / #shadowRays; return(estimatedRadiance); 蒙特卡罗积分的方差以及最终图像中的噪点，主要由 p(y) 的选择决定。理想情况下 p(y) ，等于每个点 y 对最终估计量的贡献，但实际中，这几乎是不可能的，必须进行更实际的选择，主要是易于实现。p(y) 的经常使用的选择是： 光源区域的均匀采样 光源对立体角的均匀采样 光源区域的均匀采样 在这种情况下，p(y) 是均匀的分布在灯光的表面，所以每一个点的概率是。这是对光源进行采样的最简单方法，并且可以轻松使用分层采样等优化方法进行改进。使用这个采样方案，图像中会有几个噪声源。如果一个点 x 位于阴影的半影区域，它的一些阴影光线将产生V(x,yi)=0 ，而有些则不会。这会导致在软阴影区域内产生大部分噪点。另一方面，如果在阴影外，大部分噪点都是由余弦项的变化引起的。 光源对立体角的均匀采样 为了消除由余弦项和平方因子引起的噪声，可选择根据立体角进行采样。这需要将光源区域上的积分重写为光源所对的立体角上的积分。这将从被积函数中去掉一个余弦项和距离因子。但是，可见性测试仍然存在。这种采样技术通常很难实现，因为在任意立体角上生成方向并不简单。 5.2.1 多盏灯的照明 当场景中有多个光源时，最简单的方法是依次为每个光源分别计算直接照明。我们为每个光源生成一些阴影光线，然后总结每个光源的总贡献。这样，每个光源的直接照明分量将独立地计算，并且可以根据任何标准选择每个光源的阴影射线的数量。 但是，通常最好将所有组合光源视为单个，并将蒙特卡罗积分应用于组合积分。当产生阴影射线时，它们可以被任何光源使用。因此，可以仅用单个阴影射线计算任意数量光源的直接照射，并且仍然可以获得无偏图像（尽管在这种情况下，最终图像中的噪声可能非常高）。这种方法之所以有效，是因为我们将光源完全抽象为单独的解析曲面，只需查看一个集合。然而，为了获得能工作的算法，我们仍然需要单独访问任何光源，因为任何单独的光源可能需要单独的采样程序来在其表面上生成点。 5.3 间接光照 与直接照明计算相反，间接光照这个问题通常要困难得多，因为间接光从所有可能的方向到达表面点。因此，很难按照与直接照明相同的路线优化间接照明计算。 间接照明包括在光源和x之间的中间表面上至少反射一次后到达目标点X的光。间接照明是全局光照的一个非常重要的组成部分。 indirectIllumination (x, theta) estimatedRadiance = 0; if (no absorption) for all indirect paths sample direction psi on hemisphere; y = trace(x, psi); estimated radiance +=computeRadiance(y, -psi) * BRDF *cos(Nx, psi) / pdf(psi); estimatedRadiance = estimatedRadiance / #paths; return(estimatedRadiance/(1-absorption)); computeRadiance(x, dir) estimatedRadiance = Le(x, dir); estimatedRadiance += directIllumination(x, dir); estimatedRadiance += indirectIllumination(x, dir); return(estimatedRadiance); 5.3.1 间接光的均匀采样 评估间接照明的最一般的蒙特卡罗方法是使用任意的半球PDF，，并生成n个随机方向的。这将产生以下估计量： 为了计算这个估计量，对于每个生成的方向，我们需要评估沿着方向从 x 追踪一条射线的BRDF和他的余弦项，并评估最近交处的反射辐射度。最后这个值表面了间接照明的递归性质，因为处的反射辐射可以再次分解为直接和间接贡献。这里直接给出均匀采样的PDF，为 。所产生的图像中的噪点主要将由BRDF和余弦项的评估变化以及远点处的反射辐射 Lr 的变化引起。 5.3.2 间接光的重要性采样 在半球上进行均匀采样是一种非常鲁莽的决策，因为这显得好像我们对间接照明积分中的被积函数没有任何了解。为了降低噪点，我们需要某种形式的重要性采样。我们可以构造一个与以下任何因素成比例（或近似成比例）的半球形PDF： 余弦项 BRDF 间接辐射度 上面任何形式的结合 余弦项采样 与法线 Nx 周围的余弦波成比例的采样可防止在半球的水平线附近采样过多，其中等于0。我们可以预期噪点最终会减小，因为我们降低了概率对生成的方向贡献很小的方向。所以： 假设BRDF在此点只有漫反射，我们可以得到： 这里，唯一的噪点来自于 Lr 。 BRDF采样 有些材料仅在非常特定的方向上反射光。这通常与材料的粗糙度有关。镜子（例如左边的镜子）需要的样本比右边的漫反射表面少得多。 这取决于材料的双向反射分布函数（BRDF）。该分布函数决定了在给定方向上反射的光量。通过采样方向直接从该分布函数（重要性采样）进行测试，我们可以避免采集对最终图像贡献很小的样本。 BRDF采样是一种很好的降噪技术，当有光泽或高镜面BRDF时。它降低了在BRDF值较低或为零时对方向进行采样的概率。然而，只有少数的特定的BRDF模型才有可能精确地按照BRDF比例采样。更好的办法是，试着按照BRDF和Cosine项的乘积比例取样。从分析上讲，这更难做到，除非在少数情况下。通常，根据这种PDF，需要结合拒绝进行抽样。 另一种耗时的方法是建立一个累积概率函数的数值表，并使用该表生成方向。PDF值并不完全等于BRDF和Cosine因子的乘积，但仍然可以实现显著的方差减少。说明按照BRDF采样，让我们考虑修改后的Phong BRDF。 这里 d 的是完美的镜面反射方向。是漫反射部分，是高光部分，所以间接光部分我们可以分为： 对于一个点来说，我们进行的是分类讨论，发生漫反射的比例为q1。 发生高光的比例q2。 发生吸收的比例q3，贡献值为0。 q1+q2+q3=1 对于漫反射部分，我们比较好的选择是根据余弦分布，高光部分可以根据。 另外，q1、q2、q3的值也会也会影响最后的结果，原则上，不管选择哪一组值最终都会提供一个无偏估计量，但是仔细选择这些值会对最终结果产生影响。一个好的选择是选择与不同模式中的（最大）反射能量成比例的这些值。这些值可以通过对沿表面法线 Nx 的入射方向的反射进行积分来计算： 需要注意的是，对于除法线之外的任何其他入射方向，q的值实际上大于实际反射能量，因为围绕的余弦波部分位于x处的表面下方。这可以通过不重新采样位于波形下方的波形部分中的任何方向来调整，从而保持在漫射能量，镜面能量和吸收之间取得平衡。 5.4 完整的算法 computeImage(eye) for each pixel radiance = 0; H = integral(h(p)); for each sample // Np viewing rays pick sample point p within support of h; construct ray at eye, direction p-eye; radiance = radiance + rad(ray)*h(p); radiance = radiance/(#samples*H); rad(ray) find closest intersection point x of ray with scene; return(Le(x,eye-x) + computeRadiance(x, eye-x)); computeRadiance(x, dir) estimatedRadiance += directIllumination(x, dir); estimatedRadiance += indirectIllumination(x, dir); return(estimatedRadiance); directIllumination (x, theta) estimatedRadiance = 0; for all shadow rays // Nd shadow rays select light source k; sample point y on light source k; estimated radiance +=Le * BRDF * radianceTransfer(x,y)/(pdf(k)pdf(y|k)); estimatedRadiance = estimatedRadiance / #paths; return(estimatedRadiance); indirectIllumination (x, theta) estimatedRadiance = 0; if (no absorption) // Russian roulette for all indirect paths // Ni indirect rays sample direction psi on hemisphere; y = trace(x, psi); estimatedRadiance +=compute_radiance(y, -psi) * BRDF *cos(Nx, psi)/pdf(psi); estimatedRadiance = estimatedRadiance / #paths; return(estimatedRadiance/(1-absorption)); radianceTransfer(x,y) transfer = G(x,y)*V(x,y); return(transfer);。 路径追踪适用于任何类型的几何图形，任何类型的BRDF，采样所有类型的路径，内存消耗低，但是其最大的问题就是标准的蒙特卡洛问题：慢收敛，和噪点。 缺点也是很明显的，首先效果上因为取巧不如分布式光线追踪，然而纯路径追踪的收敛速度是很慢的，从视点打出的光线有的时候很难找到一条贡献大的路径（即找到这样一条路径的概率很小），造成图像往往有很多噪点，每个像素需要上千个采样才能达到满意的效果。 关于俄罗斯轮盘赌就是不能简单的固定步长的终止追踪，因为贡献度高的应该长度越长，低的就越短。主要也是能量守恒。这个网上比较多，不说了。 六．双向路径追踪 双向路径追踪算法，是由Veach和Guibas和Lafortune和Willems分别提出的，它可以很好的生成焦散和镜面。 双向路径追踪（Bidirectional Path Tracing）的基本思想是同时从视点、光源打出射线，经过若干次反弹后，将视点子路径（eye path）和光源子路径（light path）上的顶点连接起来（连接时需要测试可见性），以快速产生很多路径。 从眼睛生成长度为nE的子路径：x1、x2、x3、x4...，从光源生成nL条子路径，y0、y1...。然后，我们分别连接这两条子路径上的每个结点，只有两个位于两条子路径上不被其他物体遮挡的结点才可以连接。这样，我们可以得到不同长度的一组路径，而且这组中相同长度的路径有多条。例如我们要得到一条长度为 k 的路径，我们可以从光源子路径中选择长度为 s 的路径然后从眼睛子路径中选择长度为 t(t=k+1) 的路径，所以对于长度为 k 的路径，我们可以有k种选择，每一种 s 的选择, 实际上就代表了一种不同的对路径空间的采样策略，这些策略每一种都有它适用的情况，对于产生某种特殊的效果产生各自的作用，将这些策略综合起来，我们就可以得到适应性更强的渲染算法，得到更好的渲染效果。这些策略大体上可以分为三类, s = 0，例如 x0、x1...xk这种情况是基本的Monte Carlo光线追踪，也就是在生成眼睛子路径的过程中打到了光源。 t=1，例如 x0(E)↔yk-1y0(L) 这种情况是光源光线追踪，直接连接视点和光源子路径。这条路径影响到的像素不是最初的像素，必须通过计算得到，而且需要注意眼睛和光源子路径的端点是否可见。另外，这里 t≠0 ，因为在这里我们不考虑视点有一定面积的情况，而把视点看作是一个抽象的点。 S=1,…k，例如，x0(E)x1...xi↔yk-i-1y0(L)，这是一般的情况，需要注意互联的两个点是否可见。 [Veach 1995]提出了多重重要性采样（Multiple Importance Sampling）的方法，可以将各种采样方法采样的结果以合适的权重合并起来，非常有效地降低图像的噪声。比如，现在要求解积分中所有长度为 4 的光路的贡献，可以有以下几种采样方法：视点子路径长度为 0， 光源子路径长度为 3，记为（0,3）；和（1,2），（2,1），（3,0）。这些方法采样出来的路径贡献可能相差很大，使用多重重要性采样合并这些采样可以达到很好的降噪效果。然而双向路径追踪也有明显的缺点，当两个顶点都在纯镜面或折射面上时，双向路径追踪方法不会去连接这两个顶点，这样就造成了它难以有效地模拟Caustics的反射和折射。 Choose a light ray from the light//从光源选择一条光线 Find ray-surface intersection//找到光线和物体表面的交点 Reflect or transmit//进行反射或者折射 u = Uniform() if u < reflectance(x) Choose new direction d drawn from BRDF(O|l)/ Goto 2/根据BRDF找到新的方向进行反射 else if u < reflectance(x) + transmittance(x) Choose new direction d drawn from BTDF(O|l) Goto 2/根据BRDF找到新的方向进行反射 七．Metropolis 光线追踪 路径追踪（Path Tracing）中一个核心问题就是怎样去尽可能多的采样一些贡献大的路径，Eric Veach等人于1997年提出了梅特波利斯光照传输（Metropolis Light Transport，常被简称为MLT）方法。 1953年，Metropolis等人为了解决计算物理领域的复杂采样问题，提出了一种新的采样方法，这就是Metropolis方法。他们最早使用Metropolis方法的初衷是为了计算流体的材质属性，现在，Metropolis方法已经在物理、化学等很多领域得到了广泛的应用。同样，我们也可以将Metropolis方法应用在全局光照领域，来解决复杂的采样问题。之前的这些光线追踪的算法最大的问题是它们都有不适和的场景，也就是说没有一种方法对任意场景都起作用，因为这些方法对场景中从光源到视点的路径进行采样时都没有考虑到场景的路径分布特征，都是采用随机采样的方法采样路径并计算贡献。因此，一旦场景的路径空间中路径分布极不均匀，这些方法就会因为大量的路径没有带来贡献而造成效率的极度降低，例如在间接光照主导的场景中，光源和视点之间被严重遮挡，只有很小的通道可以通过，随机采样时，只有很少的路径可以通过这个通道带来贡献，其它的路径只能被丢弃。 重要性采样的选取需要样本与函数 f 成比例，即采样样本要成比例于函数 f 的概率分布。而在实际无偏的渲染过程中，函数 f 为已知函数，但由于其形式较为复杂和不规则，采样过程中样本的选取所形成的概率分布很难成比例于函数 f ，这样就会导致样本的方差较大。Metropolis提出了一种方法可以生成采样，样本的概率分布可以成比例于任何可以估计的函数。 样本由马尔可夫链生成，所以每一个样本只与其前一个样本相关而与其他样本无关。新生成的样本由当前样本随机突变生成，其突变过程满足设定的突变策略。突变策略的选取较为开放，但是其需要满足遍历性。遍历性指采样过程中，无论从任何初始样本开始采样，最终采样过程都将逐渐的收敛于 pi 的分布。 采样分布的获得是通过对突变样本的接收和拒绝实现的，最终样本的接收和拒绝取决于接收概率的设置。接收概率表示接收突变样本 y 为当前样本 x 的概率表示为 a(y|x)。最终马尔可夫过程将收敛至一个平稳分布，若此过程处于平稳状态，空间中两个样本的转移密度也处于平稳状态。这种性质被称作细节平衡条件。 当接收概率满足细节平衡条件，突变策略满足遍历性，生成采样序列的概率密度将服从平稳分布。为了尽快使采样过程收敛达到平稳分布，将接收概率设置得尽可能大是一种较好的策略。 7.1 马尔科夫链 7.1.1 概念 X是一个随机变量，其可能的取值来自于集合，X在离散时刻 t的取值为Xt ,若 X 随时间变化的转移概率仅仅依赖于其当前时刻的取值Xt ，即 那么随机变量X随时间变化的过程是一个马尔科夫过程X在 [0,t] 时间内随时间变化生成的序列（X0、X1、...、Xt）就是一个马尔科夫链。 7.1.2 转移概率矩阵 设随机变量X在任意时刻t+1的取值为si的概率为，即，其中t为一个任意时刻。随机变量从状态 sj 转移到状态 si 的转移概率为Pji： t 为任意时刻，那么我们可以得到的计算公式如下： 我们注意到在上式中，因此上式可以写成以下形式： 那么我们可以将随机变量X在t+1时刻取值为的概率写为如下形式： 将上式右侧的矩阵记为Ptran，即： Ptran就是马尔科夫过程的转移概率矩阵。 7.1.3 平稳分布 如果一个马尔科夫过程的某个状态经过有限次状态转移后又回到自身，那么我们称这个马尔科夫过程具有周期性。如果一个马尔科夫过程存在两个状态，它们之间是互相转移的，那么我们称这个马尔科夫过程可约。若一个马尔科夫过程既没有周期性又不可约，那么这个马尔科夫过程是各态遍历的。各态遍历这个概念我个人的理解是，每个状态都有一定的概率会出现。 各态遍历的马尔科夫过程有一个重要性质：无论初始的状态概率取值如何，经过足够多次的状态转移，各态遍历的马尔科夫过程会趋于一个平稳分布： 这时随机变量X从取值的概率不再变化，而是服从平稳分布。 平稳分布满足性质：即平稳分布经过状态转移后随机变量取值的概率不变，这是当然的。 这是侑虎科技第1065篇文章，感谢作者papalqi供稿。欢迎转发分享，未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 作者主页：https://www.zhihu.com/people/papalqi，再次感谢papalqi的分享，如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 完整内容可前往此处查看。
近年来，卡通渲染技术在越来越多的游戏开发中被广泛运用，如《守望先锋》、《英雄联盟》、《DOTA2》、《崩坏3》等经典游戏。笔者对这些游戏中涉及的经典技术如描边、着色等做了详细的总结，对大家项目的制作开发极具参考价值，建议收藏！ 前言：卡通渲染是图形学中一个有趣的话题，属于非真实感计算机图形学（NPR）的范畴，在NPR领域中也最多地被应用到实际游戏中，近年来流行的《守望先锋》，《英雄联盟》，《DOTA2》，《崩坏3》等游戏中都或多或少地出现过卡通渲染的身影，恰好最近对这个领域的内容作了一些了解和探索，所以就对其中涉及的一些经典技术做一个概述。 壹 · 卡通渲染的分类 在具体讨论技术手段之前，先就卡通渲染做一个分类。卡通渲染最关键的特征包括不同于真实感渲染的艺术化光影效果和描边。以这两个关键的特征为卡通渲染分类的话，可以将近年来游戏中常用的卡通渲染分为美式卡通风格和日式卡通风格。美式卡通风格在色彩上比较连续，有渐变色，着色风格很大程度上依赖于艺术家定义的色调（tone），而在阴影和高光方面常常采取夸张和变形的做法，比较典型的是《军团要塞2》；日式卡通风格往往角色造型更写实，但在着色方面，则趋向于大片大片纯色色块，并有的明暗交界，例如《崩坏3》。虽然这样的分类并没有清晰界限，但易于描述，接下来我们就按照美式卡通和日式卡通的分类，从光影和描边两个维度上分别列举各类技术实现。 军团要塞2的卡通渲染，人物造型夸张，但着色连续，接近真实感光照 崩坏3游戏截图，着色以单色色块为主，有明显的明暗交界 贰· 描边 描边是一个比较常用的技术，在《Real Time Rendering》中有相当篇幅的综述，大致来说包含了三类： （1）基于视角的勾边，这部分的计算依赖于我们的一个直觉观察：当我们的视线和某个表面相切时，这个表面上的像素点往往就是模型的边缘，基于这个观察，我们可以用dot(viewDir, normal)^{k} 来估计一个像素的“边缘程度”，当然，这个值也可以用来作为纹理坐标去采样一张预定义的“轮廓纹理”。 基于视角的描边，最大的缺点是线宽粗细差别较大，不易控制 （2）基于几何生成方法的描边，这类方法的特点是描边本身是一个单独的几何体，通过特殊的方法绘制出来，比较常见的做法是shell method，原理和实现都比较简单：首先在绘制结束正常的模型后，将需要描边的物体改用正面剔除再绘制一遍，在VS中将顶点沿着法线方向膨胀一定距离，然后在FS中将模型用纯色输出。另外一种叫做z-bias的方法，也是绘制背面，但不膨胀，而是把背面顶点的Z值稍微向前偏移一点点，使得背面的些许部分显示出来形成描边效果。 基于shell method的绘制方法，实现简单，线宽较为均匀 （3）基于图像处理的描边，这类方法的实现可以说更接近于“边缘”这一概念的本质定义，什么是“边缘”呢？边缘就是在深度或者法线上不连续的位置。因此为了获取边缘，我们只需要在图片上找到深度或者法线不连续的位置即可，因此，我们需要将深度信息和法线信息以贴图的形式传入，运用边缘检测算法去寻找这些像素。这类方法的优点是描边的线宽一致，缺点是需要额外的法线和深度信息，当然，由于近年来流行的延迟渲染框架，法线和深度本来就是G-Buffer的一部分，因此往往不需要额外绘制法线和深度的信息。 基于边缘检测的描边方法，分别用深度信息和法线信息进行单独的边缘检测，而后合并起来成为最终的描边 美式卡通中的做法 美式卡通中往往倾向于使用基于图像处理的描边方法来生成均匀一致的描边效果。在《英雄联盟》中小兵和英雄的勾边效果就是用Sobel算子对深度信息进行边缘检测来获得的。由于游戏中只需要针对小兵和英雄勾边而不需要对场景地图进行勾边，因此在LOL中，勾边的计算并非全屏后处理，而是逐物体进行的，这样的好处是可以随意控制哪些物体描边，每个物体可以单独指定描边颜色，缺点是当物体较多时（尤其是skinned mesh较多时）计算量会增大。一个折衷的方案是，在进行正常绘制的阶段用stencil buffer标记出需要描边的物体，然后用一个全屏的后处理，对stencil buffer标记的像素进行边缘检测，当然这样的话，就很难给每个物体单独指定描边颜色了。 实际上，在LOL中有两种类型的描边，一种是小兵和英雄的固定描边，另一种是防御塔发出攻击警报或者某个单位被点选时才产生的红色描边，这两种描边在处理上略有差别，前者直接使用边缘检测的结果作为最终描边，而后者则是对边缘检测结果再进行一次模糊，借此来扩大和柔化描边效果。 LOL中两种类型的描边，可以看出第二类描边的线宽更宽，并且有明显的过度效果 日式卡通中的做法 日式卡通中往往倾向于使用基于几何体生成的方法去描边，这类描边方法相较于另两类方法的好处在于线宽更容易为美术所控制，而在日式卡通中，往往需要粗细有变化的描边去体现角色不同部位的特征，例如在《GUILTY GEAR Xrd》中，角色的描边就是通过几何体生成的方法，结合了shell method和z-bias method，并引入了逐物体的顶点色来控制描边细节，同时也是为了保证描边粗细不会随着摄像机视距发生变化，具体来说，顶点色存储的信息包括： R通道：控制toon shading的阈值，和描边无关，和着色有关，这个我们后面描述 G通道：控制顶点根据视距膨胀的强度（这个部分具体操作我也没有完全弄清楚，希望了解的朋友来补充） B通道：控制描边的z-bias，越大则描边越不可见 A通道：控制描边的粗细 上述做法中比较直观的理解是，通过引入逐顶点的线宽系数，使得整个描边的细节更易为美术控制，但是从我的理解来看，线宽控制只需要一个值即可，视距无关的粗细可以通过给偏移值offset.xy乘以当前顶点的z值来实现，似乎并不需要三个值来控制。 没有vertex color，轮廓线宽没有粗细变化 有vertex color， 轮廓线可以按照美术的需要去设定逐顶点粗细变化 叁 · 着色 Cel Shading和Tone Based Shading 先来描述两种经典的NPR着色方法，分别是Cel Shading和Tone Based Shading5。 Cel Shading的基本思想是把色彩从多色阶降到低色阶，减少色阶的丰富程度，从而实现类似手工着色的效果，具体来说，可以用如下计算方法： 其中，Kd表示模型自身的贴图颜色，celCoord表示法线和光照方向的点积，用作一维色彩表的查找坐标，而paletteTex则是由美术绘制的一维色阶表，一般来说是由几个纯色色块组成的，如下图： 上述做法可以用于模拟卡通渲染的漫反射分量，却并没有考虑到视角相关的光照分量的模拟，因此很难实现类似菲涅尔效果的卡通渲染。实际上，也可以用类似的查找表的思路来视角相关光照分量的色阶离散化6，只需要将一维查找表扩展到二维即可： 相应地，查找坐标也扩展到了二维。 不同于Cel Shading，Tone Based Shading的风格化是基于美术指定的色调插值，并且插值得到的色阶是连续的。首先需要由美术指定冷色调和暖色调，而最终模型的着色将根据法线和光照方向的夹角，在这两个色调的基础上进行插值，具体算法如下： ， 其中，Kd仍是模型自身色彩贴图，Kblue，Kyellow和alpha，beta则均是自定义的参数。 基于tone based shading绘制的球体 日式卡通中的着色 前面已经描述过，日式卡通在着色方面比较典型的特点是以大量纯色为主，进一步说，往往只有“明暗”或者“冷暖”两个色阶，因此光照计算往往最后也要映射到离散的色彩表上。 仍然以《GUILTY GEAR Xrd》为例，它也一定程度上包含了Cel Shading和Tone Based Shading的部分思想，将色阶离散成为“明暗”两个色调，并由美术指定冷暖色调的颜色： 上述公式表示了这个卡通渲染的漫反射部分，其中threshold表示明暗交界的阈值，在游戏中通过顶点色的R通道来实现逐顶点的控制。Kcool和Kwarm由美术逐物体地指定，Ksss是对模型次表面散射效果的模拟，对皮肤而言一般呈粉红色，通过美术绘制的SSS贴图来实现逐像素控制，并且只有暗部的像素才会受SSS贴图的影响。Kd是模型自身的颜色贴图。darkness表示了某个像素的明暗程度，用于确定色调的冷暖。除了正常的dot(normal, lightDir)项，游戏中还加入了由美术绘制的AO贴图，来实现一些边角缝隙的暗部效果。我在实现时又引入了动态的阴影部分，最终darkness的计算公式为： 其中shadow是由shadowMap的算法计算得来的。 高光的计算更简单一些： 其中，spec表示高光的强度，threshold可以由美术逐物体或逐顶点指定，specMask和specPower由美术绘制的贴图来逐像素控制，类似于phong着色中的specular和glossiness的作用。specColor可以由美术逐物体地指定，也可以把AO，shadow和明暗色调作为影响因素添加进去。最终的着色结果将漫反射和高光叠加即可。 在实际游戏中使用时，上述方法往往还需要配合美术针对具体模型进行法线修正。 根据模型顶点位置和拓扑关系计算出的法线往往细节过度，表现在上述卡通渲染的结果上就是往往会出现许多不需要的暗部细节，修正的方法是使用模型法线转印，给精细的模型一个近似的低精度proxy（比如用一个球形代表模型的头部，用一个圆柱形代表模型的胳膊或者腿），然后用proxy上附近顶点的法线作为模型的法线来使用。此外，还需要考虑到明暗交界处反走样的问题，这里不做展开。 根据我的观察和研究，《崩坏3》应该是沿用了《GUILTY GEAR Xrd》中的卡通着色方法和美术工艺，因此在效果上和后者非常相似。 基于不同的冷暖色调设定值得到的卡通渲染结果 美式卡通中的着色 Valve在其游戏《军团要塞2》7中描述了他们的卡通渲染方案，这个卡通渲染算法也在后来影响了《DOTA2》的卡通渲染的实现。他们将卡通渲染着色分为了view dependent term和view independent term。两部分的计算分别如下： 这部分的实现在其他知乎专栏文章中有详细的描述和实现，这里不再做详细的解释。直观地来说，在视角无关的照明部分，《军团要塞2》中除了考虑了一般的漫反射部分外，还加入了基于模型法线方向的环境光分量，此外，通常的漫反射分量改为了wrapped diffuse；而在视角相关的照明部分，《军团要塞2》除了考虑了一般的镜面反射外，还基于菲涅尔现象实现了类似边缘光的效果。实际上，类似ambient cube和warpped diffuse的做法也被Valve应用在《Half Life》等其他游戏中9，在早期3D游戏中用以模拟全局照明。虽然这些方法都是一些纯粹的trick，但是能够以很小的开销实现不错的效果。 《军团要塞2》的最终着色结果，可以看出明暗交界处有明显的泛红（warpped diffuse的效果），模型边缘可以看到边缘光 经典的half-lambert方法也算是warpped diffuse的一种变体 肆 · 风格化高光和阴影 在7的Future Work里，还提到了可变形状的高光10和风格化阴影11，这两个风格化渲染算法的思路都比较有趣，这里简单就其实现原理进行一个概述。 可变形状的高光 我们在日式卡通渲染的着色部分描述了一个相对较为简单的高光计算方式，从计算方法可以看出，该方法和经典的Blin-Phong模型有很多相似之处，尤其是对高光强度的计算上，都采用了这个计算项： 这个halfVec也就是我们常说的半角向量，计算方法是： 其中，L和V分别是光源方向和视线方向。 从我们上面描述的卡通渲染高光算法可以看出来，改变卡通渲染高光形状的关键就在于改变这个半角向量。因此文章中就针对半角向量定义了一系列的修改操作，这些修改操作可以叠加使用，也可以单独使用，每个操作对高光形状的影响均不同，具体有以下几个操作： （1）平移，改变高光的位置： 这里，du和dv表示的是切线空间中的x轴和y轴，也就是切线和副法线，alpha和beta是自定义平移参数，最终偏移后的向量需要进行归一化处理。 （2）有方向的缩放，沿着切线空间的某个轴缩放高光形状： sigma是自定义参数，范围是(0, 1]，上式将使高光沿着切线空间的X轴缩放。 （3）分割，将一块连续的高光切分成两块： 其中，sgn是符号函数，负数返回-1，否则返回1，gamma1和gamma2分别是自定义参数，若其中一个为0，则只沿着另一个方向将高光切为两部分，若两个参数均不为0，则高光被切成四块。 （4）方块化，将趋于圆形的高光变成方形： 其中，n是自定义整数，n越大高光形状越方，sigma则定义了方形高光区域的大小，范围是[0, 1]。 上述四个操作的具体实现可参见这篇文章12。 四个基本的操作符 风格化阴影 类似于风格化的高光，风格化的阴影也是在标准的阴影计算流程之后，定义了一系列针对标准阴影的操作，通过这些操作，配合用户自定义的参数，便可以达到风格化阴影的效果，总的来说，共有四类操作： （1）膨胀/腐蚀（Inflation）：扩大或者缩小阴影范围，用参数i来控制 （2）亮度（Brightness）：阴影区域的亮度，可以用于模拟半影区的效果，用参数b控制 （3）柔度（Softness）：阴影边界处的柔和程度，用参数s控制 （4）抽象度（Abstraction）：阴影形状的抽象程度，用参数alpha控制 几种操作和相应的效果 整个风格化阴影的生成是基于图像空间的，从一个已经生成的精确阴影图开始。可以分成五个阶段： （1）精确阴影的生成，由于是基于图像空间的，因此对精确阴影图的生成方法没有特别要求，可以是shadow volume，shadow map，ray tracing或者其他阴影生成技术，但必须要注意的是这里的阴影值一定是二值化的。 （2）有向距离场的生成，基于图像空间的精确阴影，计算每个像素距离最近阴影边界的有向距离，这是文中算法的核心，也是后面风格化的基础，在文中给出了一种有向距离场的计算方法，当然也可以采用其他方案。 （3）有向距离场的高斯模糊，这一步是抽象阴影生成的关键。 （4）过滤，通过一个转移函数，将模糊后的有向距离场重新映射为阴影图。 （5）使用过滤后的阴影进行光照计算。 整个算法的流程，图3,4中红色部分表示阴影内部，蓝色表示阴影外部 可以清楚地看出整个算法流程的核心是（2）（3）（4），其中（2）是在整个图像空间计算有向距离场，文中给出的有向距离场公式13如下： 文中p的取值为8，按照文中的说法，这个距离计算方法相较于欧几里得距离，在精确性（accuracy）和平滑度（smoothness）上有一个比较好的折衷（trade off）。这里C表示的是所有阴影的边界像素的合集（边界就是黑白发生变化的位置），分母上的积分项表示的是整个边界的长度，是一个归一化参数，离散化来看，就是屏幕上所有边界像素的个数。 从上面这个计算公式就可以知道，如果要精确计算每个像素的有向距离，则需要针对每个像素遍历整个图像空间中的其他像素，找到所有是边界的像素，并代入上述积分中进行求和运算，这个计算是比较低效的，因此文中采用的方法是在当前像素周围随机取一些像素参与到有向距离的计算，然后用计算结果去估计精确的有向距离的值，也就是所谓的蒙特卡洛方法。此外 | x - y | 在文中使用的是一个三维的欧几里得距离，因此实际上计算这个有向距离时还需要一张深度图。 计算出有向距离场后，接着要做的就是对这张图进行模糊，可以想见，如果直接针对visibility图进行模糊的话，得到的实际上是柔化的软阴影，而不是我们想要的抽象阴影，所谓抽象阴影就是把精确阴影中的一些细节给略去，恰好就对应了模糊有向距离的值。这一步是用一个标准的高斯模糊去完成的，参数alpha表示高斯模糊的方差，这个值越大，则模糊程度越高，细节丢失越多，抽象程度越高。在文中的也使用了蒙特卡洛方法来减少高斯模糊的采样次数。 在得到了经过模糊的有向距离场后，接下来就是如何把模糊后的有向距离场重新映射回阴影值，这里用了一个很巧妙的转移函数，一次性完成了边界膨胀/腐蚀，亮度和柔和度的操作： 其中，b是亮度，表示阴影区域的亮度值（非阴影区域亮度值是1），D是经过模糊处理的有向距离值，s表示柔和度，换句话说表示了亮度从b到1过渡的区域宽度，也就是软阴影的宽度，i表示了膨胀或者腐蚀的强度，正值表示膨胀，负值表示腐蚀，0表示不膨胀也不腐蚀。 上图是这个转移函数的图像，结合有向距离场的定义再来直观地看这个转移函数其实很好理解，i可以理解为等高线的值，我们认为有向距离值小于i的都是阴影区域，b作为最低亮度很好理解，smoothstep的功能是让阴影边界不再是跃变（如果是step就变成了跃变）而是有一定过渡，过渡的区间中点由i决定，区间长度则由s来决定。 上述算法中计算量最大的部分是有向距离场的生成，因为最终效果和采样数量关系密切，因此很难做到完全实时，这大概也是《军团要塞2》最终没有集成这个算法的原因。 伍 · 总结 本来是想简单的就卡通渲染做一个概述，写着写着就扯了一堆相干和不相干的技术。总的来说这篇文章偏综述性质，没有涉及到太多具体的实现，实现部分可以参考我在引用中列出的一些文章。卡通渲染这个领域从我的理解来看，是一个经验大于理论，美术大于算法的领域，因此应该主动地接受更多地trick，不要过度地思考“为什么”，毕竟效果好看，直观上好理解即可。关于卡通渲染的在本文中的分类，可能不是一个准确的描述，但是也提供了一种思考角度：有那么多的NPR相关技术，在一个具体的项目中我究竟要使用其中的哪些技术？这时候具体的画风要求就变得格外重要，因为画风决定了具体技术的选择和创新。文中的许多内容属于我自己对一些资料的解读，可能有许多理解上的错误，欢迎指正，另外今后我也会尽量避免写这么长的内容。 陆 · 引用 [1] A Trip Down The LOL Graphics Pipeline [2] 西川善司[GUILTY GEAR Xrd -SIGN-]的[纯卡通动画的实时3D图形]前篇 [3] 西川善司[GUILTY GEAR Xrd -SIGN-]的[纯卡通动画的实时3D图形]后篇 4 Cel Shading - Wikipedia https://en.wikipedia.org/wiki/Cel_shading 5 Amy Gooch, Bruce Gooch, Peter Shirley, Elaine Cohen. A non-photorealistic lighting model for automatic technical illustration 6 Pascal Barla, Joëlle Thollot, Lee Markosian. X-toon: an extended toon shader 7 Jason Mitchell, Moby Francke, Dhabih Eng. Illustrative Rendering in Team Fortress 2 [8] 风格化角色渲染实践 - 拳四郎 9 Jason Mitchell, Gary McTaggart, Chris Green. Shading in Valve’s Source Engine 10 Ken-ichi Anjyo, Katsuaki Hiramitsu. Stylized Highlights for Cartoon Rendering and Animation 11 Christopher DeCoro, Forrester Cole, Adam Finkelstein, Szymon Rusinkiewicz. Stylized Shadows [12] 【NPR】卡通渲染 13 Jianbo Peng, Daniel Kristjansson, Denis Zorin. Interactive Modeling of Topologically Complex Geometric Detail 14 Drew Card, Jason L. Mitchell. Non-Photorealistic Rendering with Pixel and Vertex Shaders 这是侑虎科技第246篇原创文章，感谢作者洛城供稿，欢迎转发分享，未经作者授权请勿转载。当然，如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 作者知乎：https://www.zhihu.com/people/luo-cheng-11-75/answers 同时，作者也是U Sparkle活动参与者哦，UWA欢迎更多开发朋友加入 U Sparkle开发者计划，这个舞台有你更精彩！