非真实感渲染 (Non-Photorealistic Rendering, NPR)

核心引言:NPR是什么?

“将大部分动物学称为‘对非大象动物的研究’”,这个比喻精准地描述了NPR的本质。

非真实感渲染 (Non-Photorealistic Rendering, NPR),也常被称为风格化渲染 (Stylized Rendering),它并非指某一种特定的技术,而是图形学中除“追求照片级真实感”之外所有渲染目标的总称。它的范畴极其广泛,从模拟手绘、卡通到用于技术说明的线条图,都属于NPR。

  • 两大主要目标:
    1. 技术插图 (Technical Illustration): 简化和突出关键信息,而非模拟现实。例如,用线条图清晰地展示发动机的内部结构,远比一张布满油污和反光的真实照片更有用。
    2. 艺术风格模拟 (Artistic Simulation): 模仿传统艺术媒介的质感和表现力,如水彩、油画、钢笔画、木炭画等。

本章的重点将放在实时渲染中常见的NPR技术,尤其是卡通着色线条渲染


15.1 卡通着色 (Toon / Cel Shading)

卡通着色(或称赛璐璐风格渲染, Cel Rendering)是NPR中最流行和最成熟的分支之一,常见于各类游戏中,如《大神(Okami)》、《塞尔达传说:旷野之息》、《原神》等。

  • 核心理念: 通过简化来增强 (Amplification through Simplification)。通过剥离不必要的真实世界细节(如复杂的光影过渡、微小的材质变化),来强化和突出角色、情感和动作,让观众更容易产生共鸣。

  • 两大核心要素:

    1. 分块的纯色/渐变色块: 物体表面的光照不再是平滑过渡的,而是被量化成几个离散的色块,形成明暗分明的效果。
    2. 清晰的物体轮廓线: 使用黑色的线条勾勒出物体的边缘,强化形状感,使其更接近传统2D动画的观感。

着色技术 (Shading Techniques)

以下是实现卡通着色中“色块化”效果的几种主流方法:

1. 双色调着色 (Two-Tone / Hard Shading)

这是最基础的卡通着色方法,将光照模型简化为只有“亮部”和“暗部”两个区域。

  • 核心观点: 根据光照强度是否超过一个阈值,来决定表面颜色

  • 实现方法: 在着色器中,计算表面法线 和光源方向 的点积。这个点积结果(通常在[0, 1]范围内)代表了光照强度。

    • 如果 > 某个阈值(例如 0.5),则该片元使用亮部颜色
    • 否则,使用暗部颜色

    伪代码如下:

    // N 和 L 都是单位向量
    float intensity = max(0.0, dot(N, L)); 
    vec3 final_color;
    if (intensity > 0.5) {
        final_color = BrightColor;
    } else {
        final_color = ShadowColor;
    }

2. 色调分离 (Posterization) / Ramp 纹理

这是对“硬着色”的自然扩展,支持两个以上的颜色层次,从而实现更丰富的色彩过渡,同时保持块状的风格。

  • 核心观点: 将连续的光照强度映射到一组离散的、预设的颜色上

  • 关键技术: 渐变纹理 (Ramp Texture)。这是一种非常高效且灵活的实现方式。

    • 创建一个一维纹理 (1D Texture),其中包含了你希望呈现的所有颜色阶梯(例如:从暗到亮的几种颜色)。
    • 在着色器中,仍然计算光照强度 intensity = dot(N, L)
    • intensity 值作为纹理坐标 u,去采样这个一维的Ramp纹理,得到最终的颜色。
    // ramp_texture 是一张预设好颜色阶梯的一维纹理
    float intensity = max(0.0, dot(N, L));
    vec3 final_color = texture(ramp_texture, vec2(intensity, 0.5)).rgb;
  • 色彩空间注意事项: 如果直接对最终图像的RGB值进行量化,很容易产生不自然的色调偏移 (hue shift)。使用Ramp纹理或在色调保持 (hue-preserving) 的色彩空间(如HSV)中进行操作,可以获得更理想的艺术效果。

3. 进阶技术 (Advanced Techniques)

  • 核心观点: 在基础卡通着色的上,通过引入更多变量(如视角、深度)来增加细节和动态效果。
  • 二维贴图 (2D Map): Barla等人提出使用二维纹理来代替一维Ramp纹理。
    • 纹理坐标:
      • U轴: 依然是光照强度 dot(N, L)
      • V轴: 可以是深度 (Depth)到摄像机的距离或是表面相对于视线的方向等。
    • 效果: 这种方法可以实现更复杂的效果,例如让远处的物体或者运动速度快的物体对比度降低、颜色变柔和,从而模拟大气透视或动态模糊的卡通感。
  • 混合风格 (Hybrid Styles): 如《军团要塞2》所示,可以将卡通着色的光照模型与写实风格的纹理、复杂的着色方程结合,创造出独特的混合艺术风格。

15.2 轮廓渲染 (Contour Rendering)

轮廓线是卡通渲染的灵魂之一。本节将深入探讨在实时渲染中用于寻找和绘制这些轮廓线的各种主流技术。这些技术各有优劣,适用于不同的场景和性能要求。

1. 核心概念:轮廓线的分类

在开始讨论技术之前,必须先精确地定义我们要渲染的“线”是什么。不同类型的线需要不同的检测方法。

  • 边界边缘 (Boundary Edge): 仅被一个三角形共享的边。想象一张纸的边缘。对于封闭的、”水密“(watertight)的模型来说,这种边通常不存在。
  • 折痕边缘 (Crease Edge): 两个相邻三角形形成一个陡峭夹角的边(例如,夹角大于60°)。最典型的例子就是立方体的棱。这是与视角无关的,是模型自身的特征。
  • 材质边缘 (Material Edge): 两个相邻三角形材质不同而形成的边。
  • Contour 边缘: 一个面朝向观察者,而其相邻面背向观察者的边。这是与视角相关的动态边缘。这是我们通常理解的“轮廓”的广义概念。
  • Silhouette 边缘: 将物体与背景或其他物体分隔开的边缘。它是 Contour 边缘的一个子集,通常指物体最外围的轮廓线。

关键区分: Silhouette 是最外圈的“剪影”线。Contour 不仅包含Silhouette,还包括物体内部因形状起伏而形成的轮廓线(例如,脸部轮廓内的鼻子和耳朵的线条)。

2. 轮廓渲染技术概览

2.1 基于法线的着色 (Normal-Based Shading)

这是一种在像素着色器中实现的、非常简单的“伪”轮廓线技术。

  • 核心观点: 当表面法线几乎垂直于视角方向时,该区域很可能位于轮廓附近,因此将其涂黑
  • 实现方法: 计算表面法线 和视角方向 的点积。当点积结果 dot(N, V) 趋近于0时,意味着视角与表面呈“擦边”状态。
    float intensity = 1.0 - abs(dot(normalize(viewDir), normalize(worldNormal)));
    float edgeFactor = smoothstep(0.0, edgeWidth, intensity);
     
    // 如果edgeFactor接近1,说明在轮廓上
    vec3 finalColor = mix(surfaceColor, edgeColor, edgeFactor);
  • 优点:
    • 实现极其简单,性能开销小。
  • 缺点:
    • 线宽不均: 线的粗细严重依赖于模型表面的曲率。
    • 对尖锐边缘无效: 在立方体这样的模型上几乎无法工作,因为很少有像素的法线恰好垂直于视角。
    • 在物体距离较远时,由于采样精度问题,效果会变差。
2.2 程序化几何轮廓 (Procedural Geometry Silhouettes)

这类技术通过渲染额外的几何体来“画”出轮廓线,是游戏中最常用、最稳健的方法之一。

  • 核心观点: 渲染两次模型。第一次正常渲染正面,第二次将背面稍微放大并渲染成轮廓色(通常是黑色)

  • 通用流程:

    1. Pass 1: 正常渲染模型(开启背面剔除)。
    2. Pass 2: 渲染同一个模型,但进行如下设置:
      • 开启正面剔除 (Cull Front),只渲染背面。
      • 将背面顶点在某个方向上沿法线挤出 (Extrude) 或在深度上前移 (Z-Offset)
      • 将背面模型渲染成纯色(如黑色)。
  • 具体实现方法:

    • Z-Offset (深度偏移): 将背面多边形在深度上向摄像机推近一点。缺点是线宽不均匀,受多边形与视角的倾斜度影响。
    • 外壳法 (Shell Method / Halo): 这是最流行的方法。在顶点着色器中,将顶点沿其法线方向向外推移一小段距离。这个距离通常与屏幕空间大小相关,以保证轮廓线宽度相对稳定。
      // 在Pass 2的顶点着色器中
      float outlineWidth = 0.05;
      // 将顶点位置沿法线方向扩展
      pos.xyz += normalize(vertexNormal) * outlineWidth; 
      gl_Position = projectionMatrix * viewMatrix * modelMatrix * pos;
    • 优点:
      • 效果稳定、健壮,线条宽度可控。
      • GPU友好,不需要CPU计算或额外的连接信息,每个三角形独立处理。
      • 实现相对简单。
    • 缺点:
      • 尖锐拐角处可能出现裂缝: 如果一个顶点被多个不同法线的面共享(硬边),扩展时会在拐角处产生缝隙。解决方法是为轮廓渲染专门生成平滑过的平均法线。
      • 只能渲染 Silhouette/Contour 边缘,无法处理折痕或材质边缘。
2.3 基于图像处理的边缘检测 (Image-Processing Based)

这是一种在后处理阶段完成的轮廓线渲染技术。

  • 核心观点: 渲染场景信息到G-Buffer(例如深度、法线、对象ID),然后对这些Buffer应用边缘检测算子(如Sobel)来寻找不连续的区域,这些区域就是轮廓线
  • 实现方法:
    1. G-Buffer Pass: 渲染场景,并将深度、世界空间法线、物体ID等信息输出到不同的渲染目标(Render Targets)中。
    2. Post-Processing Pass: 在一个全屏的Pass中,对每个像素,采样其周围像素的G-Buffer信息。
      • 深度值差异过大 Silhouette/Contour 边缘
      • 法线向量差异过大 Contour/Crease 边缘
      • 对象ID不同 Silhouette/Boundary 边缘
  • 关键技术:
    • 边缘检测算子: SobelRoberts Cross 等是常用的算子,用于计算梯度,检测不连续性。
    • 形态学操作: 检测出的线条通常只有一个像素宽,可以使用膨胀 (Dilation) 操作来加粗线条。
  • 优点:
    • 可以处理所有类型的边缘,非常灵活。
    • 对场景几何体没有要求(无需水密、无需连接信息)。
  • 缺点:
    • 可能漏检: 深度差异很小(如桌上的一张纸)或法线差异很小的情况,可能会检测不到边缘。
    • 可能误检: 由于透视或采样问题,在不该有轮廓的地方产生噪点。
    • 风格化受限: 得到的是2D图像,难以实现复杂的三维笔触风格(如虚线、锥化)。
2.4 几何边缘检测 (Geometric Edge Detection)

这是最“精确”但也是计算最复杂的方法,它在CPU或GPU上显式地找出符合条件的几何边缘。

  • 核心观点: 直接遍历模型的所有边,通过数学判断找出哪些是Contour边缘,然后将这些边缘作为独立的线段或四边形进行渲染
  • 判断公式: 一条边连接的两个三角形(法线为 ),如果满足以下条件,则为Contour边缘: 其中 是从视点到该边缘的向量。这个公式的含义是:一个面朝向你(点积>0),另一个面背向你(点积<0)。
  • 实现方法:
    • CPU端检测: 遍历所有边并进行测试。开销巨大,通常需要结合空间/数据结构优化,或利用帧间连续性来加速。
    • GPU端检测: 使用几何着色器 (Geometry Shader) 或其他技术。将每条边作为一个图元传入,在GS中判断是否为Contour边,如果是,则动态生成一个用于渲染线条的四边形。这是现代GPU上更可行的方法。
  • 优点:
    • 最高的渲染自由度。因为你得到了实际的几何边缘,可以对它进行任意风格化,如渲染成虚线、手绘抖动的线、随距离变细的线等。
  • 缺点:
    • 计算开销大,实现复杂。
    • 需要模型的边连接信息
    • 引出了下一个难题:隐藏线移除

3. 进阶话题:隐藏线处理 (Hidden Line Removal)

对于几何边缘检测方法,我们得到了一堆3D空间中的线段,但其中很多应该被模型自身遮挡。

  • 问题: 如何只渲染这些线段的可见部分?
  • 简单方法: 先正常渲染一遍模型以填充深度缓冲,然后渲染线段时开启深度测试。这可以隐藏被完全遮挡的线段。
  • 复杂问题: 简单方法只是像素级别的剔除。如果一条线段部分被遮挡,我们得到的是断断续续的像素,而不是平滑的、被裁剪过的笔触。
  • 高级解决方案: 需要复杂的多Pass算法,在GPU上找出每个边缘线段的可见区间,然后根据这些信息重新构建和渲染平滑的、风格化的笔触。这是一个活跃的研究领域,实现非常复杂,但在追求高质量手绘风格时至关重要。

15.3 笔触表面风格化 (Pen-and-Ink Surface Stylization)

这一节探讨的是,如何让物体表面本身看起来像是用笔触(如铅笔、木炭)绘制的,而不仅仅是简单的色块。

  • 核心挑战: 如何将2D的笔触纹理贴到3D模型上,当模型或相机移动、缩放时,笔触看起来依然自然,而不是“贴”在上面滑动或者被不自然地拉伸/压缩。

1. 屏幕空间贴图及其问题

  • 方法: 直接在屏幕空间上叠加一层“笔触”或“画纸”纹理。可以通过光照强度来决定使用哪种密度的笔触纹理。
  • 致命缺陷: “淋浴门效应” (Shower Door Effect)
    • 现象: 当相机或物体移动时,物体看起来像是在固定的纹理后面“游泳”,仿佛观众隔着一块花纹玻璃在看场景。这会严重破坏沉浸感。
    • 原因: 纹理与屏幕绑定,而不是与物体表面绑定。

2. 物体空间贴图与TAM

为了解决“淋浴门效应”,最直接的方案是将纹理贴在物体上。但这引入了新的问题:普通的纹理和 Mipmap 机制不适用于风格化的笔触。

  • 传统Mipmap的问题:

    • 拉近时: 笔触变得巨大、模糊。
    • 拉远时: 笔触挤在一起,变成一团噪点或模糊的色块,失去了“笔触”的感觉。
  • 核心目标: 无论物体远近,其表面笔触在屏幕上的尺寸和密度应该保持相对一致,这才能模拟手绘的感觉。

  • 关键技术: 色调艺术贴图 (Tonal Art Maps - TAM)

    • 核心观点: 创建一组特制的、包含笔触的Mipmap纹理,以在不同距离下维持笔触的视觉密度和尺寸
    • 构造方法:
      1. 准备多张不同色调(明暗)和不同笔触密度的纹理。
      2. 将它们组织成一个Mipmap链。关键之处在于:更高层级(更小尺寸)的Mipmap纹理包含其所有下层(更大尺寸)纹理的笔触
      3. 这样一来,当GPU在不同Mipmap层级之间进行三线性插值过滤时,笔触会平滑地增加或减少,而不是突兀地切换或变得模糊。
    • 渲染: 在着色器中,根据光照强度和距离选择合适的TAM Mipmap层级进行采样。
    • 优点: 效果出色,能产生非常具有手绘感、且动态稳定的表面。

3. 其他技术简介

  • 主曲线方向 (Principal Curve Direction): 沿着物体表面曲率最大和最小的方向来绘制笔触。这能极好地强化物体的三维形态感。
  • 嫁接 (Graftals): 根据规则(如距离、方向)程序化地在物体表面添加几何体或贴花,可以用来模拟笔刷的痕迹。

15.4 线条渲染 (Line Rendering)

本节讨论的是如何正确、清晰地渲染“硬”线条,例如工程模型线框、调试视图或选中物体高亮。

1. 渲染三角形边缘(处理Z-Fighting)

  • 核心问题: 当线条和其所在的三角形在完全相同的深度时,会发生Z-Fighting(深度冲突),导致线条闪烁或断裂。
  • 经典解决方案: 深度偏移 (Z-Offset)
    • 方法: 在渲染线条时,将其深度值稍微向摄像机拉近一点。现代图形API(如OpenGL的glPolygonOffset)提供了基于多边形斜率的智能偏移,效果更好。
  • 现代解决方案: 基于重心坐标的着色器方法
    • 核心观点: 不单独渲染线,而是在渲染三角形本身时,在像素着色器中“画”出它的边缘
    • 实现方法: 在像素着色器中,获取当前像素的重心坐标 (Barycentric Coordinates)。通过重心坐标可以精确计算出该像素到三角形三条边的距离。如果距离小于设定的线宽阈值,就使用线条颜色,否则使用表面颜色。
    • 优点: 彻底杜绝Z-Fighting,可实现像素完美的抗锯齿线条,线宽控制灵活。
    • 缺点: 内部共享边的线宽会是边界线的两倍(因为相邻两个三角形都画了一半),不过通常不明显。

2. 渲染被遮挡的线条

  • 目标: 渲染出被物体遮挡的线条,通常用不同的颜色或样式(如虚线)表示。
  • 实现方法(多Pass):
    1. Pass 1: 正常渲染一遍场景中的所有实体模型,但只写入深度缓冲 (Z-Buffer),不写入颜色。
    2. Pass 2: 正常渲染所有线条,并开启深度测试(LESS_EQUAL)。此时只有可见的线条会被画出来。
    3. Pass 3: 再次渲染所有线条,但使用反向深度测试GREATER)并且关闭深度写入。此时,只有在Pass 2中被遮挡的线条才会被画出来。在这一步使用不同的颜色(如灰色)。

3. 制作线条光晕 (Halos)

  • 目标: 当两条线交叉时,在后面的线条上产生一个小的“断口”,以清晰地表达它们的遮挡关系。
  • 实现方法(多Pass):
    1. Pass 1: 将每条线渲染成一条粗线(通常是一个四边形),使用背景色,并写入深度缓冲
    2. Pass 2: 将每条线渲染成正常的细线,使用前景颜色,并开启深度测试。同时使用微小的深度偏移,确保细线能画在自己的粗线“光晕”之上。

15.5 文本渲染 (Text Rendering)

文本渲染的目标是在任何尺寸、角度和距离下都能保持清晰可读。

1. 传统方法:位图字体图集 (Bitmap Font Atlas)

  • 方法: 将所有字符(字形, Glyph)预先渲染成位图,并打包到一张大纹理中。渲染时,通过一系列四边形(Quads)并映射到对应的UV区域来显示文本。
  • 缺点: 放大时模糊,缩小时锯齿严重。标准的纹理过滤技术无法很好地处理字体这种高频细节。

2. 关键技术:符号距离场 (Signed Distance Fields - SDF)

这是目前游戏和实时应用中最流行、最强大的文本渲染技术。

  • 核心观点: 纹理中存储的不是字符的颜色,而是每个点到字符轮廓的最近距离
    • 距离值为正,表示在轮廓外。
    • 距离值为负,表示在轮廓内。
    • 距离值为0,表示恰好在轮廓上。
  • 渲染方法:
    1. 在像素着色器中对SDF纹理进行采样,得到一个距离值。
    2. 根据这个距离值来决定像素的Alpha。例如,距离小于0则完全不透明,大于0则完全透明。
    3. 关键之处: 在0值附近使用smoothstep函数进行平滑过渡,即可在任意缩放和旋转下,动态生成完美抗锯齿的边缘
  • 优点:
    • 分辨率无关: 极高质量的缩放和旋转,始终保持边缘清晰。
    • 效果灵活: 通过简单调整着色器中的距离阈值,可以轻松实现轮廓、描边、发光、阴影等效果。
  • 缺点:
    • SDF纹理的生成过程计算量较大,通常需要离线预处理生成。

3. 前沿技术:GPU直接矢量渲染

  • 核心观点: 跳过预先光栅化成纹理的步骤,直接利用GPU在运行时绘制字形的矢量轮廓(贝塞尔曲线)
  • 代表技术: Loop-Blinn方法
  • 优点:
    • 达到印刷级的最高渲染质量,无任何纹理采样瑕疵。
    • 非常适合高分辨率(Hi-DPI)屏幕。
  • 缺点:
    • 实现复杂,对GPU能力要求更高。

4. 一个历史性的概念:亚像素渲染 (Subpixel Rendering)

  • 代表技术: 微软的 ClearType
  • 原理: 利用LCD屏幕每个像素由独立的R, G, B垂直条状子像素构成的物理特性。通过独立控制这些子像素的亮度,可以在水平方向上获得三倍的感知分辨率,让文字边缘更平滑。
  • 现状: 在现代高DPI屏幕普及的背景下,其重要性已大大降低。