1. 从美术到TA我的Shader学习破冰之旅最近在整理硬盘翻到了几年前跟着庄懂老师的技术美术入门课做的学习笔记特别是第14讲和第16讲。当时作为一个纯美术出身连代码都看得发怵的人这两节课可以说是我理解Shader这头“怪兽”的关键转折点。网上搜“技术美术”出来的全是“Unity Shader”、“SD贴图”、“法线贴图连接”这些让人望而生畏的词感觉门槛高得吓人。其实回过头看从美术思维过渡到TA思维核心不是让你立刻变成程序员而是搭建一座理解的桥梁。庄懂老师的课好就好在他用美术师能听懂的语言——颜色、质感、光影——去拆解那些复杂的数学公式和代码逻辑。今天我就结合当年的笔记和后来踩过的无数坑把这两节课里最核心、对美术师最友好的部分重新梳理一遍希望能给同样想入门TA的朋友们一点实实在在的参考。2. 第14讲核心理解“光”与“色”在Shader中的对话这一讲是基础中的基础目标是让你摆脱在材质球上盲目滑动参数的状态真正理解你调的每一个值在Shader世界里到底对应着什么。2.1 从“感觉”到“数据”漫反射与高光的本质我们美术做渲染常说“这里暗部不够透”、“高光太死了”。在Shader里这些感觉被拆解成了精确的数学模型。庄懂老师用了一个非常形象的比喻把物体表面想象成无数个朝向不同的小镜子微表面。漫反射Diffuse就是光线打在这些小镜子上被弹得到处都是最终均匀地混合在一起进入我们眼睛。所以漫反射光的颜色主要取决于物体表面的固有色Albedo和光源的颜色它看起来柔和、均匀。在Shader中最经典的模型是兰伯特Lambert计算起来就是表面法线Normal和光线方向LightDir的点积。点积结果越大越接近1说明法线越朝向光源这里就越亮。注意很多新手会混淆觉得漫反射强度调的是灯光亮度。其实在标准PBR工作流里你材质球的“Base Color”或“Albedo”贴图直接影响的就是漫反射的“质”而灯光强度影响的才是“量”。你用一个灰色的Base Color灯光再强也反射不出彩色光。高光反射Specular则是另一回事。它是那些恰好把光线直接反射到你眼睛里的“小镜子”的贡献。所以高光更依赖于你的观察角度View Dir。最经典的模型是冯氏Phong或Blinn-Phong计算涉及光线方向、观察方向和法线算的是反射光方向与观察方向的接近程度。高光通常很亮颜色更接近光源色比如白色的太阳光产生白色的高光并且金属材质的高光会带有其固有色。这里的一个实操心得是在Unity Standard Shader里当你把“Metallic”滑块拖到1纯金属时你会发现“Base Color”贴图其实控制的是高光颜色和强度而漫反射部分几乎变黑了。这是因为真实世界中纯金属是几乎不产生漫反射的光线要么被吸收要么被镜面反射。这个现象一下子就把PBR基于物理的渲染里“金属度”参数的意义讲活了。2.2 法线贴图欺骗光影的“浮雕术”这是我们美术最熟悉也最依赖的技术之一。但以前我只知道法线贴图能让低模看起来有高模的细节却不知道它具体是怎么“骗”过光影计算的。一张法线贴图Normal Map的本质是一张存储了每个像素点“表面朝向”即法线方向的图片。RGB三个通道分别对应了法线向量的X, Y, Z分量。在Shader采样这张贴图后会得到一个“被修改过的”法线向量然后用这个新向量去代替模型原本的平滑的法线参与我们前面说的漫反射、高光等所有光照计算。为什么是蓝色的默认的切线空间法线贴图整体偏蓝是因为其默认的法线方向是(0, 0, 1)即垂直于模型表面并指向外部。这个向量转换到RGB颜色范围0-1就是(0.5, 0.5, 1.0)呈现为淡蓝色。当表面有凹凸时法线的X、Y分量才会发生变化在贴图上体现为红、绿色的偏移。连接法线贴图时最容易踩的坑贴图类型设置错误在Unity中导入法线贴图后必须在Import Settings里将“Texture Type”设置为“Normal map”并勾选“Create from Grayscale”如果是用高度图转换的话。这个操作会让Unity对贴图进行正确的Gamma空间处理sRGB关和压缩格式优化。如果不设置Shader会把它当普通彩色图来读结果就是颜色奇怪凹凸效果完全不对。切线空间 vs 对象空间美术软件如SP、SD导出的通常是切线空间法线贴图适用性最广模型可以任意旋转变形。对象空间法线贴图是另一种它记录的法线是基于模型初始坐标的模型一动效果就错了但有时用于特定效果如三平面投影。99%的情况我们用切线空间的。强度不对有时法线效果太强或太弱。除了在Shader里乘上一个强度系数外更根本的方法是回到底层软件去调整烘焙或生成时的强度。在Substance Designer/Painter里生成法线时那个“强度”或“对比度”参数非常关键。3. 第16讲核心Shader中的纹理混合艺术如果说第14讲是理解原理那第16讲就是动手实践把多张贴图的信息有机混合起来形成丰富的表面细节。这是技术美术发挥创造力的核心环节。3.1 混合的基础加减乘除与插值Shader中的纹理混合远不止在PS里用图层模式那么简单。它是在像素级别进行的数学运算。庄懂老师当时强调要像理解调色原理一样理解这几个基本操作乘法Multiply最常用。用于颜色Albedo之间的混合或者用一张灰度图如污渍遮罩去调制另一张颜色图的亮度。A * B结果永远不会比原图更亮。常用于制作暗部、脏迹、颜色变化。加法Add让东西变亮。常用于自发光Emission效果或者模拟一些光晕、叠加效果。A B容易过曝值超过1.0变成纯白需要谨慎控制强度。插值Lerp / Linear Interpolation这是纹理混合的瑞士军刀。函数是lerp(A, B, T)T是一个0到1的系数。当T为0时输出AT为1时输出BT为0.5时输出A和B的中点。这个T通常就是我们的遮罩贴图Mask。比如用一张灰度图作为T在岩石A和苔藓B两种材质之间进行混合黑色部分显示岩石白色部分显示苔藓灰色部分则是过渡。一个高级技巧是遮罩贴图不一定非要用软件画出来。我们可以利用模型固有的信息来生成。比如用世界空间或物体空间的Y轴坐标作为遮罩实现物体底部积雪、顶部干净的效果顶点着色器传递的坐标数据参与计算或者用一张简单的噪声图Noise配合三角函数的运算来生成自然随机的混合遮罩这比手绘的遮罩看起来更有机。3.2 实战构建一个基础的PBR材质Shader我们用一个具体的例子把前面学的串起来。目标是做一个简单的、支持基础PBR贴图的Shader不是用Unity Standard Shader而是自己写一个简化版来理解。1. 输入准备我们需要准备几张图Albedo固有色贴图。MetallicSmoothnessR通道存金属度MetallicA通道存光滑度Smoothness。这是一种常见的打包方式节省纹理采样次数。Normal法线贴图。AO环境光遮蔽贴图用于模拟缝隙、角落的阴影增加体积感。2. Shader结构拆解在Unity的Surface Shader框架下这对新手最友好我们主要工作在surf函数里。// 首先定义输入结构从顶点着色器获取必要信息 struct Input { float2 uv_MainTex; // 主UV坐标 // 如果用法线贴图需要声明切线空间信息 INTERNAL_DATA }; // 表面着色器函数 void surf (Input IN, inout SurfaceOutputStandard o) { // 1. 采样Albedo贴图 fixed4 albedoColor tex2D(_MainTex, IN.uv_MainTex); o.Albedo albedoColor.rgb; // 2. 采样并解包MetallicSmoothness贴图 fixed4 metallicSmoothness tex2D(_MetallicSmoothnessTex, IN.uv_MainTex); o.Metallic metallicSmoothness.r; // R通道是金属度 o.Smoothness metallicSmoothness.a; // A通道是光滑度 // 3. 采样并应用法线贴图 // UnpackNormal是Unity内置函数负责将压缩的法线贴图纹理数据转换为法线向量 o.Normal UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex)); // 4. 采样AO贴图通常只影响环境光部分的亮度可以乘到Albedo上或者有更复杂的处理 fixed ao tex2D(_AOTex, IN.uv_MainTex).r; o.Occlusion ao; // SurfaceOutputStandard结构体有专门的Occlusion成员 // 5. 自发光Emission这里简单采样通常需要HDR颜色 fixed3 emissionColor tex2D(_EmissionTex, IN.uv_MainTex).rgb; o.Emission emissionColor * _EmissionStrength; }3. 关键点解析与避坑UnpackNormal这个函数至关重要。因为法线贴图为了节省空间经常使用压缩格式如DXT5nm它并不是直接存储XYZ而是做了编码。UnpackNormal就是用来正确解码的。如果你自己写片元着色器忘记解包直接使用法线贴图的rgb效果会完全错误。光滑度Smoothness与粗糙度Roughness这俩是反义词。光滑度 1 - 粗糙度。不同引擎和软件习惯不同。Unity的Standard Shader用SmoothnessSubstance Designer输出贴图通常用Roughness。在混合或使用时一定要搞清楚必要时用1.0 - 采样值进行转换。AO的使用上面例子中我们把AO直接赋给了o.Occlusion。在Unity的标准光照模型中这个值会用于减弱环境光GI和反射探针的贡献这是一种物理正确的做法。更简单但不完全物理的做法是直接o.Albedo * ao让固有色变暗来模拟阴影这在移动端或追求性能的场景常用。4. 结合SDSubstance Designer的贴图创作流程学Shader如果只停留在“用”贴图那功力就减了一半。技术美术的核心能力之一是“制作”贴图。Substance DesignerSD正是为此而生。庄懂老师的课虽然没直接教SD但理解了Shader对贴图的需求后再学SD会事半功倍。4.1 理解贴图管线从灰度到信息在SD里你不是在“画”贴图而是在“构建”一个生成贴图的程序化流程Graph。这个流程的起点常常是各种灰度信息。高度图Height是基石很多效果始于一张高度图或法线图。你可以用噪声Noise、渐变Gradient、形状Shape等节点来生成基础的高度信息。这张图定义了表面的凹凸起伏。从高度衍生万物法线贴图通过“Normal”节点输入高度图SD会自动计算出对应的法线贴图。你可以控制强度。环境光遮蔽AO通过“Ambient Occlusion”节点输入高度图SD会模拟光线在缝隙处被遮挡的效果生成AO图。参数“Max Distance”最大距离非常关键它决定了计算AO时考虑多远的遮挡值太小则细节AO不明显值太大会导致整体变暗。曲率Curvature通过“Curvature”节点生成。这张图能标识出表面凸起亮和凹陷暗的区域是制作边缘磨损、污渍堆积等效果极其重要的遮罩源。用遮罩控制混合得到了AO、曲率等灰度图后把它们作为遮罩Mask用“Blend”节点混合不同的颜色或材质。例如用曲率图经过一些处理如Levels节点调整对比度作为遮罩在基础金属材质上混合锈迹材质让锈迹只出现在凹陷和边缘。4.2 SD与Shader的联动思维在SD里做材质时要时刻想着它在Shader里怎么用通道打包为了节省内存和采样次数经常需要把多张灰度图打包到一张贴图的RGBA四个通道里。比如金属度R、粗糙度G、AOB、自发光强度A可以打包成一张图在Shader里一次采样然后.r,.g,.b,.a分开使用。SD里可以用“Compose”节点轻松实现。范围意识Shader计算通常期望输入值在0-1线性空间。SD默认工作在sRGB空间为了显示好看。对于非颜色信息如金属度、粗糙度、AO在输出时务必连接“Grayscale Conversion”节点并设置为“Linear线性”以确保数据正确。否则在Shader里会得到错误的光照反馈。平铺与无缝程序化材质的优势在于无缝平铺。在SD Graph的最后一定要通过“Tile Sampler”节点预览平铺效果并用“Histogram Scan”检查色彩/值分布是否均匀避免出现明显的重复图案或接缝。5. 常见问题排查与性能意识初探自己写Shader或者调整材质时总会遇到各种奇怪的问题。这里分享几个我踩过的典型坑及其排查思路。5.1 我的材质为什么这么亮/这么暗这是最常见的问题。排查链如下检查贴图色彩空间首先确认你的Albedo贴图在Unity导入设置中sRGB (Color Texture)是勾选的。对于金属度、粗糙度、AO等非颜色数据贴图这个选项必须取消勾选即使用线性空间。如果一张存储粗糙度的灰度图被错误地当作sRGB处理它的中间灰度值如0.5在Shader里会变成约0.73经过Gamma校正导致材质异常光滑。检查灯光和场景设置确认场景中灯光的强度Intensity和模式Realtime, Mixed, Baked是否合理。检查Window Rendering Lighting Settings 中的环境光Environment Lighting强度和环境反射Environment Reflections设置。一个全黑的Skybox会导致物体缺乏环境光照显得很暗。检查Shader中的数值范围在自定义Shader中你是否对某些颜色值进行了不恰当的乘法或指数运算确保o.Albedo的值在合理的0-1范围内HDR颜色除外超出部分会被截断或产生不可预料的结果。检查后处理Post-Processing如果开启了后处理特别是Bloom泛光或Tonemapping色调映射可能会极大地改变最终屏幕输出的亮度和对比度。暂时关闭后处理栈Post Process Volume看看是否是它导致的问题。5.2 法线贴图没效果或效果奇怪贴图类型重复一遍确认导入设置里是“Normal map”。切线信息模型是否包含切线Tangents信息在模型的导入设置Model分页里确保Normals Tangents计算选项不是“None”。对于动态批处理的模型也需要切线信息。Shader中的采样和转换是否使用了UnpackNormal函数采样法线贴图时使用的UV是否正确有没有可能采样到了错误的UV集双面材质问题如果模型是双面渲染的例如树叶背面可能会因为法线方向相反而导致光照计算错误。可能需要特殊的双面光照处理。5.3 建立初步的性能意识作为美术向的TA不需要像引擎程序员那样深入但要有基本概念纹理采样次数Texture Samples是性能杀手每多采样一张纹理就对GPU多一份压力。这就是为什么我们要进行通道打包RMAO贴图。尽可能将信息合并到更少的贴图中。纹理尺寸Texture Size能用512x512就别用1024x1024面积和内存占用是平方关系。对于远处的小物体或次要细节256甚至128都可能足够。利用Unity的纹理导入设置中的“Max Size”进行限制。Shader复杂度Surface Shader虽然方便但Unity背后会为你生成很多代码。如果对性能有极致要求或者需要实现非常规效果最终可能需要手写顶点/片元着色器Vertex/Fragment Shader以精确控制每一步计算。但对于大多数常规材质Surface Shader已经足够优化。静态批处理与动态批处理在Player Settings里可以开启。静态批处理对包含复杂顶点变换如蒙皮的Shader不友好可能导致合批失败。了解这些概念有助于你和程序同学沟通性能瓶颈。回顾这两节课最大的收获不是记住了几个Shader语法或节点而是建立了一种“数据驱动”的视觉思维。以前调效果靠猜和试现在我会先问我想表现的视觉属性湿润、粗糙、陈旧对应着PBR的哪些参数光滑度、法线强度、Base Color这些参数应该由哪些贴图或算法来提供有了这个思维框架无论是学习新的Shader技术还是使用像SD这样的工具方向都清晰了很多。这条路还很长但至少第一步算是踏稳了。