Unity中广告牌效果实现

前言

        我并非没写过关于广告牌效果的文章,但是那时候我是在写《Unity Shader入门精要》读书笔记的时候提过。那时候我不明白书中实现的原理,所以我就没给出具体的代码。现在我终于明白了一些便想着补上这个实现。

环境

    Unity版本:2022.3.34f1c1

        操作系统:Windows 10         Universal RP 14.0.8

广告牌的效果的原理

        在了解原理之前,我们首先要了解一下什么是广告牌效果(Billboarding)。在《Real-Time Rendering 4th Edition》中是这么描述它的:

1
2
3
4
原文:Orienting a textured rectangle based on the view direction is called billboarding, and
the rectangle is called a billboard.As the view changes, the orientation of the rectangle is modi ed in response.

译文:基于观察⽅向来修改纹理矩形朝向的技术被称为⼴告牌技术(Billboarding),这个矩形被称为⼴告牌(Billboard)。随着视图的改变,矩形的朝向也会随之改变。

简单来说广告牌效果就是让一个二维平面(不是平面的话,有些设定会出现问题)某个方向始终朝向一个特定的方向。这种效果实现感觉是可以使用代码来实现,但是在当前GPU运算性能远超CPU的时代,这种效果的实现就丢给GPU来做更好。实际上因为不同的需求,所以广告牌效果也有不同实现方式。而究其根本都是创建一个新的变化矩阵(也可以说是建立新的坐标系,或是说对模型的顶点再进行一次旋转)以满足自己的需求。接下来,我们设定一个前提。以下所有的实现都基于Unity提供的默认物体QuadQuad的特点是其是单面渲染,其法线和其向前方向相反,且中心点在平面的中心。为了避免异议,这里我截图了一份,如下所示:

这里显示了Quad的本地坐标系,蓝色的箭头表向前方向(z轴),绿色的箭头表向上方向(y轴)。

屏幕对齐的广告牌(Screen-Aligned Billboard)

        屏幕对齐的广告牌是最简单的,他的效果就类似于2D的Sprite效果,其图像与屏幕平⾏。这种类型的⼴告牌⽽⾔,其所需的表⾯法线是视平⾯法线(相机向前的方向)的负值,其向上方向向量是相机向上的方向,以此来达到⼴告牌平面和视平⾯平行的效果。通过这两个信息,我们就可以得到最终向右的方向,最终得到转换的旋转矩阵。这里我再次说明一下,对于我们用来实现的Quad而言,其法线和向前方向是相反的。下面创建变化矩阵的代码中,我直接使用了相机向前的方向。当然我们也可以判断出法线和物体向前方向的角度来进行修改,但是我这里觉得有些麻烦了,所以我并不想多想。转换矩阵的建立如下:

1
2
3
real3 fwdDir = TransformViewToWorldDir(real3(0,0,1),true);
real3 upDir = TransformViewToWorldDir(real3(0,1,0),true);
real3 rightDir = normalize(cross(fwdDir,upDir));

TransformViewToWorldDir是URP自带的函数,其实现代码如下:

1
2
3
4
5
6
7
8
9
// Transforms vector from view space to world space
real3 TransformViewToWorldDir(real3 dirVS, bool doNormalize = false)
{
float3 dirWS = mul((real3x3)GetViewToWorldMatrix(), dirVS).xyz;
if (doNormalize)
return normalize(dirWS);

return dirWS;
}

其效果是将视图空间(View Space)中的方向转到世界坐标系(World Space)下。在视图坐标系中,相机向前的方向就是(0,0,1),相机向上方向则为(0,1,0)。所以我们只要将这两个方向转换到世界坐标系下就好了。


这里额外说明为什么这里我们不用对法线做特殊处理,如果你好奇可以展开它。

这里你或许有一个疑问,法线在进行空间系变化的时候,我们需要做一些额外的操作。因为只有进行这样的额外操作,你才能保证在转换后在新坐标系中法线仍然垂直于物体的表面。比如下面shader代码中的操作(下面代码截取至URP源码中的SpaceTransforms.hlsl):

1
2
3
4
5
6
7
8
9
10
11
12
13
float3 TransformObjectToWorldNormal(float3 normalOS, bool doNormalize = true)
{
#ifdef UNITY_ASSUME_UNIFORM_SCALING
return TransformObjectToWorldDir(normalOS, doNormalize);
#else
// Normal need to be multiply by inverse transpose
float3 normalWS = mul(normalOS, (float3x3)GetWorldToObjectMatrix());
if (doNormalize)
return SafeNormalize(normalWS);

return normalWS;
#endif
}

上面的代码是将法线从模型空间转换到世界空间。这里它做了一个判断,如果UNITY_ASSUME_UNIFORM_SCALING为真,那么就直接返回它这里仍然做了一个正常的模型空间转换到世界空间的变化矩阵。而如果UNITY_ASSUME_UNIFORM_SCALING为假,它就反向乘以变换矩阵。这里我不对这个操作做过多的解释,这不是重点。这里的重点是为什么在某些情况下,我们不需要这额外的操作。UNITY_ASSUME_UNIFORM_SCALING这个变量名可直译为————Unity假定统一缩放,也就是说这时候Unity认为物体的xyz缩放值是一致的。所以只要是缩放一致的情况下,我们就可以不对法线做任何操作。在视图空间转换到世界空间的过程中,我们并没有做缩放操作(一般而言是不会有缩放操作的),所以这里我们不需要做额外的操作。其实URP中仍然提供法线从视图空间转换到世界空间的函数,其实现如下:

1
2
3
4
5
real3 TransformWorldToViewNormal(real3 normalWS, bool doNormalize = false)
{
// assuming view matrix is uniformly scaled, we can use direction transform
return TransformWorldToViewDir(normalWS, doNormalize);
}
从注释中,我们也可以知道其假设了视图矩阵是统一缩放的,所以它这里直接使用方向变换函数来进行法线的转换。其实按照我的实现方法也用不到这样的操作。

现在我们的旋转矩阵是在世界坐标系下的,所以接下来我们要将模型从模型空间转换到世界空间,然后再使用转换矩阵。从模型空间转换到世界空间的操作有旋转,缩放和平移。这里的旋转操作不是我想要的操作。因为原本我们选中的模型(Quad模型)是四四方方的,我们直接使用旋转矩阵模型的边就是和屏幕对其的。这时我们再加上旋转操作,这就会导致原本于屏幕平行的边变得不会再平行。或是我们先进行旋转在通过这个旋转矩阵也是一样会不再平行。所以我们要将这个旋转操作去掉。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 修改模型矩阵。移除旋转
float4x4 mode = UNITY_MATRIX_M;
float3 rotX = float3(mode[0].x,mode[1].x,mode[2].x);
float3 rotY = float3(mode[0].y,mode[1].y,mode[2].y);
float3 rotZ = float3(mode[0].z,mode[1].z,mode[2].z);

// 乘以各个方向上的缩放
rightDir *= length(rotX);
upDir *= length(rotY);
fwdDir *= length(rotZ);

// 构建变换矩阵
mode[0].xyz = float3(rightDir.x,upDir.x,fwdDir.x);
mode[1].xyz = float3(rightDir.y,upDir.y,fwdDir.y);
mode[2].xyz = float3(rightDir.z,upDir.z,fwdDir.z);

因为平移不会受旋转和缩放影响,所以mode[3]直接保留下来。


这里额外说明了为什么缩放大小可以靠求长度得到

我们先只考虑旋转矩阵(我个人喜欢以行为主序的表达,所以下面的表达方式都是以行为主序的)。下面是旋转矩阵的表达(各轴之间的乘法顺序是Z * X * Y,Unity的矩阵乘法顺序也是如此。Transform Inspector 中的非默认欧拉顺序):

我们可以发现,这个矩阵每一行(以行为主序)按照向量计算长度的方法得到的结果都为1。如果你有看Unity的旋转矩阵,你会发现这我和给出来的矩阵算的不太一样。这是因为我这里使用的右手坐标系的旋转矩阵,Unity则左手坐标系,但是原理是一样的。缩放矩阵如下所示:

当缩放矩阵和旋转矩阵进行矩阵乘法后,如下所示:

其实就是给每一行(以行为主序)的各个数乘上对应的缩放值(主要还是因为Unity是先缩放后旋转的,虽然我还没见过先旋转后缩放的,但是Unity确实是这样的),所以我们对每一行进行类似向量求长度的方法,就可以得到缩放的大小。

最终效果我们仍然要视需求而定,这里我想体现物体边与屏幕平行才需要去掉旋转。后文我会在⾯向视点的⼴告牌中讨论加上自身旋转的问题。

我们将顶点坐标乘以我们修改后的mode矩阵(新的模型矩阵),便得到在世界坐标系下的顶点坐标。我们将这个顶点坐标乘以我们使用rightDirfwdDirupDir构成的矩阵后,最终转换到屏幕空间。当然我们也可以直接按照下面的方法来构建mode矩阵:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 修改模型矩阵。移除旋转
float4x4 mode = UNITY_MATRIX_M;
float3 rotX = float3(mode[0].x,mode[1].x,mode[2].x);
float3 rotY = float3(mode[0].y,mode[1].y,mode[2].y);
float3 rotZ = float3(mode[0].z,mode[1].z,mode[2].z);

// 乘以各个方向上的缩放
rightDir *= length(rotX);
upDir *= length(rotY);
fwdDir *= length(rotZ);

// 构建变换矩阵
mode[0].xyz = float3(rightDir.x,upDir.x,fwdDir.x);
mode[1].xyz = float3(rightDir.y,upDir.y,fwdDir.y);
mode[2].xyz = float3(rightDir.z,upDir.z,fwdDir.z);

完整代码如下:

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
71
72
73
74
75
76
77
Shader "Billboard/ScreenAligned"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" }
Blend SrcAlpha OneMinusSrcAlpha

Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

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

CBUFFER_START(UnityPerMaterial)
sampler2D _MainTex;
float4 _MainTex_ST;
CBUFFER_END

v2f vert (appdata v)
{
v2f o;
real3 fwdDir = TransformViewToWorldDir(real3(0,0,1),true);
real3 upDir = TransformViewToWorldDir(real3(0,1,0),true);

real3 rightDir = normalize(cross(fwdDir,upDir));

float3 vertex;
// 修改模型矩阵。移除旋转
float4x4 mode = UNITY_MATRIX_M;
float3 rotX = float3(mode[0].x,mode[1].x,mode[2].x);
float3 rotY = float3(mode[0].y,mode[1].y,mode[2].y);
float3 rotZ = float3(mode[0].z,mode[1].z,mode[2].z);

// 乘以各个方向上的缩放
rightDir *= length(rotX);
upDir *= length(rotY);
fwdDir *= length(rotZ);

// 构建变换矩阵
mode[0].xyz = float3(rightDir.x,upDir.x,fwdDir.x);
mode[1].xyz = float3(rightDir.y,upDir.y,fwdDir.y);
mode[2].xyz = float3(rightDir.z,upDir.z,fwdDir.z);
vertex = mul(mode, float4(v.vertex.xyz, 1.0)).xyz;
o.vertex = TransformWorldToHClip(vertex);
//o.vertex =
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}

half4 frag (v2f i) : SV_Target
{
// sample the texture
half4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDHLSL
}
}
}

PS: 如果你要实现固定大小的广告牌效果,那么在模型矩阵中,你要把平移的信息进行修改,使得在世界坐标系下模型和摄像机始终保持相同的位置。但是这里我建议你还是用代码进行实现,这是因为在摄像机外的物体根本就不会近到渲染流程中。这里我猜测是因为这个模型已经在摄像头外面了,所以Unity没有把这个模型的传给GPU去渲染。不过这只是我的猜测。因为我自己在做测试的时候就发现只要把物体移出摄像头的可视范围内,即使我Shader按照理论上会存在在摄像机内,摄像机中也不会存在物体。


面向世界的广告牌(World-Oriented Billboard)

        面向世界的广告牌会牌始终保持⾯向观众,但同时也要沿着它们的观察轴进⾏旋转,以保持世界空间中的朝向。为了保持保持⾯向观众,广告牌表⾯法线仍然是视平⾯法线的负值(即摄像机视线方向)。然后我们设定一个观察轴,比如我们现在设定世界坐标系中的向上方向为观察轴。那么我们可以先通过这两个方向得到另一个轴的值,最后我们将再通过叉乘得到最后的一个方向。以此我们来构建变化矩阵。如下所示

1
2
3
4
real3 fwdDir = TransformViewToWorldDir(real3(0,0,1),true);
real3 upDir = real3(0,1,0);
real3 rightDir = SelfNormalize(cross(fwdDir,upDir));
upDir = SelfNormalize(cross(rightDir,fwdDir));

这里的SelfNormalize是我自己实现的函数,其代码如下:

1
2
3
4
5
6
7
real3 SelfNormalize(real3 v)
{
real len = length(v);
real sum = v.x + v.y + v.z;
len = lerp(0.001,len,step(0.00001,abs(sum)));
return v / len;
}

因为这里会存在一个问题,当fwdDirupDir处于平行状态的时候,rightDir会变成(0,0,0)。我自己使用Unity的normalize函数实现的时候在发现在这些特定视角的表现下会有些奇怪(其实也不能算是很奇怪,因为角度稍微偏一些效果就差不多了。但是我还是想着在这种特定视角下看不见算了。当然具体还是看你项目的需求)。这样操作后会在rightDir(0,0,0)时保持原来的样子。如果你有查过其他的资料,你会发现类似下面这样的写法。

1
float3 tmpUpDir = lerp(float3(0,0,1),float3(0,1,0),step(abs(fwdDir.y),0.999));

这段代码表面意思是判断fwdDir的y值是否足够接近1,这是实际上就是在判断此时的fwdDir是否和我们选定的upDir平行。因为当我们进行归一化后,fwdDir的长度便是1,而数值越是存于fwdDir的y值上则x、z就会减少,即fwdDir更接近于upDir。这里我们并不使用这个方法,因为我想要保证从我们选定的世界观察轴中衍生。这样的做法我会放到下文的⾯向视点的⼴告牌中展示。

        我们得到这些变量后,接下来的流程就和屏幕对齐的流程一致了,我们求出缩放值并忽略旋转。完整代码如下:

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
Shader "Billboard/WorldOriented"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" }
Blend SrcAlpha OneMinusSrcAlpha

Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

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

CBUFFER_START(UnityPerMaterial)
sampler2D _MainTex;
float4 _MainTex_ST;
CBUFFER_END

real3 SelfNormalize(real3 v)
{
real len = length(v);
real sum = v.x + v.y + v.z;
len = lerp(0.001,len,step(0.00001,abs(sum)));
return v / len;
}

v2f vert (appdata v)
{
v2f o;
real3 fwdDir = TransformViewToWorldDir(real3(0,0,1),true);
real3 upDir = real3(0,1,0);

// 因为此时rightDir会变成(0,0,0),所以如果使用Unity的Normalize函数在一些特定视角的表现下会有些奇怪,所以这里我们自己实现一个Normalize函数
real3 rightDir = SelfNormalize(cross(fwdDir,upDir));
// 因为rightDir会变成(0,0,0),所以叉乘的结果也是(0,0,0),所以这里我们自己实现一个叉乘函数
upDir = SelfNormalize(cross(rightDir,fwdDir));

float3 vertex;
// 修改模型矩阵。移除旋转
float4x4 mode = UNITY_MATRIX_M;
float3 rotX = float3(mode[0].x,mode[1].x,mode[2].x);
float3 rotY = float3(mode[0].y,mode[1].y,mode[2].y);
float3 rotZ = float3(mode[0].z,mode[1].z,mode[2].z);

rightDir *= length(rotX);
upDir *= length(rotY);
fwdDir *= length(rotZ);

// 构建旋转矩阵
mode[0].xyz = float3(rightDir.x,upDir.x,fwdDir.x);
mode[1].xyz = float3(rightDir.y,upDir.y,fwdDir.y);
mode[2].xyz = float3(rightDir.z,upDir.z,fwdDir.z);

// 得到世界空间系下的坐标
vertex = mul(mode, float4(v.vertex.xyz,1)).xyz;

o.vertex = TransformWorldToHClip(vertex);
//o.vertex =
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}

half4 frag (v2f i) : SV_Target
{
// sample the texture
half4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDHLSL
}
}
}

⾯向视点的⼴告牌(View-Point-Oriented Billboard)

        在我查到的资料中有提到对所有的物体都使⽤相同的旋转矩阵是有⻛险的。由于透视投影的特性,到观察轴有⼀定距离的物体会被扭曲拉伸,人们会起来有些奇怪。实际上关于这一点,我没看出来也能复现。但是书中也提到了解决的方法,就是⼴告牌的法线需要为从⼴告牌中⼼指向观察者位置的向量。即平面的表面法线始终与摄像机看向物体中心点的视线反向。这就是⾯向视点的⼴告牌(View-Point-Oriented Billboard)。这里我们仍然以世界坐标系中的向上方向为观察轴来建立变换矩阵。前面都是不考虑物体自身的旋转,这里我写一个考虑自身旋转的实现。首先我们要先得到相机的位置,然后得到视线方向。

        摄像机的视线方向,我们可以用摄像头的世界坐标归一化后取反得到。这是因为如果一个物体不进行缩放,旋转和平移,那么世界坐标系就是物体本身的模型坐标系。而Quad的中心点就是(0,0,0)(当然一般而言用来做广告牌的物体中心点大都是这样的)。我们前文说我们构建一个新的矩阵实际上就是让物体的顶点进行多一次的旋转。所以如果我们先假定物体并不进行任何的旋转缩放和平移,我们就可以用这样的方式来考虑构建矩阵。如果我们考虑物体的自身的旋转和缩放,那么情况就要分两种了。其实缩放并不用考虑什么反正沿着方向乘以缩放值就好。但是旋转我们就要考虑两种情况了。一种是物体先旋转本身,然后再进行在进行我们的新建的旋转。代码如下:

1
2
// mode是我们根据 fwdDir、upDir、rightDir 构建的矩阵
newMode = mul(UNITY_MATRIX_M,mode);

另一种是物体先进行我们的新建的旋转,然后再旋转本身。代码如下:

1
2
// mode是我们根据 fwdDir、upDir、rightDir 构建的矩阵
newMode = mul(mode,UNITY_MATRIX_M);

网上还有另外一种做法就是直接将摄像头的世界坐标转到模型坐标系中real3 fwdDir = normalize(TransformWorldToObject(-GetCameraPositionWS()));。这样做法的观察轴就是一般模型坐标系的Y轴。如果你要是世界坐标系的Y轴,又想要使用float3 tmpUpDir = lerp(float3(0,0,1),float3(0,1,0),step(abs(fwdDir.y),0.999));,那么你就不能直接转换到模型坐标系中,而是需要先转换到世界坐标系中比对是否平行。然后再使用URP自带的TransformWorldToObjectNormal函数将我们得到的fwdDirtmpUpDir转换到模型坐标系中去创建矩阵。这种方法创建出来的矩阵本身自带了物体的旋转,但是对于缩放需要我们额外进行处理。最终的效果和newMode = mul(mode,UNITY_MATRIX_M);是一样的。这里我是选择使用newMode = mul(mode,UNITY_MATRIX_M);来进行操作,完整代码如下:

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
71
72
Shader "Billboard/ViewPointOriented"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" }
Blend SrcAlpha OneMinusSrcAlpha

Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

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

CBUFFER_START(UnityPerMaterial)
sampler2D _MainTex;
float4 _MainTex_ST;
CBUFFER_END

v2f vert (appdata v)
{
v2f o;
real3 fwdDir = normalize(-GetCameraPositionWS());
// 判断我们选定的方向是否和摄像机的视线方向平行
float3 tmpUpDir = lerp(float3(0,0,1),float3(0,1,0),step(abs(fwdDir.y),0.999));
float3 rightDir = normalize(cross(tmpUpDir,fwdDir));
float3 upDir = normalize(cross(fwdDir,rightDir));

float3 vertex;
// 构建旋转矩阵
float4x4 mode;
mode[0] = float4(rightDir.x,upDir.x,fwdDir.x,0);
mode[1] = float4(rightDir.y,upDir.y,fwdDir.y,0);
mode[2] = float4(rightDir.z,upDir.z,fwdDir.z,0);
mode[3] = float4(0,0,0,1);
mode = mul(mode,UNITY_MATRIX_M);

// 得到世界空间系下的坐标
vertex = mul(mode, float4(v.vertex.xyz,1)).xyz;

o.vertex = TransformWorldToHClip(vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}

half4 frag (v2f i) : SV_Target
{
// sample the texture
half4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDHLSL
}
}
}

轴向广告牌(Axial Billboard)

        轴向广告牌(Axial Billboard)通常并不会直接⾯对观察者。它可以围绕⼀些固定的世界空间轴进⾏旋转,并在这个范围内尽可能多地⾯向观察者。在游戏中,草地或是远处的树⽊便可以用这个技术来实现。这就是和面向世界反过来了。我们先选定观察轴为固定方向,然后物体法线从摄像机的观察方向衍生出来。矩阵构建的代码如下:

1
2
3
4
5
6
7
real3 fwdDir = TransformViewToWorldDir(real3(0,0,1),true);
real3 upDir = real3(0,1,0);

// 因为此时rightDir会变成(0,0,0),所以如果使用Unity的Normalize函数在一些特定视角的表现下会有些奇怪,所以这里我们自己实现一个Normalize函数
real3 rightDir = SelfNormalize(cross(fwdDir,upDir));
// 因为rightDir会变成(0,0,0),所以叉乘的结果也是(0,0,0),所以这里我们自己实现一个叉乘函数
fwdDir = SelfNormalize(cross(upDir,rightDir));

这里我们仍然考虑本身的旋转,所以我们这里仍用⾯向视点的⼴告牌中提到的方法,代码如下:

1
2
// mode是我们根据 fwdDir、upDir、rightDir 构建的矩阵
newMode = mul(mode,UNITY_MATRIX_M);

最终完整代码如下:

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
71
72
73
74
75
76
77
78
79
80
81
82
83
Shader "Billboard/AxialBillboard"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" }
Blend SrcAlpha OneMinusSrcAlpha

Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

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

CBUFFER_START(UnityPerMaterial)
sampler2D _MainTex;
float4 _MainTex_ST;
CBUFFER_END

real3 SelfNormalize(real3 v)
{
real len = length(v);
real sum = v.x + v.y + v.z;
len = lerp(0.001,len,step(0.00001,abs(sum)));
return v / len;
}

v2f vert (appdata v)
{
v2f o;
real3 fwdDir = TransformViewToWorldDir(real3(0,0,1),true);
real3 upDir = real3(0,1,0);

// 因为此时rightDir会变成(0,0,0),所以如果使用Unity的Normalize函数在一些特定视角的表现下会有些奇怪,所以这里我们自己实现一个Normalize函数
real3 rightDir = SelfNormalize(cross(fwdDir,upDir));
// 因为rightDir会变成(0,0,0),所以叉乘的结果也是(0,0,0),所以这里我们自己实现一个叉乘函数
fwdDir = SelfNormalize(cross(upDir,rightDir));

float3 vertex;
// 构建旋转矩阵
float4x4 mode;
mode[0] = float4(rightDir.x,upDir.x,fwdDir.x,0);
mode[1] = float4(rightDir.y,upDir.y,fwdDir.y,0);
mode[2] = float4(rightDir.z,upDir.z,fwdDir.z,0);
mode[3] = float4(0,0,0,1);
mode = mul(mode,UNITY_MATRIX_M);

// 得到世界空间系下的坐标
vertex = mul(mode, float4(v.vertex.xyz,1)).xyz;

o.vertex = TransformWorldToHClip(vertex);
//o.vertex =
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}

half4 frag (v2f i) : SV_Target
{
// sample the texture
half4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDHLSL
}
}
}

最后的扩展

        在网上乘以旋转矩阵的方式还有下面的这样的情况:

1
vertex = rightDir * v.vertex.x + upDir * v.vertex.y + fwdDir * v.vertex.z;

这就是对于矩阵乘法的化简:

大家照着上面的式子验证一下就好了,并不算困难。

参考文章

闲言赘语

一些趣事

        我在用英文搜索广告牌效果的相关资料时,我一直以为是Billboard Effect。然后我搜索出来一堆非图形学领域的文章,即使我加上了计算机图形学的关键词也没太大用。直到我看到一篇关于计算机图形学广告牌效果的文章中广告牌效果的英文名,其写上了Billboarding,我才知道原来Billboarding也可以表达效果。 Billboarding和Billboard Effect

有些遗憾,有些惶恐

        这篇文章几乎是我找资料最久的文章了。我一直在不同的搜索软件中搜索广告牌效果相关的资料,但始终没有找到那份资料说明谁最先提出了这个效果的实现。而写这篇文章所花费的时间几乎可以和我之前写描边文章花费时间差不多了。主要是网上的写法确实五花八门,但究其原理,其实都是一样的,只是想法的不同。最终我发觉网上文章中的介绍都出现在了《Real-Time Rendering》中,但是《Real-Time Rendering》并没有具体的实现。我只好根据网上代码和书中所讲述的效果来实现。我总感觉有些问题,但是我又感觉就是这样。这里我少列举了一个广告牌效果:Impostor。因为我根本就看不懂里面的内容。如果有一天我用到了这个效果,那我一定将其补上。

        于是乎我在这惶恐之下写完了这篇文章。我倒是希望别又错了,但是如果你有发现错误,我仍然欢迎你指出。参考文章中的文章链接是我看的最多的几篇文章,所以才出现在。