让 Quad 始终面朝摄像机 — 球形与柱状 Billboard 详解
一、什么是 Billboard?
Billboard(公告板)是一种在游戏开发中广泛使用的技术——用一块始终朝向观察者的平面(通常为 Quad)来模拟体积效果。经典应用包括粒子、烟雾、远景树木、光晕等。
核心原理
Billboard 的本质是在顶点着色器中,丢弃模型的本地轴向(local axes),改用摄像机空间中的方向向量来计算顶点位置。
原始顶点→计算相机右向量→计算相机上向量→偏移顶点→朝向相机
二、球形 Billboard(Spherical Billboard)
球形 Billboard 平面始终正对摄像机位置,无论摄像机如何移动或旋转,Quad 都会完美地"面对"它。这是粒子系统中最常用的 Billboard 类型。
Shader 实现
完整的天空球形 Billboard Shader,包含两种矩阵变换方式的对比:
Shader "Custom/SphericalBillboard" { Properties { _MainTex ("Main Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) _Softness ("Softness", Range(0,1)) = 0.1 } SubShader { Tags { "RenderPipeline"="UniversalPipeline" "Queue"="Transparent" "RenderType"="Transparent" } Pass { Name Blend SrcAlpha OneMinusSrcAlpha ZWrite Off HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" // ---------- 声明变量 TEXTURE2D(<_MainTex>); SAMPLER(sampler_MainTex); float4 _MainTex_ST; half4 _Color; float _Softness; struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings vert(Attributes input) { Varyings output = (Varyings)0; // 获取世界空间位置 float3 worldPos = TransformObjectToWorld(input.positionOS.xyz); // --- 关键:提取 CameraRight 和 CameraUp --- float camRight = -UNITY_MATRIX_V[0].x; // 矩阵第1列.x → Camera Right float camUp = -UNITY_MATRIX_V[1].x; // 矩阵第2列.x → Camera Up float3 camRightVec = float3(camRight, 0, 0); float3 camUpVec = float3(0, camUp, 0); // 计算 Billboard 尺寸 float2 size = float2(1.0, 1.0); float2 offset = input.positionOS.xy * size; // --- 顶点偏移:用相机向量替换模型轴向 --- worldPos += camRightVec * offset.x; // X轴替换为相机右向量 worldPos += camUpVec * offset.y; // Y轴替换为相机上向量 output.positionCS = TransformWorldToHClip(worldPos); output.uv = TRANSFORM_TEX(input.uv, _MainTex); return output; } half4 frag(Varyings input) : SV_Target { half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); col *= _Color; // 软边缘 alpha col.a *= 1.0 - saturate(length(input.uv - 2.0); return col; } ENDHLSL } } }关键解析:第 55-58 行是 Billboard 的核心。通过读取 View Matrix(UNITY_MATRIX_V)的第 0 列和第 1 列,分别提取相机右向量(Camera Right)和相机上向量(Camera Up)。然后用这两个向量替换 Quad 原本的本地 X 和 Y 轴方向,使平面始终朝向摄像机。
三、柱状 Billboard(Cylindrical Billboard)
柱状 Billboard 与球形 Billboard 的核心区别在于:它只绕世界 Y 轴旋转,始终保持"站立"姿态,水平方向始终朝向摄像机。这种变体常用于远景树木、公告牌、圆柱形烟囱等场景。
Shader 实现
柱状 Billboard 的关键区别:Y 轴保持世界 Up 向量,只有水平方向绕 Y 轴旋转。
Shader "Custom/CylindricalBillboard" { Properties { _MainTex ("Main Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) } SubShader { Tags { "RenderPipeline"="UniversalPipeline" "Queue"="Transparent" } Pass { Name Blend SrcAlpha OneMinusSrcAlpha ZWrite Off HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" TEXTURE2D(<_MainTex>); SAMPLER(sampler_MainTex); float4 _MainTex_ST; CBUFFER_START(UnityPerMaterial) half4 _Color; CBUFFER_END struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings vert(Attributes input) { Varyings output = (Varyings)0; // 世界空间位置 float3 worldPos = TransformObjectToWorld(input.positionOS.xyz); // --- 水平朝向:计算指向相机的水平方向 --- float3 cameraPos = GetCameraPositionWS(); float3 dirToCam = normalize(cameraPos - worldPos); // 投影到 XZ 平面(水平面),去掉 Y 分量 float3 horizontalDir = normalize(float3(dirToCam.x, 0.0, dirToCam.z)); // Billboard 水平右向量(绕 Y 轴旋转) float3 billboardRight = horizontalDir; float3 billboardUp = float3(0, 1, 0); // 世界 Up,固定不变 // 顶点偏移 float2 offset = input.positionOS.xy * float2(1.0, 1.0); worldPos += billboardRight * offset.x; worldPos += billboardUp * offset.y; output.positionCS = TransformWorldToHClip(worldPos); output.uv = TRANSFORM_TEX(input.uv, _MainTex); return output; } half4 frag(Varyings input) : SV_Target { half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); return col * _Color; } ENDHLSL } } }核心区别:第 56 行 `float3(..., 0.0, ...)` 将朝向向量投影到水平面(XZ),只保留水平分量。这样 Billboard 始终保持站立(Y 轴固定为世界 Up),只在水平面内旋转朝向摄像机。适合树木、公告牌、圆柱形物体。
四、两种 Billboard 对比
| 特性 | 球形 Billboard | 柱状 Billboard |
|---|---|---|
| 旋转轴 | 完全自由,始终朝向相机 | 仅绕世界 Y 轴旋转 |
| 典型用途 | 粒子、烟雾、光晕、特效 | 远景树木、公告牌、路灯 |
| 向量来源 | Camera Right + Camera Up | 水平朝向 + 世界 Up |
| 俯视效果 | 会看到 Billboard 侧面(穿帮) | 俯视仍保持正确朝向 |
| 计算量 | 从 View Matrix 提取(略低) | 需要计算水平朝向向量(略高) |
| 适合场景 | 空中粒子、3D 视角变化多的特效 | 地形树木、户外建筑立面 |
五、实际应用:带噪声的烟雾 Billboard
配合半透明纹理和噪声动画,可以做出自然的烟雾效果。关键思路是在片段着色器中叠加 UV 偏移,实现软边衰减和动态扰动。
烟雾片段着色器片段
// 软边缘衰减 float2 center = float2(0.5, 0.5); float dist = length(input.uv - center) * 2.0; float softEdge = 1.0 - smoothstep(0.3, 1.0, dist); // UV 噪声扰动(时间动画) float noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, input.uv * 2.0 + float2(0, -_Time.y * 0.3)).r; float alpha = softEdge * noise; col.a *= saturate(alpha);六、性能优化建议
- 批次合并(Batching)
Billboard 对象通常数量极多(数千粒子),务必开启 GPU Instancing 或 SRP Batcher,减少 Draw Call。 - 深度排序
半透明物体无法写入深度缓冲,常见排序问题是闪烁(Z-fighting)。可设置合适的 Render Queue 或使用深度预通道。 - 纹理尺寸
Billboard 依赖 alpha 通道做软边缘,远景 Billboard 建议 256×256 即可,近景可适当增大,但超过 1024 通常收益不大。 - 避免过度使用
Billboard 的本质是"欺骗",适合中远景。近景、特写镜头建议用真实 3D 模型替代,以免穿帮。 - GPU Instancing Shader
如需同时控制数千个 Billboard,使用 `UNITY_VERTEX_INPUT_INSTANCE_ID` + `UNITY_VERTEX_OUTPUT_STEREO` 配合 Instancing Buffer 传入位置/缩放/颜色。
七、总结
Billboard 是一种简单却强大的伪体积技术。通过顶点着色器中的向量替换,Quad 可以模拟复杂的体积效果。两种变体的选择取决于场景需求:
球形 Billboard→ 粒子、烟雾、爆炸光效等全方位视角的场景
柱状 Billboard→ 树木、公告牌、灯柱等保持垂直的地物
在实际项目中,可以进一步扩展为 GPU Instancing 版本,配合噪声纹理和时间变量,实现更丰富的动态效果。核心原理始终不变——用相机向量替换模型轴向,让平面"面朝观众"。