
1. 项目概述从“看懂”到“用活”高级纹理很多刚开始接触Shader编程的朋友可能都经历过这样一个阶段跟着教程把漫反射、高光这些基础光照模型跑通了感觉已经“入门”了。但一旦打开那些商业游戏或者引擎里的复杂材质看到动辄十几行的纹理采样和眼花缭乱的混合操作瞬间又被打回原形感觉之前学的都是“玩具”。这个“Chapter10 高级纹理”的学习笔记就是专门为了捅破这层窗户纸而准备的。它不是一个简单的API罗列而是试图帮你构建一套关于纹理的“工具箱”思维——理解每一种高级纹理技术背后的设计意图、适用场景以及它们是如何组合起来去模拟真实世界中那些复杂而迷人的视觉现象的。我自己在项目里踩过不少坑。比如早期做水面只知道用一张法线贴图去扰动结果远看还行近看就是一潭毫无细节的死水又比如想做雪地足迹吭哧吭哧写顶点偏移性能开销巨大且效果生硬。直到系统性地梳理了凹凸映射、渐变纹理、遮罩纹理这些高级纹理技术才明白原来很多看似高级的效果其底层都是几种基础技术的巧妙组合与参数控制。这个笔记的核心就是要带你越过“单个技术点”的层面去掌握如何像搭积木一样灵活运用这些纹理技术来解决实际的美术需求真正让你写的Shader从“能跑”变成“好用”、“好看”。2. 核心纹理技术矩阵不止于“贴图”当我们谈论“高级纹理”时我们指的远不止是那张.png或.tga图片文件。它是一种数据载体和算法媒介。理解这一点至关重要否则你很容易陷入“为用而用”的误区。下面我们来拆解这个技术矩阵里的几个核心成员。2.1 法线贴图与凹凸映射欺骗视觉的“魔法”法线贴图大概是游戏图形学中最著名也最被误解的技术之一。很多人以为它的作用就是“让平面看起来有凹凸”这没错但太笼统了。它的本质是通过改变模型表面每一点的法线方向来欺骗光照计算从而在不增加几何顶点的情况下模拟出复杂的表面细节。这里有一个关键区别法线贴图 vs. 高度贴图。法线贴图存储的是法线向量RGB对应XYZ直接参与光照方程如兰伯特、Blinn-Phong中的点乘计算。而高度贴图或称凹凸贴图、置换贴图存储的是灰度高度信息它需要通过额外的计算比如在像素着色器中采样相邻纹素计算梯度来“推导”出法线的变化。在移动端或性能敏感的场景我们几乎只用法线贴图因为它的计算开销更小。高度贴图则更多用于需要精确轮廓剪影如砖墙边缘或配合曲面细分的场合。在Shader中实现法线贴图有几个必须注意的“坑”切线空间 vs. 模型空间引擎中使用的法线贴图几乎都是切线空间下的。这意味着你需要构建一个从切线空间到世界空间或观察空间的变换矩阵即TBN矩阵Tangent, Bitangent, Normal。在顶点着色器中计算好TBN矩阵并传递给片元着色器是标准做法。纹理采样与解码法线贴图通常经过压缩采样的RGB值范围是[0,1]而法线分量范围是[-1,1]。所以你需要一个解码操作normal tex2D(_BumpMap, uv).xyz * 2.0 - 1.0。强度控制一个好的材质应该允许美术通过一个_BumpScale参数来控制凹凸感的强弱。这通常在解码后对xy分量切线空间下的法线扰动方向进行缩放而保持z分量不变并重新归一化。注意千万不要在片元着色器里直接对解码后的法线做normalize除非你确信它没有被缩放。正确的做法是只对缩放后的xy分量和原有的z分量进行归一化或者更高效地在缩放前就确保采样得到的切线空间法线是单位向量。2.2 渐变纹理与遮罩纹理控制的艺术如果说法线贴图是“造型师”那么渐变纹理和遮罩纹理就是“化妆师”和“导演”它们不直接参与核心光照计算而是用来控制和丰富视觉效果。渐变纹理通常是一张一维或二维的渐变条。它的经典应用是卡通渲染中的色调分离。比如我们计算兰伯特漫反射项diffuse dot(normal, lightDir)得到一个范围在[0,1]的值。传统光照模型会直接用这个值乘以光颜色和材质颜色。但在卡通渲染中我们希望只有有限的几个色阶。这时我们可以用diffuse作为UV的u坐标去采样一张预设的渐变纹理比如从暗到明只有3-4个阶跃的色带用采样得到的颜色来代替原始的diffuse计算。这样平滑的光照过渡就被转化为清晰的色块实现了卡通效果。遮罩纹理的作用是“分区治理”。一张RGBA四个通道的纹理可以分别控制四种不同效果的强度。例如R通道控制高光反射的强度。可以让金属部分高光强烈而皮革部分高光柔和。G通道控制环境光遮蔽的细节强度。B通道控制自发光或边缘光的区域。A通道控制透明度混合的区域如树叶、网格。在Shader中你只需要采样一次遮罩纹理然后分别用它的各个通道值去调制对应的效果参数。这比准备多张单通道纹理并多次采样要高效得多。这种技术极大地提升了材质的表现力和美术的控制粒度是制作高质量PBR材质的必备手段。2.3 立方体纹理与渲染纹理环境与动态这两类纹理代表了纹理技术从“静态数据”走向“动态环境”的飞跃。立方体纹理最常见的应用就是天空盒和环境映射。对于反射效果我们不再需要复杂的光线追踪只需要在片元着色器中根据视角方向viewDir和表面法线normal计算出反射方向reflectDir然后用这个方向向量去采样立方体纹理就能得到逼真的环境反射。折射效果同理使用折射方向refractDir进行采样。这里的关键在于立方体纹理的采样坐标是一个三维方向向量而不是二维的UV。渲染纹理则更为强大它意味着Shader可以读取由摄像机实时渲染的画面。这开启了无限的可能性镜子/水面反射在角色或场景下方放置一个摄像机将其渲染目标设为一张渲染纹理然后在镜子或水面的Shader中采样这张纹理并做适当的扭曲用水流法线贴图扰动UV。安全监控画面在游戏内的电视屏幕上显示另一个区域的实时画面。高级后期效果如景深、运动模糊需要上一帧的画面数据渲染纹理是唯一的实现途径。使用渲染纹理的性能开销需要谨慎评估因为它意味着多一次或多次的完整场景渲染。在移动端通常会严格控制使用渲染纹理的摄像机的渲染层级和分辨率。3. 实战构建一个多纹理混合的复杂材质理解了单个技术我们来看如何将它们组合起来。假设我们要实现一个带有积雪效果、湿滑反射且边缘磨损的岩石材质。这个材质将综合运用我们上面提到的多项技术。3.1 材质设计与纹理准备首先我们需要美术提供一组纹理基础颜色纹理岩石的固有色。法线纹理岩石表面的凹凸细节。遮罩纹理R通道高光遮罩。岩石凸起部分高光强缝隙处弱。G通道积雪区域遮罩。标识出岩石顶部、平坦处等容易积雪的区域。B通道湿度遮罩。标识出靠近地面、背阴处等容易潮湿的区域。A通道边缘磨损遮罩。标识出岩石尖锐的边缘。积雪颜色纹理可以是一张纯色或带有细微噪点的纹理。环境立方体纹理用于反射。3.2 Shader实现步骤拆解在片元着色器中我们将按以下流程进行计算第一步采样与解码fixed4 albedo tex2D(_MainTex, uv); fixed3 normal UnpackNormal(tex2D(_BumpMap, uv)); // 解码切线空间法线 fixed4 mask tex2D(_MaskMap, uv);UnpackNormal是Unity内置函数负责将压缩的法线贴图数据解码并归一化。第二步积雪效果混合积雪效果的核心是根据表面法线与世界空间“向上”方向的夹角来决定积雪的覆盖程度。// 将法线从切线空间转换到世界空间 float3 worldNormal normalize(mul(normal, (float3x3)unity_WorldToObject)); float snowFactor dot(worldNormal, float3(0, 1, 0)); // 点积越大表面越“平” snowFactor saturate((snowFactor - _SnowThreshold) * _SnowHardness); // 应用阈值和硬度 snowFactor * mask.g; // 使用遮罩G通道进一步控制积雪区域 // 混合基础色和雪色 float3 finalAlbedo lerp(albedo.rgb, _SnowColor.rgb, snowFactor);这里_SnowThreshold控制开始积雪的角度例如0.7_SnowHardness控制混合边缘的软硬程度。第三步高光与反射计算高光采用Blinn-Phong模型并用遮罩R通道控制强度。反射则采用立方体纹理采样并用湿度遮罩B通道和积雪因子共同控制强度雪面和湿面反射更强。// 高光 float3 halfDir normalize(lightDir viewDir); float specular pow(max(0, dot(worldNormal, halfDir)), _Gloss) * _SpecularIntensity; specular * mask.r; // 高光遮罩 // 反射 float3 reflectDir reflect(-viewDir, worldNormal); float3 reflection texCUBE(_Cubemap, reflectDir).rgb; float reflectIntensity (mask.b * 0.7 snowFactor * 0.3) * _ReflectIntensity; // 混合湿度和积雪因子第四步边缘磨损使用遮罩A通道在边缘处混合一个更暗、饱和度更低的颜色模拟磨损和污渍。float3 wornColor albedo.rgb * _WornDarken; // 变暗 wornColor lerp(wornColor, fixed3(0.5,0.5,0.5), _WornDesaturate); // 降低饱和度 finalAlbedo lerp(finalAlbedo, wornColor, mask.a);第五步最终合成将漫反射、高光、反射等所有光照分量叠加输出最终颜色。float3 diffuse finalAlbedo * _LightColor0.rgb * max(0, dot(worldNormal, lightDir)); float3 finalColor diffuse specular reflection * reflectIntensity; return fixed4(finalColor, 1.0);这个例子展示了如何将多种纹理和逻辑有条不紊地编织在一起。关键在于每个效果都有独立的控制因子遮罩通道、物理计算因子并且混合顺序符合物理直觉例如先混合基底颜色再计算基于新表面的光照。4. 性能优化与常见问题排查高级纹理用得好是画龙点睛用不好就是性能灾难。尤其在移动平台纹理采样和复杂计算是主要的性能瓶颈。4.1 性能优化核心策略纹理压缩与Mipmap对于颜色贴图使用ASTC或ETC2压缩格式能极大减少内存带宽和体积。法线贴图切记不要使用有损的颜色压缩格式如JPEG或ETC2的RGB模式这会严重破坏法线向量的精度导致凹凸感出现色块或条纹。应该使用专门的法线贴图压缩格式如Unity中的“Normal map”导入设置它会使用DXT5nm等格式或至少保证RGBA通道的完整性。务必开启Mipmap。这不仅能在远处减少摩尔纹更重要的是能提升缓存命中率。当像素在屏幕上覆盖的纹素很小时GPU会自动采样更低级别的Mip这比采样高级别大图要快得多。减少采样次数与计算合并纹理如上文的遮罩纹理将四个单通道遮罩合并到一张RGBA纹理中采样次数从4次降为1次。利用纹理通道有时可以将高度信息A通道和粗糙度信息G通道打包到同一张纹理避免额外采样。简化计算在移动端考虑用half或fixed精度代替float。对于非核心的计算如边缘光可以放到顶点着色器中进行计算虽然精度下降但性能提升显著。Shader LOD与变体管理使用#pragma multi_compile或shader_feature为不同质量的设备生成不同的Shader变体。例如低配设备关闭反射和复杂积雪计算。利用LOD指令当摄像机距离物体超过一定范围时自动切换到更简化的Shader版本。4.2 常见问题与排查技巧问题一法线贴图导致接缝或光照断裂排查这几乎100%是切线空间计算错误导致的。首先检查模型导入设置是否生成了正确的切线Tangents。然后在Shader中确保在顶点着色器正确计算了TANGENT_SPACE_ROTATIONUnity旧版或使用UnityObjectToWorldNormal和UnityObjectToWorldDirUnity新版来构建TBN矩阵。一个快速的调试方法是在片元着色器中直接输出切线空间法线的rgb值作为颜色观察是否在UV接缝处有突兀的跳变。问题二遮罩纹理控制效果“不听话”排查首先在Photoshop等软件中单独查看遮罩纹理的各个通道确认黑白区域是否符合设计预期。其次在Shader中可以临时将mask.r、mask.g等直接输出为颜色在游戏视图中查看它是否被正确地“贴”在了模型对应的部位。常见错误是UV映射错误或者纹理导入时被错误地转换为sRGB遮罩纹理通常应保持线性空间。问题三反射效果模糊或扭曲排查立方体纹理分辨率用于反射的立方体纹理分辨率不能太低否则会非常模糊。Mipmap级别反射采样时可以通过texCUBElod函数手动指定Mipmap级别粗糙度越高的表面应该采样越高级别的Mip即更模糊的图。反射向量计算确保viewDir和normal是在同一坐标系通常是世界空间下并且都已归一化。一个常见的错误是在计算反射后没有对反射向量进行归一化就直接用于采样虽然texCUBE内部可能会处理但显式归一化是好习惯。问题四性能Profiler显示GPU耗时过高排查使用渲染调试工具如Unity的Frame Debugger或RenderDoc抓取一帧。查看Draw Call数量是否因材质变体过多而暴涨。查看该材质Shader的耗时重点关注片元着色器。如果耗时集中在某个复杂计算如循环、多次采样考虑能否用查找表纹理来简化。检查纹理带宽看是否有大量未压缩或分辨率过高的纹理。5. 从学习到创作构建你的纹理工具箱思维学完这些技术不应该只是记住了一堆tex2D和lerp的写法。更重要的是培养一种“工具箱”思维。当美术同事提出“我想要一个生锈的、部分潮湿的金属表面”这样的需求时你能立刻在脑海中拆解基底一张金属基础色法线贴图。生锈需要一张生锈的颜色纹理和法线纹理。使用一张遮罩纹理的R通道来控制锈蚀区域可能是基于高度贴图生成的凸起部分先锈。混合方式可能是lerp也可能是更复杂的叠加模式。潮湿潮湿区域反射增强。可以用另一张遮罩纹理的G通道或基于世界空间位置/法线朝向计算来标识。在潮湿区域降低漫反射权重增加来自立方体纹理的反射贡献并可能轻微扰动法线模拟水膜不平。性能评估这个材质需要几次采样能否合并纹理是否需要在移动端关闭潮湿的反射效果这种思维模式让你从被动的技术实现者转变为主动的问题解决者和效果设计师。高级纹理的学习终点不是Shader代码本身而是你如何利用这些代码作为画笔去高效、可控地绘制出你想要的视觉世界。我个人的习惯是为每一种常用的混合模式覆盖、叠加、滤色等和效果积雪、湿润、边缘磨损编写一个可复用的函数并整理成一个自定义的Shader库。这样在新项目或新需求到来时我就能像搭积木一样快速组合出新的材质把更多精力放在艺术效果的调试上而不是底层代码的反复编写上。