【RTR4笔记】 第十五章 非真实感渲染

卡通渲染,描边

Posted Kongouuu's Blog on May 11, 2022

前言

这一章是一个非常简短的章节为整个非真实感渲染进行了介绍。简单的介绍了一下什么是卡通渲染,怎么进行描边,以及一些其他的特殊效果。并不会特别的去深入特定的某种渲染的实操,跟以往一样是进行大略的介绍。不过说白了 NPR 这里每个厂家都会有根据自己美术风格需要去进行独特的调整,所以也不会像前面的章节那样提供较为详细的思路。

15.1 Toon Shading

一般提到非真实感渲染 (NPR) 的时候大部分人应该都会先想到 卡通(赛璐璐)着色这边。 我们考虑最简单的卡通渲染的时候,一般就是首先为要绘制的物体进行描边;再来就是把一定区间内的颜色都阶段到特定的值上面。达到上面两点的话画面就会更加的 2D

虽然现在的卡通渲染模型都整的非常复杂,为了更好的渲染效果吧,会进行非常多其他的操作,例如根据摄像机位置啥的修改物体的顶点\法线之类的。我们这里不考虑这种比较美术上的操作。从单纯绘制的角度来看,主要还是上面提到的描边以及颜色范围的截断。拿最近比较知名的游戏 Guilty Gear Strive 来看,其实也是通过这样的方法去达到一个更加卡通的效果:

image-20220510163804475

15.2 Outline Rendering

描边需要做的就是查找物体的边框,并且为其进行特定的着色。书中主要提到的是三角形的边为基础去考虑,不过因为很多着色应用中我们肯定都是在像素着色器这里去进行相关的操作,也没有什么三角不三角形的了。

下面部分中不同的边 (Contour / Silhouette Edge) 中文直译都是轮廓,就打原文了。

15.2.1 Shading Normal Contour Edge

Contour Edge 是一个比较基础的描边的做法。选择的方法是:当像素所属的三角形的朝向与我们手动设置的一个向量之间角度过大时,该像素视作轮廓边。 这里我们手动设置的向量一般来说都是摄像机的朝向,也就是说当我们物体的边缘部分的三角形跟我们视线方向形成近 90度 的角度的时候,我们就进行描边。

这个是一个很好理解的手法,因为大部分立体的物体相对于我们视角的边框都会有近乎 90度 的法线。根据我们选择的角度阈值可以达到不同等级的描边效果。不过这个做法也有很明显的物体,当我们看像类似立方体的物体的时候,他们的边缘并不会朝向外侧,因此也没办法进行描边。

除此之外,这样的轮廓涵盖了物体几何内的边。就是例如说我们在渲染人的时候,会同时为鼻子等可能造成角度过大的部分描边。这也并不是个缺点,单纯是个特性。

15.2.2 Procedural Geometry Silhouetting

上面提到的问题是很难在同个方式里面解决的,外加上这样做出来的是占据几何本身的一个内描边方式,可能不一定是我们需要的效果。Silhouette Edge 则是用完全不同的思路去达到一个描边的效果。主要做的是描绘整个物体本身相较于环境的一个边框,不考虑物体自己内部的种种轮廓。

实际流程如下:

  1. 普通的渲染一次要绘制的物体
  2. Pass 设置为只描绘背面 (三角形法线靠视线方向的 )
  3. 用自定义的边框颜色重新绘制一遍放大后的物体

这样就可以做出一个外边框,并且不会因为几何形状的法线导致没办法渲染出边框。

物体的放大我们一般是选择根据法线方向延申,因为如果几何形状较为复杂的情况,对整个物体进行缩放会没有办法成功的描边。不过这样的做法也不是十全十美的,例如我们如果让一个立方体随着法线方向增长,会变成下面这个样子 (实际上只有外面六个面片,立体起来只是为了示意):

这样会导致最后我们按照绘制背面的三角形整出来的边在角落部分会分离;当然是可以通过特殊操作去限制顶点,不过就是比较麻烦了。

15.2.3 Edge Detection by Image Processing

上面提到的两种描边手法都是可以移动到图片空间,也就是在后处理的环节去进行操作的。

Contour Edge 本身就是一个基于法线的算法,因此我们可以通过深度缓冲或者几何缓冲去算出差不多的这么一个描边效果。

Silhouette Edge 是把物体看作一个整体来进行外描边,不考虑内部的种种轮廓。我们可以在常规批次渲染的时候在缓冲中标注出每个像素所属的物体id,那么我们在后处理阶段可以直接根据每个像素周围的物体id去判断描边数值。

使用缓冲所造成的精度问题也会造成一些麻烦,例如噪点或者说无法识别边缘等,不过基于图像的一般速度都挺快的,也很方便去开发以及适配特定的管线,其实就还好。

一般来说我们会跑完一个 Pass 去识别这些边缘,然后再用后续的 Pass 去再判断怎么对这个边缘去进行操作,例如放大,或者使用这个信息去达到其他的效果。

15.2.4 Geometric Contour Edge Detection

上面提到的办法对于边缘的基础绘制都是十分有效的。不过这样的做法没有办法让我们轻易的得到一个风格化的边缘,例如虚线描边。原因是上面的做法是把像素识别成边缘并且着色,但我们本身是对几何边缘没有太多概念的。例如如果我们要用上面的办法做到虚线效果,会比较难去判断每个像素是沿着什么边进行描绘的,也就比较不方便选择哪些不绘制。更简单的来说,就是我们不实际的找出什么线是边缘这个信息,很多效果不好做。

最暴力的解决办法就是我们不去用我们缓冲了的各种信息去判断,而是直接根据几何本身的顶点以及三角形去判断。这样的做法非常简单理解,因为我们只需要遍历所有边去判断邻近的两个三角形是否符合条件。不过很明显,这样的做法开销太大了。当然主要是我们任何一个边框都肯定是能形程一个环的;使用这个信息我们在每一帧可以根据上一帧的边框信息来加速我们的计算( 可以不用去考虑离上一帧边框太远的三角形 ),不过这只限定于物体以及我们的摄像机移动较为平滑的情况下。

这个做法同时可以让 GPU 帮忙进行,也就是把所有的边当作四边形放进我们渲染管线里面,并且把周围两个三角形的法线存在顶点里面。即使这样能进行加速,为了描边去把所有的数据转换到适合的格式再把场景几何进行第二次渲染这个行为在我看来是开销过大的。

不过这样的手法可以达到一些比较有趣的风格化效果。当我们能把边缘识别成一个连贯的线条,就可以为他们进行一个风格化的笔触处理:

image-20220510183830604

15.3 Stroke Surface Stylization

整个非真实感渲染除了卡通渲染之外还包含着许多不同的领域,例如手绘风格的表现。我们可以准备好几种不同的手绘风格的阴影的贴图,然后根据渲染时的表面的亮度决定套用哪个贴图。就会得到类似于下面的一个效果:

20210525224033314

上面的大致做法就时准备不同表面的贴图,然后我们可以大致的把不同的表面阶段到特定的值上面;之后再根据屏幕空间去套用这些纹理贴图,达到一个手绘画面的效果。

不过这样做会衍生出一些小问题,由于我们是根据屏幕空间去套用这个纹理,当我们的画面在移动的时候,即便物体在动,每个屏幕空间的纹理只要没有变成的值,就会保持原样;这样看起来非常的不动态、不自然。我们可以使用套在物体表面上的纹理,或者使用适当的噪音来解决这样的问题。如果我们把纹理套用在物体表面上,那么我们还需要考虑到 Mip 层级问题,这样可以保持画面上的笔触最终大小都是一致的。

15.4 Lines

15.4.1 Triangle Edge Rendering

在很多情况下我们会希望吧三角形的边和物体本身同时渲染出来,例如在开发编辑功能的时候。不过这样的功能有一个主要的功能,就是怎么让我们的线条处于物体的前面不被遮挡,但也不会在不该被渲染的地方被渲染。

最先被想到的办法是在渲染三角形边框的时候加一定程度的偏移。这样的做法是非常好实现好理解的,不过问题在于偏移量的选择。因为精度问题,如果偏移过大,边框可能会遮挡到其它的几何;如果偏移太小,可能有的边框在斜面的三角形上无法显示出来。

为了解决上面的问题,有个使用模板缓冲的方法被提出。就是我们在绘制每个三角形的时候使用模板缓冲去绘制边框,并且一个三角形一个三角形处理。很明显这样的做法时间开销太大,因此也只适用于三角形数量不多的场景。

为了考虑到效率和效果,我们最好能更好的去利用 GPU, 因此被提出的是在像素着色器中使用重心坐标把靠近边框的像素上边框色。这样的做法避免了第一个办法可能造成的问题,效率也能保证。唯一的遗憾是因为每个边都是根据两个三角形去决定宽度的,因此靠近物体外框的部分只会有一半的宽度(只有一个三角形的贡献)。

15.4.2 Rendering Obscured Lines

我们可以很直接的调用 API 去渲染网格图 (Wireframe), 如果要考虑到遮蔽问题的话可以先把几何写入深度缓冲再去做网格的渲染。 遮蔽的线的部分也是差不多个道理,如果我们要渲染被遮挡的线那只需要参考深度缓冲并且把被遮挡的部分按照自己期望的颜色绘制出来就可以了。

这样的技术可以在渲染中区分出能看得见的几何的网格,以及被隐藏的几何的网格,如下图第三个:

https://www.realtimerendering.com/figures/RTR4.15.25.png

15.4.3 Haloing

白边 (Haloing) 就是像上图第四个这样,当线条重叠的时候使靠后侧的线条在重叠部分被截断。

这样的效果的实现办法是:

  1. 先把所有线条以略粗于线条的四边形写入深入缓冲
  2. 正常的渲染线条,并且和深度缓冲做对比

这样就可以达到类似的效果,不过要注意的是靠近顶点的位置很容易会让四边形遮挡住后面的线条。为了解决这个问题,我们可以设置其他形状让线条接近顶点的部分不要做太多遮挡。

15.4 Text Rendering

我们几乎所有的渲染系统都是需要考虑到一个文字渲染的问题的,无论是考虑到 UI 还是系统内物体上面的字体等信息。要绘制字体也并不是一个很容易的事情。

很早期被相处的办法是使用不同纹理来储存不同大小的文字。使用这样的办法可以让我们很简答的在屏幕或者任何 3D 物体上绘制出字体。不过问题也是比较多的,首先就是有大小的限制,如果我们没办法对着屏幕按照一个纹理素一个像素的这么一个方法去使用这些贴图;例如我们要对文字进行旋转拉伸等;很容易出现字体走样导致难以辨别。再加上这样的做法对储存的要求也是有的,在字体多的情况下需要非常大量的储存来不同的情况。

后来被开发出来的是使用直线曲线信息来储存文字。也就是说比起直接的储存到纹理,我们把每个字母的大体形状给储存下来,这样在缩放旋转上可以保持不走样,并且储存也是比较少的。不过为了达到这样的效果设置上是相对来说比较困难的,无论是我们的储存还是我们展开字母的方式。很可能需要让我们用到曲面细分着色器以及计算着色器去做这方面的操作来达到一个更好的效果。