您当前的位置: 首页 >  ar

Jave.Lin

暂无认证

  • 2浏览

    0关注

    704博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Unity Shader - Billboard 广告板/广告牌 - BB树,BB投影

Jave.Lin 发布时间:2020-03-25 12:02:57 ,浏览量:2

文章目录
  • 实现
    • 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;
/// 
/// jave.lin 2020.03.24 使用Transform.LookAt(Transform target)的方式
/// 
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 → [ r i g h t . x u p . x n o r m a l . x 0 r i g h t . y u p . y n o r m a l . y 0 r i g h t . z u p . z n o m r a l . z 0 0 0 0 1 ] \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;。所以我们将第四行第四列删除,变换下面的矩阵: [ r i g h t . x u p . x n o r m a l . x r i g h t . y u p . y n o r m a l . y r i g h t . z u p . z n o m r a l . 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形式再得矩阵:

[ r i g h t . x r i g h t . y r i g h t . z ] = [ x 1 x 2 x 3 ] , [ u p . x u p . y u p . z ] = [ y 1 y 2 y 3 ] , [ n o r m a l . x n o r m a l . y n o r m a l . z ] = [ z 1 z 2 z 3 ] \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​⎦⎤​

[ r i g h t . x u p . x n o r m a l . x r i g h t . y u p . y n o r m a l . y r i g h t . z u p . z n o r m a l . z ] = [ x 1 y 1 z 1 x 2 y 2 z 2 x 3 y 3 z 3 ] \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,可以表达为: [ a b c ] \begin{bmatrix} a\\ b\\ c\\ \end{bmatrix} ⎣⎡​abc​⎦⎤​ 因为本地偏移后的坐标我们是知道的,所以作为参数a,b,c来记,而上面的矩阵我们是不知道的,是通过right=cross(normal,up),up=cross(right,normal)求得的,而矩阵先当作我们以前学习的未知数: 将矩阵(未知数)与顶点(常数项)结合,等于新的顶点,可以写为: [ a b b b b b c b b ] \begin{bmatrix} a_{bb}\\ b_{bb}\\ c_{bb} \end{bmatrix} ⎣⎡​abb​bbb​cbb​​⎦⎤​

其中 X X X b b XXX_{bb} XXXbb​是BB下新的坐标顶点 但在Unity的CSharp的Matrix4x4封装类中,Matrix4x4.MultiplePoint(Vector3)是一个矩阵行x向量列的方式,所以可以表达为下面的方式: [ x 1 y 1 z 1 x 2 y 2 z 2 x 3 y 3 z 3 ] . 行 × [ a b c ] . 列 = [ a b b b b b c b b ] \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 ∗ x 1 + b ∗ y 1 + c ∗ z 1 = a b b a ∗ x 2 + b ∗ y 2 + c ∗ z 2 = b b b a ∗ x 3 + b ∗ y 3 + c ∗ z 3 = c b b a*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​ 而未知数的矩阵,我们求得后,代入公式,就可以得到: X X X b b XXX_{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();
                meshRenderer = gameObject.AddComponent();
                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  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中,还是不对。。。我使用了FrameDebugger、Vusial Studio Graphics Debugger查看输出,传入的数据都是没有问题的(就差PIX和RenderDocs没用上,原因:打不开,太慢,我没梯子,本想使用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。
关注
打赏
1664331872
查看更多评论
立即登录/注册

微信扫码登录

0.0506s