1. 图形渲染管线

应用阶段 (CPU)-> 几何处理 (GPU)-> 光栅化 -> 像素处理(后处理?)

几何处理阶段

顶点着色 -> 投影 -> 裁剪 (clip) -> 屏幕映射

每个顶点储存各种材质数据,比如位置、法线、颜色,计算之后的着色结果可能是颜色、向量、纹理坐标发送到光栅化阶段

曲面细分的作用:生成具有相当数量三角形的曲面来节省时间和内存,根据距离相机的远近来合并顶点

几何着色器:功能类似于反向细分,比如一个顶点渲染为一个正方形来达成某些视觉效果(Geometry Modifier?)

光栅化

三角形设置 -> 遍历 -> 像素着色 -> 合并

合并阶段处理可见性

帧缓冲区 :使用双缓冲,避免屏幕上看到颜色缓冲区的内容(垂直同步?)

GPU数据并行

一个warp包含多个线程,一个线程负责一个图元的渲染,GPU一次执行一个warp,遇到需要读取的数据的时候会stall(比如纹理),就可以先切换执行下一个warp,等待第一个warp的数据读取完成。

xyzw 坐标,w是perspective的参数

流式输出

在GPU渲染管线中,流式输出(Stream Output,在OpenGL/Vulkan中通常被称为转换反馈 Transform Feedback)是一个非常强大且灵活的机制。

简单来说,它的核心作用是:允许GPU将几何阶段处理完的顶点数据,跳过后续的光栅化和像素着色阶段,直接重定向(写回)到显存中的缓冲区(Buffer)中。

2. 变换

如果要让物体绕某个轴,旋转中心为某个点p,可以先将这个物体平移,使p点与原点重合,运用旋转矩阵之后再平移回其原始位置。

如果要在某一方向上进行缩放,则需要进行一个复合变换,为了构建正交矩阵 F,我们需要另外两个相互正交的单位向量,使得它们构成一个右手坐标系。也就是说,这个逆矩阵在进行basis的转换,将原来方向vec 变为以缩放方向为basis,最后再转换回x,y,z的basis

复合矩阵的通常顺序为C = TRS

是物体在相机这个“新世界”里的相对坐标,达到了改变相机朝向的目的,也就是“移动了整个世界”。M_view x P_world = (x', y',z')

顶点混合

一个用来解决类似于“手臂弯曲伸展的真实性”的算法,简单理解为给skin的矩阵变换添加了一个权重。使用线性混合蒙皮可能会让关节处的处理没有那么自然,用双四元数可以改善外观,但也可能会导致隆起效应。

几何体缓存回放

解决类似过场动画内存处理效率的问题(精细的动画对读取速率的要求高)。使用量化技术,例如用16位整数储存每个顶点的位置和纹理坐标,或是进行空间和时间维度上的预测(预测n+1帧的位置,预测下一个顶点的位置)。

正交vs透视投影

透视本质是用数学方法来模拟人眼观察的近大远小的效果,一个带”透视“一个不带。除以 "w"

视场(field of view) 越小 -> 透视效果越小。可以这么理解(所谓的广角镜头):你的视野越大,你看到的东西就越多,然而却依然要投影到有限的一个屏幕范围内,所以透视效果自然而然就变强了。

书中还介绍了更高级的透视算法实现,以及对这些算法进行优化以提升精度,从而影响最终的渲染效果,当然在实际代码中还有更具体的操作,比如对于颜色和深度buffer的清理和处理。

3. 着色基础

Gooch着色模型

如果法线指向光源,则使用较暖的颜色,反之则用较冷的颜色。这其实给风格化渲染提供了一种思路,用某一些属性来座位渲染效果的参数

Lambertian shading model

Lambertian shading model formula
Lambertian shading model formula

该模型适用于理想的漫反射表面,即完全没有光泽的表面

光源

本质上是一种用程序思维来进行艺术效果的模拟,可以通过调整各种光源对物体颜色的影响程度来达到想要的视觉效果,比如,我希望不被光照到的部分为纯黑色等等。这也给风格化渲染提供了一种思路,重点是要思考“我想要什么样的效果”,先确定思路再实行工程。

点光源的模拟上很重要的一点是考虑性能,运用一些衰减函数来设定一个r_max这样不至于太远之后光线还在渲染。

材质系统

材质模板 + 材质实例(instance)

模块化和可组合性(?)是材质系统的重要任务

抗锯齿

进行更高精度的渲染 -> 采样(滤波器)

由于对于每个像素的采样率不足而导致的失真画面是我们常说的锯齿,可以在一个像素内增加采样点。

FSAA = SSAA (full screen antialiasing)

概念上最简单,细分为2x2采样,但更需要性能。在实践中,可以增加物体的大小,比如电线,让它尽可能的覆盖确定的像素。

MSAA multisampling antialiasing

一个像素内有四个点,看哪个样本(要渲染的物体)覆盖的点位多,就渲染哪个物体。这样的好处是性能更好。

Nvidia TXAA 帧间抗锯齿

FXAA fast approximate antialiasing

SMAA subpixel morphological antialiasing

这些算法的开销基本都在1-2毫秒,所以很受欢迎

累计缓冲区accumulation buffer

与顺序无关的透明度

under运算符的用法是将所有透明物体绘制到一个单独的颜色缓冲区,然后用over运算符将该颜色缓冲区合并到场景的不透明视图之上。然而这种算法性能消耗较大,学者们尝试了不同的数据结构进行优化,来将算法更好的映射到GPU上(1984)。

A缓冲区是另一种多重采样的形式,每个被渲染的三角形都会为其每个覆盖的屏幕网格单元创建一个覆盖蒙版。每个像素都储存了一个所有相关片元的列表。

常见的 Pass 类型:

  • Shadow Pass(阴影通道):这个 Pass 不画任何颜色。它把摄像机放在光源的位置看世界,只记录物体的深度信息,生成一张“阴影图(Shadow Map)”。
  • Base Pass / Forward Pass(基础/前向渲染通道):真正开始画物体的颜色。它会读取上一步生成的阴影图,结合当前的光照计算,把物体的色彩和细节画出来。
  • Post-processing Pass(后处理通道):当场景全部画完后,整个画面被当作一张普通的 2D 图片。后处理 Pass 针对这张全屏图片进行二次加工,实现如辉光(Bloom)、景深(Depth of Field)、色彩校正(Color Grading)等电影级特效。

透明度算法的领域十分深且广阔,更复杂的算法包括加权重,加权平均,随机透明度......由于有很多不同类型的透明问题,渲染方法和GPU功能也多种多样,因此目前还没有一种能完美渲染透明物体的结局方案(2018)。更主要的是,作为技术美术,你应该去更加全面而概括的了解这些算法,知道他们的优势劣势,以及适用场景,而不是钻研其中的实现以及如何优化。比如,如果在一个很大的场景中,两个彼此很相近的物体的距离权重会基本相同,所以用不用距离权重来渲染这个物体差别不会很大。

显示编码

gamma correction:将着色器输出的颜色值限定在[0,1]范文内,并将其提高1/2.2幂次(非线性!)

总的来说,为了使得让我们计算的值加倍,同时输出的辐射亮度也加倍,我们需要应用显示传输函数的逆函数来抵消其非线性效应,这个抵消显示器响应曲线的过程被称为伽马矫正。

4.纹理

(x,y,z) -> (u,v) 纹理投影(UV mapping)

避免纹理投影的周期性问题的一种解决方法是将纹理值与另一个非平铺纹理组合起来。

纹理放大映射->双线性插值,即在两个方向进行线性插值,类似blender中的color ramp?只管的来说,这个函数的目的是让距离样本位置更近的纹素对最终值产生更多的影响。

在实时渲染中,滤波器(Filter)*的本质是一个*加权求和的数学算子。无论是纹理采样(Texture Sampling)还是抗锯齿(Anti-Aliasing),其核心任务都是连续信号的数字化离散采样,而滤波器就是用来控制“采样点周围的像素如何影响最终输出颜色”的工具。

从信号处理的角度来看,像素是一个个离散的格子,但现实/虚拟世界是连续的。当我们把连续的图像强行塞进像素格子时,就会产生走样(Aliasing,即锯齿或闪烁)。滤波器的作用,就是在空间域上对局部区域进行加权平均,在频域上滤除高频信号(平滑细节),从而消除锯齿。

任何滤波器都由两个核心要素决定:

  • 支持域(Support Window): 滤波器覆盖的像素范围(如 $3 \times 3$ 或 $1 \times 1$ 像素区域)。
  • 权重函数(Weight Function): 决定邻近的像素对中心点贡献多少权重的数学公式。

mipmap (multum in parvo - “在一个小地方有很多东西”) 多级渐远纹理

将一个纹理图片不断的进行下采样,d轴越远,这个新的生成的子纹理就越小,直到只有一个纹素的大小,注意 d 并不是线性的。在实践中,d并不是整数值,我们会对d两侧的mipmap等级分别进行采样(线性插值),再根据每个纹理等级到d的距离,对得到的样本进行线性插值,整个过程成为 三线性插值。

加和表面积(summed area table)

各向异性(Anisotropic) 描述的是一种非均匀的、具有方向性的缩放现象。当一个表面(比如铺着地砖的地面)远离相机并斜向延伸时,它在屏幕上的投影会发生严重的形变:沿视线方向(纵向)被剧烈压缩,而垂直于视线的方向(横向)压缩得很小。与之相对的词语就是各向同性。

各向异性滤波

像素单元的反投影(带有透视效果)形成一个四边形,利用与四边形长边平行的轴来作为采样轴(各向异性线)。可以理解为在透视的情况下,横向(短边)的信息应该被压缩了,而纵向(长边)的信息应该被保留,所以有更多的采样点来沿着这个方向来进行最终的加权平均计算。

纹理在材质表达上的应用

法线贴图和高度场是比较熟悉的用法,这些算法现在还在持续进行着优化来搭配纹理压缩、抗锯齿等功能来继续提高性能,同时让纹理的材质表达更加真实。值得在实现的层面上总结的就是“切线场”的概念,因为既然要改变法线的方向,你必须先建立一个坐标系,所以我们就需要切线来作为第二个向量,这是所有纹理材质表现的底层逻辑。

凹凸映射 、Blinn(高度场)

计算比较繁琐,本质是displacement mapping看,顶点密度过高

法线映射

缺点是在沿着平面观察的时候就不会有凹凸了

视差映射 / 视差遮挡映射(浮雕映射)

img
img

简单来讲,就是我们根据离散的高度场信息,来推算假设这个材料是真的有几何上的凹凸的话,我们的视线所看到的材质是怎样的。最简单的方法就是假设这个真实的几何走势是以A点为圆心,H(A)为坡度的一个圆形坡,与视线的相交点就是最终估算的数值。浮雕映射,视察遮挡等等算法都是对这个思路进行的优化,大致来说就是在视线向量上进行更多的采样,来尽量确定更精确的相交点。还有很多更高级的思路和优化,这里不多研究了。

5. 阴影

阴影贴图

基于深度缓冲区。核心思路是灯光所“看到”的区域会被照亮,而其它地方在阴影中。进行一个阴影的pass,与阴影贴图进行比较。如果一个被渲染的点到光源的距离比阴影贴图中的相应值更远,则该点就在阴影中(灯光看不到这个点)。建立阴影贴图的成本与渲染图元的数量大致呈线性关系,而且访问时间是恒定的。

阴影映射的缺点是,阴影的质量取决于阴影贴图的分辨率和深度缓冲区的数值精度,所以比较容易出现锯齿,一个常见的问题是自阴影。

自阴影

当灯光斜着照射在一个表面上时,阴影贴图中的一个像素(格)会覆盖场景表面的一片区域。

  • 在阴影 Pass 中:这个格子里记录的是这整块区域中心点(或者某个采样点)到光源的深度值 d_map。
  • 在渲染 Pass 中:表面上属于这个格子覆盖范围内的多个连续片元,都会去比对这同一个 d_map。由于表面是有倾角的,这些片元的实际深度值 d_real 在连续变化。

这就导致在同一个阴影贴图像素覆盖的区域内,有一半片元的实际深度 d_real 会略小于 d_map(被判定为照亮),而另一半片元的实际深度 d_real 会略大于 $ d_map(被错误地判定为在阴影中)。

这种对错交替的结果,反映在视觉上就是一圈圈、一团团交错的黑白条纹,好像物体自己把阴影投射在了自己身上。

法线偏移 (与恒定深度偏移(Constant Bias)结合使用) -> 每个多边形原理光源一点-> 避免可能出现的精度误差。这样做d_real 永远会小于d_map,这样不会让片元一半在阴影中一半不在阴影中。

分辨率增强

改变光照矩阵,让接近人眼的地方有更高采样率来对阴影进行抗锯齿,例如沿着观察轴放置更多的“层级阴影贴图”(单张2048x2048 -> 四张 1024x1024),这样观察更近(更仔细)的区域的阴影的分辨率不会因为这个场景很大,而导致的阴影贴图在近处不清晰。运用这个方法时,划分阴影层级(最近到最远)十分重要。

光照样本的助阵移动而产生的闪烁效应是一个问题,并且还会随着物体在层级之间移动而变得更糟。当物体跨越两个阴影贴图之间的边界时,阴影质量可能会发生突然变化。一种解决方案是使视域体稍微重叠,在这些重叠区域采集的样本会收集来自两个相邻阴影贴图的结果,并进行混合。

取巧的方法:使用一个低细节层次的模型作为代理来实际投射阴影,另一种技术是将微小的遮挡物从计算中移除。较远的阴影贴图的更新频率可以低于每帧一次。

让阴影更加真实(模拟软阴影)

接近百分比的滤波(PCF,percentage-closer filtering)

从投射阴影点位置反向滤波,找到遮挡光源的物体,近似出光线的软阴影

接近百分比软银鹰(PCSS,percentage-closer soft shadows)

在阴影图上搜索附近的区域来找到所有可能的遮挡物,用这些遮挡物与光源位置的平均距离来确定样本区域的宽度,如果跟没有遮挡物,该位置就会被完全照亮,无需进一步处理。

阴影贴图滤波(方差阴影贴图,VSM,variance shadow map)

非常适合在地形上的阴影,因为没有很多的遮挡物。

最终像素颜色 = 环境光 + 光照可见度 [0 ,1] x 直接光照

VSM的目的就是在解决:该区域内有多少比例的遮挡物深度 x (深度贴图中,以光源视角来看的各个物体的深度)大于等于我们的深度 t (当前正在被着色的场景表面像素点的深度)? 这个比例就作为光照可见度来应先阴影边缘的过度。

平均深度越接近光源 -> 光照可见度减小 -> 阴影更硬(因为被光照的影响更小)

还有很多对软阴影进行模拟的算法,比如利用卷积采样等等,但需要跟高的算力,也许在今年(2026)已经有了一定的普及,也可能跟没有。

自适应体积阴影贴图

透明物体会使光线衰减并改变光的颜色。头发、烟雾等的渲染不能依赖普通和单一的阴影贴图。深层度阴影贴图的概念是,其中每个纹素都储存一个光线如何随深度下降的函数。自适应体积阴影贴图每个纹素都储存了不透明度和层的深度。

锥体追踪/ 体素锥体追踪 (Voxel Cone Tracing, VCT)

既然发射了一个逐渐变宽的圆锥,如何检测圆锥一路上扫过的遮挡物?

在 VCT 中,场景首先被体素化(Voxelized)存入一个 3D 纹理中,并生成类似 Mipmap 的三维层级结构(被称为占用率金字塔 Occupancy Pyramid):

  • Level 0:分辨率最高,体素极小、极精确。
  • Level N:分辨率低,体素很大,代表了一个大区域内体素密度的平均值。

当圆锥从着色点 $P$ 出发向光源延伸时:

  1. 起始阶段(近处):圆锥的横截面积非常小。算法采样的 3D 纹理层级非常低(如 Level 0)。由于体素分辨率极高,遮挡物边界非常精准,此时能算出来极窄、极精确的硬阴影边缘。
  2. 前进阶段(远处):随着行进距离变远,圆锥的半径迅速增大。此时算法会自适应地采样更高、更模糊的 3D 纹理层级(如 Level 3 或 4)。这一层级中的一个体素代表了很大一片区域的平均不透明度。

如何计算阴影的淡出?(步进累积)

圆锥在 3D 体素空间中步进(Marching)时,会在一路上累积不透明度(Opacity/Occlusion)。

设每一个步进点的单步遮挡度为 $\alpha_i$(可以通过当前的 Mipmap 体素直接采样得到),光线透过率 $T$ 初始为 $1.0$:

$$T_{i} = T_{i-1} \times (1.0 - \alpha_i)$$

  • 如果在起始阶段(近处)*碰到了一个实心体素($\alpha = 1.0$),透过率 $T$ 瞬间归 0,形成边缘锋利的*硬阴影
  • 如果在远处碰到了半透的、稀疏的体素($\alpha$ 较小,比如由于高层级 Mipmap 均值化导致的渐变),透过率 $T$ 就会逐渐平滑地衰减(例如 $1.0 \to 0.7 \to 0.4 \to 0.1$),从而形成非常平滑自然的软阴影

算法与硬件的映射具体来讲是怎样的?

6.光照与颜色

可见光波长范围大约 380nm - 780nm

色度学与光度学

光度学与色度学是光学的重要分支,它们研究的核心并非单纯的“物理光”,而是物理光作为刺激源,在人类视觉系统中引发的“亮度”与“色彩”的心理物理学(Psychophysics)感受

人类对光和色彩的感知,始于视网膜上的两类感光细胞:视杆细胞(Rod Cells)与视锥细胞(Cone Cells)。其中,视锥细胞是色度学与光度学研究的生理学基础。

  • 视杆细胞(暗视觉):数量多、对光极度敏感,但不参与色觉。在微弱光线下起主导作用,只让人感知黑白与轮廓。
  • 视锥细胞(明视觉/色觉):工作于较亮环境,负责捕捉细节和色彩。人眼视网膜上拥有三种不同类型的视锥细胞,它们含有不同的视色素,分别对不同波长范围的光产生最大共振响应:

    • L 锥细胞(Long-wave):响应长波长光,敏感峰值在红色光谱区域(约 $564\text{ nm}$)。
    • M 锥细胞(Medium-wave):响应中波长光,敏感峰值在绿色光谱区域(约 $534\text{ nm}$)。
    • S 锥细胞(Short-wave):响应短波长光,敏感峰值在蓝色光谱区域(约 $420\text{ nm}$)。

三种视锥细胞将接收到的光谱辐射转化为神经电信号,传入大脑皮层进行综合处理。这种“三原色式”的生理结构,决定了人类视网膜捕获光信息的方式,也是一切色彩空间与照明标准诞生的源头。

光度学回答了“光有多亮”(将辐射功率加权转化为亮度感知值)。

色度学回答了“光是什么颜色”(将复杂的光谱分布降维成三维的颜色刺激值坐标)。

色度图 vs 颜色空间 (sRGB等编码标准)

如果我们想要一个能够彻底盖住整个马蹄形色度图的三角形,唯一的办法是将三角形的三个顶点设在马蹄形外面(比如 CIE XYZ 颜色空间的原色点)。但问题是,落在马蹄形外部的颜色在物理上是虚构原色(Imaginary Colors)——它们对应的物理光谱是不存在的,人眼根本无法感知。

为什么物理上能发出的光,无法让人眼在三原色混合下达到“绝对纯度”的感知?这源于我们视锥细胞的生理特性:

  • 人眼的 L(红)、M(绿)、S(蓝)三种视锥细胞的光谱响应曲线是高度重叠的
  • 特别是在中波(绿光)和长波(红光)区域,L 和 M 细胞的重叠度极高。
  • 当一个纯度极高的单色绿光(比如 $520\text{ nm}$ 的激光)照进人眼时,它会在几乎不刺激 L 细胞的情况下强烈刺激 M 细胞。此时人眼感知到的是极其纯净、饱和度拉满的绿色(对应色度图最顶端的马蹄形边缘)。
  • 但是,如果我们试图用显示器的物理红光和蓝光去混合绿光:任何物理红光和蓝光在刺激 M 细胞的同时,都会不可避免地刺激到 L 或 S 细胞。这种“顺带的刺激”在视觉上相当于给绿色中混入了杂质,从而降低了颜色的饱和度
  • 因此,通过 RGB 混合出来的颜色,其饱和度永远无法达到单色激光的纯度。这导致 RGB 三角形的边界被迫向内缩进。

物理与工业极限:sRGB 诞生时的妥协

sRGB 标准是由微软和惠普在 1996 年联合制定的。在那个时代,主流的显示设备是庞大的阴极射线管显示器(CRT 显器)

sRGB 选定的红、绿、蓝三个原色顶点,实际上就是当时 CRT 显像管所能使用的荧光粉(P22 荧光粉)在激发时的物理发光极限

  • 绿点妥协:当时的绿色荧光粉纯度很低,发出的绿色带有一点点偏黄的杂质。因此,sRGB 的绿色顶点被迫定在了一个非常靠内(靠近白色中心)的位置,直接导致 sRGB 缺失了大量的纯绿、青色(Cyan)和高饱和度的蓝绿色。
  • 兼容性与成本:虽然现在有了 LED、OLED 和量子点(Quantum Dot)技术,我们完全可以制造出原色纯度极高、色域大得多的显示屏(如 DCI-P3、Rec.2020 色域,如上方对比图所示),但 sRGB 作为互联网、标准网页和大多数普通显示器的“通用语言”,为了保证数十亿台老旧设备的色彩兼容性,依然将这个较窄的三角形范围沿用至今。

色调映射

场景参考图像显示参考图像有本质上的不同,物理操作只有在对场景参考数据执行时才有效。

延迟着色