文章目录

  • 实现
    • CPU层
      • 使用简单实现方式
      • 模仿Shader层的复杂逻辑写法
      • 向量叉乘的顺序
      • 新的BB本地坐标系矩阵:newLocalMatrix可以不构建
      • 2D的Billboard
    • GPU层
      • 带有可指定是否Y轴垂直的
      • 调试带有是否Y轴垂直的GPU BB
      • WorldSpace下的BB
      • ViewSpace下的BB
      • ClipSpace下的BB(不透视大小时:固定大小)
      • ScreenSpace下的BB(固定大小,按像素控制大小)
  • 游戏中的 BB 树
  • 游戏中的 BB树,BB阴影
    • Shader
    • 运行效果
  • BB 树 Instancing 注意合批失败、绘制闪烁的问题
    • 解决方法
  • 总结
  • Project
  • References

前面翻译了一篇:OpenGL Tutorials - Billboards,这篇我们就在Unity中实现Billboard的功能。
后面一篇是使用Billboard来制作火堆热扭曲:Unity Shader - Billboard火堆热扭曲

Billboard一般应用于:

  • 单位顶部的血条,名字等
  • 树,草
  • 3D中场景中的2D人物(如:《饥荒》)
  • 粒子特效
  • 热扭曲的面片

先看看看一张ShadowGun里将Billboard应用在绿色水管两旁,当做发光效果用,即使镜头怎么转动,左右两边的面片出了Y轴,X,Z轴都依然对着镜头(有些是X,Y,Z三轴都对齐的,一般都是写偏圆球体的可以这么用,对于非球体的一般都会不对齐Y轴,这样可在镜头的高低上,表达透视,像:树,草,还有下面这个长条形的水管,都可以不对齐Y轴就可以了):

开始制作之前,先说面一下,下面我们叫Billboard简称为:BB

实现BB的方法太多了,而且根据不同的需要,处理的细节也是不一样的。

实现

CPU层

使用简单实现方式

将下面的脚本挂载到任意GameObject上,设置好cam镜头,与bb需要billboard处理的对象,alignYAxis是否对应Y轴的意思,下面的RotationType,我提供了三种写法,最要是后面对forward的.y处理alignYAxis的功能。

using UnityEngine;
/// <summary>
/// jave.lin 2020.03.24 使用Transform.LookAt(Transform target)的方式
/// </summary>
public class TransformRotationScript : MonoBehaviour
{public enum RotationType{One,Two,Three}public RotationType rotationType;public Transform cam;public Transform bb;[Range(0, 1)]public float alignYAxis = 1;public void Update(){if (rotationType == RotationType.One){bb.LookAt(cam);}else if (rotationType == RotationType.Two){bb.forward = (cam.position - bb.position).normalized;}else{bb.rotation = cam.transform.rotation;}var f = bb.forward;f.y *= alignYAxis;bb.forward = f;}
}

我们的BB对象是一个Quad面片:

运行效果:

模仿Shader层的复杂逻辑写法

Shader层, 的封装比较少,所以实现某些功能就不想Unity脚本层那么方便了。

我们先用CSharp层模拟Shader层逻辑的写法,思路是:

  • 先获得相机相对BB的模型空间的坐标:var camPos = W2L.MultiplyPoint(cam.transform.position);
  • 再利用BB的锚点指向相机的方向作为BB的面片法线:var normal = camPos - anchorPos;
  • 再通过up或是forward来作为up顶部方向向量:var up = Mathf.Abs(normal.y) > 0.999f ? Vector3.forward : Vector3.up;,Mathf.Abs(normal.y) > 0.999f的判断是为了防止法线几乎与Vector3.up相同,或是直接向量或相反方向而平行,导致选后面叉乘为零向量的问题(可以看我下面的代码描述的很清楚)。
  • 再通过法线与顶部向量叉乘获得向右的向量:var right = Vector3.Cross(normal, up).normalized;
  • 这时normal,right都是坐标基向量了,同通过叉乘获得up的基向量:up = Vector3.Cross(right, normal).normalized;
  • 再通过:right, up, normal三个坐标基向量,构建BB的新的本地坐标的变换矩阵:Matrix4x4 newLocalMatrix = new Matrix4x4(right,up,normal);
  • 再通过将原来的v.vertex的本地坐标通过newLocalMatrix变换到新的BB本地坐标:var newLocalPos = newLocalMatrix.MultiplyPoint(localPos_AfterOffsetToAnchor);
  • 最后将将新的BB本地坐标变换回世界坐标,然后更新到四个代表顶点的球体的世界坐标上:bbp.transform.position = L2W.MultiplyPoint(newLocalPos);
    private void UpdateBillboard1(){var W2L = transform.worldToLocalMatrix;var camPos = W2L.MultiplyPoint(cam.transform.position);var anchorPos = anchor.transform.localPosition;var normal = camPos - anchorPos;// reconstructs the base-vector of coordinate// 从原点到相机的方向作为法向量,还是保持垂直的Y分量normal.y *= normalYLockToCam;normal = normal.normalized;var up = Mathf.Abs(normal.y) > 0.999f ? Vector3.forward : Vector3.up;/* var up = normal.y > 0.999f ? Vector3.forward : Vector3.up;* 这句代码的理解为* 如果normal法线是相当接近与Vector3.up或是-Vector3.down的话,我们就认为这个Billboard的位置,基于是位于相机的正上方或是正下方* 所以法线几乎就是指向上,或是下,因为我们要先用一个假定是相对法线来说是向上的的向量:up,与normal来叉乘求的right的向量。* 那么首先就要保证up与normal是不平行的,因为两个平行的向量叉乘的结果一个:零向量,零向量是没有方向的* 所以Mathf.Abs(normal.y) > 0.999f这句就是判断是否与Vector3.up几乎平行,为何不用normal == Vector3.up,因为浮点数会有精度限制问题* 所以如果abs(normal.y)相当接近1的话,但有不等于1,也是有可能导致两向量叉乘还是等于0* 所以abs(normal.y)相当接近1的时,我们就将up向量假设位:Vector3.forward,即:(0,0,1),否者的话,我们就用回Vector3.up作为up向量* *//* 为何用假设的up也可以求出,正确的right呢,是因为我们先用up, normal当作是某个平面上的两个向量,这两个向量是不一定相互垂直的* 但是我们可以先使用这两个向量叉乘求出垂直于这两个向量的向量* * 这里头的叉乘需要注意:* cross(vec1, vec2),如果平面中vec1, vec2两向量叉乘* 先把该平面对准我们屏幕,cross(vec1, vec2) ,如果平面上的vec1在vec2右边,那么叉乘的结果是对着我们人的方向的* 否者如果平面上的vec1在vec2左边,叉乘对着屏幕里面的方向*/var right = Vector3.Cross(normal, up).normalized;/* cross(up, normal)出来right肯定是垂直于normal的* 然后再用两个相互垂直的right与normal计算出正确的up* 这时normal, right, up都是相互垂直的向量了* 而且这三个向量我们都归一化了,就可以作用这个Billboard的,相对normal指向镜头的本地坐标系的三个基向量*/up = Vector3.Cross(right, normal).normalized;// 用三个基向量构建:新的Billboard的坐标系 矩阵// unity 矩阵构建时是主列,向量当列排列,matrix * vector = matrix行 * vector列// shaderLab的mul(matrix, vec)是matrix行 * vec列,这点与CSharp的Matrix4x4是一样的Matrix4x4 newLocalMatrix = new Matrix4x4();newLocalMatrix.SetColumn(0, right);newLocalMatrix.SetColumn(1, up);newLocalMatrix.SetColumn(2, normal);newLocalMatrix.SetColumn(3, new Vector4(0, 0, 0, 1)); // 不需要位移newLocalMatrix.SetRow(3, new Vector4(0, 0, 0, 1)); // 因为我们不知道Vector3隐藏转换到Vector4时,不知道第四个w分量是1还是0,所以为了保证,我们都对1~3列的w分量赋值一遍var L2W = transform.localToWorldMatrix;foreach (var bbp in bbps){// anchorPos就相当于这个新本地坐标系的原点// 先将顶点的偏移到锚点的位置,这里减去锚点,可以理解为,将顶点都偏移到相对anchorPos,以anchorPos作为原点var offsetPos = bbp.sourceLocalPos - anchorPos;// 再将偏移到锚点后的顶点坐标做变换(就只旋转),做变换到新的坐标系下(就是我们前面构建的新的Billboard本地坐标系)var newLocalPos = newLocalMatrix.MultiplyPoint(offsetPos);// 相对anchorPos锚点变换完后的坐标,要记得偏移回来// 这样就可以实现相对anchorPos锚点的缩放或旋转newLocalPos += anchorPos;// 再将新的本地坐标变换到世界坐标,更新表示顶点坐标的四个球体的世界坐标bbp.transform.position = L2W.MultiplyPoint(newLocalPos);}...}

上面的代码中有几点需要给注意:

  • 向量叉乘的顺序
  • 新的BB本地坐标系矩阵:newLocalMatrix可以不构建

向量叉乘的顺序

我就画一张图:

简单理解特性为:某个面向你的平面中vec1,vec2,如果叉乘的vec1在vec2右手边,那么叉乘的结果将指向屏幕内(forward),如果vec1在左边,那么叉乘结果指向你(back)。注意:如果两个向量:vec1,vec2平行,就是:vec1与vec2的方向相同或是相反,那么叉乘结果是一个:零向量。

还有另一个需要注意:上面是左手坐标系下的向量叉乘;如果是右手坐标系下的叉乘的话,只需要将上面的forward与back对调就可以了。

新的BB本地坐标系矩阵:newLocalMatrix可以不构建

其实矩阵就是一个次多项式,矩阵的每一行就是一次多项式中的项的未知数,而被乘向量就是项的系数
上面代码中的:

Matrix4x4 newLocalMatrix = new Matrix4x4();
newLocalMatrix.SetColumn(0, right);
newLocalMatrix.SetColumn(1, up);
newLocalMatrix.SetColumn(2, normal);
newLocalMatrix.SetColumn(3, new Vector4(0, 0, 0, 1));
newLocalMatrix.SetRow(3, new Vector4(0, 0, 0, 1));

对应就是下面的矩阵:→\to→
[right.xup.xnormal.x0right.yup.ynormal.y0right.zup.znomral.z00001]\begin{bmatrix} right.x & up.x & normal.x & 0\\ right.y & up.y & normal.y & 0\\ right.z& up.z & nomral.z & 0\\ 0 & 0 & 0 & 1 \end{bmatrix} ⎣⎢⎢⎡​right.xright.yright.z0​up.xup.yup.z0​normal.xnormal.ynomral.z0​0001​⎦⎥⎥⎤​
第四行,第四列我们都不需要,一般用于位移用的,我们BB的新本地坐标都是相对Anchor点处理的,在应用BB矩阵前,我们都先位移到原点了:var offsetPos = bbp.sourceLocalPos - anchorPos;。然后应用完矩阵后,再移动回原来Anchor的偏移上:newLocalPos += anchorPos;。所以我们将第四行第四列删除,变换下面的矩阵:
[right.xup.xnormal.xright.yup.ynormal.yright.zup.znomral.z]\begin{bmatrix} right.x & up.x & normal.x\\ right.y & up.y & normal.y\\ right.z& up.z & nomral.z\\ \end{bmatrix} ⎣⎡​right.xright.yright.z​up.xup.yup.z​normal.xnormal.ynomral.z​⎦⎤​
将right,up,normal分别简写为x,y,z形式再得矩阵:

[right.xright.yright.z]=[x1x2x3],[up.xup.yup.z]=[y1y2y3],[normal.xnormal.ynormal.z]=[z1z2z3]\begin{bmatrix} right.x\\right.y\\right.z \end{bmatrix}=\begin{bmatrix} x1\\x2\\x3 \end{bmatrix}, \begin{bmatrix} up.x\\up.y\\up.z \end{bmatrix}=\begin{bmatrix} y1\\y2\\y3 \end{bmatrix}, \begin{bmatrix} normal.x\\normal.y\\normal.z \end{bmatrix}=\begin{bmatrix} z1\\z2\\z3 \end{bmatrix} ⎣⎡​right.xright.yright.z​⎦⎤​=⎣⎡​x1x2x3​⎦⎤​,⎣⎡​up.xup.yup.z​⎦⎤​=⎣⎡​y1y2y3​⎦⎤​,⎣⎡​normal.xnormal.ynormal.z​⎦⎤​=⎣⎡​z1z2z3​⎦⎤​

[right.xup.xnormal.xright.yup.ynormal.yright.zup.znormal.z]=[x1y1z1x2y2z2x3y3z3]\begin{bmatrix} right.x & up.x & normal.x\\ right.y & up.y & normal.y\\ right.z & up.z & normal.z\\ \end{bmatrix}= \begin{bmatrix} x1 & y1 & z1\\ x2 & y2 & z2\\ x3 & y3 & z3\\ \end{bmatrix} ⎣⎡​right.xright.yright.z​up.xup.yup.z​normal.xnormal.ynormal.z​⎦⎤​=⎣⎡​x1x2x3​y1y2y3​z1z2z3​⎦⎤​

而我们的原始偏移后的坐标:var offsetPos = bbp.sourceLocalPos - anchorPos,可以表达为:
[abc]\begin{bmatrix} a\\ b\\ c\\ \end{bmatrix} ⎣⎡​abc​⎦⎤​
因为本地偏移后的坐标我们是知道的,所以作为参数a,b,c来记,而上面的矩阵我们是不知道的,是通过right=cross(normal,up),up=cross(right,normal)求得的,而矩阵先当作我们以前学习的未知数:
将矩阵(未知数)与顶点(常数项)结合,等于新的顶点,可以写为:
[abbbbbcbb]\begin{bmatrix} a_{bb}\\ b_{bb}\\ c_{bb} \end{bmatrix} ⎣⎡​abb​bbb​cbb​​⎦⎤​

其中XXXbbXXX_{bb}XXXbb​是BB下新的坐标顶点
但在Unity的CSharp的Matrix4x4封装类中,Matrix4x4.MultiplePoint(Vector3)是一个矩阵行x向量列的方式,所以可以表达为下面的方式:
[x1y1z1x2y2z2x3y3z3].行×[abc].列=[abbbbbcbb]\begin{bmatrix} x1 & y1 & z1\\ x2 & y2 & z2\\ x3 & y3 & z3\\ \end{bmatrix}.行 \times \begin{bmatrix} a\\b\\c \end{bmatrix}.列= \begin{bmatrix} a_{bb}\\ b_{bb}\\ c_{bb} \end{bmatrix} ⎣⎡​x1x2x3​y1y2y3​z1z2z3​⎦⎤​.行×⎣⎡​abc​⎦⎤​.列=⎣⎡​abb​bbb​cbb​​⎦⎤​

该式子其实可对应为:
a∗x1+b∗y1+c∗z1=abba∗x2+b∗y2+c∗z2=bbba∗x3+b∗y3+c∗z3=cbba*x1+b*y1+c * z1 = a_{bb}\\ a*x2+b*y2+c * z2 = b_{bb}\\ a*x3+b*y3+c * z3 = c_{bb} a∗x1+b∗y1+c∗z1=abb​a∗x2+b∗y2+c∗z2=bbb​a∗x3+b∗y3+c∗z3=cbb​
而未知数的矩阵,我们求得后,代入公式,就可以得到:XXXbbXXX_{bb}XXXbb​BB下新的坐标顶点
经过前面的描述,理解后,还有向量乘以标量:Vector3 * Scale = (Scale*x,Scale*y,Scale*z),所以var newLocalPos = newLocalMatrix.MultiplyPoint(offsetPos);可以写成不使用矩阵的方式,这样就不用new Matrix4x4了,如下:

var newLocalPos = right * offsetPos.x + up * offsetPos.y + normal * offsetPos.z;

所以经过简化后,删除注释后(比较方便阅读)的写法:

    private void UpdateBillboard2(){var W2L = transform.worldToLocalMatrix;var camPos = W2L.MultiplyPoint(cam.transform.position);var anchorPos = anchor.transform.localPosition;var normal = camPos - anchorPos;normal.y *= normalYLockToCam;normal.Normalize();var up = Mathf.Abs(normal.y) > 0.999f ? Vector3.forward : Vector3.up;var right = Vector3.Cross(normal, up);right.Normalize();up = Vector3.Cross(right, normal);up.Normalize();var L2W = transform.localToWorldMatrix;foreach (var bbp in bbps){var offsetPos = bbp.sourceLocalPos - anchorPos;var newLocalPos = anchorPos + right * offsetPos.x + up * offsetPos.y + normal * offsetPos.z;bbp.transform.position = L2W.MultiplyPoint(newLocalPos);}}

最后创建一个Quad网格显示:

    private void CreateQuadHandle(){if (createQuad){if (meshFilter == null){meshFilter = gameObject.AddComponent<MeshFilter>();meshRenderer = gameObject.AddComponent<MeshRenderer>();meshRenderer.material = quadMat;mesh = new Mesh();mesh.MarkDynamic();meshFilter.mesh = mesh;mesh.vertices = vertices;mesh.uv = uvs;mesh.triangles = indices;mesh.RecalculateBounds();}for (int i = 0; i < bbps.Length; i++){vertices[i] = bbps[i].transform.localPosition;}mesh.vertices = vertices;//transform.rotation = cam.transform.rotation;}if (meshRenderer) meshRenderer.enabled = createQuad;}

运行效果如下:(中间的绿色的光,就是一张面片,即使我怎么装着个片面,说是转动镜头,它都一直朝向镜头)

如果你看不出来效果,那我就转动一下没有BB功能的面片:

明显没有BB的面片就不会自动朝向镜头

2D的Billboard

引用OpenGL教程里的一段CPP代码,该代码在前面翻译了一篇:OpenGL Tutorials - Billboards也有:

// Everything here is explained in Tutorial 3 ! There's nothing new.
glm::vec4 BillboardPos_worldspace(x,y,z, 1.0f);
glm::vec4 BillboardPos_screenspace = ProjectionMatrix * ViewMatrix * BillboardPos_worldspace;
BillboardPos_screenspace /= BillboardPos_screenspace.w;if (BillboardPos_screenspace.z < 0.0f){// Object is behind the camera, don't display it.
}

就是将BB面片的世界坐标转到NDC下,判断z<0就不渲染。原文:OpenGL Tutorials - Billboards。
改成CSharp,差不多就是这样:(伪代码)

Vector4 localPos = new Vector4(x,y,z,1);
Vector4 clipPos = MVP * localPos;
Vector4 ndc = clipPos / clipPos.w;
if (ndc.z < 0.0f) {// NDC.z超出0不绘制,我记得DX是-1~1的NDC.Z范围,OPENGL的话是:0~1,忘记了,有兴趣可以去查一下,这些差异重视需要的时候再去查,谁会用心记这些平台导致的差异(手动哭笑一下)
}

关于什么是NDC,可以查考我之前写的:OpenGL Transformation 几何变换的顺序概要(MVP,NDC,Window坐标变换过程)

GPU层

有时候,如果硬件允许的情况下,如果GPU负担不重时,是可以考虑将Billboard的面向相机的计算逻辑放到GPU上的VS处理的,特别是一个粒子,如果数量多,分担CPU计算就更加明显:

带有可指定是否Y轴垂直的

在《Unity Shader 入门精要》-11.3.2也有类似的代码处理,更具体的描述其实与我在CPU层中第二种比较复杂的方式的是一模一样的,所以原理什么的,直接看上面的描述也是可以的,我在这里就不再重复了,shader代码里注释我也尽量不添加了:
CSharp代码:

using UnityEngine;/// <summary>
/// jave.lin 2020.03.24 测试Shader来控制的Billboard
/// </summary>
public class BillboardScene3 : MonoBehaviour
{private static int _NormalYLockToCam_hash = Shader.PropertyToID("_NormalYLockToCam");private static int _AnchorPos_hash = Shader.PropertyToID("_AnchorPos");private static int _CamPos_hash = Shader.PropertyToID("_CamPos");public Transform cam;           // 相机public Transform anchor;        // Billboard的锚点public Transform billboard;     // 需要BB的对象// 1:完全是;// 0:将指向法线的Y分量设置为0//      不控制Billboard朝向镜头的Y分量//      一般树,草,都会设置这个为0就好[Header("是否完全将法线Y面指向镜头")][Range(0, 1)]public float normalYLockToCam = 1f;    // 法线Y趋向镜头的强度public Material mat;private void Update(){var W2L = billboard.worldToLocalMatrix;mat.SetFloat(_NormalYLockToCam_hash, normalYLockToCam);mat.SetVector(_AnchorPos_hash, W2L.MultiplyPoint(anchor.position));mat.SetVector(_CamPos_hash, W2L.MultiplyPoint(cam.position));}
}

将上面的CSharp脚本挂载到任意的GameObject上,设置好:

    public Transform cam;           // 相机public Transform anchor;        // Billboard的锚点public Transform billboard;     // 需要BB的对象

三个字段就好了。

接着就是Shader,也比较简单,和我上面的

// jave.lin 2020.03.24 - Billboard
Shader "Custom/T2_GpuBBMat" {Properties {[KeywordEnum(T1,T2)] _CAM_POS ("Camera Pos Type", Float) = 0[MaterialToggle(_BB_ON)] _BB_ON ("Billboard On", Float) = 1_MainTex ("Texture", 2D) = "white" {}_MainColor ("Color", Color) = (1,1,1,1)_NormalYLockToCam ("Normal's Y Lock To Cam", Range(0, 1)) = 1_AnchorPos ("Anchor Pos", Vector) = (0, 0, 0, 0)}SubShader {Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" "DisableBatching"="True" }LOD 100 // ZWrite Off// Cull Off// Blend SrcAlpha OneMinusSrcALphaBlend One OneCull OffPass {CGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile _CAM_POS_T1 _CAM_POS_T2#pragma multi_compile _ _BB_ON// #pragma enable_d3d11_debug_symbols#include "UnityCG.cginc"struct appdata {float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f {float4 vertex : SV_POSITION;float2 uv : TEXCOORD0;};sampler2D _MainTex;float4 _MainTex_ST;fixed4 _MainColor;float _NormalYLockToCam;float3 _AnchorPos;          // 本地坐标的锚点float3 _CamPos;             // 镜头相对本地坐标的位置v2f vert (appdata v) {v2f o;#if _BB_ON// 这里的注释我就不写了,逻辑上与CSharp脚本的差不多// 想要了解,就直接查看CSharp脚本吧// 下面的代码部分是可以优化写法的// 但是为了可读性,我就不优化了#if _CAM_POS_T1float3 camPos = _CamPos;#elsefloat3 camPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1)).xyz;#endiffloat3 normal = camPos - _AnchorPos;normal.y *= _NormalYLockToCam;normal = normalize(normal);float3 up = abs(normal.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);float3 right = normalize(cross(normal, up));// * -1;up = normalize(cross(right, normal));// * -1;// 与CSharp脚本相同,这里是ShaderLab是主列,CSharp脚本的Matrix4x4是主列float3x3 newLocalMatrix = { /*col0*/right, /*col1*/up, /*col2*/normal };newLocalMatrix = transpose(newLocalMatrix); // row0:right, row1:up, row2:normalfloat3 offsetPos = v.vertex.xyz - _AnchorPos;// 上面的newLocalMatrix可以不用transpose,可以将下面的mul(matrix,vec)改为mul(vec, matrix)就好了float3 newLocalPos = mul(newLocalMatrix, offsetPos);newLocalPos += _AnchorPos;o.vertex = UnityObjectToClipPos(newLocalPos);#elseo.vertex = UnityObjectToClipPos(v.vertex.xyz);#endifo.uv = TRANSFORM_TEX(v.uv, _MainTex);return o;}fixed4 frag (v2f i) : SV_Target {fixed4 col = tex2D(_MainTex, i.uv);return col * _MainColor * _MainColor.a;}ENDCG}}
}

运行效果就不发了,和前面的没什么区别

调试带有是否Y轴垂直的GPU BB

CSharp

using UnityEngine;#if UNITY_EDITOR
using UnityEditor;
#endif/// <summary>
/// jave.lin 2020.03.22 测试Shader来控制的Billboard
/// </summary>
public class T2_GpuBB_DebuggingBBScript : MonoBehaviour
{private static int _NormalYLockToCam_hash = Shader.PropertyToID("_NormalYLockToCam");private static int _AnchorPos_hash = Shader.PropertyToID("_AnchorPos");private static int _CamPos_hash = Shader.PropertyToID("_CamPos");public Transform cam;public Transform anchor;        // Billboard的锚点public Transform billboarCube_X;  // Billboard的Cubepublic Transform billboarCube_Y;  // Billboard的Cubepublic Transform billboarCube_Z;  // Billboard的Cube// 1:完全是;// 0:将指向法线的Y分量设置为0//      不控制Billboard朝向镜头的Y分量//      一般树,草,都会设置这个为0就好[Header("是否完全将法线Y面指向镜头")][Range(0, 1)]public float normalYLockToCam = 1f;    // 法线Y趋向镜头的强度private void Update(){Transform[] trs = new Transform[] { billboarCube_X, billboarCube_Y, billboarCube_Z };foreach (var item in trs){var W2L = item.worldToLocalMatrix;var mat = item.GetComponent<MeshRenderer>().sharedMaterial;mat.SetFloat(_NormalYLockToCam_hash, normalYLockToCam);mat.SetVector(_AnchorPos_hash, W2L.MultiplyPoint(anchor.position));mat.SetVector(_CamPos_hash, W2L.MultiplyPoint(cam.position));}}
}

将这个CSharp脚本挂载都任意GameObject上,设置好:

    public Transform cam;public Transform anchor;        // Billboard的锚点public Transform billboarCube_X;  // Billboard的Cubepublic Transform billboarCube_Y;  // Billboard的Cubepublic Transform billboarCube_Z;  // Billboard的Cube

billboarCube_X,Y,Z就是我们要调试BB的重构BB正交基矩阵的三个基向量显示用的三个Cube

Shader代码:

// jave.lin 2020.03.22 - 调试Billboard 的立方体材质shader
Shader "Custom/DebuggingBillboardCube" {Properties {[KeywordEnum(T1,T2)] _CAM_POS ("Camera Pos Type", Float) = 0[KeywordEnum(X,Y,Z)] _AXIS ("AxisType", Float) = 0_AxisScale ("AxisScale", Range(0, 10)) = 2_MainTex ("Texture", 2D) = "white" {}_MainColor ("Color", Color) = (1,1,1,1)_NormalYLockToCam ("Normal's Y Lock To Cam", Range(0, 1)) = 1_AnchorPos ("Anchor Pos", Vector) = (0, 0, 0, 0)}SubShader {Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" "DisableBatching"="True" }LOD 100  ZWrite OffCull OffPass {CGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile _CAM_POS_T1 _CAM_POS_T2#pragma multi_compile _AXIS_X _AXIS_Y _AXIS_Z// #pragma enable_d3d11_debug_symbols#include "UnityCG.cginc"struct appdata {float4 vertex : POSITION;float2 uv : TEXCOORD0;uint id : SV_VertexID;};struct v2f {float4 vertex : SV_POSITION;float2 uv : TEXCOORD0;fixed4 c : TEXCOORD1;};sampler2D _MainTex;float4 _MainTex_ST;fixed4 _MainColor;float _NormalYLockToCam;float3 _AnchorPos;          // 本地坐标的锚点float3 _CamPos;             // 镜头相对本地坐标的位置float _AxisScale;v2f vert (appdata v) {v2f o;// 这里的注释我就不写了,逻辑上与CSharp脚本的差不多// 想要了解,就直接查看CSharp脚本吧// 下面的代码部分是可以优化写法的// 但是为了可读性,我就不优化了#if _CAM_POS_T1float3 camPos = _CamPos;#elsefloat3 camPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1)).xyz;#endiffloat3 normal = camPos - _AnchorPos;normal.y *= _NormalYLockToCam;normal = normalize(normal);float3 up = abs(normal.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);float3 right = normalize(cross(normal, up));// * -1;up = normalize(cross(right, normal));// * -1;// 与CSharp脚本相同,这里是ShaderLab是主列,CSharp脚本的Matrix4x4是主列float3x3 newLocalMatrix = { /*col0*/right, /*col1*/up, /*col2*/normal };newLocalMatrix = transpose(newLocalMatrix); // row0:right, row1:up, row2:normal// float3x3 newLocalMatrix = { float3(1,0,0), float3(0,1,0), float3(0,0,1) };float3 offsetPos = v.vertex.xyz - _AnchorPos;// 上面的newLocalMatrix可以不用transpose,可以将下面的mul(matrix,vec)改为mul(vec, matrix)就好了float3 newLocalPos = mul(newLocalMatrix, offsetPos);// float3 newLocalPos = // right * offsetPos.x +// up * offsetPos.y +// normal * offsetPos.z;newLocalPos += _AnchorPos;o.c = 1;float3 vertexPos = v.vertex;// newLocalPos = vertexPos;// 控制1,3顶点// if (v.id == 1 || v.id == 3)// {//     #if _AXIS_X//     vertexPos = newLocalPos * _AxisScale;//     #elif _AXIS_Y//     vertexPos = newLocalPos * _AxisScale;//     #else // _AXIS_Zx//     vertexPos = newLocalPos * _AxisScale;//     #endif// }// right = float3(1,0,0);// up = float3(0,1,0);// normal = float3(0,0,-1);#if _AXIS_Xif (v.id == 21 || v.id == 10 || v.id ==  4 ||v.id == 12 || v.id ==  6 || v.id == 20 ||v.id == 22 || v.id ==  2 || v.id ==  8 ||v.id == 23 || v.id ==  0 || v.id == 13){// vertexPos = newLocalPos * _AxisScale;vertexPos = newLocalPos + (right * _AxisScale);o.c = fixed4(1,0,0,1);}#elif _AXIS_Yif (v.id ==  5 || v.id == 18 || v.id == 11 ||v.id == 10 || v.id == 21 || v.id ==  4 ||v.id ==  9 || v.id ==  3 || v.id == 17 ||v.id == 22 || v.id ==  2 || v.id ==  8){// vertexPos = newLocalPos * _AxisScale;vertexPos = newLocalPos + (up * _AxisScale);o.c = fixed4(0,1,0,1);}#else // _AXIS_Zif (v.id == 22 || v.id ==  2 || v.id ==  8 ||v.id ==  9 || v.id ==  3 || v.id == 17 ||v.id == 16 || v.id == 14 || v.id ==  1 ||v.id == 23 || v.id == 13 || v.id ==  0){// vertexPos = newLocalPos * _AxisScale;vertexPos = newLocalPos + (normal * _AxisScale);o.c = fixed4(0,0,1,1);}#endifo.vertex = UnityObjectToClipPos(vertexPos);o.uv = TRANSFORM_TEX(v.uv, _MainTex);return o;}fixed4 frag (v2f i) : SV_Target {// #if _AXIS_X//     return fixed4(1,0,0,1);// #elif _AXIS_Y//     return fixed4(0,1,0,1);// #else // _AXIS_Z//     return fixed4(0,0,1,1);// #endifreturn i.c;fixed4 col = tex2D(_MainTex, i.uv);return col * _MainColor * _MainColor.a;}ENDCG}}
}

运行调试效果:

每个Cube都设置单独的材质,然后依次调整AxisType材质属性,X轴色红,Y轴绿色,Z轴蓝色。
还可以调整轴的长度,如下GIF:

然后可以调整他们的容器的旋转,或是镜头旋转,来查看GPU的VS的Billboard轴向实时计算是否正确:

上面的红色轴就是right绿色轴up蓝色轴toCameraDir瞄准镜头的方向)

在Shader中我们看到:_AXIS_X,Y,Z都有判断很多ID,这些ID就是unity Cube Primitive的顶点顺序ID号,你可能会好奇,这些ID我是怎么知道的,因为我用了对ID==0~23的分别使用了红色的标记顶点颜色,然后一个一个的记录下来的,为了调试这是有多痛苦(手动哭笑一下),下面就是我记录的顶点ID的图片:

WorldSpace下的BB

在我之前翻译的OpenGL Billboard,也有在WorldSpace下的处理:OpenGL Tutorials - Billboards

我将下面翻译的引进来:我用个分割线间隔起来


CameraRight_worldspace = {ViewMatrix[0][0], ViewMatrix[1][0], ViewMatrix[2][0]}
CameraUp_worldspace = {ViewMatrix[0][1], ViewMatrix[1][1], ViewMatrix[2][1]}

(译者jave.lin:这里我要说明一下,ViewMatrix是视图矩阵,不是视图矩阵的逆矩阵,但其实使用其逆矩阵的方式获取会更加简便,只不过会需要更多性能,只要获取ViewMatrix的逆矩阵即可,而ViewMatrix是个正交基矩阵,所以其逆矩阵就是其转置矩阵,所以right = transpose(ViewMatrix)[0], up = traspose(ViewMatrix)[1]

一旦获取这些数据后,就可以非常简单的计算出最终的顶点位置:

vec3 vertexPosition_worldspace =particleCenter_wordspace+ CameraRight_worldspace * squareVertices.x * BillboardSize.x+ CameraUp_worldspace * squareVertices.y * BillboardSize.y;
  • particleCenter_worldspace 正如其名字描述的,就是billboard的世界坐标中心点位置。它是指定为uniform vec3
  • squareVertices 是网格的原点。squareVertices.x-0.5 就是左边的顶点,因此根据他来位移就是相对镜头左边方向移动(因为是乘以 CameraRight_worldspace的)
  • BillboardSize 是大小,使用世界单位,也是个uniform。

OK上面都是引用内容

所以我们获取相机的Right,Up都可以直接通过ViewMatrix的逆矩阵的第一行与第二行即可拿到。在Unity中,Shader获取ViewMatrix的逆矩阵会很简单,因为内置了一些宏定义: #define UNITY_MATRIX_IT_MV unity_MatrixITMV,所以下面我们就直接使用:UNITY_MATRIX_IT_MV
WorldSpace下的BB 的Shader如下:(可以看我写的注释)

// jave.lin 2020.03.25 WorldSpace下的BB
Shader "Custom/T4_GpuBB_WorldSapce" {Properties {_MainTex ("Texture", 2D) = "white" {}_BillboardSize ("BillboardSize", Vector) = (1, 1, 0, 0)}SubShader {Tags { "RenderType"="Transparent" "Queue"="Transparent" }LOD 100 Cull Off Blend One One ZWrite OffPass {CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct appdata {float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f {float4 vertex : SV_POSITION;float2 uv : TEXCOORD0;};sampler2D _MainTex;float4 _MainTex_ST;float4 _BillboardSize;v2f vert (appdata v) {v2f o;// 先获取BB的世界坐标点float4 worldPos = mul(unity_ObjectToWorld, float4(0,0,0,1));// 再获取cam的世界坐标下的Right,Up,通过ViewMatrix的逆矩阵的第一行与第二行float3 camRight = UNITY_MATRIX_IT_MV[0].xyz;float3 camUp = UNITY_MATRIX_IT_MV[1].xyz;worldPos.xyz += // 使用原始顶点的大小与缩放系数:_BillboardSize来控制世界坐标下的顶点位置camRight * v.vertex.x * _BillboardSize.x + camUp * v.vertex.y * _BillboardSize.y;// 最后将按照camRight,camUp重新调整后的worldPos,再转换到clipSpace,片段需要o.vertex = mul(UNITY_MATRIX_VP, worldPos);o.uv = TRANSFORM_TEX(v.uv, _MainTex);return o;}fixed4 frag (v2f i) : SV_Target {fixed4 col = tex2D(_MainTex, i.uv);return col;}ENDCG}}
}

ViewSpace下的BB

// jave.lin 2020.3.25 在View Space 下处理的Billboard
// jave.lin 2020.3.25 在View Space 下处理的Billboard
Shader "Custom/T5_GpuBB_ViewSapce" {Properties {_MainTex ("Texture Image", 2D) = "white" {}_BillboardSize ("BillboardSize", Vector) = (1, 1, 0, 0)}SubShader {Tags { "RenderType"="Transparent" "Queue"="Transparent" }Pass {ZWrite Off Cull Off Blend One One CGPROGRAM#pragma vertex vert  #pragma fragment fragsampler2D _MainTex;        float4 _BillboardSize;struct a2v {float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f {float4 vertex : SV_POSITION;float2 uv : TEXCOORD0;};v2f vert(a2v v) {v2f o;float4 viewSpacePos = mul(UNITY_MATRIX_MV, float4(0.0, 0.0, 0.0, 1.0));viewSpacePos.xy += v.vertex.xy * _BillboardSize.xy;o.vertex = mul(UNITY_MATRIX_P, viewSpacePos);o.uv = v.uv;return o;}float4 frag(v2f i) : SV_Target {return tex2D(_MainTex, i.uv);   }ENDCG}}
}

这种方式和WorldSpace的差不多:

ClipSpace下的BB(不透视大小时:固定大小)

Shader代码也比较简单,具体看注释,我都说明的比较详细

// jave.lin 2020.3.25 Clip Space 下处理的Billboard
Shader "Custom/T6_GpuBB_ClipSpace" {Properties {[MaterialToggle(_PERSPECTIVE_SIZE)] _PERSPECTIVE_SIZE ("Perspective Size", Float) = 0_MainTex ("Texture Image", 2D) = "white" {}_BillboardSize ("BillboardSize", Vector) = (1, 1, 0, 0)}SubShader {Tags { "RenderType"="Transparent" "Queue"="Transparent" }Pass {ZWrite Off Cull Off Blend One One CGPROGRAM#pragma vertex vert  #pragma fragment frag#pragma multi_compile _ _PERSPECTIVE_SIZEsampler2D _MainTex;        float4 _BillboardSize;struct a2v {float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f {float4 vertex : SV_POSITION;float2 uv : TEXCOORD0;};v2f vert(a2v v) {v2f o;// 將先BB的中心变换到ClipSpaceo.vertex = UnityObjectToClipPos(float3(0,0,0));// 是否需要透視大小#if _PERSPECTIVE_SIZE// 这里手动透视除法,即使底层流水线再次透视除法也关系,因为我们前面透视除法后: o.vertex.w == 1o.vertex /= o.vertex.w;#endif// 在ClipSpace下做尺寸控制o.vertex.xy += v.vertex.xy * _BillboardSize.xy;o.uv = v.uv;return o;}float4 frag(v2f i) : SV_Target {return tex2D(_MainTex, i.uv);   }ENDCG}}
}

这里留意我开了一个_PERSPECTIVE_SIZEMaterialToggle开关,这样可以关闭或是开启:透视大小的效果。

o.vertex /= o.vertex.w;

这句话我注释里也描述了:这里手动透视除法,即使底层流水线再次透视除法也关系,因为我们前面透视除法后: o.vertex.w == 1

ScreenSpace下的BB(固定大小,按像素控制大小)

其实 ScreenSpace下的BB与ClipSpace很类似,只不过我们将控制大小的系数的作用改为:以像素为单位来控制,这样也符合Screen的控制方式。

下面是Shader代码:

// jave.lin 2020.3.25 Screen Space 下处理的Billboard - 起始于Clip Space下的是很类似,只不过我们给外部控制大小的单位是:像素
Shader "Custom/T7_GpuBB_ScreenSpace" {Properties {_MainTex ("Texture Image", 2D) = "white" {}_PixelSize ("Pixel Size", Vector) = (1, 1, 0, 0)}SubShader {Tags { "RenderType"="Transparent" "Queue"="Transparent" }Pass {ZWrite Off Cull Off Blend One One CGPROGRAM#pragma vertex vert  #pragma fragment fragsampler2D _MainTex;        float4 _PixelSize;struct a2v {float4 vertex : POSITION;float2 uv : TEXCOORD0;uint id : TEXCOORD1;};struct v2f {float4 vertex : SV_POSITION;float2 uv : TEXCOORD0;};v2f vert(a2v v) {v2f o;// 將先BB的中心变换到ClipSpaceo.vertex = UnityObjectToClipPos(float3(0,0,0));// 这里手动透视除法,即使底层流水线再次透视除法也关系,因为我们前面透视除法后: o.vertex.w == 1o.vertex /= o.vertex.w;// 在ClipSpace下做尺寸控制,并以像素为单位的方式来控制,已达到屏幕下控制大小的感觉// 使用到了:_ScreenParams,的全局uniform,下面列出了Unity对_ScreenParams的描述与定义// x = width// y = height// z = 1 + 1.0/width// w = 1 + 1.0/height// uniform vec4 _ScreenParams;o.vertex.xy += v.vertex.xy * (_PixelSize.xy / _ScreenParams.xy);o.uv = v.uv;return o;}float4 frag(v2f i) : SV_Target {return tex2D(_MainTex, i.uv);   }ENDCG}}
}

主要与ClipSpace的区别,看这么一句:

o.vertex.xy += v.vertex.xy * (_PixelSize.xy / _ScreenParams.xy);

使用到了:_ScreenParams,的全局uniform,下面列出了Unity对_ScreenParams的描述与定义

            // x = width// y = height// z = 1 + 1.0/width// w = 1 + 1.0/height// uniform vec4 _ScreenParams;

_PixelSize是材质外部可以控制的大小,单位是:像素,这样无论什么分辨率下都可以按固定大小来控制。


游戏中的 BB 树

// jave.lin
Shader "BB/WS_AlphaTest" {Properties{_MainTex("Texture", 2D) = "white" {}_BBSizePos("Billboard Position & Size", Vector) = (1, 1, 0, 0)_Culloff ("Alpha Cull Off", Range(0, 1)) = 0.5}SubShader{Tags { "RenderType" = "Opaque" "Queue" = "AlphaTest" }LOD 100 Cull Off Blend One Zero ZWrite On ZTest LEqualPass {CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct appdata {float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f {float4 vertex : SV_POSITION;float2 uv : TEXCOORD0;};sampler2D _MainTex;float4 _MainTex_ST;float4 _BBSizePos;fixed _Culloff;v2f vert(appdata v) {v2f o = (v2f)0;float4 worldPos = mul(unity_ObjectToWorld, float4(0,0,0,1));float3 R = UNITY_MATRIX_IT_MV[0].xyz;float3 U = UNITY_MATRIX_IT_MV[1].xyz;worldPos.xyz += R * v.vertex.x * _BBSizePos.x + U * v.vertex.y * _BBSizePos.y;worldPos.xy += _BBSizePos.zw;o.vertex = mul(UNITY_MATRIX_VP, worldPos);o.uv = TRANSFORM_TEX(v.uv, _MainTex);return o;}fixed4 frag(v2f i) : SV_Target {fixed4 col = tex2D(_MainTex, i.uv);clip(col.a - _Culloff);return col;}ENDCG}}
}

效果


游戏中的 BB树,BB阴影


Shader

Shader "BB/Default - GPUInstance - WS_AlphaTest" {Properties{_MainTex("Texture", 2D) = "white" {}_BBSizePos("Billboard Size & Position", Vector) = (1, 1, 0, 0)_Cutoff("Alpha Cut Off", Range(0, 1)) = 0.5_NormalYLockToCam("Normal's Y Lock To Cam", Range(0, 1)) = 1_AnchorPos("Anchor Pos", Vector) = (0, 0, 0, 0)}SubShader{Tags { "RenderType" = "Opaque" "Queue" = "AlphaTest" }LOD 100 Cull Off Blend One Zero ZWrite On ZTest LEqualPass {// Bill boardCGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile_fog#pragma multi_compile_instancing#pragma multi_compile _ LIGHT_AREA_CIRCLE#include "UnityCG.cginc"#include "../CGInclude/LightCircleEffectCommon.cginc"#include "../CGInclude/FogLinearCommon.cginc"struct appdata {float4 vertex : POSITION;float2 uv : TEXCOORD0;
#if INSTANCING_ONUNITY_VERTEX_INPUT_INSTANCE_ID
#endif};struct v2f {float4 vertex : SV_POSITION;float2 uv : TEXCOORD0;float3 worldPos : TEXCOORD1;V2F_FOG_COORD_TEXCOORD(2)
#if INSTANCING_ON//UNITY_VERTEX_INPUT_INSTANCE_ID
#endif};sampler2D _MainTex;float4 _MainTex_ST;float4 _BBSizePos;fixed _Cutoff;float _NormalYLockToCam;float3 _AnchorPos;v2f vert(appdata v) {v2f o = (v2f)0;#if INSTANCING_ONUNITY_SETUP_INSTANCE_ID(v)//UNITY_TRANSFER_INSTANCE_ID(v, o);
#endiffloat3 camPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1)).xyz;float3 normal = camPos - _AnchorPos;normal.y *= _NormalYLockToCam;normal = normalize(normal);float3 up = abs(normal.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);float3 right = normalize(cross(normal, up));// * -1;up = normalize(cross(right, normal));// * -1;float3 offsetPos = v.vertex.xyz - _AnchorPos;float3x3 newLocalMatrix = { /*col0*/right, /*col1*/up, /*col2*/normal };float3 newLocalPos = mul(offsetPos, newLocalMatrix);newLocalPos += _AnchorPos;newLocalPos.xyz += right * v.vertex.x * _BBSizePos.x + up * v.vertex.y * _BBSizePos.y;newLocalPos.xy += _BBSizePos.zw;o.vertex = UnityObjectToClipPos(newLocalPos);o.worldPos = mul(unity_ObjectToWorld, float4(newLocalPos, 1)).xyz;o.uv = TRANSFORM_TEX(v.uv, _MainTex);V2F_HANDLE_FOG_COORD(o.vertex.z)return o;}fixed4 frag(v2f i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uv);clip(col.a - _Cutoff);CAL_LIGHT_CIRCLE(i.worldPos.xyz, col)FAG_LERP_FOG_COL(i.fogCoord.x, col)return col;}ENDCG}Pass {// ShadowCasterTags { "LightMode" = "ShadowCaster" "RenderType" = "AlphaTest" "Queue" = "AlphaTest" }ZWrite On ZTest LEqual Cull OffCGPROGRAM#pragma vertex vert#pragma fragment frag#pragma target 2.0#pragma multi_compile_shadowcaster#pragma multi_compile_instancing#include "UnityCG.cginc"struct appdata {float4 vertex : POSITION;float2 uv : TEXCOORD0;
#if INSTANCING_ONUNITY_VERTEX_INPUT_INSTANCE_ID
#endif};struct v2f {V2F_SHADOW_CASTER;float2 uv : TEXCOORD1;UNITY_VERTEX_OUTPUT_STEREO};sampler2D _MainTex;float4 _MainTex_ST;float4 _BBSizePos;fixed _Cutoff;float _NormalYLockToCam;float3 _AnchorPos;v2f vert(appdata_base v){v2f o;
#if INSTANCING_ONUNITY_SETUP_INSTANCE_ID(v);
#endifUNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);// jave.lin : 这里使用的是 灯光方向来处理float3 wpos = mul(unity_WorldToObject, v.vertex).xyz;float3 camPos = UnityWorldSpaceLightDir(wpos);float3 normal = camPos - _AnchorPos;normal.y *= _NormalYLockToCam;normal = normalize(normal);float3 up = abs(normal.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);float3 right = normalize(cross(normal, up));// * -1;up = normalize(cross(right, normal));// * -1;float3 offsetPos = v.vertex.xyz - _AnchorPos;float3x3 newLocalMatrix = { /*col0*/right, /*col1*/up, /*col2*/normal };float3 newLocalPos = mul(offsetPos, newLocalMatrix);newLocalPos += _AnchorPos;newLocalPos.xyz += right * v.vertex.x * _BBSizePos.x + up * v.vertex.y * _BBSizePos.y;newLocalPos.xy += _BBSizePos.zw;v.vertex.xyz = newLocalPos;TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);return o;}float4 frag(v2f i) : SV_Target{fixed4 texcol = tex2D(_MainTex, i.uv);clip(texcol.a - _Cutoff);SHADOW_CASTER_FRAGMENT(i)}ENDCG}}
}

主要看:Pass ShadowCaster 部分

ShadowCaster 的思路也是比较简单的,就是 BB 朝向不再是相机,而是朝向光源方向即可

运行效果


BB 树 Instancing 注意合批失败、绘制闪烁的问题

一般是由于 Unity 项目配置中 开启了 DynamicBatch 导致的:

解决方法

  • ShaderLab 中添加 Tags :"DisableBatching" = "True"
  • 或是 ShaderLab 不添加 Tags 也行,如果说你的项目不需要 unity 的 DynamicBatch 的功能的话,可以通过:Edit/Project Settings.../Player/Other Settings/Dynamic Batching 的复选框的勾 去掉,如下图

具体参考: Unity - Instancing 合批失败、绘制闪烁的问题解决(Dynamic Batching 动态合批导致)


总结

Billboard应用还是非常多的,但是要根据需求来选择Billboard的类型也是很重要的。
如:

  • 远处的树,草,灯,或是ShadowGun里的水管外发光,都可以用Y轴对齐的旋转就好。
  • 热扭曲的,直接放在View Space下按顶点偏移就好。这个在References Heat Distortion Shader Tutorial 有实现,下一篇我就根据他的来实现一遍,要动手写,至少要写一次,光看,是很容易理解的,但就怕Unity有什么坑,快速实现过一篇就好了。
  • 粒子的Billboard,我们在用Unity自带的粒子系统也会看到Render那项也有RenderMode设置,可以设置为:Billboard,它就是视图+缩放系数控制的。
  • 还有就是是否需要保留透视的近大远小的效果来选择,如头顶上的名字,或是血条,一般都是使用这种方式。

最后吐槽:

这里要吐槽一下,Unity的材质属性有时候不及时刷新的问题:
Unity Shader中制作Billboard过程中,给ShaderLab中的[KeywordEnum()][MaterialToggle()]属性不及时刷新的问题搞得头都晕,因为这两个属性有时候添加了不会自动刷新,需要到Unity选择材质,在材质面板调整这两类属性后,它才会生效,搞得我写了很多测试用例,结果都对的,但就不知道为和在ShaderLab在不对。。。原来是这个问题。。。真的要气死。。。浪费我好多时间。

因为我当时遇到一个问题,我在CSharp中,演算的数据都对的,放到Shader中就不对了。
然后我怀疑是否Shader与CSharp有些差异,我就直接将《入门精要》的代码都抄一遍了,结果还是不对,然后我在将OpenGL的一些Billboard也弄到Unity Shader结果也不对,然后我又大量的在网上查找其他的Billboard代码,放到UnityShader中,还是不对。。。我使用了FrameDebuggerVusial Studio Graphics Debugger查看输出,传入的数据都是没有问题的(就差PIXRenderDocs没用上,原因:打不开,太慢,我没梯子,本想使用Visual Studio Graphics Debugger来调试shader的,但报错了,而且第一次用,不熟悉,后来解决了,我就暂时没去研究怎么用,后面有空的话,还是需要去学习一下怎么用,还有PIX),最后,真的要疯了,我就对着材质的一些KeywordEnum或是MaterialToggle中的属性随便点了一下(切断了属性值),结果就对了。最后我发现,其他之前部分效果对的Shader,就是因为没加这两类属性。。。

所以:在开发UnityShader中,如果添加了这类属性,验证效果时,觉得效果不对时,就需要调整一个:KeywordEnum或是MaterialToggle中的属性。

Project

backup : UnityShader_BillboardTesting_2018.3.0F2

References

  • Unity Shader 入门精要 - 11.3.2
  • UnityShader实例10:广告牌(Billboard)材质
  • Heat Distortion Shader Tutorial - 里面也有Billboard公告板效果。
  • Billboards 技术在Unity 中的几种使用方法
  • OpenGL Tutorials - Billboards
  • Unity 广告牌 (Billboard)的实现
  • unity billboard 最简单实现
  • 【Unity Shaders】ShadowGun系列之二——雾和体积光 - 里面也有将ShadowGun的Billboard。

Unity Shader - Billboard 广告板/广告牌 - BB树,BB投影相关推荐

  1. Unity Shader 顶点动画 广告牌

    另一种常见的顶点动画就是广告牌技术(Billboarding).广告牌技术会根据视角方向来旋转一个被纹理着色的多边形(通常就是简单的四边形,这个多边形就是广告牌),使得多边形看起来好像总是面对这摄影机 ...

  2. Unity Shader - ddx/ddy偏导函数测试,实现:锐化、高度图、Flat shading应用、高度生成法线

    文章目录 ddx, ddy 说明 DirectX - ddx, ddy OpenGL - dFdx, dFdy 伪代码表示 可用它来做什么 简单的边缘突出应用 Shader 边缘突出-锐化-增加差值 ...

  3. Unity Shader: Shader粒子广告牌

    广告牌效果既是不论物体与摄像机的角度,被渲染物体总是正对着摄像机. 此技术广泛利用于粒子效果中,例如Unity内置的Particle System.下文将要介绍如何在Shader中实现广告牌效果. 在 ...

  4. Unity Shader ASE——输出面板详情

    目录 一.基本配置 1.General 通用设置 2.Blend Mode 混合模式 3.Stencil Buffer 模板缓冲 4.Tessellation 镶嵌 5.Outline 轮廓 6.Bi ...

  5. 《Unity Shader 入门精要》读书笔记

    <Unity Shader 入门精要>读书笔记 --记录一下自己看书时遇到的一下困惑的地方和自己的一些想法,愿明天的我更加强大 1.要正确获得阴影和光照衰减效果,需要#pragma mul ...

  6. Unity Shader中各部分定义内容详解

    Unity Shader中各部分定义内容详解 样板 Shader "Practice/Unlit/SimpleUnlit" {Properties{_MainTex (" ...

  7. Unity Shader学习案例一: 流光效果

    Unity Shader Lab新手宝典简单Shader案例一:流光效果 + 相关基础知识说明 Shader "Samples/Light Flow"//shader名称 {Pro ...

  8. unity Shader 入门精要 EX

    unity Shader 入门精要: 1.shader概念 2.shader分类(顶点Shader.像素Shader) 3.Shader编程语言 4.Unity Shader 4.1概述 4.2分类( ...

  9. Unity Shader(一) Lowpoly动态低多边形 (QQ登录界面低边动画)

    前言 在逛论坛的时候偶然发现有人在问动态低多边形(Lowpoly)是如何实现的,因为经常编写UGUI拓展对顶点操作较为熟悉的我立马就想到利用继承UnityEngine.Graphic,重写OnPopu ...

最新文章

  1. 解决kubernetes中ingress-nginx配置问题
  2. (C#) 调用执行批处理文件
  3. 又跌!6月全国程序员工资新统计,太扎心
  4. 理解DDoS防护本质:基于资源较量和规则过滤的智能化系统
  5. php常驻对象,php对象
  6. 流水灯verilog实验原理_IC设计实例解析之“流水线技术”
  7. Win7系统账户被禁用的解决方法
  8. 大一下学期的自我目标
  9. C#移除HTML标记
  10. unity 自动将文件上传_unity如何存储文件夹
  11. 今晚7点见!红魔5G游戏手机联手热门手游打造专属主题
  12. rectiveCocoa进阶
  13. php 单位食堂订餐,职工食堂微信订餐系统 单位饭卡消费系统
  14. 意派导出html,三款专业H5工具评测:意派Epub360、ih5、mugeda
  15. ps怎么将png做成gif_【AE教程】AE如何导出背景透明的图层到PS中做gif动图?
  16. 一则 HTTP 405 Method Not Allowed 的解决办法
  17. python公开直播课_今晚Python与人工智能直播课来袭,Mars喊你快上车
  18. pwntcha库的安装依赖
  19. Linux: meld 对比工具的安装和使用
  20. 【LEACH协议】粒子群算法改进LEACH协议【含Matlab源码 2052期】

热门文章

  1. 分享google+facebook+twitter(Eclipse)
  2. simulink 报错Derivative of state ‘1‘ in block ..... at time 0.0 is not finite.
  3. 前端html 不规则表格制作
  4. 无线通信与云智能技术结合的ISE智能家居套件
  5. 谈谈对于Promise简单的理解
  6. 周鸿祎的微创新和中国的电子书
  7. 【转贴】上海二级以上医院名录
  8. 骚操作!一行Python代码能干嘛
  9. android录音波浪动画_Android 自定义波浪动画--让进度浪起来~
  10. python入门语言教程_Python入门教程(1)