拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Unity URP外发光Shader实战:从原理到高性能实现

1. 项目概述为什么我们需要一个外发光Shader在Unity3D游戏开发中视觉表现力是抓住玩家眼球的第一道关卡。无论是角色身上的能量护盾、武器附魔时的光效还是UI界面的高亮提示“发光”效果都扮演着至关重要的角色。而“外发光”Outer Glow作为一种从物体边缘向外柔和扩散光晕的效果更是营造氛围、突出重点的利器。你可能会想Unity自带的粒子系统或者后期处理Post-Processing里的Bloom泛光不能实现吗确实可以但它们要么控制不够精细要么性能开销较大尤其是在移动端或需要大量发光物体的场景中。这就是自定义Shader的价值所在。一个专为外发光编写的Shader能让你以极低的性能成本实现高度可控、风格化强烈的发光效果。它不依赖于昂贵的屏幕空间后处理而是基于物体本身的网格和法线信息进行计算真正做到“按需发光”。无论是将SolidWorks导入的精密机械模型赋予能量核心的光晕还是为你开发的AR/VR应用中的交互元素添加醒目光标一个高效的外发光Shader都是提升项目质感的秘密武器。本教程将带你从零开始深入ShaderLab和HLSL的世界手把手构建一个高性能、可定制的Unity3D外发光Shader并解决实战中遇到的各种坑。2. 核心原理与方案选型在动手写代码之前我们必须搞清楚外发光效果的本质并选择最适合当前项目渲染管线的实现方案。不同的方案决定了不同的复杂度、性能表现和兼容性。2.1 外发光的实现原理剖析外发光并非物体自身发光而是一种视觉模拟。其核心思想可以概括为“膨胀渲染”。我们并不直接修改原始物体而是先让物体的“轮廓”沿着其表面法线方向向外“膨胀”一层渲染出一个稍大的、半透明的“壳”然后再在这个壳上应用颜色和衰减最后与原始物体混合。这个过程主要依赖两个关键信息顶点法线Normal决定了“膨胀”的方向。每个顶点沿着其法线方向移动一定距离就能生成一个包裹原模型的新网格。深度Depth或模板Stencil测试用于确保发光层只出现在原始物体轮廓的外部而不会覆盖物体本身。这是实现“外”发光的关键否则发光层会和物体内部重叠造成错误的视觉效果。2.2 主流实现方案对比与选型在Unity中实现外发光主要有以下几种思路我们需要根据项目使用的渲染管线Built-in, URP, HDRP和具体需求来选择。方案一多Pass渲染传统Built-in管线常用这是最经典、最直观的方法。在同一个Shader中编写两个Pass渲染通道。第一个Pass发光Pass负责渲染发光层。在这个Pass里我们开启模板写入Stencil Write将绘制过的像素标记为一个特定的值比如1。同时顶点着色器将顶点沿法线外扩。第二个Pass物体本色Pass负责渲染物体本身的颜色。在这个Pass里我们设置模板测试Stencil Test只渲染模板值不等于1的像素即没有被发光层覆盖的像素。这样就确保了物体本色会覆盖掉发光层在物体内部的区域只留下外部的光晕。注意在URPUniversal Render Pipeline中由于渲染架构的优化单个Shader通常只支持一个UniversalForward或SRPDefaultUnlitPass。多Pass渲染在URP中不是最佳实践可能会破坏SRP Batcher合批导致性能下降。因此此方案更适用于遗留的Built-in Render Pipeline项目。方案二基于深度的后处理通用性强这种方法完全在屏幕空间操作。先渲染整个场景的深度图然后通过后处理Shader识别出物体边缘深度发生突变的地方并在这些边缘处生成发光效果。Unity的Post-Processing v2包中的Bloom效果某种程度上类似但它是对高亮区域整体泛光而非针对特定物体的轮廓。优点一次处理场景中所有物体无需为每个物体单独设置。缺点控制精度较低难以让特定物体发出特定颜色或强度的光性能开销集中在后处理环节分辨率越高开销越大对于透明物体或复杂重叠物体的边缘处理容易出错。方案三命令缓冲器Command Buffer与单独渲染队列这是一种更高级、更灵活的方案尤其在URP/HDRP中推荐。思路是使用CommandBuffer在相机渲染的特定阶段例如在渲染不透明物体之后渲染透明物体之前将需要外发光的物体单独渲染到一个临时渲染纹理Render Texture中。渲染时使用一个只输出纯色发光色且顶点外扩的简单Shader。然后将这个渲染纹理与相机的主渲染纹理进行混合通常使用类似“屏幕Screen”或“相加Additive”的混合模式。优点控制粒度极细可以动态决定哪些物体发光与物体本身的材质完全解耦不影响合批效果稳定。缺点实现复杂度较高需要处理渲染顺序、相机事件和资源管理。本教程方案选择考虑到教程的实用性和普适性我们将聚焦于方案一的多Pass渲染方法但会基于URP管线进行现代化改造。URP虽然不鼓励多Pass但我们可以通过“渲染状态覆盖Render State Override”和巧妙的渲染队列Queue设置来模拟这一效果同时尽可能保持较好的性能。这是目前独立开发者和小团队项目中最容易上手且效果可控的方案。3. URP外发光Shader实战编码我们将创建一个名为URP_OuterGlow.shader的文件。请注意URP Shader的结构与Built-in Shader有显著不同。3.1 Shader基础结构与属性定义首先定义Shader的基本信息和可供美术师调节的属性。Shader Custom/URP_OuterGlow { Properties { // 物体主纹理和颜色 [MainTexture] _BaseMap(Albedo, 2D) white {} [MainColor] _BaseColor(Color, Color) (1,1,1,1) _Glossiness(Smoothness, Range(0.0, 1.0)) 0.5 _Metallic(Metallic, Range(0.0, 1.0)) 0.0 // 外发光专属属性 [HDR] _GlowColor(Glow Color, Color) (1, 0.5, 0, 1) // 使用HDR属性支持高亮度颜色 _GlowIntensity(Glow Intensity, Range(0.0, 5.0)) 1.0 _GlowRange(Glow Range, Range(0.0, 0.5)) 0.1 _GlowPower(Glow Falloff, Range(0.1, 10.0)) 2.0 // 控制光晕衰减的指数 } SubShader { // 重要Tags告诉URP这是一个不透明物体 Tags { RenderTypeOpaque RenderPipelineUniversalPipeline QueueGeometry } // HLSLINCLUDE块用于定义供多个Pass共享的代码如顶点/片元着色器结构体、函数 HLSLINCLUDE #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl // 定义从Properties映射过来的变量 TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); float4 _BaseMap_ST; // 用于纹理缩放偏移 CBUFFER_START(UnityPerMaterial) float4 _BaseColor; float _Glossiness; float _Metallic; float4 _GlowColor; float _GlowIntensity; float _GlowRange; float _GlowPower; CBUFFER_END // 定义顶点着色器输入结构 struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 texcoord : TEXCOORD0; }; // 定义顶点着色器输出/片元着色器输入结构用于发光Pass struct Varyings_Glow { float4 positionCS : SV_POSITION; float3 normalWS : TEXCOORD0; float2 uv : TEXCOORD1; float3 positionWS : TEXCOORD2; }; // 定义顶点着色器输出/片元着色器输入结构用于主Pass包含更多光照信息 struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 normalWS : TEXCOORD1; float3 positionWS : TEXCOORD2; }; ENDHLSL代码解析与注意事项[HDR] _GlowColor[HDR]属性标签允许颜色值超过1这对于表现强烈的发光效果如霓虹灯、能量体至关重要。Tags { QueueGeometry }将渲染队列设置为Geometry默认不透明物体队列。我们后续会通过覆盖Offset来调整两个Pass的绘制顺序。HLSLINCLUDE这是一个关键优化。它将共用的代码如变量声明、结构体包裹起来避免在两个Pass中重复编写使Shader更简洁。CBUFFER_START(UnityPerMaterial)在URP中材质属性必须放在常量缓冲区CBUFFER中这是为了支持SRP Batcher能显著提升使用相同Shader的不同材质的渲染性能。3.2 实现发光渲染通道接下来我们实现第一个Pass专门用于渲染外发光层。这个Pass的核心是顶点外扩和简单的颜色输出。// Pass 1: 渲染外发光层 Pass { Name OuterGlow // 覆盖Tags让这个Pass在主体之前渲染并使用透明的混合模式 Tags { LightModeUniversalForward QueueGeometry1 } // 关键渲染状态设置 Blend SrcAlpha OneMinusSrcAlpha // 传统Alpha混合 ZWrite Off // 关闭深度写入防止发光层挡住后面的物体 ZTest LEqual // 深度测试仍然采用小于等于确保被遮挡时不发光 Cull Back // 背面剔除通常发光只看正面 // 使用Offset将多边形深度值稍微推远避免与主体Pass深度冲突产生的闪烁Z-fighting Offset 0, -1 HLSLPROGRAM #pragma vertex vert_glow #pragma fragment frag_glow Varyings_Glow vert_glow(Attributes input) { Varyings_Glow output; // 将顶点和法线从物体空间变换到世界空间 VertexPositionInputs positionInputs GetVertexPositionInputs(input.positionOS.xyz); VertexNormalInputs normalInputs GetVertexNormalInputs(input.normalOS); output.positionWS positionInputs.positionWS; output.normalWS normalInputs.normalWS; output.uv TRANSFORM_TEX(input.texcoord, _BaseMap); // 核心顶点外扩。在世界空间中将顶点位置沿法线方向移动_GlowRange距离 float3 expandedPositionWS positionInputs.positionWS normalInputs.normalWS * _GlowRange; // 将外扩后的世界坐标转换回裁剪空间输出给GPU output.positionCS TransformWorldToHClip(expandedPositionWS); return output; } half4 frag_glow(Varyings_Glow input) : SV_Target { // 计算边缘衰减因子基于法线与视角方向的点积。 // 当视线与法线垂直即看物体侧面时点积为0发光最强。 // 当视线与法线平行即看物体正面时点积接近1发光最弱。 float3 viewDirWS GetWorldSpaceNormalizeViewDir(input.positionWS); float rim 1.0 - saturate(dot(viewDirWS, normalize(input.normalWS))); // 使用指数函数让衰减更锐利或更平滑 rim pow(rim, _GlowPower); // 最终颜色 发光颜色 * 强度 * 衰减因子 half4 glowColor _GlowColor; glowColor.rgb * _GlowIntensity; glowColor.a * rim; // 将衰减应用于Alpha通道实现边缘透明 return glowColor; } ENDHLSL }实操心得与避坑指南ZWrite Off与Offset的妙用关闭深度写入是为了避免发光层“挡住”它后面的物体包括物体自身的不透明部分。但关闭后发光层之间、发光层与场景的深度关系可能混乱。Offset -1是一个“小伎俩”它把这个Pass渲染的多边形在深度上稍微“推远”一点虽然不写入深度但能确保在深度测试时它不会与紧接着渲染的物体主体下一个Pass产生深度冲突Z-fighting从而避免闪烁。边缘衰减计算rim 1.0 - saturate(dot(viewDir, normal))是菲涅尔效应Fresnel Effect的简化版。它模拟了光线在掠射角边缘反射更强烈的物理现象是外发光看起来自然的关键。通过_GlowPower控制衰减曲线值越大发光越集中在极边缘。性能考量这个Pass非常简单仅做了一次顶点变换和一次点积运算片元着色器开销极低。即使是在移动设备上为少量关键物体添加此效果性能影响也微乎其微。3.3 实现物体本色渲染通道第二个Pass渲染物体本身的材质使用URP标准的PBR光照模型。// Pass 2: 渲染物体本身URP标准光照 Pass { Name ForwardLit Tags { LightModeUniversalForward } // 使用默认的不透明渲染状态 Blend One Zero ZWrite On ZTest LEqual Cull Back HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS #pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING #pragma multi_compile_fragment _ _REFLECTION_PROBE_BOX_PROJECTION Varyings vert(Attributes input) { Varyings output; VertexPositionInputs positionInputs GetVertexPositionInputs(input.positionOS.xyz); VertexNormalInputs normalInputs GetVertexNormalInputs(input.normalOS); output.positionCS positionInputs.positionCS; output.positionWS positionInputs.positionWS; output.normalWS normalInputs.normalWS; output.uv TRANSFORM_TEX(input.texcoord, _BaseMap); return output; } half4 frag(Varyings input) : SV_Target { // 采样纹理 half4 albedo SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor; // 准备光照计算所需数据 InputData lightingData (InputData)0; lightingData.positionWS input.positionWS; lightingData.normalWS normalize(input.normalWS); lightingData.viewDirectionWS GetWorldSpaceNormalizeViewDir(input.positionWS); lightingData.shadowCoord TransformWorldToShadowCoord(input.positionWS); // 阴影坐标 // 配置表面数据PBR SurfaceData surfaceData; surfaceData.albedo albedo.rgb; surfaceData.alpha albedo.a; surfaceData.metallic _Metallic; surfaceData.smoothness _Glossiness; surfaceData.emission half3(0,0,0); // 自发光留空因为我们用单独的Pass处理外发光 surfaceData.occlusion 1.0; surfaceData.clearCoatMask 0.0; surfaceData.clearCoatSmoothness 0.0; surfaceData.normalTS half3(0,0,1); // 法线贴图本教程未使用 // 调用URP核心光照函数 half4 color UniversalFragmentPBR(lightingData, surfaceData); return color; } ENDHLSL } } // 可选的回退Shader用于不支持的平台 FallBack Hidden/Universal Render Pipeline/FallbackError }关键点解析LightMode标签两个Pass都使用了LightModeUniversalForward。在URP中这表示它们都属于前向渲染路径。URP的渲染器会按照SubShader中Pass的顺序和Queue标签来执行它们。我们的OuterGlowPass设置了QueueGeometry1意味着它会在所有QueueGeometry的物体之后渲染但又在我们的ForwardLitPass默认Geometry之前渲染这里有个细微之处Queue偏移主要影响不同Shader之间的排序。在同一Shader内Pass的渲染顺序就是它们在文件中的编写顺序。因此OuterGlowPass会先执行ForwardLitPass后执行完美实现了先画光晕后盖本体的逻辑。与发光Pass的解耦注意在surfaceData.emission中我们设置为0。这意味着物体本体的PBR计算完全不考虑自发光。外发光效果由第一个Pass独立、叠加地渲染上去。这种设计更清晰也避免了光照计算的复杂性。光照变体#pragma multi_compile指令用于为光照和阴影生成多个Shader变体确保在不同光照环境下都能正确工作。这是URP Shader的标配。4. 材质配置与场景调试技巧Shader写好了接下来就是在Unity编辑器中创建材质并应用到模型上同时进行效果微调。4.1 材质参数详解与调节心法创建材质在Project窗口右键 - Create - Material命名为Mat_OuterGlow并将Shader选择为我们刚创建的Custom/URP_OuterGlow。基础属性调节Albedo和Color控制物体本身的颜色和纹理。Metallic和Smoothness控制物体的金属感和光滑度影响反射。外发光属性精细调节核心Glow Color点击颜色框可以使用HDR颜色选择器。将强度Intensity调到1以上可以获得超亮发光。对于能量体常用蓝、青、洋红等冷色对于火焰、魔法常用红、橙、黄暖色。Glow Intensity发光颜色的乘数。即使Glow Color亮度很高也可以通过这个参数整体缩放发光强度。从0.5开始调节避免过曝。Glow Range控制顶点外扩的距离单位是世界单位米。这是影响发光“厚度”最直接的参数。对于小物件如宝石0.02-0.05即可对于大型建筑或角色可能需要0.1-0.3。值太大会导致发光层严重变形尤其是低多边形模型。Glow Power控制边缘衰减的锐利度。这是让发光效果“高级”的关键。值较低如0.5-1.5衰减平缓发光柔和像雾状光晕。值适中如2.0-4.0衰减明显发光集中在轮廓线效果清晰锐利最常用。值很高5发光几乎只出现在最极端的边缘形成极细的“描边”效果。4.2 场景布置与效果优化模型要求外发光Shader依赖于顶点法线。确保你的模型法线信息正确且连续。对于从SolidWorks等CAD软件导入的模型有时法线可能需要重新计算在3D软件中或使用Unity的Mesh.RecalculateNormalsAPI。光照环境将发光物体置于暗部或对比明显的背景前效果最突出。在过于明亮均匀的环境下发光效果会大打折扣。抗锯齿AA由于发光层是通过顶点膨胀生成的在模型边缘尤其是低多边形模型的硬边处可能会看到锯齿。启用Unity的后期处理抗锯齿如SMAA、FXAA或MSAA可以显著改善。渲染顺序问题如果场景中有多个使用此外发光Shader的物体它们可能会因为渲染队列相同而出现错误的叠加顺序。如果需要精确控制比如A物体的发光永远在B物体之上可以编写脚本动态修改材质的renderQueue属性。5. 常见问题排查与性能优化在实际项目中使用自定义Shader总会遇到一些“坑”。这里记录了几个最常见的问题及其解决方案。5.1 效果异常问题排查表问题现象可能原因解决方案没有发光效果1. Shader编译错误。2.Glow Range或Glow Intensity为0。3. 模型法线有问题如全部朝内。1. 查看Console窗口是否有Shader错误粉色提示。2. 检查材质面板参数。3. 在模型导入设置中勾选Read/Write Enabled或在代码中检查法线。发光出现在物体内部深度测试/写入设置冲突。发光Pass的ZWrite未关闭或Offset设置不当与主体Pass产生深度冲突。确保发光Pass的ZWrite为Off并尝试调整Offset的第二个参数单位偏移如Offset 0, -10。发光边缘有锯齿锯齿状1. 模型本身面数低顶点膨胀后轮廓不光滑。2. 未开启抗锯齿。1. 适当增加模型细分或考虑在片元着色器中采用基于屏幕空间导数的边缘平滑技术较复杂。2. 在URP Asset或相机设置中开启抗锯齿。发光颜色过曝、一片白色Glow Color的HDR强度或Glow Intensity值过高超出了显示范围。降低Glow Color的强度或Glow Intensity值。在Scene窗口的渲染模式中选择HDR视图来准确评估HDR效果。在移动设备上性能差1. 对大量物体使用了此Shader。2. 模型顶点数过高顶点着色器负担大。1. 严格控制使用外发光的物体数量优先用于主角、关键道具等。2. 使用LODLevel of Detail为远处的模型使用更简单的Shader或无发光版本。透明物体发光不正常本Shader的Tags为RenderTypeOpaque不适用于半透明物体。半透明物体的渲染顺序和混合方式完全不同。对于透明物体需要重写Shader将Tags改为RenderTypeTransparentQueueTransparent并仔细处理混合模式和深度读写。这是一个进阶话题。5.2 性能优化实战建议基于距离的衰减Distance Fade在片元着色器中计算像素到相机的距离并据此衰减发光的强度或Alpha值。这可以防止远处的物体产生不必要的、可能带来性能开销的发光效果。// 在frag_glow函数中添加 float distanceToCamera length(_WorldSpaceCameraPos - input.positionWS); float distanceFade saturate(1.0 - distanceToCamera / _FadeDistance); // _FadeDistance为自定义属性 glowColor.a * distanceFade;使用Shader LOD在SubShader开头添加LOD 300或适当的值。当相机的LOD设置超过这个值时Unity会自动使用FallBack指定的更简单的Shader从而节省性能。避免动态批处理破坏我们的Shader使用了两个Pass这可能会阻止Unity进行动态批处理Dynamic Batching。对于大量相同的小物体如子弹、粒子可以考虑使用方案三Command Buffer或者将这些小物体的发光效果整合到其本身的粒子Shader中而不是使用这个多Pass的Mesh Shader。针对VR/AR的优化在VR中单眼分辨率高且需要渲染两次性能压力巨大。务必将Glow Range和Glow Intensity调到最低可接受值。考虑仅在玩家近处的交互物体上使用此效果。彻底测试在目标VR设备如一体机上的帧率。如果串联PC时遇到SteamVR未检测到头戴式显示器等问题那是SteamVR设置或驱动问题与Shader无关但确保你的Shader不会成为性能瓶颈。5.3 进阶扩展思路当你掌握了基础的外发光后可以尝试以下扩展让效果更上一层楼噪声扰动在片元着色器中采样一张噪声图用其RG通道扰动发光层的UV或强度可以做出能量流动、火焰摇曳的动态效果。这与Shader Graph中制作扰动效果思路一致。纹理遮罩使用一张额外的纹理R通道来控制哪些区域发光更强。比如让武器上的符文图案发光更亮。与后处理结合将此外发光作为“发光源”再结合一个轻量级的屏幕空间Bloom后处理可以让高亮部分的光晕“溢出”到周围场景效果更加震撼。但要注意性能叠加。Shader Graph实现如果你更倾向于可视化编程可以在Shader Graph中复制这一逻辑。使用Position和Normal节点计算外扩顶点用Fresnel Effect节点计算边缘衰减然后混合输出。Shader Graph版本更易于调试和迭代但底层原理完全相同。编写自定义Shader是一个不断调试和权衡的过程。这个URP外发光Shader提供了一个坚实可靠的起点它平衡了效果、性能和复杂度。最重要的是你理解了从顶点膨胀到边缘衰减的每一步原理这让你能够自信地修改它、优化它并将其适配到项目最需要的地方。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门