US3S9L5——顶点根据摄像机位置移动-广告牌效果

广告牌效果

广告牌效果,它是一种图形技术,用于确保对象(通常是二维纹理面片或精灵(Sprite)图片)始终面向摄像机
同时在某些轴上保持固定的方向(一般分为全向广告牌和轴对齐广告牌)
在3D游戏中非常有用,它可以确保无论从哪个角度看、对象始终面向玩家,创造出一种始终可见的效果。

imageimage

  • 全向广告牌效果

    即无论摄像机位置如何变化,对象在所有轴上始终面向摄像机
    适用于烟雾、火焰等需要从任何角度看都要正对摄像机的效果

  • 轴对齐广告牌效果

    即对象在一个特定轴上保持固定方向,而在其他轴上面向摄像机,
    适用于树木、花草、人物等需要在特定轴上保持正确方向的效果,
    其中垂直广告牌就是一种特殊的轴对齐广告牌,对象在水平面(XZ平面)上旋转
    但在垂直方向上始终保持不变

广告牌效果基本原理

想要实现广告牌效果,核心原理是:旋转模型空间坐标系让其始终面向摄像机
想要达到这个目的,我们需要构建一个基于模型空间的新坐标系:
坐标系由两个关键因素决定

  1. 原点(新坐标系中心点)

    基于模型空间的,可以自定义,但一般还是用 (0,0,0)(0,0,0)

  2. 三个轴向(X轴、Y轴、Z轴)

    通常情况下三个轴向由视角方向、垂直视角方向向上方向、右方向构成

image

因此,关键点就是求出三个轴向(X轴、Y轴、Z轴):

image

  • 新坐标系 Z 轴 = normalnormal = 摄像机位置转换到模型空间的位置–模型空间下新轴向空间中心点摄像机位置转换到模型空间的位置 – 模型空间下新轴向空间中心点

    其中模型空间下的新轴向空间中心点一般还是 (0,0,0)(0,0,0)
    也就是说,新坐标系 Z 轴就是视角方向

  • 新坐标系 X 轴 = rightright = normal×oldupnormal \times oldup (知识回顾:两个向量叉乘可以得到垂直于两向量所在平面的向量)

    其中 oldupoldup 就是原来的模型空间中 Y 轴 (0,1,0)(0,1,0)
    也就是说,新坐标系 X 轴是 新坐标系 Z 轴 与 旧坐标系 Y 轴 所在平面的法向量(新坐标系遵循右手坐标系

  • 新坐标系 Y 轴 = newupnewup = normal×rightnormal \times right

通过以上方法即可获取到新坐标空间的 X、Y、Z 轴,有了轴向,再定义一个新坐标系的中心点(相对于模型空间的)
一般我们会将中心点 CenterCenter 定为 (0,0,0)(0,0,0),即原模型空间原点

那么此时我们只需要计算以下两步即可:

  1. 偏移位置 = 顶点坐标 – CenterCenter
  2. 新顶点位置 = Center+X轴方向×偏移位置.x+Y轴方向×偏移位置.y+Z轴方向×偏移位置.zCenter + X轴方向 \times 偏移位置.x + Y轴方向 \times 偏移位置.y + Z轴方向 \times 偏移位置.z

通过以上的计算,我们便可以让顶点更新位置,让对象一直面朝我们了,实现出一个全向广告牌效果

如果想要实现出垂直广告牌效果,那只需要在计算轴向向量时进行修改即可

image

只需要在计算 normalnormal 向量时,让其的 y 值变为 0 即可,相当于 normalnormal 向量只在 xz 平面变化

广告牌效果实现关键点总结:

  1. 计算新坐标系

    1. 原点确定(一般是 (0,0,0)(0,0,0)
    2. 坐标轴计算 (x,y,z)(x,y,z)
  2. 顶点计算

    1. 偏移位置 = 顶点坐标 – Center
    2. 新顶点位置 = Center + X轴 * 偏移位置.x + Y轴 * 偏移位置.y + Z轴 * 偏移位置.z
  3. 全向广告牌和垂直广告牌区别

    计算 normal 轴时,y 为 0 则为垂直广告牌

广告牌效果具体实现

使用下图的星星图实现广告牌效果:

star

  1. 新建 Shader,删除无用代码

  2. 声明属性,属性映射

    主纹理、颜色叠加、垂直程度(新坐标系的Y轴垂直于视角方向的程度,0为垂直广告牌,1为全向广告牌)

    1
    2
    3
    4
    5
    6
    Properties
    {
    _MainTex("Texture", 2D) = "white"{} // 主贴图
    _Color("Color", Color) = (1, 1, 1, 1) // 颜色叠加
    _VerticalDegree("VerticalDegree", Range(0, 1)) = 1 // 垂直程度,控制垂直广告牌到全向广告牌的变化
    }
  3. 透明 Shader 相关

    注意:关闭批处理,并让其两面渲染(也就是不剔除)

    1
    2
    3
    4
    5
    6
    7
    8
    Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" "DisableBatching"="True" }
    Pass
    {
    ZWrite Off
    Blend SrcAlpha OneMinusSrcAlpha
    Cull Off
    // ...
    }
  4. 结构体相关

    顶点和纹理坐标

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"

    struct v2f
    {
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
    };

    sampler2D _MainTex;
    float4 _MainTex_ST;
    fixed4 _Color;
    float _VerticalDegree;
  5. 顶点着色器

    1. 确定新坐标中心点 center
    2. 计算新坐标系 Z 轴(normalDir),需要先将摄像机坐标转到模型空间,再减去上一步确认的中心点
    3. 用垂直广告牌程度改变 Z 轴 y 值后,单位化
    4. 声明 Y 轴(upDir​,此时的 upDir 存储的是原来的模型空间下的 Y 轴方向)
    5. 利用新坐标系 Z 轴(normalDir​)和旧坐标系 Y 轴(upDir​)叉乘计算出新坐标系 X 轴(rightDir
    6. 利用新坐标系 Z 轴(normalDir​)和新坐标系 X 轴(right)叉乘计算出新坐标系 Y 轴(upDir,这里是复用之前的变量)
    7. 得到顶点相对于新坐标系中心点的偏移位置
    8. 利用新中心点和3轴计算出顶点新位置
    9. 新顶点转到裁剪空间
    10. UV 缩放偏移
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    v2f vert (appdata_base v)
    {
    v2f o;
    // 取新坐标系的中心点(默认使用原来的模型空间原点)
    float3 center = float3(0, 0, 0);
    // 计算新坐标系Z轴
    float3 cameraInObjectPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));
    float3 normalDir = cameraInObjectPos - center;
    normalDir.y *= _VerticalDegree; // 相当于把y向下压,若垂直程度为0,就代表X轴被我们压到了旧坐标系的xz平面
    normalDir = normalize(normalDir);
    // 计算新坐标系X轴
    float3 upDir = float3(0, 1, 0);
    float3 rightDir = normalize(cross(upDir, normalDir));
    // 计算新坐标系Y轴
    upDir = normalize(cross(normalDir, rightDir));
    // 得到顶点相对于新坐标系的偏移位置,再利用三个轴向进行最终顶点位置的计算
    float3 centerOffset = v.vertex.xyz - center;
    float3 newVertexPos = center + rightDir * centerOffset.x + upDir * centerOffset.y + normalDir * centerOffset.z;
    // 将新顶点转换到裁剪空间
    o.vertex = UnityObjectToClipPos(float4(newVertexPos, 1));
    // uv坐标偏移缩放计算
    o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
    return o;
    }
  6. 片元着色器

    直接采样 叠加颜色即可

    1
    2
    3
    4
    5
    6
    fixed4 frag (v2f i) : SV_Target
    {
    fixed4 color = tex2D(_MainTex, i.uv);
    color.rgb *= _Color.rgb;
    return color;
    }

显示效果(左图为垂直程度为1,即全向广告牌效果,右图垂直程度为0,即垂直广告牌效果):

imageimage

可见,星星现在会跟随摄像机转动,使得星星能够始终面向摄像机,如果将垂直程度设置为0,那么贴图只有一个 Y 轴会转向摄像机

不过,目前的 Shader 会在某些极限情况下出现问题,例如,在其 Y 轴垂直向下看星星图时,
新坐标系 Z 轴 快要与 模型坐标系 Y 轴会重合,导致新坐标系的 X 轴计算得到零向量,这会导致渲染出现问题
为此,为了避免这个问题,就可以在 新坐标系 Z 轴 快要与 模型坐标系 Y 轴 重合时,使用三目运算符让 upDir 使用别的模型坐标系的轴
避免因为新坐标系 Z 轴 与 模型坐标系 Y 轴会重合计算出零向量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
v2f vert (appdata_base v)
{
v2f o;
// 取新坐标系的中心点(默认使用原来的模型空间原点)
float3 center = float3(0, 0, 0);
// 计算新坐标系Z轴
float3 cameraInObjectPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));
float3 normalDir = cameraInObjectPos - center;
normalDir.y *= _VerticalDegree; // 相当于把y向下压,若垂直程度为0,就代表X轴被我们压到了旧坐标系的xz平面
normalDir = normalize(normalDir);
// 计算新坐标系X轴,如果新坐标系Z轴快要和模型坐标系Y轴重合了,就转而使用模型坐标系X轴
float3 upDir = normalDir.y < 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
float3 rightDir = normalize(cross(upDir, normalDir));
// 计算新坐标系Y轴
upDir = normalize(cross(normalDir, rightDir));
// 得到顶点相对于新坐标系的偏移位置,再利用三个轴向进行最终顶点位置的计算
float3 centerOffset = v.vertex.xyz - center;
float3 newVertexPos = center + rightDir * centerOffset.x + upDir * centerOffset.y + normalDir * centerOffset.z;
// 将新顶点转换到裁剪空间
o.vertex = UnityObjectToClipPos(float4(newVertexPos, 1));
// uv坐标偏移缩放计算
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
return o;
}

完整 Shader 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
Shader "TeachShader/Lesson95"
{
Properties
{
_MainTex("Texture", 2D) = "white"{} // 主贴图
_Color("Color", Color) = (1, 1, 1, 1) // 颜色叠加
_VerticalDegree("VerticalDegree", Range(0, 1)) = 1 // 垂直程度,控制垂直广告牌到全向广告牌的变化
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" "DisableBatching"="True" }

Pass
{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Off

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
float _VerticalDegree;

v2f vert (appdata_base v)
{
v2f o;
// 取新坐标系的中心点(默认使用原来的模型空间原点)
float3 center = float3(0, 0, 0);
// 计算新坐标系Z轴
float3 cameraInObjectPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));
float3 normalDir = cameraInObjectPos - center;
normalDir.y *= _VerticalDegree; // 相当于把y向下压,若垂直程度为0,就代表X轴被我们压到了旧坐标系的xz平面
normalDir = normalize(normalDir);
// 计算新坐标系X轴,如果新坐标系Z轴快要和模型坐标系Y轴重合了,就转而使用模型坐标系X轴
float3 upDir = normalDir.y < 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
float3 rightDir = normalize(cross(upDir, normalDir));
// 计算新坐标系Y轴
upDir = normalize(cross(normalDir, rightDir));
// 得到顶点相对于新坐标系的偏移位置,再利用三个轴向进行最终顶点位置的计算
float3 centerOffset = v.vertex.xyz - center;
float3 newVertexPos = center + rightDir * centerOffset.x + upDir * centerOffset.y + normalDir * centerOffset.z;
// 将新顶点转换到裁剪空间
o.vertex = UnityObjectToClipPos(float4(newVertexPos, 1));
// uv坐标偏移缩放计算
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 color = tex2D(_MainTex, i.uv);
color.rgb *= _Color.rgb;
return color;
}
ENDCG
}
}
}