Skip to content

几何(网格处理)、阴影图

前两章讨论了几何如何表示,以及曲线曲面如何由连续描述离散成网格。真正进入渲染系统时,我们经常面对的是三角网格:它可能来自建模软件、扫描重建、程序生成、曲面离散化,也可能是从高模烘焙后得到的低模。

这一章先看网格处理的基本问题:法线如何计算,网格为什么要简化、细分、重网格化,拓扑问题为什么会影响后续算法。随后,我们引入一个非常重要的可见性技术:阴影图 (Shadow Map)。它把几何从光源视角渲染成深度图,再在相机视角中用深度比较判断一个点是否被光照到。


三角网格可以写成:

Mesh=(V,F)Mesh = (V, F)

其中 VV 是顶点集合,FF 是三角形面集合。实际项目中,网格还会携带法线、UV、切线、材质 ID、骨骼权重等属性。

网格处理要解决的不是“能不能画出三角形”,而是:

  • 网格是否足够干净,能被后续算法稳定处理。
  • 网格密度是否合适,既不过重,也不丢失重要形状。
  • 法线、UV、切线等属性是否与几何一致。
  • 拓扑是否适合变形、细分、布尔、碰撞或制造。

同一个模型在不同阶段可能需要不同版本:建模时需要高细节,运行时需要低成本,碰撞时需要简单形状,阴影渲染时又可能需要单独的 shadow caster 版本。


三角形的面法线由两条边的叉乘得到:

Nf=normalize((P1P0)×(P2P0))N_f = normalize((P_1 - P_0) \times (P_2 - P_0))

面法线垂直于三角形平面,适合 Flat Shading。如果每个三角形都直接使用自己的面法线,模型会呈现明显的折面感。

为了让表面看起来更平滑,渲染时常使用顶点法线 (Vertex Normal)。一个常见做法是把共享同一顶点的相邻面法线加权平均:

Nv=normalize(iwiNfi)N_v = normalize(\sum_i w_i N_{f_i})

权重 wiw_i 可以按面积、夹角或其它规则设置。顶点法线再经过光栅化插值,就能让低多边形模型呈现更连续的光照变化。


不是所有边都应该平滑。立方体的边缘如果把相邻面法线平均,就会看起来像被磨圆;而真实立方体应该保留硬边。

解决方法是法线拆分:同一个空间位置可以在网格数据中对应多个顶点副本,每个副本拥有不同法线。这样,面之间虽然位置重合,但着色时不会共享平滑法线。

建模软件中的平滑组、硬边、Auto Smooth,本质上都在控制哪些面共享顶点法线,哪些面需要拆开。


网格简化 (Mesh Simplification) 的目标是在尽量保留形状的前提下降低顶点和三角形数量。它常用于:

  • 生成 LOD 模型。
  • 降低移动端或实时场景负担。
  • 清理扫描模型中过密的三角形。
  • 为碰撞或阴影生成低成本代理网格。

一种常见思想是不断折叠边:把一条边的两个端点合并成一个点,同时删除相关三角形。每次折叠都会引入误差,因此算法需要估计“折叠哪条边最不影响形状”。

网格简化不只是减少数量。它还要尽量保留轮廓、锐边、UV 边界、材质边界和动画变形关键区域。否则,模型虽然轻了,视觉和功能却可能坏掉。


简化是减少几何,细分 (Subdivision) 则是增加几何。细分可以让模型更平滑,也可以为置换贴图、雕刻或模拟提供更高分辨率。

细分曲面会根据邻域规则插入新顶点并调整位置,使粗控制网格逐渐变成平滑表面。它适合角色、有机形状和动画资产。

重网格化 (Remeshing) 则关注重新组织三角形分布。一个扫描模型可能三角形大小不均、方向混乱、拓扑不佳;重网格化可以让面片更均匀,或者让边流更适合动画变形。

操作目标
简化减少三角形,降低成本
细分增加几何,提升平滑度或细节承载能力
重网格化改善三角形分布、边流和拓扑质量

网格拓扑描述顶点、边、面之间如何连接。很多算法要求网格接近“正常表面”,也就是局部看起来像一张二维薄片。

常见问题包括:

  • 非流形边:一条边被超过两个三角形共享。
  • 孤立点或孤立面:与主体没有连接的小碎片。
  • 翻转面:三角形顶点顺序错误,导致法线朝内。
  • 自交:表面穿过自己。
  • 洞和裂缝:本应闭合的边界没有连接。

这些问题可能不会立刻阻止渲染,但会影响法线计算、阴影、布尔运算、物理碰撞、路径追踪求交和 3D 打印。


有了光照模型后,一个表面是否亮,并不只取决于法线是否朝向光源。还要问:光源和这个表面点之间有没有被其它物体挡住?

这就是阴影问题。局部光照模型本身不会自动产生阴影,因为它只计算当前点、光源和观察方向的关系,不知道中间是否有遮挡物。

阴影可以理解为从光源出发的可见性测试:

visibility(P,light)={1,light can see P0,light is blockedvisibility(P, light) = \begin{cases} 1, & \text{light can see } P \\ 0, & \text{light is blocked} \end{cases}

最终光照可以写成:

L=Lambient+visibility(Ldiffuse+Lspecular)L = L_{ambient} + visibility \cdot (L_{diffuse} + L_{specular})

环境光通常不乘这个直接光可见性,因为它近似的是间接照明;直接漫反射和高光则需要被阴影遮挡。


Shadow Mapping 是实时渲染中最常见的阴影技术之一。它的思想非常直接:

  1. 从光源视角渲染场景:把相机放到光源位置或沿光源方向观察场景。
  2. 只记录深度:生成一张深度纹理,称为 shadow map。
  3. 从相机视角正常渲染:对每个片元,计算它在光源视角下的位置和深度。
  4. 比较深度:如果片元到光源的深度大于 shadow map 中记录的最近深度,说明它被挡住。
  5. 调制光照:被挡住的片元不接受该光源的直接漫反射和高光。

用一句话概括:先问光源看到了什么,再问当前片元是不是光源看到的最近表面。

Shadow Mapping 的两次渲染流程

Shadow Map 可以理解成“光源视角下的可见性缓存”。第一次渲染只关心深度,第二次渲染才从相机视角生成颜色,并用光源空间深度比较决定直接光是否被遮挡。

橙色线表示光线方向,左上条带是从光源视角记录的 shadow map。右侧画面中,每个地面采样点会被投影回光源空间,与 shadow map 中的深度比较;若它比记录值更远,就处在阴影中。

上面的组件把二维场景简化成一条光源深度图。调低阴影图分辨率会看到阴影边缘更粗糙;调节深度偏移可以观察 bias 对错误自阴影和阴影漂移的影响;开启柔化则模拟 PCF 对阴影边界做邻域平均。


Shadow Mapping 的关键是把当前片元投影到光源空间。假设片元世界坐标为 PworldP_{world},光源也有自己的视图投影矩阵:

PlightClip=MlightProjMlightViewPworldP_{lightClip} = M_{lightProj} M_{lightView} P_{world}

然后把裁剪空间坐标转换到 shadow map 的纹理坐标,并得到当前片元在光源视角下的深度:

vec4 lightClip = lightProjection * lightView * vec4(worldPosition, 1.0);
vec3 lightNdc = lightClip.xyz / lightClip.w;
vec2 shadowUV = lightNdc.xy * 0.5 + 0.5;
float currentDepth = lightNdc.z * 0.5 + 0.5;

再从 shadow map 里读取光源曾经记录的最近深度:

float closestDepth = texture(shadowMap, shadowUV).r;
float shadow = currentDepth > closestDepth ? 1.0 : 0.0;

实际代码通常会加入 bias、边界判断、PCF 和不同光源类型的投影处理。


Shadow Mapping 最经典的问题是 Shadow Acne,也就是表面出现密密麻麻的错误自阴影。

它的原因是当前片元深度和 shadow map 中存储的深度来自两次不同采样。由于分辨率、浮点精度、斜面投影和光栅化误差,理论上同一个表面点在比较时可能被判断为“比最近深度更远”,于是错误地阴影了自己。

常见解决方法是加入深度偏移:

float bias = 0.005;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;

但 bias 不能无限增大。太小会有自阴影,太大则会让阴影脱离物体,看起来像悬浮在地面上,这叫 Peter Panning


shadow map 是一张纹理,因此它有有限分辨率。当阴影投射到屏幕上时,一个 shadow map 纹素可能覆盖很多屏幕像素,阴影边缘就会呈现块状或锯齿。

影响阴影清晰度的因素包括:

  • shadow map 分辨率。
  • 光源视锥覆盖范围。
  • 物体与光源、相机之间的关系。
  • 阴影接收面和光线夹角。

如果光源视锥覆盖了巨大场景,但 shadow map 分辨率有限,每个区域得到的深度精度就会变低。方向光的级联阴影 (Cascaded Shadow Maps) 正是为了解决大场景中近处阴影需要更高分辨率、远处阴影可以更粗的问题。


最简单的 shadow map 比较只返回 0 或 1,阴影边缘会非常硬。PCF (Percentage-Closer Filtering) 会在 shadowUV 附近采样多个深度值,分别做比较,然后平均结果:

shadow=1Nicompare(currentDepth,closestDepthi)shadow = \frac{1}{N}\sum_i compare(currentDepth, closestDepth_i)

它不是物理真实的软阴影,但能让边缘更柔和,也能隐藏一部分分辨率锯齿。真实软阴影还和光源面积、遮挡物距离、接收面距离有关,后续更高级技术会继续处理这些问题。


阴影图看似是渲染技术,但它和几何质量关系很大。

如果模型法线错误,光照会怪;如果模型面朝向错误,shadow caster 可能被剔除;如果网格有裂缝,阴影可能漏光;如果 shadow caster 的低模代理和可见模型差距太大,阴影轮廓会不匹配。

常见排查方向包括:

现象可能原因
阴影断裂网格裂缝、深度偏移过大、shadow map 分辨率不足
表面脏斑bias 太小、法线偏移不足、斜面深度精度差
阴影漂浮bias 太大、caster 代理模型偏离可见模型
背面不投影面剔除设置或法线/绕序错误
远处阴影闪烁shadow map texel 与相机运动不稳定

这说明网格处理和阴影并不是两个完全割裂的话题。高质量几何数据会让可见性、光照和后续渲染技术更稳定。


本章从网格处理过渡到阴影图,重点建立两个观念:网格质量会影响后续渲染,阴影本质上是从光源视角做可见性判断。

  1. 面法线来自三角形边的叉乘,顶点法线通常由相邻面法线加权平均,用于获得更平滑的光照。
  2. 硬边和平滑边需要通过法线共享或法线拆分控制;错误法线会直接造成异常明暗。
  3. 网格简化、细分和重网格化分别服务于降低成本、提升平滑度和改善拓扑质量。
  4. Shadow Mapping 先从光源视角记录最近深度,再从相机视角把片元投影回光源空间做深度比较。
  5. shadow acne、peter panning、阴影锯齿和漏光都与深度精度、bias、分辨率、采样和几何质量有关。

下一章会进入光线追踪的基本原理:不再从三角形覆盖屏幕出发,而是从相机发射光线,直接求解光线与场景几何的交点。