ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Built-in角色Shader实战:Pass结构、卡渲技法与移动端优化

2026/10/2 7:59:55 拓冰建站 浏览量
Built-in角色Shader实战:Pass结构、卡渲技法与移动端优化 1. 为什么今天还在写Built-in角色Shader三个现实理由先说明一个背景近两年新开的Unity项目默认推荐管线几乎都是URP网上能搜到的角色Shader教程也大多基于URP Shader Graph或URP下的自定义Shader。但我在实际项目里接触到的角色渲染需求仍然有相当一部分落在Built-in Render Pipeline内置渲染管线上。写这篇文章不是劝大家退回旧管线而是把Built-in下角色Shader的常见写法、常用技法、坑点和性能底线整理一遍。很多朋友拿到的项目是2021年之前立项的或者要接的第三方SDK只支持Built-in又或是微信小游戏、Pico4这类设备上的轻量渲染方案这些场景下Built-in依然是主流。先说结论角色Shader在Built-in里并没有过时反而因为文档多、兼容性好、性能模型清楚成了很多外包和定制渲染的最佳落点。文章围绕三个问题展开角色Shader的骨架怎么写、常用技法怎么选、移动端怎么不炸性能。最后给一份能直接抄的三Pass卡渲角色Shader。1.1 老项目与新项目的迁移成本差异很多项目从立项起就绑定了Built-in不是不想换是换不起。一个中大型项目里场景光照、材质参数、第三方插件、美术生产管线都围绕Built-in搭好了强行迁移到URP意味着重写大部分Shader重新调整Lighting设置还要处理URP下各种Render Feature不兼容的问题。纯技术角度看URP确实更现代但从项目风险和时间成本看待在Built-in里是明智选择。新项目倒是可以愉快地用URP但如果你需要深度定制角色渲染比如卡通描边、色阶渐变、MatCap眼部高光、头发各向异性高光Built-in反而更直接。URP Shader Graph能拼出大部分效果可真要精确控制每个Pass的渲染顺序、Cull模式、光照衰减最终还是得写HLSL。而在Built-in里Unity帮我们把光照、阴影、雾、光照探针这些基础设施都接好了ShaderLab写起来反而更省心。1.2 商店生态和插件兼容性Asset Store里大量角色相关资源——尤其是一些老牌卡渲插件、皮肤材质包、头发Shader——都是在Built-in下发布的。这些资源拿到现在仍然能用但拿到URP下通常需要转换。转换过程中最常见的问题URP下没有内置的ForwardBase/ForwardAdd语义改用Universal Forward Pass贴图采样方式也有差异导致一批老材质渲染结果和原版完全不同。如果你买的是角色模型材质一体化资源在Built-in下打开就是原汁原味的效果省去大量调参时间。不只资源很多工具链也偏向Built-in。比如顶点色烘焙工具、平滑法线工具、贴图通道管理工具它们生成的顶点数据格式和Built-in的Shader习惯是配套的。我见过不少团队用这些工具给角色补描边用平滑法线换到URP后这些工具还能用但要重新适配Shader代码反而容易引入新问题。1.3 移动端与微信小游戏的轻量化需求移动端和小游戏平台是Built-in的另一个活跃阵地。这类项目的渲染目标是省电、不发热、运行帧率稳定。Built-in的Shader模型比较简单管线开销低一个顶点/片元Shader跑起来很容易预估性能。URP虽然也支持移动端但它默认带一堆Pass和Feature如果不开SRPBatcher去精细调优在低端机上反而可能比Built-in更吃力。微信小游戏打包到Unity里很多团队都曾遇到WebGL平台下URP支持不全的问题。反观Built-in从Unity 5时代就一直在WebGL上稳定运行渲染管线和GLES的适配相对成熟。做角色展示类小游戏时Built-in下写一个卡渲Shader跑出来的效果和帧率都比较稳。我自己的经验是在拿不准目标平台的时候Built-in是最不容易翻车的选项。2. 角色Shader的骨架Pass结构、光照模式与特性开关写角色Shader之前先理解Built-in的渲染架构。这个管线把光照处理切分成两个主要PassForwardBase和ForwardAdd。ForwardBase处理主方向光、环境光、Light Probe、阴影和雾气的计算每个物体渲染一次ForwardAdd处理额外的逐像素光源每多一个光源就多执行一次Pass。这个结构对角色Shader意味着什么主光在Base Pass里搞定轮廓光、补光、点光源这类附加光在Add Pass里叠加。如果不小心漏写了ForwardAdd角色在场景里就会只受主光和环境影响加个点光源完全没反应。2.1 ForwardBase / ForwardAdd 的分工逻辑Base Pass里要做的事很多计算主光的漫反射、高光、阴影、环境光、光照探针、雾、还有你可能自定义的Ramp采样。按照Unity默认的Standard Shader做法Base Pass还会把物体渲染到ShadowMap的接收列表中。Add Pass则要考虑光的衰减、多光源累加、避免高光过曝。写角色Shader时我一般这样切分Base Pass处理主光的NdotL、自定义高光、Ramp色阶再加上环境光项。Add Pass只做附加光的Lambert和Blinn-Phong这样附加光不参与阴影计算视觉上是叠加了一层照明不会把角色切得太乱。这个方案在二次元卡渲里挺常用——主光形成明暗面附加光点缀边缘和背光整体不会糊成一团。补一点如果你不写Add Pass物体在Built-in渲染模式下遇到多个实时光源时Unity会自动回退到逐顶点光照。效果是能看但光源多时顶点插值会让角色脸上出现色块。角色模型顶点数一般够密逐顶点光照还勉强能接受不过为了稳定性还是建议把Add Pass写出来。2.2 顶点/片元的渲染主循环讲完Pass划分说一下CGPROGRAM内部的骨架。一个标准角色Shader从appdata结构开始顶点位置、法线、切线、UV、顶点色。顶点着色器里把顶点从模型空间变换到裁剪空间将法线变换到世界空间或视空间传递UV和顶点色给片元着色器。片元着色器里做光照计算。以最简单的材质为例struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldNormal : TEXCOORD1; }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv v.uv; o.worldNormal UnityObjectToWorldNormal(v.normal); return o; } fixed4 frag (v2f i) : SV_Target { fixed3 N normalize(i.worldNormal); fixed3 L normalize(_WorldSpaceLightPos0.xyz); fixed ndotl dot(N, L) * 0.5 0.5; fixed3 col _BaseColor.rgb * ndotl; return fixed4(col, 1.0); }这段代码里UnityObjectToWorldNormal处理了非统一缩放下的法线变换_WorldSpaceLightPos0是Built-in提供的主光源方向。实际项目里还要加上纹理采样、阴影贴图采样、雾计算、高光等。注意不同版本的Unity内置变量写法略有差异比如主光方向可以是_WorldSpaceLightPos0方向光时它的w分量为0阴影采样用SHADOW_COORDS和SHADOW_ATTENUATION宏。2.3 用一个开关管理卡渲和PBR风格同一个角色Shader里可以塞两套漫反射模型一套PBR风格一套Toon/Ramp风格然后用shader_feature控制。这样做的好处是美术在不换材质的情况下切换风格坏处是变体会增加后面性能部分再细说。实际应用时的做法是定义多编译指令#pragma shader_feature_local _TOON_RAMP在片元着色器里用#ifdef _TOON_RAMP分支处理Ramp采样否则走普通Lambert材质面板上浮现一个Toggle美术可以自由切换这招在风格探索阶段特别好用项目前期不确定做写实还是卡渲时一套Shader两边试省下反复更换材质资源的时间。3. 从零整理的五种常见角色技法说起常见角色Shader实际项目里很少只用一个漫反射模型。根据角色风格不同我把常用技法归成五类每一类都有明确的适用场景和实现方法。3.1 Ramp渐变漫反射卡渲角色的色阶核心Ramp技术的核心是把漫反射的NdotL结果映射到一张渐变纹理上。这张纹理可以只有两个颜色块硬卡渲也可以有几十个色阶软卡渲。我的习惯是让美术直接控制Ramp贴图的灰度或彩色分布而不是在Shader里硬编码梯度值这样调整起来不需要动代码。fixed ndotl dot(N, L) * 0.5 0.5; fixed ramp tex2D(_RampTex, fixed2(ndotl, ndotl)).r; fixed3 col _BaseColor.rgb * ramp;这里有个隐蔽细节Ramp贴图的WrapMode如果设成Clampndotl超过1时采样不会异常如果设成Repeat高光边缘容易出现接缝。我在项目里统一把Ramp贴图设成Clamp模式。另外如果要让卡通角色脖子下方的阴影里有一点点暖色可以在Ramp贴图的高光端加一个小范围的亮黄色制造类似皮肤透光的效果。3.2 外扩法线描边宽度控制与平滑法线问题描边最常见做法是额外一个Cull Front的Pass在顶点着色器里把顶点沿法线方向外扩再乘以描边宽度系数。代码大致是这样的Pass { Name Outline Cull Front ZWrite On v2f vert (appdata v) { v2f o; float3 dir normalize(v.normal) * _OutlineWidth; float4 pos UnityObjectToClipPos(v.vertex); float3 viewNormal mul((float3x3)UNITY_MATRIX_IT_MV, v.normal); float dist distance(_WorldSpaceCameraPos, mul(unity_ObjectToWorld, v.vertex).xyz); pos.xy viewNormal.xy * _OutlineWidth * dist * 0.01; o.pos pos; return o; } }这个做法的坑在于低模角色。角色衣服和头发的法线通常不平滑外扩后描边会出现明显的折线不够圆润。解决方案是制作顶点色平滑法线在DCC工具里生成一个平滑法线烘焙到顶点色的RGB通道Shader里采样顶点色替代原始法线做外扩。注意描边宽度在近处太粗、远处太细的问题上面代码里加了dist系数做距离补偿。我一般是把_OutlineWidth范围设在0~0.02配合顶点色Mask控制局部描边的粗细变化比如眼睛周围、嘴角这些细碎地方描边要细头发飘带描边要粗一些。3.3 眼睛、金属、宝石的MatCap近似MatCap材质捕捉本质是用一张预先烘焙的球体贴图模拟光照。对眼睛这种小体积、高光集中的物体特别有效。它的采样方法是用视空间法线作为UV公式为uv normal.xy * 0.5 0.5然后直接采样MatCap纹理。float3 viewNormal mul((float3x3)UNITY_MATRIX_IT_MV, v.normal); float2 matcapUV viewNormal.xy * 0.5 0.5; fixed3 matcapCol tex2D(_MatCapTex, matcapUV).rgb;MatCap用在角色眼睛上能让眼球的光泽感跟视点联动看哪里都亮晶晶的这正是二次元角色需要的效果。用在金属饰品和宝石上也比传统Blinn-Phong更接近材质质感。代价是MatCap是静态光照近似角色转动时反射不跟随光源变化。因此它适合作为辅助项叠在主光之后不适合单独作为主光照。3.4 头发高光的Kajiya-Kay双层高光头发的高光和其他材质不同它是沿发丝方向的条状高光而不是圆形高光。Kajiya-Kay模型利用切线方向计算高光当视线和光源的夹角在发丝方向上有偏移时产生高光。实现上顶点着色器里要提供切线T然后计算float3 T normalize(i.tangent); float3 B normalize(cross(N, T)); float3 H normalize(L V); float TH dot(T, H); float sinTH sqrt(1 - TH * TH); float spec pow(saturate(sinTH), _Shininess);两层高光就是把上面的过程做两次一层沿切线正方向、一层沿负方向各乘一个系数再叠加起来。这样头发高光会呈现两条亮带顺着发丝的走向自然延伸。想让高光带更柔和就把Shininess调低并把两层高光的落点错开一点。在Shader里头发的半透明排序也要额外注意通常是双Pass先渲染背面再渲染正面避免发丝之间出现透穿。3.5 皮肤的Wrapped漫反射与SSS假象阴影过渡太硬的皮肤会显得塑料太软又像蜡。常用做法是Wrap漫反射给NdotL加一个偏移量让光的衰减更平滑fixed wrap (dot(N, L) _Wrap) / (1.0 _Wrap);当_Wrap设为0.2~0.4时皮肤背光面会带一点暖色过渡模拟次表面散射。更进一步可以用第二层颜色贴图在暗面叠加皮肤深处的血色比如口鼻两侧、耳朵这些透光区域。到这一步已经能糊弄过大部分玩家了毕竟大部分角色在游戏里的脸区域很小没必要为SSS写一个完整的次表面散射算法。4. 角色和场景的融合阴影接收、雾与颜色空间做过角色渲染的人都知道最尴尬的瞬间不是材质难看而是角色站在场景里像一层贴纸完全没融进去。问题多半出在阴影接收、颜色空间和雾效上。4.1 让角色不飘ShadowCaster与阴影淡出给角色Shader加阴影接收和投射要在Shader里加上FallBack Diffuse或者在每个Pass里手动声明ShadowCaster Pass。我的推荐是显式写一个ShadowCaster Pass这样不会被FallBack干扰也方便在阴影Pass里做一些特殊处理。角色使用场景中最典型的问题是角色站的位置背光主阴影把整个正面全遮住了导致脸部一团黑。解决思路有两个一是给角色单独调ShadowStrength在Quality设置里把阴影强度调低二是写一个半透明阴影淡出让阴影离物体越远越透明改善脸上阴阳脸的突兀感。漫反射里也可以把阴影采样的结果做一个SmoothStep让阴影边缘在过渡带上稍微偏移脸部高光区域不完全消失。#if defined(SHADOWS_SCREEN) fixed shadow SHADOW_ATTENUATION(i); float s smoothstep(0.3, 0.6, shadow); diffuse * lerp(0.6, 1.0, s); #endif4.2 线性空间下肤色和贴图的Gamma校正Built-in管线在Project Settings里可以选择Gamma或Linear颜色空间。如果项目是Linear角色贴图必须正确标记sRGB否则在Shader里做乘法运算时颜色会先被当作线性值处理采出来的贴图比美术在Photoshop里看到的要灰很多。一个常见坑美术在PS里把肤色调成RGB(255, 230, 220)进Unity线性空间后如果贴图导入设置里sRGB没勾上肤色就会明显偏灰。另一点是Shader里写的颜色常量比如fixed4(1, 0.85, 0.8, 1)在线性空间下要做一次GammaToLinear转换。因为Shader里直接写的颜色在渲染时会被Unity当成线性值如果你希望它在屏幕上显示成之前设计的颜色就得手动转换。实际操作中我建议颜色常量尽量用_Color暴露出来不要在代码里写死大理石粉这类颜色。4.3 雾效和全局烘焙的注意事项Built-in的雾效默认是均匀雾在角色Shader里可以通过UNITY_FOG_COORDS和UNITY_APPLY_FOG接收。卡渲角色通常不太需要雾把它关掉反而能让角色更清晰地衬托在场景前。如果使用了烘焙全局光照角色材质要在Base Pass里加上#pragma multi_compile LIGHTMAP_ON和光照贴图的UV变换宏UNITY_LIGHTMAP_COORDS、UNITY_TRANSFER_LIGHTMAP。角色这种动态物体如果也要吃烘焙光照往往是因为场景里有个大范围的室内环境角色需要跟墙面、地面共享明暗关系。这一块容易漏漏了之后角色在烘焙场景里就像带了自发光一样完全没有环境光接触感。5. 性能与变体移动端做角色Shader的底线Built-in角色Shader写起来很自由但自由是要付出代价的。移动端性能预算是硬约束只顾效果不管性能的做法最后要么帧率掉到20要么发热严重被玩家差评。5.1 指令预算和常见超支点移动端角色的片元Shader通常建议控制在一两百条ALU指令以内贴图采样控制在6~8次以内。容易超支的地方都是叠出来的Base Pass里既做PBR高光又做Ramp又叠MatCap又做头发高光再加阴影采样和雾很容易就超了。我的做法是给角色Shader分层次主角、NPC、小兵分别用不同档位的Shader主角可以稍贵一些NPC和小兵只保留2~3个采样。常见超支点多重嵌套分支语句虽然GPU对分支有优化但移动端的执行效率仍然不稳高精度变量过多half和fixed在移动端性能差异明显高光pow指数过大虽然pow本身只是一条指令但反三角函数asin/acos很贵能用sqrt近似就不用反三角函数顶点着色器里做矩阵求逆等重操作每帧跑成千上万个顶点性能吃不消5.2 控制shader_feature与multi_compile变体变体数量是所有自定义Shader的隐形杀手。一个角色Shader如果不加控制地叠加shader_feature、multi_compile打包后体积可能翻好几倍运行时首次加载还会卡一下。我的经验是美术可切换的大效果才用shader_feature平台相关的必须用multi_compile能合并成一张贴图控制的效果就不要开开关写完Shader一定要在Build报告里检查变体数量。另外角色Shader用的shader_feature_local和multi_compile_local只作用于当前材质不会全局生成变体能有效减少变体膨胀。我以前踩过一个坑给角色Shader加了一堆Toggle结果打包后ShaderVariantCollection里有几百个变体加载时间肉眼可见地变长最后是靠把所有本地变体改成local才压下去。5.3 精度、贴图采样密度和LOD移动端GPU对半精度运算友好所以数据能用half就尽量用half。法线、切线这些方向向量我用half3颜色用fixed4。注意深度和位置相关的计算要保留float否则顶点位置在大场景里精度不够会出现闪烁。贴图采样密度也是角色渲染的常见问题。角色贴图一般比场景贴图精细但如果你给每个角色都开4096贴图一个场景几十个角色显存价格感人。我的建议是主角用2048普通NPC用1024远处角色用512配合Unity的Texture Streaming按距离加载。Shader侧不必刻意为LOD写多档只需通过贴图精度控制就能省下不少带宽。6. 一份可直接用的示例三Pass卡渲角色Shader完整代码前面说到头发的半透明要双Pass处理描边又要单独一个Pass我最后整理出一份常用的三Pass结构。这份Shader在Built-in下可以直接建材质挂到角色模型上适合二次元/卡渲风格。它把渲染拆成描边Pass、头发半透明背面Pass、角色主体Pass。这样头发丝之间不再透穿描边也稳定。6.1 总体结构和贴图槽位说明材质面板上预留这些参数_BaseMap角色主体漫反射贴图_RampTex色阶渐变贴图控制卡通明暗_OutlineWidth描边宽度_HairTex头发贴图走半透明Pass_HairAlpha头发的Alpha裁剪阈值_SpecColor和_Shininess高光颜色和强度渲染队列设置在Transparent和Geometry之间角度Alpha混合只给头发主体部分继续用不透明渲染减少排序问题。6.2 核心代码块Shader Custom/ToonCharacter { Properties { _BaseColor (Base Color, Color) (1,1,1,1) _BaseMap (Base Map, 2D) white {} _RampTex (Ramp, 2D) white {} _OutlineWidth (Outline Width, Range(0.001, 0.02)) 0.005 _HairTex (Hair Mask, 2D) white {} _HairAlpha (Hair Alpha, Range(0, 1)) 0.5 _SpecColor (Spec Color, Color) (1,1,1,1) _Shininess (Shininess, Range(8, 128)) 32 } SubShader { Tags { QueueGeometry2 RenderTypeOpaque } LOD 200 // Pass 1: 描边 Pass { Name OUTLINE Cull Front ZWrite On CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; }; float _OutlineWidth; v2f vert (appdata v) { v2f o; float3 viewNormal mul((float3x3)UNITY_MATRIX_IT_MV, v.normal); float3 pos UnityObjectToViewPos(v.vertex); pos.xy viewNormal.xy * _OutlineWidth; o.pos mul(UNITY_MATRIX_P, float4(pos, 1.0)); return o; } fixed4 frag (v2f i) : SV_Target { return fixed4(0.08, 0.08, 0.1, 1); } ENDCG } // Pass 2: 头发透明排序 Pass { Name HAIR_BACK Tags { QueueTransparent RenderTypeTransparent } Cull Front Blend SrcAlpha OneMinusSrcAlpha ZWrite Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _HairTex; float _HairAlpha; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 c tex2D(_HairTex, i.uv); clip(c.a - _HairAlpha); return fixed4(c.rgb, c.a); } ENDCG } // Pass 3: 主体光照 Pass (ForwardBase) Pass { Name FORWARD_BASE Tags { LightModeForwardBase } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase #include UnityCG.cginc #include Lighting.cginc #include AutoLight.cginc sampler2D _BaseMap; sampler2D _RampTex; float4 _BaseColor; float4 _SpecColor; float _Shininess; struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldNormal : TEXCOORD1; float3 worldPos : TEXCOORD2; SHADOW_COORDS(3) }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv v.uv; o.worldNormal UnityObjectToWorldNormal(v.normal); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; TRANSFER_SHADOW(o); return o; } fixed4 frag (v2f i) : SV_Target { fixed3 N normalize(i.worldNormal); fixed3 L normalize(_WorldSpaceLightPos0.xyz); fixed3 V normalize(_WorldSpaceCameraPos - i.worldPos); fixed3 H normalize(L V); fixed ndotl saturate(dot(N, L)); fixed ramp tex2D(_RampTex, fixed2(ndotl, ndotl)).r; fixed3 albedo tex2D(_BaseMap, i.uv).rgb * _BaseColor.rgb; fixed3 diffuse albedo * ramp * _LightColor0.rgb; fixed shadow SHADOW_ATTENUATION(i); diffuse * saturate(shadow 0.25); fixed spec pow(saturate(dot(N, H)), _Shininess); diffuse _SpecColor.rgb * spec * _LightColor0.rgb; return fixed4(diffuse, 1.0); } ENDCG } } FallBack Diffuse }6.3 参数调节建议把这Shader挂上角色模型后我建议先从三个参数入手调风格_Shininess控制在16~32之间二次元角色高光不宜太锐利太大会像塑料_OutlineWidth先调到0.005再按角色体型和摄像机距离微调注意小人的描边要更细_HairAlpha调到接近头发贴图Alpha边缘的阈值配合Queue调整避免头发之间出现明显的黑边如果角色在场景里整个偏暗优先检查SHADOW_ATTENUATION的shadow0.25这一项这个值压低了阴影对主光的消减程度但这只是简化处理。你要在项目里精细控制阴影权重就把这个常量改成材质参数_ShadowStrength暴露出去让美术去调节。另外提醒一句这个示例我把绕开URP的部分全去掉了它只能在Built-in下直接用如果以后要迁到URP主体光照部分需要重写成Universal Forward Pass结构Ramp和描边逻辑可以复用。这算是我在多个项目里反复用过的一套骨架。角色Shader这东西没有银弹也没有万能模板。真正管用的是搞清楚自己的风格目标、性能预算和管线边界然后从最简单的光照模型起步一层层叠效果。希望这份整理对正在Built-in里做角色的朋友有点实际帮助。