1. 项目概述为什么URP下的Billboard效果值得深究在Unity的通用渲染管线URP里实现一个Billboard广告牌效果听起来像是图形学里的“Hello World”——基础、简单似乎没什么好讲的。但如果你真的在项目里大规模使用过比如做一片随风摇曳的草地、一群始终面向玩家的NPC头顶图标或者远处飘动的粒子特效你大概率踩过坑为什么我的Billboard在VR里会抖动为什么远处的一片广告牌突然就“闪”了一下为什么GPU耗时莫名其妙就上去了这就是我们今天要聊的。一个基础的Billboard Shader网上能找到几十个版本但能把性能、稳定性和视觉效果都做到生产级别的并不多。很多教程只告诉你“用叉乘算出朝向矩阵”却不会告诉你这个计算在Shader里怎么优化在不同渲染队列下怎么处理深度更不会提在移动端或VR项目里那些要命的性能陷阱。我接手过好几个项目都是因为前期在这些“小效果”上优化不足后期重构起来苦不堪言。所以这篇文章不是另一个“如何写一个Billboard Shader”的教程。而是基于我这些年踩过的坑、做的性能分析和优化实验总结出的5个实战优化技巧。我会附上完整的、可直接用于URP 12/14版本的Shader代码但更重要的是我会拆解每一行代码背后的“为什么”以及在不同场景下你应该如何选择和调整。无论你是想优化现有的植被系统还是为你的开放世界项目添加高效的远景细节这些技巧都能让你少走弯路。2. Billboard的核心原理与URP下的实现差异在深入优化之前我们必须统一认知什么是Billboard以及在URP里做这件事和内置管线或HDRP有什么不同。2.1 Billboard的两种基本形态与数学本质Billboard的核心目标很简单让一个通常由两个三角形构成的四边形Quad在3D空间中始终“看”着摄像机。根据“看”的方式不同主要分为两类轴向约束式Axis-Aligned Billboard四边形只围绕一个世界空间轴通常是Y轴旋转保持其“向上”方向与世界Y轴一致。这就像路边的广告牌你可以绕着柱子转但牌子本身不会倾斜。常用于树木、角色头顶的UI等。完全面向式Viewpoint-Oriented Billboard四边形完全“盯”着摄像机其法线方向始终与视线方向相反。这就像一直对着你脸的一张纸。常用于粒子、精灵、远景的云朵等。其数学本质是在顶点着色器中我们不再使用物体自身的模型矩阵unity_ObjectToWorld来变换顶点而是根据摄像机位置动态构造一个新的朝向矩阵然后用这个矩阵去变换顶点。网上最常见的代码片段是这样的float3 worldPos mul(unity_ObjectToWorld, float4(0, 0, 0, 1)).xyz; float3 up float3(0, 1, 0); float3 forward normalize(_WorldSpaceCameraPos - worldPos); float3 right normalize(cross(up, forward)); // 重新构造 worldMatrix float4x4 billboardMatrix float4x4( right, 0, up, 0, forward, 0, worldPos, 1 ); // 变换顶点 float4 vertexWorld mul(billboardMatrix, v.positionOS);这段代码实现了完全面向式Billboard。它清晰但问题很多我们后面会逐一拆解。2.2 URP带来的约束与机会URP不是内置管线的简单升级。它为了跨平台性能和简洁性做出了一系列设计选择直接影响我们的Shader编写Shader Library的迁移核心函数从UnityCG.cginc移到了Packages/com.unity.render-pipelines.universal/ShaderLibrary/下的各个HLSL文件中。我们必须使用Core.hlsl、Lighting.hlsl等。常量缓冲区CBUFFER的强制使用为了兼容SRP Batcher所有材质属性Properties里定义的必须包裹在CBUFFER_START(UnityPerMaterial)和CBUFFER_END中。否则SRP Batcher失效Draw Call无法合批性能会急剧下降。这是很多从内置管线迁移过来的Shader第一个踩的坑。渲染队列Queue的敏感性增强URP对物体的渲染顺序管理更为严格。Billboard物体如果涉及透明Alpha Blend或镂空Alpha Clip其队列“Queue”标签设置不正确极易出现前后错乱、半透明排序错误的问题。更明确的Pass标签Pass里的Tags必须正确指定“LightMode”例如前向渲染用“UniversalForward”深度写入用“DepthOnly”。这对于需要参与阴影、深度纹理等复杂效果的Billboard至关重要。理解这些差异是进行任何优化的前提。一个不考虑SRP Batcher和渲染队列的Billboard Shader在URP项目里几乎是不可用的。3. 实战优化技巧一精炼且高效的顶点变换算法我们来看第一个也是最核心的优化点顶点着色器里的矩阵计算。直接套用网上常见的公式在每帧渲染成千上万个广告牌时会成为GPU的负担。3.1 常见实现的问题分析回顾一下最常见的实现它计算了forward、right、up三个向量来构造矩阵。这里有几个问题冗余计算normalize()函数开销相对较大而这里调用了两次。cross()也调用了两次。忽略缩放构造矩阵时直接使用了归一化的方向向量这意味着原始模型通常是单位Quad的缩放信息unity_ObjectToWorld中的缩放值丢失了。你的广告牌会永远保持1:1的世界空间大小。潜在的零向量问题当摄像机正好在物体正上方或正下方时forward向量与up向量可能共线导致cross(up, forward)结果为零向量进而使normalize()产生不可预测的结果可能引发Nan或图形异常。3.2 优化后的算法实现优化的思路是减少计算量安全地处理边界情况并保留缩放。以下是优化后的vert函数核心部分Varyings vert(Attributes IN) { Varyings OUT; // 1. 获取物体世界空间原点及缩放 float3 worldPos unity_ObjectToWorld._m03_m13_m23; // 直接取平移分量比mul快 float3 scale; scale.x length(float3(unity_ObjectToWorld._m00, unity_ObjectToWorld._m10, unity_ObjectToWorld._m20)); scale.y length(float3(unity_ObjectToWorld._m01, unity_ObjectToWorld._m11, unity_ObjectToWorld._m21)); scale.z length(float3(unity_ObjectToWorld._m02, unity_ObjectToWorld._m12, unity_ObjectToWorld._m22)); // 2. 计算朝向向量 (优化版) float3 viewDir _WorldSpaceCameraPos - worldPos; float3 forward normalize(viewDir); // 仍需一次normalize // 3. 构建安全的、带缩放的右向量和上向量 // 使用一个稳定的世界朝上向量作为初始上向量参考 float3 worldUp float3(0, 1, 0); // 如果视线方向与世界朝上几乎平行则切换参考轴以避免叉乘为零 if (abs(dot(forward, worldUp)) 0.999) { worldUp float3(0, 0, 1); } float3 right normalize(cross(forward, worldUp)) * scale.x; // 应用X轴缩放 float3 up cross(right, forward) * scale.y; // 第二次叉乘无需normalize结果已正交且长度正确应用Y轴缩放 // forward方向本身应用Z轴缩放对于完全面向式BillboardZ缩放通常影响深度可视情况调整 forward * scale.z; // 4. 直接构造MVP变换跳过显式的世界矩阵乘法 // 原理: PV * (LocalToWorld) * v.vertex 我们可以直接构造 (PV * LocalToWorld) 矩阵的旋转缩放部分 // 但更清晰且性能可接受的做法是构造世界矩阵后与VP矩阵相乘 float4x4 billboardWorldMatrix float4x4( float4(right, 0), float4(up, 0), float4(forward, 0), float4(worldPos, 1) ); float4 vertexWorld mul(billboardWorldMatrix, float4(IN.positionOS.xyz, 1.0)); OUT.positionHCS mul(UNITY_MATRIX_VP, vertexWorld); OUT.uv TRANSFORM_TEX(IN.uv, _BaseMap); return OUT; }优化点解析提取缩放通过计算unity_ObjectToWorld各列向量的长度来获取原始物体的缩放值并将其分别应用到right、up、forward向量上。这样你在Unity编辑器里缩放GameObject广告牌的大小就会正确改变。安全的叉乘添加了一个条件判断当视线与默认世界朝上向量几乎平行时切换到一个备用轴如Z轴确保叉乘不会产生零向量。这是一个非常实用的健壮性技巧。减少归一化将两次normalize和两次cross优化为一次normalize和两次cross并且第二次cross的结果由于向量已正交其长度就是scale.x * scale.y无需再次归一化直接应用缩放即可。直接获取位置unity_ObjectToWorld._m03_m13_m23直接获取矩阵的平移分量比使用mul(unity_ObjectToWorld, float4(0,0,0,1))计算更高效。注意对于轴向约束式Billboard算法更简单只需计算right向量up向量固定为世界Y轴并应用缩放forward向量由right和up叉乘得到。计算量更小。4. 实战优化技巧二深度写入与渲染队列的精准控制Billboard的渲染顺序问题尤其是透明和镂空物体是视觉错误的重灾区。错误通常表现为该被遮挡的广告牌浮在了前景物体之上或者半透明的广告牌之间交错时出现乱序。4.1 理解深度测试与写入深度缓冲区Z-Buffer存储了每个像素距离摄像机的深度值。ZWrite控制是否写入这个缓冲区ZTest控制如何进行深度比较如Less、Equal、LessEqual。对于不透明物体OpaqueZWrite OnZTest LEqual。先渲染的物体会写入深度后渲染的物体如果深度更大更远则被丢弃。这是正确的。对于完全透明混合物体TransparentZWrite OffZTest LEqual。关闭深度写入但进行深度测试。这样它不会挡住后面的不透明物体但透明物体之间的渲染顺序就完全依赖于**渲染队列Queue**了。对于镂空物体Alpha Clip/Cutout比如我们的广告牌纹理有透明边缘。这类物体的像素要么完全不透明alpha cutoff要么完全透明alpha cutoff。它应该被视为不透明物体来处理深度否则透明边缘后面的物体会透过来。所以应该ZWrite OnZTest LEqual并且使用AlphaToMask或Clip指令。4.2 URP中渲染队列的配置策略在Shader的SubShader的Tags中“Queue”是关键。URP默认的渲染顺序大致是Background-Geometry-AlphaTest-Transparent-Overlay。对于Billboard Shader我的建议是纯色或不透明广告牌使用“Queue”“Geometry”。这是默认的不透明队列性能最优。镂空广告牌如树叶、栅栏必须使用“Queue”“AlphaTest”。这是专门为Clip或AlphaToMask材质设计的队列。它位于不透明物体之后、透明物体之前并且支持深度写入。这是最常见也是最容易配错的情况。很多开发者误用“Transparent”队列导致深度问题。SubShader { Tags { “RenderPipeline”“UniversalPipeline” “RenderType”“TransparentCutout” “Queue”“AlphaTest” “IgnoreProjector”“True”} ... Pass { ZWrite On ZTest LEqual AlphaToMask On // 或使用 clip() 指令 ... } }半透明广告牌如烟雾、云朵使用“Queue”“Transparent”。并且需要设置混合模式例如Blend SrcAlpha OneMinusSrcAlpha。务必关闭深度写入ZWrite Off但保持深度测试ZTest LEqual。此时渲染顺序完全由物体到摄像机的距离决定通常由RenderQueue值进一步细化。你需要确保这些广告牌物体在Unity中的排序是正确的或者考虑使用粒子系统来管理。实操心得在URP的Renderer Asset配置中你可以检查“Renderer Features”和“Opaque Layer Mask”、“Transparent Layer Mask”。确保你的Billboard物体所在的Layer被正确包含。有时深度问题不是Shader的错而是渲染层被意外过滤掉了。5. 实战优化技巧三利用GPU Instancing实现极致合批当场景中有大量相同的广告牌比如一片草地时每个广告牌都是一个独立的Draw Call这是不可接受的。GPU InstancingGPU实例化是URP下解决此问题的利器。5.3 在Billboard Shader中启用GPU Instancing启用GPU Instancing后GPU可以一次性绘制多个使用相同网格和材质的物体仅通过一个常量缓冲区传递不同的变换、颜色等参数极大减少CPU到GPU的通信开销。为你的Billboard Shader启用Instancing需要以下步骤在Properties中添加Instancing相关的属性可选用于传递每实例数据如颜色。Properties { _BaseMap(“Base Map”, 2D) “white” {} _BaseColor(“Base Color”, Color) (1,1,1,1) _Cutoff(“Alpha Cutoff”, Range(0,1)) 0.5 // 以下是为Instancing声明的属性它们会被每实例数据覆盖 [HDR] _InstanceColor(“Instance Color”, Color) (1,1,1,1) _InstanceScale(“Instance Scale”, Float) 1.0 }在CGPROGRAM/HLSLPROGRAM上方添加编译指令#pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing // 关键指令启用多编译实例化 #pragma instancing_options procedural:setup // 可选用于自定义实例化设置在顶点着色器输入结构体和常量缓冲区中支持Instancing#include “Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl” #include “Packages/com.unity.render-pipelines.universal/ShaderLibrary/UnityInput.hlsl” // 可能包含实例化宏 struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID // 声明实例ID }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_OUTPUT_STEREO // 如果支持VR需要这个 UNITY_VERTEX_INPUT_INSTANCE_ID // 传递实例ID到片元着色器如果需要 }; CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half _Cutoff; CBUFFER_END // 如果定义了每实例属性需要另一个缓冲区 UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(half4, _InstanceColor) UNITY_DEFINE_INSTANCED_PROP(half, _InstanceScale) UNITY_INSTANCING_BUFFER_END(Props)修改顶点着色器应用实例化数据Varyings vert(Attributes IN) { Varyings OUT; UNITY_SETUP_INSTANCE_ID(IN); // 设置实例ID UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); // 初始化VR相关 UNITY_TRANSFER_INSTANCE_ID(IN, OUT); // 传递ID // 使用宏获取每实例数据 half instanceScale UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceScale); half4 instanceColor UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceColor); float3 worldPos unity_ObjectToWorld._m03_m13_m23; // 将实例缩放应用到计算中 float3 scale; scale.x length(float3(unity_ObjectToWorld._m00, unity_ObjectToWorld._m10, unity_ObjectToWorld._m20)) * instanceScale; scale.y length(float3(unity_ObjectToWorld._m01, unity_ObjectToWorld._m11, unity_ObjectToWorld._m21)) * instanceScale; scale.z length(float3(unity_ObjectToWorld._m02, unity_ObjectToWorld._m12, unity_ObjectToWorld._m22)) * instanceScale; // ... 后续的Billboard计算使用新的scale ... // 在片元着色器中可以使用 instanceColor 来影响最终输出 // OUT.instanceColor instanceColor; return OUT; }在Unity编辑器中使用编写一个简单的脚本挂载在预制体上通过MaterialPropertyBlock动态修改_InstanceColor和_InstanceScale然后使用Graphics.DrawMeshInstanced或通过让多个GameObject共享同一材质并自动满足合批条件变换、材质参数相同来触发Instancing。注意事项SRP Batcher 与 GPU Instancing它们是两种不同的合批技术。SRP Batcher优化的是CPU提交Draw Call的成本要求Shader符合其规范使用CBUFFER。GPU Instancing优化的是GPU渲染本身。一个Shader可以同时受益于两者。我们的Shader已经使用了CBUFFER所以兼容SRP Batcher。当有大量相同材质、相同网格的物体时URP会优先尝试使用GPU Instancing。移动端限制并非所有移动GPU都同等支持Instancing或者对一次实例化调用的数量有限制如256或512个实例。需要进行性能和兼容性测试。自定义数据通过Instancing传递的数据量是有限的且需要谨慎管理。对于非常动态的数据如每帧变化的位置可能不如使用Compute Shader或间接绘制高效。6. 实战优化技巧四LOD与视锥裁剪的集成策略对于大规模部署的广告牌如远景森林我们不能让成千上万的广告牌始终以最高精度渲染。Level of DetailLOD和视锥体裁剪Frustum Culling是必备的优化手段。6.1 在Shader中实现简单的距离LOD我们可以在Shader中根据顶点到相机的距离动态调整广告牌的“精细度”但这通常不是指几何LOD因为广告牌本身就是一个Quad而是指着色计算的复杂度或纹理采样的方式。一个更实用的技巧是基于距离的Alpha渐变Dithering或尺寸衰减用于在广告牌消失前平滑过渡避免突兀的Pop-in。// 在顶点着色器中计算距离并传递给片元 OUT.worldPos worldPos; // 将世界坐标传递给片元着色器 // 在片元着色器中 float distanceToCamera distance(IN.worldPos, _WorldSpaceCameraPos); float fadeStart _FadeStartDistance; // 属性例如50 float fadeEnd _FadeEndDistance; // 属性例如70 // 计算透明度衰减因子 float fadeFactor saturate((fadeEnd - distanceToCamera) / (fadeEnd - fadeStart)); // 或者使用更平滑的曲线 // float fadeFactor smoothstep(fadeEnd, fadeStart, distanceToCamera); // 应用到最终颜色 half4 color tex2D(_BaseMap, IN.uv) * _BaseColor; color.a * fadeFactor; // 距离越远越透明 clip(color.a - _Cutoff); return color;对于完全面向式Billboard还可以在顶点着色器中根据距离轻微缩小广告牌的尺寸模拟其在远处更小的视觉效果但这需要修改顶点变换逻辑。6.2 在Unity中配置LOD Group与CullingShader内部的LOD是有限的。更强大的LOD管理需要在Unity的C#脚本层面完成。使用LOD Group组件为你的广告牌预制体创建一个LOD Group。你可以设置多个LOD级别LOD0使用完整的Billboard Shader和高质量纹理。LOD1使用一个更简化的Shader例如去掉法线贴图、高光计算甚至使用一个更小的纹理图集。LOD2使用一个极其简单的Shader或者直接替换为一个更低面数的模型如果广告牌是树木等复杂物体的替代品。LOD3 (Culled)完全剔除不渲染。通过调整每个LOD级别的屏幕相对高度Screen Relative Height阈值可以控制切换时机。视锥裁剪与遮挡裁剪这是Unity引擎自动进行的。确保你的广告牌物体被正确地放置在渲染层中并且其包围盒Bounds设置合理。对于Billboard其包围盒应该覆盖其旋转后可能占据的最大空间区域否则可能在旋转时被意外裁剪。你可以在脚本中通过Renderer.bounds或手动计算来设置MeshFilter.sharedMesh.bounds。基于距离的脚本控制对于超大规模的广告牌群如数万棵草即使有LODDraw Call数量也可能惊人。此时可以考虑使用脚本根据相机位置动态启用/禁用一定距离外或屏幕外的广告牌GameObject。或者更高级的做法是使用GPU驱动的渲染方式如Compute Shader配合间接绘制Indirect Draw将所有广告牌的位置、旋转、缩放数据存储在Compute Buffer中在Compute Shader中执行视锥裁剪和LOD选择然后通过Graphics.DrawMeshInstancedIndirect一次性提交所有可见实例。这是顶级性能优化的领域复杂度较高但能带来数量级的性能提升。实操心得对于中小型项目结合Shader距离衰减和Unity的LOD Group通常就够了。在设置LOD Group时一定要在Scene视图的“LOD可视化”模式下点击LOD Group组件上的相机图标检查切换是否平滑避免在玩家移动时出现明显的“跳变”。7. 实战优化技巧五针对移动端与VR的特殊优化移动端和VR虚拟现实设备对性能、功耗和帧稳定性有极其苛刻的要求。Billboard效果在这里更容易暴露问题。7.1 精度与性能取舍半精度浮点数移动端GPU如Adreno、Mali通常对半精度浮点数half/float16有更好的支持运算速度更快功耗更低。在片元着色器中对于颜色、UV等不需要全精度的数据使用half或half2/3/4是很好的习惯。// 在片元着色器中 half4 frag(Varyings IN) : SV_Target { half2 uv IN.uv; // UV坐标通常用half精度足够 half4 baseColor tex2D(_BaseMap, uv); half alpha baseColor.a; clip(alpha - _Cutoff); return half4(baseColor.rgb * _BaseColor.rgb, alpha); }注意在顶点着色器中位置、法线、变换矩阵等通常仍需使用float全精度因为精度不足可能导致顶点抖动Vertex Jittering在VR中这会引发严重的眩晕感。7.2 避免在顶点着色器中进行复杂分支移动端GPU的SIMD架构使得同一波束Warp/Wavefront内的所有线程通常需要执行相同的指令。如果顶点着色器中有基于顶点数据如顶点ID的复杂分支if-else可能导致线程束分化显著降低性能。我们的Billboard计算是统一的这个问题不突出但如果你在Billboard中加入了基于顶点位置的特殊变形如模拟风吹草动就需要警惕。一个常见的优化模式是将分支判断移到片元着色器或者使用step()、lerp()等函数来替代条件语句。7.3 VR中的双目渲染与单通道立体渲染在VR中相机有左眼和右眼两个视图。传统的渲染方式会绘制两遍场景。URP通过单通道立体渲染Single-Pass Stereo Rendering来优化只需一次Draw Call在顶点着色器中为左右眼分别计算位置。我们的Billboard Shader需要稍作修改以兼容单通道立体在Varyings结构体中添加UNITY_VERTEX_OUTPUT_STEREO宏。在顶点着色器开头调用UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT)。在计算最终的裁剪空间位置positionHCS时使用UnityObjectToClipPos()或TransformWorldToHClip()等URP提供的宏它们内部会处理立体渲染。实际上我们之前代码中使用的UNITY_MATRIX_VP和mul(UNITY_MATRIX_VP, vertexWorld)在单通道立体模式下是有问题的因为它没有考虑双眼的视口偏移。正确的做法是使用URP Shader Library提供的函数// 替换 OUT.positionHCS mul(UNITY_MATRIX_VP, vertexWorld); OUT.positionHCS TransformWorldToHClip(vertexWorld.xyz);TransformWorldToHClip函数来自Core.hlsl它会自动处理单通道立体渲染所需的变换。7.4 纹理采样与带宽优化纹理图集Atlas如果有很多不同的广告牌纹理将它们合并到一张大图集中可以大幅减少纹理采样器的切换和绑定提升合批效率。Mipmaps确保广告牌纹理启用了Mipmaps。这对于在远处减少纹理锯齿和节省带宽至关重要。在URP的纹理导入设置中检查。压缩格式针对移动端使用合适的纹理压缩格式如ASTC、ETC2。ASTC通常能提供更好的质量与压缩比平衡。避免全屏后处理对Billboard的过度影响某些全屏后处理效果如Bloom、Color Grading在低端移动设备上开销很大。如果广告牌是场景中的次要元素可以考虑将其排除在某些后处理效果之外通过Layer和URP Renderer Feature的过滤设置。8. 完整优化版URP Billboard Shader代码结合以上所有优化技巧这里提供一个完整的、生产可用的URP Billboard Shader代码轴向约束式。它包含了GPU Instancing支持、正确的AlphaTest队列、安全的朝向计算并考虑了移动端和VR的兼容性。// Optimized URP Billboard Shader (Axis-Aligned) with Instancing Support // Shader “Universal Render Pipeline/Optimized Billboard” Shader “Universal Render Pipeline/Optimized Billboard” { Properties { [MainTexture] _BaseMap(“Base Map”, 2D) “white” {} [MainColor] _BaseColor(“Base Color”, Color) (1, 1, 1, 1) _Cutoff(“Alpha Cutoff”, Range(0.0, 1.0)) 0.5 _FadeStart(“Fade Start Distance”, Float) 50.0 _FadeEnd(“Fade End Distance”, Float) 70.0 // Instancing Properties [HDR] _InstanceColor(“Instance Color”, Color) (1,1,1,1) _InstanceScale(“Instance Scale”, Float) 1.0 _WindStrength(“Wind Strength”, Range(0, 1)) 0.1 _WindSpeed(“Wind Speed”, Float) 1.0 } SubShader { Tags { “RenderPipeline”“UniversalPipeline” “RenderType”“TransparentCutout” “Queue”“AlphaTest” “IgnoreProjector”“True” “DisableBatching”“False” // 允许合批我们自己在Shader中处理Billboard } Pass { Name “ForwardLit” Tags { “LightMode” “UniversalForward” } Cull Off // 双面渲染广告牌通常需要 ZWrite On ZTest LEqual AlphaToMask On // 使用AlphaToMask实现抗锯齿的Alpha Clip在移动端可能开销大可根据情况改用clip() HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _SHADOWS_SOFT #pragma multi_compile_fog #include “Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl” #include “Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl” struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; float3 normalOS : NORMAL; // 可选用于简单光照 UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; float3 worldPos : TEXCOORD1; half fogFactor : TEXCOORD2; #if defined(_MAIN_LIGHT_SHADOWS) || defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) float4 shadowCoord : TEXCOORD3; #endif UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half _Cutoff; float _FadeStart; float _FadeEnd; CBUFFER_END UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(half4, _InstanceColor) UNITY_DEFINE_INSTANCED_PROP(half, _InstanceScale) UNITY_DEFINE_INSTANCED_PROP(half, _WindStrength) UNITY_DEFINE_INSTANCED_PROP(half, _WindSpeed) UNITY_INSTANCING_BUFFER_END(Props) // 简单的风动模拟函数 float3 ApplyWind(float3 vertexWorld, float2 uv, float strength, float speed, float time) { // 使用顶点世界XZ坐标和UV Y值作为噪声输入模拟风吹草动的摆动 float windWave sin(time * speed vertexWorld.x * 0.1 vertexWorld.z * 0.1) * uv.y; float3 windOffset float3(strength * windWave * 0.1, 0, strength * windWave * 0.05); return vertexWorld windOffset; } Varyings vert(Attributes IN) { Varyings OUT; UNITY_SETUP_INSTANCE_ID(IN); UNITY_TRANSFER_INSTANCE_ID(IN, OUT); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); // 获取实例数据 half instanceScale UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceScale); half windStrength UNITY_ACCESS_INSTANCED_PROP(Props, _WindStrength); half windSpeed UNITY_ACCESS_INSTANCED_PROP(Props, _WindSpeed); // 1. 提取原始变换和缩放 float3 objectWorldPos unity_ObjectToWorld._m03_m13_m23; float3 scale; scale.x length(float3(unity_ObjectToWorld._m00, unity_ObjectToWorld._m10, unity_ObjectToWorld._m20)); scale.y length(float3(unity_ObjectToWorld._m01, unity_ObjectToWorld._m11, unity_ObjectToWorld._m21)); scale.z length(float3(unity_ObjectToWorld._m02, unity_ObjectToWorld._m12, unity_ObjectToWorld._m22)); scale * instanceScale; // 应用实例缩放 // 2. 轴向约束 Billboard 计算 (围绕世界Y轴) float3 viewDir _WorldSpaceCameraPos - objectWorldPos; viewDir.y 0; // 忽略Y轴差异保持广告牌垂直 float3 forward normalize(viewDir); float3 up float3(0, 1, 0); float3 right normalize(cross(up, forward)); // 应用原始缩放 right * scale.x; up * scale.y; forward * scale.z; // 对于轴向约束Z缩放可能影响不大但保留 // 3. 构造世界矩阵并变换顶点 float4x4 billboardWorldMatrix float4x4( float4(right, 0), float4(up, 0), float4(forward, 0), float4(objectWorldPos, 1) ); float4 vertexWorldPos mul(billboardWorldMatrix, float4(IN.positionOS.xyz, 1.0)); // 4. 应用风动效果 (可选) float time _Time.y; vertexWorldPos.xyz ApplyWind(vertexWorldPos.xyz, IN.uv, windStrength, windSpeed, time); // 5. 输出 OUT.worldPos vertexWorldPos.xyz; OUT.positionHCS TransformWorldToHClip(vertexWorldPos.xyz); OUT.uv TRANSFORM_TEX(IN.uv, _BaseMap); // 计算阴影坐标和雾效因子 #if defined(_MAIN_LIGHT_SHADOWS) || defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) OUT.shadowCoord TransformWorldToShadowCoord(vertexWorldPos.xyz); #endif OUT.fogFactor ComputeFogFactor(OUT.positionHCS.z); return OUT; } half4 frag(Varyings IN) : SV_Target { UNITY_SETUP_INSTANCE_ID(IN); half4 baseMap SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv); half4 instanceColor UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceColor); half4 finalColor baseMap * _BaseColor * instanceColor; // Alpha Test with distance fade float dist distance(IN.worldPos, _WorldSpaceCameraPos); half fadeFactor saturate((_FadeEnd - dist) / (_FadeEnd - _FadeStart)); finalColor.a * fadeFactor; clip(finalColor.a - _Cutoff); // 简单光照 (如果需要) #ifdef _MAIN_LIGHT_SHADOWS Light mainLight GetMainLight(IN.shadowCoord); half3 diffuse mainLight.color * mainLight.shadowAttenuation; finalColor.rgb * diffuse; #endif // 应用雾效 finalColor.rgb MixFog(finalColor.rgb, IN.fogFactor); return finalColor; } ENDHLSL } // 深度Only Pass用于生成阴影贴图或深度纹理 Pass { Name “DepthOnly” Tags { “LightMode” “DepthOnly” } ZWrite On ColorMask 0 // 不写入颜色 Cull Off HLSLPROGRAM #pragma vertex DepthOnlyVertex #pragma fragment DepthOnlyFragment #pragma multi_compile_instancing #include “Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl” #include “Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl” ENDHLSL } // 阴影投射Pass让广告牌能投射阴影 Pass { Name “ShadowCaster” Tags { “LightMode” “ShadowCaster” } ZWrite On ZTest LEqual ColorMask 0 Cull Off HLSLPROGRAM #pragma vertex ShadowPassVertex #pragma fragment ShadowPassFragment #pragma multi_compile_instancing #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE #include “Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl” #include “Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl” ENDHLSL } } FallBack “Universal Render Pipeline/Unlit” }这个Shader是一个功能齐全的起点你可以根据项目需求增减功能如法线贴图、高光反射或将其改造成完全面向式Billboard。