作者:王子饼干 ——镇江虹视游戏科技有限公司联合创始人、《多洛可小镇》制作人
原文链接:UnityShader入门精要17:着色器动画2
原文发布时间:2020年3月28日
文章类型: 授权转载
根本就没有什么食神,或者说,人人都是食神。——周星驰《食神》
笔记当前使用的Unity版本:“2019.3.3”
笔记当前Unity最新发行版:“2019.3.6”
上一节我们提到了纹理动画和顶点动画,这一节主要是补充上一节的内容,我们会提到如何实现“广告牌技术”,并且提一些关于着色器动画的注意事项。
广告牌技术也就是所谓的Billboarding,它可以让一个quad或者模型始终面向摄像机,在游戏当中有非常多的应用,比如很多手游中的树,爆炸效果,其实就是一张图,我们给它加上billboarding效果,它就可以一直面向玩家,是一种惯用的,效果比较好的trick手法。除了这些,角色的血条,或者早期游戏当中的一些跟随性的UI组件也是这样的。
广告牌技术的原理相对来说比较简单,无非是根据视角方向来构建一个旋转矩阵。用这个旋转矩阵对模型进行变换。我们已经知道了,构建一个矩阵需要三个基向量。
所谓基向量就是从目标原点(可以是灯光,可以是模型,可以是很多东西)引出三条两两垂直的标准向量。比如(1,0,0),(0,1,0),(0,0,1)就可以构建出一个变换矩阵。由于三个基向量两两垂直,因而我们也可以称其为正交基。
水平方向的Billboard通常说的是只围绕y轴进行旋转的效果。一般用于不应该围绕x轴或者z轴旋转的模型。比如场景中的小草,树之类的。
这样,我们先有了两个基本的向量,一个是视角方向,这个可以通过摄像机的位置减去模型的位置计算出来。一个是模型朝上的方向,这个方向是固定的,(因为我们绕y轴旋转)我们对这两个向量做叉积,可以得到一个同时垂直于up和view方向的向量,暂且命名为right。
第一是注意一下right向量不在平面上,它和平面有一定的夹角,有了right向量之后,我们就可以再使用up和right进行叉积计算,得到一个同时垂直于up和right的方向view'
然后我们就有了三个正交基,up,right,view',接着我们可以通过这三个向量构建出一个矩阵,用于旋转平面。大概就是下面的感觉。
ok,知道了这个原理之后就可以开始写代码了。
Shader "UShaderMagicBook/Billboard1"{
Properties{
_MainTex("Main Texture",2D) = "white"{}
}
SubShader{
Tags{"Queue"="Transparent" "RenderType"="Transparent" "IgnoreProjector"="True" "DisableBatching"="True"}
/*
这里我们使用了DisableBatching标签,一些SubShader在使用Unity的批处理功能的时候会产生问题,
这个时候可以通过该标签来直接指明是否对SubShader使用批处理功能。而这些需要特殊处理的Shader通常
就是指包含了模型空间的顶点动画的Shader。批处理会导致模型空间的丢失,而这正好是顶点动画所需要的
所以我们在这里关闭Shader的批处理操作。
*/
pass{
Tags{"LightMode"="Always"}
Cull Off //关闭剔除,让模型的两面都可以显示
CGPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
#include "Lighting.cginc"
#include "UnityCG.cginc"
sampler2D _MainTex;
struct vertexOutput{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
vertexOutput Vertex(appdata_base v){
vertexOutput o;
float3 center = float3(0,0,0);
/*设(0,0,0)为模型的中心点,用于代替模型的位置,这个一定要有,
本质上中心点的位置不变其他顶点围绕中心点发生变化
问1:为什么不使用worldPos=mul(unity_ObjectToWorld,v.vertex).xyz?
答1:使用worldPos代表了每个顶点的位置,每个顶点都有自己的方向,如果使用
worldPos,那么每个顶点便会自顾自的旋转,而非整体旋转。(ps:我们是在模型
空间计算)
*/
float3 viewer = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos,1)).xyz;
/*把摄像机的位置转换到模型空间*/
float3 viewDir = viewer - center;
viewDir = normalize(viewDir); //始终保持center不是(0,0,0)的假设
float3 upDir = float3(0,1,0); //up方向
float3 rightDir = normalize(cross(upDir,viewDir)); //右方向
viewDir = normalize(cross(upDir,rightDir)); //新的模型视角方向
float3 centerOffset = v.vertex.xyz - center;
float3x3 _RotateMatrix = transpose(float3x3(rightDir,upDir,viewDir));
float3 pos = mul(_RotateMatrix,centerOffset) + center;
o.pos = UnityObjectToClipPos(float4(pos,1));
o.uv = v.texcoord;
return o;
}
fixed4 Pixel(vertexOutput i):SV_TARGET{
return tex2D(_MainTex,i.uv);
}
ENDCG
}
}
}
按照之前的想法,先把摄像机位置转换到模型空间,然后计算出它面向中心点的方向,完成后我们设定上方向,根据上方向和视角方向来计算出右方向,然后再根据右方向和上方向重新调整视角方向,随后我们用三个方向构建一个矩阵,用其对每个顶点的偏移进行空间转换,然后再将这个偏移还原到新的空间(加上center)
我特别不理解为什么要用矩阵的转置,后来我才知道了,mul似乎不是矩阵左乘,而是右乘。(原书是手动乘的,这里是为了大家方便理解)这里放一下原书的代码:
float3 pos = centerOffset.x * rightDir + centerOffset.y * upDir + centerOffset.z * viewDir + center;
ok~让我们看下最终效果。
看起来还不错,不过它是水平方向的,也就是上方向始终不变。
接下来我们需要编写一个始终面向摄像机的Billboarding,同样,我们用图来描述这个计算过程。还是先给出上方向和视角方向。
有了上方向和视角方向,我们可以根据上方向和视角方向的叉积来计算出右方向,如下图所示。
接着就是重点了,由于这次我们需要面向视角方向,所以我们根据右方向和视角方向来计算出上方向。
接着按照右方向,上方向,视角方向的顺序(这个顺序其实就是xyz坐标的顺序)排列这三个正交基,可以构建出一个变换矩阵。
ok,了解了原理之后我们就来实践一下吧,理论上是可行的。
Shader "UShaderMagicBook/Billboard2"{
Properties{
_MainTex("Main Texture",2D) = "white"{}
}
SubShader{
Tags{"Queue"="Transparent" "RenderType"="Transparent" "IgnoreProjector"="True" "DisableBatching"="True"}
/*
这里我们使用了DisableBatching标签,一些SubShader在使用Unity的批处理功能的时候会产生问题,
这个时候可以通过该标签来直接指明是否对SubShader使用批处理功能。而这些需要特殊处理的Shader通常
就是指包含了模型空间的顶点动画的Shader。批处理会导致模型空间的丢失,而这正好是顶点动画所需要的
所以我们在这里关闭Shader的批处理操作。
*/
pass{
Tags{"LightMode"="Always"}
Cull Off //关闭剔除,让模型的两面都可以显示
CGPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
#include "Lighting.cginc"
#include "UnityCG.cginc"
sampler2D _MainTex;
struct vertexOutput{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
vertexOutput Vertex(appdata_base v){
vertexOutput o;
float3 center = float3(0,0,0);
/*设(0,0,0)为模型的中心点,用于代替模型的位置,这个一定要有,
本质上中心点的位置不变其他顶点围绕中心点发生变化
问1:为什么不使用worldPos=mul(unity_ObjectToWorld,v.vertex).xyz?
答1:使用worldPos代表了每个顶点的位置,每个顶点都有自己的方向,如果使用
worldPos,那么每个顶点便会自顾自的旋转,而非整体旋转。(ps:我们是在模型
空间计算)
*/
float3 viewer = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos,1)).xyz;
/*把摄像机的位置转换到模型空间*/
float3 viewDir = viewer - center;
viewDir = normalize(viewDir); //始终保持center不是(0,0,0)的假设
float3 upDir = float3(0,1,0); //up方向
float3 rightDir = normalize(cross(upDir,viewDir)); //右方向
upDir = normalize(cross(viewDir,rightDir)); //新的模型视角方向
float3 centerOffset = v.vertex.xyz - center;
float3x3 _RotateMatrix = transpose(float3x3(rightDir,upDir,viewDir));
float3 pos = mul(_RotateMatrix,centerOffset) + center;
//float3 pos = centerOffset.x * rightDir + centerOffset.y * upDir + centerOffset.z * viewDir + center;
o.pos = UnityObjectToClipPos(float4(pos,1));
o.uv = v.texcoord;
return o;
}
fixed4 Pixel(vertexOutput i):SV_TARGET{
return tex2D(_MainTex,i.uv);
}
ENDCG
}
}
}
代码几乎是没有变化的,只不过在计算viewDir的地方换成了计算upDir而已,看一下最终的效果。
看起来还不错~不过在视角垂直于z轴的时候,会发生图像的倒转。
顶点动画虽然非常灵活有效,但是有一些注意事项。
首先我们在模型空间计算了转向,所以我们必须取消SubShader的批处理,但是这种计算会让DarwCall增加,使得我们总体的渲染效率降低。其次,虽然我们看起来它像是一直在面向我们,但是实际的mesh位置却没有发生任何改变。
这会导致在阴影计算的时候会产生问题,所以我们需要同时重新编写一个pass来生成阴影映射纹理。在这个pass中,我们需要对阴影进行同样的顶点变换。关于这部分,本章节就不再阐述了,后面会作为该系列的补充来讲解。
这个章节就算结束了,最近有点别的事情,所以导致了更新速度缓慢,看起来在4月来临之前把《UnityShader入门精要》笔记搬完的目标是达成不了了。
另外,本次使用到的所有代码都已经上传到了我的github,在下面的链接中可以找到。如果你喜欢这个系列的文章,欢迎给我的github贡献一颗⭐,感谢阅读。