Skip to main content

Chapter 46: Advanced Texture Techniques

高级纹理技术的主问题是:当同一帧里同时出现地形墙面、贴花、天空环境、体积云雾和 PBR 材质时,如何判断一张纹理应按哪种坐标、哪种资源形态、哪种采样策略和哪种材质语义进入 shader。读完本章,读者应能追踪一个像素从几何表面到纹理采样、环境反射、体积查询和 BRDF 计算的完整路径,并能根据视觉症状定位问题属于 UV、投影、Cubemap、3D Texture、采样成本或材质通道绑定。

本章贯穿一个雨后金属街区的单帧:镜头看向一面潮湿混凝土墙,墙面使用 triplanar 纹理混合,路面叠加积水贴花,远处由 Cubemap 提供天空和反射,街道深处有体积雾,墙面材质进入金属度/粗糙度 PBR 管线。这个场景覆盖纹理技术的主要分层:二维纹理解决表面细节,投影纹理解决跨网格覆盖,Cubemap 解决方向采样,3D Texture 解决空间密度,PBR 纹理通道解决材质参数。

判断纹理方案时,先确定采样坐标来自哪里,再确定纹理资源表达什么,随后检查 sampler 状态、mip 选择、cache 访问和材质语义。纹理问题常表现为拉伸、接缝、闪烁、反射方向错误、云雾切片感、采样过多或材质能量失衡。每一种现象都能回到一条可检查路径:坐标生成 → 资源读取 → 过滤和 LOD → shader 合成 → 输出颜色或材质参数。

下面的路径图描述本章贯穿帧的纹理数据流。图中只覆盖纹理相关路径,光源列表、阴影图和后处理暂时不展开。

图中的关键分界是坐标来源。表面纹理通常从 UV 或世界位置得到坐标,贴花从投影空间得到坐标,Cubemap 从方向向量得到坐标,体积纹理从三维位置得到坐标。采样对象变化后,错误症状也随之变化:UV 错误会让墙面花纹变形,方向向量错误会让反射旋转,体积坐标错误会让雾随相机漂移,材质通道错误会让金属、粗糙度和环境光响应同时失真。

46.1 纹理坐标变换与投影映射技巧

纹理坐标变换回答的是“当前 fragment 应该读取纹理的哪个位置”。普通 UV 把二维表面展开到二维纹理;planar mapping 用一个平面投影三维点;triplanar mapping 同时从三个轴向采样再按法线权重混合;projective mapping 使用投影矩阵把世界位置变成投影纹理坐标;decal projection 把贴花看作一个局部投影盒,只有盒内 fragment 接受覆盖。

在雨后街区这一帧中,混凝土墙来自大块静态网格。墙的建模 UV 在转角处容易产生拉伸,triplanar 可以把世界位置映射到三个平面,减轻大型墙面和石块模型的 UV 维护成本。路面积水贴花属于另一路径:它不改变原始 mesh UV,而是从贴花体积生成局部投影坐标,将水膜 roughness、normal 和 mask 叠加到地面材质上。

UV transform 的作用边界

UV transform 是最轻量的坐标变换。它通常由 scale、offset、rotation 组成,用于控制平铺次数、纹理滚动和局部偏移。它的输入是顶点插值后的 UV,输出仍是二维坐标。它适合规则表面,例如地砖、墙纸、UI 面板和可控展开的模型表面。

UV transform 的风险集中在 mip 选择和接缝。scale 过大时,同一屏幕像素覆盖更多纹理周期,shader 会进入更高 mip;scale 过小时,纹理细节会被放大并暴露压缩块、低分辨率源图或过滤模糊。rotation 会改变梯度方向,硬件通过屏幕空间导数估计 LOD,所以在 fragment shader 中对 UV 做强烈非线性扭曲时,自动 LOD 可能和真实采样频率不匹配。

下面的 GLSL 片段展示一个最小 UV transform。它用于墙面 base color,目的只是说明坐标变换发生在纹理采样之前。

vec2 transformUv(vec2 uv, vec2 scale, vec2 offset) {
return uv * scale + offset;
}

vec3 sampleWallBaseColor(sampler2D baseColorMap, vec2 meshUv) {
vec2 uv = transformUv(meshUv, vec2(4.0, 4.0), vec2(0.0, 0.0));
return texture(baseColorMap, uv).rgb;
}

这段代码的判断点是资源路径。meshUv 来自顶点属性,scaleoffset 来自材质参数,texture 根据变换后的坐标读取二维纹理。画面出现方向正确但密度错误的花纹时,优先检查 scale、模型单位和 texel density;画面出现局部断裂时,优先检查 UV seam、顶点拆分和 mip 边缘填充。

Planar mapping 与 triplanar mapping

Planar mapping 从世界位置或模型位置取两个分量作为纹理坐标,例如墙面使用 xxyy,地面使用 xxzz。它的优势是生成稳定,适合单一方向的表面;限制是侧面会被拉伸,因为被投影轴方向上的几何变化被压缩到同一个纹理坐标。

Triplanar mapping 同时采样三个平面:xyxyxzxzyzyz。它根据法线朝向分配权重,朝向 xx 轴的表面使用 yzyz 采样,朝向 yy 轴的表面使用 xzxz 采样,朝向 zz 轴的表面使用 xyxy 采样。混合权重通常来自 abs(normal),再经过幂函数控制过渡硬度。

vec3 triplanarColor(sampler2D map, vec3 worldPos, vec3 worldNormal, float tile) {
vec3 blend = pow(abs(normalize(worldNormal)), vec3(4.0));
blend /= max(blend.x + blend.y + blend.z, 0.0001);

vec3 xColor = texture(map, worldPos.yz * tile).rgb;
vec3 yColor = texture(map, worldPos.xz * tile).rgb;
vec3 zColor = texture(map, worldPos.xy * tile).rgb;

return xColor * blend.x + yColor * blend.y + zColor * blend.z;
}

这段代码把一张二维纹理读取三次,所以它用采样次数换取坐标稳定性。墙角、岩石、破碎混凝土和程序化地形通常接受这类成本,因为 UV 修复成本更高。角色皮肤、布料图案和带方向图案的资产通常保留 UV,因为 triplanar 会破坏美术手工绘制的方向关系。

Normal map 的 triplanar 处理还要转换切线空间。直接把三个方向采样到的 tangent-space normal 混合,会把不同投影平面的法线坐标系混在一起。工程实现通常把每个投影平面的 normal 解码到同一个 world space,再按权重混合并归一化。否则墙角高光会出现方向跳变,反射和粗糙度看起来像被旋转。

Projective mapping 与 decal projection

Projective mapping 的输入是世界位置,处理过程是乘以投影矩阵,输出是投影纹理坐标。它和相机投影同构:把三维点变换到投影空间,再除以 ww,得到可采样的二维坐标。投影灯、幻灯片效果、屏幕空间贴图和部分阴影技术都使用这一类思路。

Decal projection 在工程中更常见。一个贴花通常有自己的局部坐标系和盒状范围。shader 把 fragment 的世界位置变换到贴花局部空间;局部坐标在盒内时,将它映射到二维贴花纹理;局部坐标超出范围时,贴花 mask 为零。雨后街区的积水贴花就走这条路径:它在地面局部区域写入更低 roughness、更强法线扰动和一层水痕颜色。

float decalMask(vec3 decalLocalPos) {
vec3 inside = step(abs(decalLocalPos), vec3(1.0));
return inside.x * inside.y * inside.z;
}

vec2 decalUv(vec3 decalLocalPos) {
return decalLocalPos.xz * 0.5 + 0.5;
}

贴花的核心风险是空间归属。投影盒使用世界空间时,贴花会稳定贴在地面;使用视图空间时,贴花会随相机产生漂移。贴花影响 G-buffer 时,还要明确它写哪些通道:只写 base color,还是同时写 normal、roughness、metallic、AO。写 normal 时要统一空间,写 roughness 时要控制混合方式,写 depth 时要处理深度偏移和排序。

坐标问题的检查顺序

坐标失真可以按四步定位。第一步看坐标来源:顶点 UV、世界位置、模型位置、贴花局部坐标或投影矩阵。第二步看坐标单位:模型缩放、世界单位、tile 系数和贴图分辨率是否匹配。第三步看导数和 LOD:是否在 fragment shader 中做了非线性坐标扰动,是否需要 textureGrad 或显式 LOD。第四步看边界:UV seam、投影盒边缘、triplanar 混合区、贴花深度范围和 atlas padding。

这一节建立的结论是:高级纹理映射首先是坐标生成问题。采样前的坐标如果不稳定,后续压缩、过滤、PBR 参数和工具分析都只能看到结果,无法修复根因。

46.2 立方体纹理(Cubemap)与环境映射

Cubemap 把六张方形二维纹理组织成一个方向可寻址的环境资源。Khronos 的 OpenGL Cubemap Texture 文档说明,Cubemap 的每个 mip level 包含六个方形面,shader 使用三维方向向量访问它。Microsoft 的 Direct3D 11 texture overview也把 texture cube 描述为包含六个面的二维纹理数组,并说明 shader 以从 cube 中心指向外部的三维向量读取它。

在雨后街区这一帧中,Cubemap 承担两类任务。第一类是 skybox:远景天空直接由方向向量采样。第二类是环境映射:潮湿金属、玻璃窗和积水区域使用反射方向读取预过滤环境图,得到间接高光和环境色。Skybox 关注背景视觉连续性;环境映射关注材质表面把周围光照反射回相机的结果。

Skybox 的方向采样

Skybox 渲染通常使用一个包围相机的立方体或全屏三角形。它的坐标不来自物体 UV,而是来自视线方向。相机旋转会改变方向,相机平移通常不改变远景采样,因此远处天空保持无限远效果。方向向量的坐标系必须和 Cubemap 面的约定一致,否则太阳、云层和地平线会被旋转或翻转。

vec3 sampleSky(samplerCube skyboxMap, vec3 viewDirectionWorld) {
return texture(skyboxMap, normalize(viewDirectionWorld)).rgb;
}

这段代码的输入是 world-space direction,输出是背景颜色。采样结果上下颠倒时,优先检查贴图导入时的面顺序和 V 方向;左右方向错乱时,检查坐标系 handedness;相机平移导致天空移动时,检查 skybox 顶点是否错误使用了相机位置之外的平移量。

Reflection probe 与 prefiltered environment

环境反射使用反射向量 r=reflect(v,n)r = reflect(-v, n) 采样 Cubemap。这里 vv 是从表面指向相机的视线方向,nn 是表面法线。光滑表面读取低 LOD 的清晰环境,粗糙表面读取更高 LOD 或预过滤 mip,得到模糊反射。Google Filament 的 PBR 文档在 Image Based Lighting 部分把环境图、BRDF 和 roughness 的关系整理为实时 IBL 路径,并使用预过滤环境图与 DFG lookup table 组合间接镜面项。

vec3 sampleSpecularIbl(
samplerCube prefilteredEnvMap,
vec3 normalWorld,
vec3 viewDirWorld,
float perceptualRoughness,
float maxMipLevel
) {
vec3 reflectionDir = reflect(-normalize(viewDirWorld), normalize(normalWorld));
float lod = perceptualRoughness * maxMipLevel;
return textureLod(prefilteredEnvMap, reflectionDir, lod).rgb;
}

这段代码把 roughness 转成 mip 选择。它的视觉边界很清楚:roughness 接近 0 时,反射应接近镜面;roughness 接近 1 时,反射应明显扩散。画面中金属表面始终像镜子,优先检查 roughness 通道是否在线性空间读取、是否被错误反转、是否进入 lod 计算。画面中所有材质都灰暗,优先检查环境图曝光、HDR 格式和 tone mapping 前后的空间。

Seam handling 与面边界

Cubemap 的经典问题是面边界 seam。Khronos 文档指出,标准 cubemap 过滤通常不会跨面边界进行,因此面边界可能出现缝;现代 OpenGL 可通过 seamless cubemap filtering 支持跨面插值。跨 API 工程里,仍要从资源制作、采样状态和预过滤算法三个层面处理 seam。

资源制作层面,六个面要来自同一环境投影并保持一致曝光。采样状态层面,要开启平台支持的无缝 cubemap 过滤或使用引擎封装的等效路径。预过滤层面,roughness mip 生成要考虑面边界,直接逐面模糊会把边缘当作孤立图像处理,粗糙反射处会出现十字状边界。

Cubemap seam 的检查顺序是:先把 skybox 直接显示出来,确认面顺序和方向;再显示 reflection direction 为颜色,确认反射向量空间;随后固定 roughness 为 0、0.5、1 三档,观察 seam 是否随 mip 变强;最后检查导入工具是否在 cubemap 边界做了 padding、solid angle 校正或跨面卷积。

Local probe 的空间边界

Distant probe 假设环境在无限远处,每个表面点使用同一个环境图。Local probe 记录某个位置附近的环境,适合室内房间、街角和局部反射。它的难点是视差:同一个反射方向从不同表面点出发,会击中不同墙面。工程里通常使用 box projection 或 parallax correction,把反射方向和局部包围盒求交,再用修正后的方向采样 cubemap。

雨后街区里的玻璃橱窗适合 local probe,因为窗前建筑、霓虹牌和街道在反射中应具有局部位置关系。远处天空仍适合 distant probe,因为云层和天光可以视为远距离光源。一个材质可以同时使用两类环境来源:远处 IBL 提供整体照明,局部 probe 提供近场反射细节,权重由 probe volume、粗糙度和美术规则控制。

这一节的核心判断是:Cubemap 是方向纹理。它的错误通常来自方向空间、面顺序、mip 预过滤和 probe 体积边界。把它当作普通二维贴图处理,会直接破坏 skybox、反射和 IBL 的空间一致性。

46.3 体积纹理(3D Texture)与云/体积效果

3D Texture 把 texel 扩展成三维网格,也常被称为 volume texture。Microsoft 的 Direct3D 11 纹理文档把 3D texture 描述为包含三维 texel 体积的纹理资源,并指出它可以包含 mipmap levels。它的采样坐标是三维坐标,输出通常是密度、颜色、噪声或查找表结果。

在雨后街区这一帧中,体积雾来自一个 density field。shader 沿视线从相机向场景深度范围步进,每一步把世界位置转换到体积纹理坐标,读取 density,再累积散射和透射。云层也可使用类似路径,只是体积范围更大,噪声层级更多,光照近似更复杂。

Density field 的资源意义

Density field 表示空间中每个点的介质浓度。值为 0 的位置接近透明,值高的位置会增强遮蔽、散射或吸收。它可以来自预烘焙三维噪声、模拟结果、手工体积数据或多个噪声函数组合。3D Texture 的意义是把这个空间函数缓存成可过滤资源,让 shader 通过硬件采样得到连续值。

体积坐标通常来自 world position。对于一个局部雾盒,先把 world position 变换到 volume local space,再把坐标从盒范围映射到 0 到 1。对于无限或大范围云层,常使用世界位置乘以 scale,再叠加时间偏移和多层噪声。坐标使用世界空间时,雾会稳定附着在场景里;坐标使用视图空间时,雾纹理会随相机移动,画面会出现漂浮感。

float sampleDensity(sampler3D densityTex, vec3 worldPos, mat4 worldToVolume) {
vec3 localPos = (worldToVolume * vec4(worldPos, 1.0)).xyz;
vec3 uvw = localPos * 0.5 + 0.5;
return texture(densityTex, uvw).r;
}

这段代码的输入是世界位置和体积变换矩阵,输出是密度。它能定位两个常见问题:雾盒边界错误通常来自 worldToVolume;噪声比例错误通常来自 world unit 到 texture unit 的 scale。若密度在相机移动时发生滑动,直接把 worldPos 可视化为颜色,确认坐标来源是否稳定。

Ray marching 的最小路径

Ray marching 把一条视线分成多个采样点。每一步读取密度,根据步长累积光学厚度,再得到雾颜色和透明度。实时渲染通常使用固定步数、深度限制、随机起点抖动和 temporal filtering。目标是在可接受成本内得到连续体积感。

vec4 marchFog(
sampler3D densityTex,
vec3 rayOrigin,
vec3 rayDir,
float maxDistance,
mat4 worldToVolume
) {
const int stepCount = 48;
float stepLength = maxDistance / float(stepCount);
float transmittance = 1.0;
vec3 scattered = vec3(0.0);

for (int i = 0; i < stepCount; ++i) {
float t = (float(i) + 0.5) * stepLength;
vec3 p = rayOrigin + rayDir * t;
float density = sampleDensity(densityTex, p, worldToVolume);
float absorb = density * stepLength;
scattered += transmittance * absorb * vec3(0.55, 0.62, 0.70);
transmittance *= exp(-absorb);
}

return vec4(scattered, 1.0 - transmittance);
}

这段代码是简化路径,重点在输入输出关系。rayOriginrayDir 来自相机,maxDistance 通常来自场景深度或雾体边界,densityTex 提供空间密度,输出的 alpha 表示雾对背景的覆盖程度。步数增加会提高连续性,也会增加 3D Texture 采样次数和带宽压力。步长过大时,雾会出现层状断裂;步长过小时,成本升高且 temporal filtering 的收益下降。

3D noise、mip 与 temporal filtering

体积云和雾常使用 3D noise。低频噪声决定云团轮廓,高频噪声决定边缘破碎,天气图或高度函数决定宏观分布。多个 3D noise 叠加时,采样次数会迅速增加,所以常把多层噪声预计算到一张或几张 3D Texture 中。

Mip 在体积纹理中同样影响稳定性。远处云层如果读取最高分辨率 mip,会出现闪烁和锯齿;过早读取低 mip,会显得糊成一团。硬件自动 LOD 对 ray marching 中的体积采样并不总能表达真实步进频率,因为采样点沿视线变化,屏幕导数只覆盖相邻像素的坐标变化。复杂实现会显式控制 LOD 或根据步长、距离和云层尺度选择 mip。

Temporal filtering 把多帧结果累积起来,用较少步数换取较平滑的体积效果。它依赖 reprojection、depth、motion vector 和 history rejection。体积效果的 history rejection 要关注遮挡关系:新出现的墙面前方不应保留上一帧的远处雾;快速相机运动时,要降低历史权重以控制拖影。

体积效果的调试证据

体积纹理调试应先把 density 作为灰度图输出,确认体积坐标和边界;再输出 ray marching 的步数或累积 alpha,确认采样距离;随后关闭 temporal filtering,确认单帧噪声和层状断裂;最后开启 temporal filtering,检查拖影、闪烁和 history rejection。

这个顺序能把问题拆到具体对象。坐标错误表现为雾随相机漂移;密度范围错误表现为全白或全透明;步长错误表现为层状切片;LOD 错误表现为远处闪烁或过度模糊;temporal 错误表现为残影。每一种症状都能对应到 shader 输入、纹理资源、循环参数或上一帧历史缓存。

这一节的结论是:3D Texture 的核心价值是用可过滤资源表达空间函数。云和雾的画质取决于三维坐标稳定性、密度范围、ray marching 步长、LOD 选择和 temporal 策略之间的平衡。

46.4 高效纹理采样策略

高效纹理采样的目标是用更少的无效读取得到同样稳定的视觉结果。纹理采样成本来自多个层面:采样次数、纹理格式、mip 访问、cache locality、分支发散、sampler 状态切换、资源绑定数量和带宽。优化时先判断瓶颈属于采样次数、带宽、cache 还是绑定,再选择 atlas、array texture、sampler reuse、LOD 控制或通道打包。

雨后街区这一帧中,一个 fragment 可能读取 base color、normal、roughness、metallic、AO、decal mask、decal normal、prefiltered environment、DFG LUT 和 volume density。若不加约束,一个材质很容易变成十几次纹理读取。片元面积大、overdraw 高、透明雾层多时,纹理读取会成为主要 GPU 成本。

采样次数的预算

采样次数优化从材质路径开始。墙面静态材质可以接受 base color、normal、ORM 三张纹理;ORM 表示 occlusion、roughness、metallic 合并到一张纹理的三个通道。积水贴花只在 mask 大于零的区域采样额外 normal 和 roughness。体积雾则按屏幕分辨率和步数控制总采样次数。

一个可用的预算表如下。它作为定位成本来源的初始估算使用,具体数值要按平台、分辨率和材质复杂度复查。

路径常见采样主要风险优先证据
基础 PBR 材质3 到 5 次通道过多、normal 成本shader 纹理读取数
Triplanar 材质每张纹理 3 次采样数三倍增长材质变体对比
贴花1 到 4 次overdraw、分支、G-buffer 写入decal 覆盖热力图
IBL2 到 4 次Cubemap mip、DFG LUTroughness 分档输出
体积雾步数乘以密度读取屏幕覆盖和循环步数 heatmap

表中的“优先证据”对应工具观察入口。RenderDoc 可以检查 draw call 的绑定纹理、shader 反编译和采样指令;Nsight、Radeon GPU Profiler 或 Xcode GPU tools 可以观察纹理单元、缓存、带宽和 shader stall。工具名称本身不给结论,结论来自“某个 pass 的采样次数和带宽是否与帧耗时变化同步”。

Cache locality 与资源布局

Texture cache 偏向相邻 fragment 读取相近地址。普通 UV 连续、mip 合理、纹理平铺稳定时,cache 命中率较好。随机索引、屏幕空间噪声扰动、巨型 atlas 中跳跃读取、不同材质频繁切换会降低局部性。Triplanar 的三轴采样会读取三组坐标,cache 压力通常高于单一 UV。

Texture atlas 把多张小纹理放进一张大纹理,减少绑定变化并提高批处理机会。它的代价是 atlas padding、mip bleeding 和压缩块对齐。Array texture 把同尺寸同格式的多张纹理放在不同 layer,坐标仍是二维加 layer index。它通常比 atlas 更容易控制 mip 边界,但要求资源尺寸和格式统一。Direct3D 文档中也强调 texture array 的各层格式和尺寸保持一致,这一约束会影响资产管线设计。

Bindless 或 descriptor indexing 可以降低绑定频率,让 shader 通过索引访问大量纹理。它适合材质数量巨大、draw 合批需求强的场景。它的风险是索引分散导致 cache locality 下降,且不同平台对 descriptor 数量、动态索引和 residency 的支持不同。工程实现应把材质排序、纹理数组、bindless 策略和 fallback path 放在同一套能力查询里管理。

Sampler reuse 与 LOD 控制

Sampler state 包含过滤模式、地址模式、各向异性、LOD clamp 和比较采样等信息。多个纹理使用相同过滤和寻址规则时,可以复用 sampler。复用 sampler 能降低状态对象数量,也能让材质系统更容易生成稳定的 pipeline key。

LOD 控制用于匹配采样频率。常规二维纹理优先使用自动 LOD;强烈坐标扭曲、手写导数、parallax mapping、ray marching 和预过滤环境图通常使用 textureGradtextureLod。显式 LOD 的判断依据是:shader 是否改变了硬件导数无法追踪的采样频率,或者采样含义是否本身绑定到某个 mip,例如 roughness 到 prefiltered cubemap mip。

各向异性过滤适合斜视角表面,例如湿路面和长走廊地砖。它提升远处斜面纹理清晰度,同时增加采样成本。开启它之前,应先确认纹理 mip 正确、法线不过度高频、roughness 不因压缩失真。否则各向异性过滤会让高频 aliasing 更明显。

采样优化的可复用顺序

采样优化按五步推进。第一步统计采样次数:按 pass、材质和 shader variant 列出二维纹理、Cubemap、3D Texture、LUT 的读取数量。第二步确定覆盖面积:同样的 shader 在全屏 pass、透明雾层和小物体上的成本完全不同。第三步检查 mip 和格式:高分辨率、未压缩 HDR、normal map 和 3D Texture 都可能成为带宽热点。第四步调整布局:优先通道打包、array texture、atlas padding、sampler reuse 和材质排序。第五步复查画面:用固定相机、固定光照和局部放大比较 aliasing、模糊、seam 和材质响应。

这一节建立的工程结论是:纹理采样优化要从“哪条资源路径在多少 fragment 上读取了多少数据”开始。单独减少一条 shader 指令意义有限;把采样次数、覆盖面积、格式带宽和 cache locality 放在一起,才能判断优化是否真正减少 GPU 压力。

46.5 纹理与 PBR 管线融合技巧

PBR 管线中的纹理不再只是颜色图。它们分别提供 base color、normal、metallic、roughness、ambient occlusion、emissive、height、clear coat、anisotropy 等材质参数。纹理采样的输出会进入 BRDF、IBL、阴影、后处理和 tone mapping,所以每个通道都必须有明确语义、颜色空间和混合规则。

雨后金属街区的墙面材质包含混凝土 base color、normal、roughness 和 AO。金属管线使用 metallic 和 roughness 控制高光。积水贴花降低 roughness、改变 normal,并可能加深 base color。Cubemap 提供 IBL,roughness 控制预过滤 mip。体积雾在 PBR 着色之后或合成阶段加入,影响最终颜色和对比度。

纹理通道的材质语义

Base color 通常使用 sRGB 采样并转换到线性空间参与光照。Roughness、metallic、AO、height、mask 属于数据纹理,应在线性空间读取。Normal map 存储的是向量编码,读取后要从 0 到 1 解码到 -1 到 1,并根据 tangent space 或 world space 进入光照计算。

常见的 ORM 打包把 AO 放 R,roughness 放 G,metallic 放 B。这样做能减少采样次数,也会把三个通道绑定到同一分辨率、同一压缩格式和同一 mip 行为。对于粗糙度和金属度相对稳定的材质,这是合理选择;对于 AO 分辨率需求不同、mask 需要硬边或 roughness 极度敏感的材质,可以拆出独立纹理或使用更合适的格式。

struct MaterialParams {
vec3 baseColor;
vec3 normalWorld;
float occlusion;
float roughness;
float metallic;
};

MaterialParams samplePbrMaterial(
sampler2D baseColorMap,
sampler2D normalMap,
sampler2D ormMap,
vec2 uv,
mat3 tangentToWorld
) {
MaterialParams m;
m.baseColor = pow(texture(baseColorMap, uv).rgb, vec3(2.2));

vec3 normalTs = texture(normalMap, uv).xyz * 2.0 - 1.0;
m.normalWorld = normalize(tangentToWorld * normalTs);

vec3 orm = texture(ormMap, uv).rgb;
m.occlusion = orm.r;
m.roughness = clamp(orm.g, 0.04, 1.0);
m.metallic = clamp(orm.b, 0.0, 1.0);
return m;
}

这段代码把纹理通道映射到材质参数。它的检查点是颜色空间、normal 解码、roughness 下限和 metallic 范围。若 base color 在线性空间纹理上再次做 gamma 转换,材质会偏暗;若 roughness 从 sRGB 纹理读取,反射会出现非预期的对比变化;若 normal map 以压缩 base color 的格式存储,法线方向会产生块状伪影。

BRDF、IBL 与纹理参数的耦合

PBR 的核心是材质参数和光照模型分离。纹理提供参数,BRDF 负责根据法线、视线、光线、roughness 和 metallic 计算反射。IBL 使用 Cubemap 表达来自环境的光。Filament 文档的 IBL 路径说明,环境图需要预处理,并和 BRDF 近似组合,才能在实时 shader 中得到可承受的成本。

在 shader 中,roughness 同时影响直射高光和环境反射 mip。Metallic 影响 diffuse 与 specular 的能量分配。AO 通常只调制间接光或环境项,直接把 AO 乘到所有光照上会让明亮直射区域也被压暗。Normal map 改变 nn,因此同时影响 direct lighting、IBL reflection direction、Fresnel 角度和 shadow bias 的视觉感受。

积水贴花是观察耦合关系的好例子。它降低 roughness 后,预过滤 Cubemap 会读取更清晰的 mip,路面出现更锐利的霓虹反射;它修改 normal 后,反射方向会产生波纹;它改变 base color 后,水膜区域颜色会变深;它如果错误提高 metallic,混凝土地面会出现金属感。一个贴花参数可能同时影响多个光照项,所以调试时要逐通道关闭。

材质混合与能量边界

PBR 材质混合不能简单把最终颜色混合后再进入光照。更稳定的做法是在材质参数层混合:base color、normal、roughness、metallic、AO 先根据 mask 组合,再交给同一套 BRDF。这样直射光、IBL、阴影和 tone mapping 都能看到一致材质参数。

Normal 混合要重新归一化,并尽量在同一空间完成。Roughness 混合要考虑感知 roughness 和物理 roughness 的映射,许多引擎内部会把 perceptual roughness 平方后进入微表面模型。Metallic 通常适合二值或接近二值的资产语义,中间值多用于边缘混合或扫描资产的过渡,不宜成为大量随意灰值。

贴花叠加到 G-buffer 时,应明确写入顺序。先写基础材质,再按 decal mask 修改局部参数,最后 lighting pass 使用修改后的材质。若多个 decal 重叠,需要定义排序、混合公式和通道优先级。水痕、泥渍、弹孔和标记图案对通道的需求不同,材质系统应允许 decal 只写它负责的通道。

PBR 纹理问题的排查顺序

PBR 纹理排查按材质通道逐层展开。第一步显示 base color 的线性值,确认颜色空间和曝光。第二步显示 normalWorld,确认 tangent basis、法线贴图格式和接缝。第三步显示 roughness、metallic、AO 灰度图,确认通道打包和读取空间。第四步固定 light 和 environment,观察 roughness 分档下的高光和 IBL mip。第五步逐个开启 decal、triplanar、probe 和 volume,确认每一路纹理技术对材质参数的影响范围。

这一路径把视觉现象和数据绑定起来。高光过锐通常先看 roughness;反射方向跳变先看 normal 和 tangent;金属边缘发黑先看 metallic、base color 和 IBL;AO 让直射面变脏先看 AO 应用位置;贴花出现塑料感先看 roughness 和 normal 的混合。

本章最终结论是:高级纹理技术的统一判断入口是“采样结果进入哪里”。进入 base color 的纹理影响颜色,进入 normal 的纹理影响方向,进入 roughness 的纹理影响高光宽度,进入 Cubemap 的方向影响环境反射,进入 3D Texture 的坐标影响空间介质。把坐标、资源、采样和材质语义分开检查,就能把复杂纹理效果拆成可定位、可复现、可优化的工程路径。

最小自检任务

给定一个单帧问题:雨后街区场景中,混凝土墙的三轴纹理在墙角过渡处出现高光方向突变,路面积水反射过于清晰,远处体积雾在相机平移时产生滑动感。请按本章的方法判断这三个现象分别属于哪条纹理路径,并给出排查顺序。允许使用一个简化 shader 或帧调试工具观察纹理绑定,但答案必须基于坐标、资源、采样和材质语义分析。

答案要点

墙角高光方向突变属于 triplanar normal 路径。先显示 triplanar 混合权重,再把三个投影方向采样到的 normal 解码到同一 world space,随后混合并归一化。若直接混合 tangent-space normal,就会把不同投影平面的方向基混在一起,高光会在墙角跳变。

路面积水反射过于清晰属于贴花修改 PBR 参数和 Cubemap IBL 的耦合路径。先显示 decal mask,确认水痕只覆盖目标区域;再显示 roughness,确认贴花是否把 roughness 压得过低;随后固定 prefiltered environment 的 LOD,检查 roughness 到 mip 的映射。若 roughness 通道在线性/非线性空间读取错误,积水会读取过清晰的环境 mip。

远处体积雾滑动属于 3D Texture 坐标路径。先输出 density 或体积坐标为灰度/颜色,确认采样坐标来自 world position 与 volume transform;再关闭 temporal filtering,排除 history 残影;随后检查 worldToVolume、噪声 time offset 和相机矩阵。若体积坐标来自 view space,雾纹理会随相机平移而滑动。

整体排查顺序是:先定位坐标来源,再显示中间纹理数据,随后固定光照和环境,最后逐个恢复混合路径。三个问题分别落在 triplanar normal 空间、PBR roughness 到 Cubemap mip、3D Texture world-space density 三条路径上。

本章知识点总结

  • 坐标优先:高级纹理问题先检查采样坐标来源,再检查资源格式、采样状态和材质语义。
  • UV 变换:UV scale、offset 和 rotation 改变二维采样位置,也会影响 mip 选择和接缝表现。
  • 三轴映射:Triplanar 用三次平面采样换取大型几何表面的坐标稳定性,成本主要体现在采样次数。
  • 法线空间:Triplanar normal 要转换到同一空间后混合,否则墙角和坡面高光会产生方向跳变。
  • 贴花投影:Decal projection 通过局部投影盒写入材质通道,排查时要确认坐标空间、mask 和写入通道。
  • 方向纹理:Cubemap 使用三维方向向量采样,skybox、反射和 IBL 都依赖一致的方向空间。
  • 环境预过滤:Roughness 通常映射到 prefiltered cubemap mip,错误的 roughness 通道会直接改变反射清晰度。
  • 体积资源:3D Texture 表达空间密度或噪声,云雾效果依赖稳定 world-space 坐标和合理步长。
  • 采样预算:纹理成本要结合采样次数、覆盖面积、格式带宽、mip 和 cache locality 判断。
  • 资源布局:Atlas、array texture、bindless 和 sampler reuse 都服务于绑定成本、批处理和 cache 行为的平衡。
  • PBR 通道:Base color、normal、roughness、metallic 和 AO 各自进入不同光照路径,颜色空间和数据语义必须分清。
  • 材质混合:贴花和材质层优先在参数层混合,再交给统一 BRDF,画面响应更容易预测和调试。