着色(着色频率、图形管线、纹理映射)
上一章讨论了局部光照模型:给定表面法线、光照方向、观察方向和材质参数,如何计算一个表面点的颜色。接下来还要回答另一个很实际的问题:这个颜色应该在哪里计算?
同一个 Blinn-Phong 公式,可以在每个三角形上算一次,也可以在每个顶点上算一次,还可以在每个片元上重新计算。计算位置不同,画面质量、性能成本和可表达的细节都会不同。这就是本章的第一条主线:着色频率 (Shading Frequency)。
本章的第二条主线是把着色放回完整的图形管线中:顶点数据如何进入 GPU,经过顶点着色、图元装配、光栅化、片元着色,最后写入帧缓冲。最后,我们会引入纹理映射:让材质不再只是一个固定颜色,而是能从图像中读取空间变化的细节。
着色频率是什么
Section titled “着色频率是什么”着色频率指的是“每隔多密集的几何或采样单位计算一次颜色”。在实时渲染中,常见的三种基础频率是:
- Flat Shading:每个三角形只计算一次颜色,整个三角形使用同一个结果。
- Gouraud Shading:每个顶点计算一次颜色,三角形内部通过插值得到片元颜色。
- Phong Shading:每个片元插值法线,再在片元级别重新计算光照。
这里的 Phong Shading 容易和上一章的 Phong 镜面反射模型混淆。前者描述“光照在哪个频率上计算”,后者描述“镜面高光怎么算”。也就是说,可以使用 Blinn-Phong 光照模型,同时采用 Phong Shading 的片元级计算方式。
Flat Shading:每个三角形一次
Section titled “Flat Shading:每个三角形一次”Flat Shading 会为整个三角形使用同一个法线和同一个光照结果。这个法线通常来自三角形面法线:
然后使用上一章的光照模型计算颜色:
由于整个三角形颜色相同,Flat Shading 的优点是成本低、形体结构清晰;缺点是面片边界非常明显。对于低模风格、调试法线、观察三角网格结构时,它反而很有用。
但如果目标是表现光滑球体、角色皮肤、抛光陶瓷这类连续表面,Flat Shading 会让模型显得像由许多硬折面拼起来。
Gouraud Shading:每个顶点一次
Section titled “Gouraud Shading:每个顶点一次”Gouraud Shading 会先在三角形的三个顶点分别计算光照:
光栅化之后,片元颜色通过重心坐标插值得到:
其中 。这种方法比 Flat Shading 平滑得多,因为颜色会在三角形内部渐变。早期硬件和固定管线时代,Gouraud Shading 非常常见,因为它能用较少的计算获得还不错的明暗过渡。
不过它有一个典型问题:高光可能丢失。如果一个小而亮的镜面高光落在三角形内部,但三个顶点都没有正好采到它,那么顶点颜色里就没有这个高光,插值后也不会凭空出现。
Phong Shading:每个片元一次
Section titled “Phong Shading:每个片元一次”Phong Shading 会在顶点之间插值法线、位置等数据,然后在每个片元上重新执行光照公式:
接着使用插值得到的法线 、片元位置 、光源方向 、观察方向 计算最终颜色。这种做法能让明暗变化更加连续,也能稳定表现小范围高光。
需要注意的是,法线插值后必须重新归一化。因为三个单位法线线性插值后,长度通常不再等于 ;如果直接拿去点乘,光照强度会被错误放大或缩小。
在上面的组件中,Flat 的三角形边界最明显;Gouraud 的亮度过渡变得平滑,但高光会被顶点采样“平均掉”;Phong 则在每个像素重新计算法线与光照,因此高光位置和形状更稳定。
三种频率的取舍
Section titled “三种频率的取舍”三种着色频率没有绝对优劣,选择取决于画面目标和性能预算。
| 方式 | 计算位置 | 优点 | 局限 |
|---|---|---|---|
| Flat Shading | 每个三角形 | 成本低,面结构清楚 | 面片感明显,不适合光滑表面 |
| Gouraud Shading | 每个顶点 | 比 Flat 平滑,成本较低 | 小高光和细节容易丢失 |
| Phong Shading | 每个片元 | 明暗和高光更准确 | 片元计算成本更高 |
在现代实时渲染中,绝大多数材质会在片元级别或更复杂的屏幕采样级别执行主要着色。但 Flat 和 Gouraud 仍然有价值:Flat 常用于风格化表现和调试,Gouraud 可以用于非常轻量的移动场景、粒子或远处低细节对象。
图形管线中的数据流
Section titled “图形管线中的数据流”理解着色频率之后,可以把它放回 GPU 图形管线中观察。一个简化的实时渲染管线可以写成:
- 输入顶点数据:CPU 将顶点位置、法线、颜色、UV、索引等数据提交给 GPU。
- 顶点着色器:每个顶点独立执行,通常完成坐标变换,并输出后续阶段需要插值的数据。
- 图元装配:GPU 按索引或顶点顺序把顶点组成点、线或三角形。
- 裁剪与屏幕映射:丢弃视锥体外的部分,并把 NDC 坐标映射到屏幕坐标。
- 光栅化:判断三角形覆盖哪些像素采样点,生成候选片元,并为每个片元插值属性。
- 片元着色器:每个片元计算颜色,常见操作包括光照、纹理采样、法线贴图、透明度判断等。
- 输出合并:执行深度测试、模板测试、混合等操作,把结果写入颜色缓冲和深度缓冲。
这张流程图可以帮助区分两件事:顶点着色器处理的是顶点频率的数据,光栅化器负责把顶点输出插值到片元频率,片元着色器再用这些插值结果计算最终颜色。
前面几章其实已经分别看过这些阶段的一部分:MVP 变换发生在顶点处理阶段,光栅化负责把三角形离散成片元,深度测试决定候选片元能否覆盖已有结果,而本章的着色讨论的是“颜色如何在这些阶段中产生”。
顶点着色器负责什么
Section titled “顶点着色器负责什么”顶点着色器 (Vertex Shader) 的最核心任务是把模型顶点变换到裁剪空间:
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);除了位置,顶点着色器还会准备片元着色器需要的数据。例如:
worldPosition = vec3(modelMatrix * vec4(position, 1.0));worldNormal = normalize(normalMatrix * normal);texCoord = uv;这些输出不会只停留在顶点上。光栅化生成片元时,GPU 会自动对它们进行插值。也就是说,片元着色器拿到的 worldPosition、worldNormal 和 texCoord,通常已经是当前片元对应的插值结果。
片元着色器负责什么
Section titled “片元着色器负责什么”片元着色器 (Fragment Shader / Pixel Shader) 的目标是为当前片元输出颜色。它经常会做这些事情:
- 归一化插值得到的法线。
- 根据片元位置计算光照方向和观察方向。
- 从纹理中读取基础颜色、粗糙度、法线等材质数据。
- 计算漫反射、高光、透明度或自发光。
- 在需要时丢弃片元,例如透明裁剪材质中的
discard。
一个非常简化的片元着色器可以写成:
vec3 N = normalize(worldNormal);vec3 L = normalize(lightPosition - worldPosition);vec3 V = normalize(cameraPosition - worldPosition);vec3 H = normalize(L + V);
vec3 baseColor = texture(baseColorMap, texCoord).rgb;float diffuse = max(dot(N, L), 0.0);float specular = pow(max(dot(N, H), 0.0), shininess);
outColor = vec4(baseColor * (ambient + diffuse) + specularColor * specular, 1.0);这里的 texture(baseColorMap, texCoord) 就是本章后半部分要引入的纹理采样。
为什么需要纹理
Section titled “为什么需要纹理”如果一个物体只有固定的 baseColor,那么它的表面只能呈现单一材质颜色。即使光照模型再完整,也很难表现木纹、砖缝、皮肤斑点、布料花纹、金属划痕这些空间细节。
纹理 (Texture) 本质上是一份可以在着色器中查询的数据。最常见的是二维图像:给定一个二维坐标,读取对应位置的颜色。纹理让材质参数可以随表面位置变化:
- 基础颜色纹理控制物体不同位置的颜色。
- 法线纹理让平面也能表现细小凹凸。
- 粗糙度纹理控制高光在不同位置的扩散程度。
- 金属度、透明度、自发光等也可以由纹理提供。
因此,纹理不是“贴图装饰”这么简单。更准确地说,纹理是一种把二维或三维空间坐标映射到材质数据的函数。
UV 坐标与纹理映射
Section titled “UV 坐标与纹理映射”为了从二维纹理中读取数据,模型表面需要携带一组二维坐标,通常记作 。这就是 UV 坐标。
可以把 UV 理解为“模型表面到图片平面的地址”。三角形的每个顶点保存一个 UV:
光栅化时,片元的 UV 会通过重心坐标插值得到:
片元着色器再用这个 UV 去纹理中采样颜色:
vec3 baseColor = texture(baseColorMap, uv).rgb;于是,一个三角形不再只能拥有一个颜色,而是可以把图片上的不同位置映射到三角形内部的不同片元上。整个模型由许多带 UV 的三角形组成,就能把一张或多张纹理图像铺到复杂表面上。
透视正确插值
Section titled “透视正确插值”在三维透视投影中,直接在屏幕空间线性插值 UV 会产生错误:远处和近处的纹理拉伸关系会不自然。原因是透视投影不是线性变换,深度变化会影响屏幕上的插值比例。
现代 GPU 会对顶点输出属性执行透视正确插值 (Perspective-Correct Interpolation)。直观地说,它不是简单插值 、,而是先考虑裁剪空间中的 :
也同理。这样可以保证纹理在透视下看起来像真正贴在三维表面上,而不是像屏幕上的平面图案被错误拉伸。
纹理采样的基本问题
Section titled “纹理采样的基本问题”纹理映射听起来像“用 UV 查一张图片”,但实际采样还会遇到几个问题。
首先,UV 可能落在像素之间。此时需要决定读取最近的纹素,还是在周围纹素之间插值。这对应最近邻过滤和双线性过滤。
其次,一个屏幕像素可能覆盖纹理中的大片区域。远处地面、墙面、细密网格最容易出现闪烁和摩尔纹。为了解决这个问题,渲染器通常会使用 Mipmap:为同一张纹理预先生成多级低分辨率版本,并根据屏幕覆盖范围选择合适层级。
最后,UV 可能超出 范围。此时可以选择重复、镜像重复或夹取到边缘:
- Repeat:纹理像瓷砖一样重复铺开。
- Mirrored Repeat:每隔一段镜像翻转一次,减少接缝感。
- Clamp to Edge:超出范围后使用边缘颜色。
这些采样规则会在后续高级纹理映射章节中继续展开。本章先建立最核心的理解:纹理采样是片元着色器读取材质数据的过程,而 UV 是读取地址。
本章把上一章的光照模型放回了图形管线,并引入了纹理映射的入口。请重点记住以下结论:
- 着色频率描述颜色或光照计算发生的密度:Flat 每个三角形一次,Gouraud 每个顶点一次,Phong 每个片元一次。
- Gouraud Shading 插值的是顶点颜色,因此小范围高光可能丢失;Phong Shading 插值法线并在片元级重新计算光照,通常能得到更稳定的明暗和高光。
- 顶点着色器负责逐顶点处理,常见任务是坐标变换与输出待插值属性;片元着色器负责逐片元计算颜色,常见任务是光照和纹理采样。
- 纹理是一种可查询的数据源,不只存颜色,也可以存法线、粗糙度、金属度、透明度等材质参数。
- UV 坐标是模型表面访问二维纹理的地址;在透视投影下,UV 等属性需要透视正确插值,才能看起来真正贴在三维表面上。
下一章会继续深入纹理映射,讨论插值细节、高级纹理采样、法线贴图与纹理如何参与更复杂的材质表达。