使用顶点动画中的广告牌技术(Billboarding),来实现大卢恩在竖直方向上保持始终朝向玩家的效果。
1、广告牌技术原理
广告牌技术会根据视角方向来旋转一个被纹理着色的多边形,通常用于渲染烟雾、云朵、火焰等。
使用三个向量确定这个广告牌的方向:
- 表面法线(normal)
- 指向上的方向(up)
- 指向右的方向( right)
存在两种广告牌:
- 广告牌完全朝向摄像机。此时广告牌的法向量就是视角方向
- 广告牌在保持垂直的同时朝向摄像机。(如艾尔登法环中的大卢恩)此时广告牌的向上的方向始终垂直。
构建这三个向量的过程:
- 通过初始计算得到广告牌的表面法线(即视角方向) 和指向上的方向。
两者往往不垂直,但是两者其中之一是固定的。 - 我们假设广告牌完全朝向摄像机。那么法线方向是固定的。
我们根据初始的表面法线和指向上的方向来计算出目标方向的指向右的方向(叉积)
right = normal * up
- 对其归一化后,再由法线方向与指向右的方向计算出正交的指向上的方向即可得到新的 up。
up = normal * right
- 如果广告牌在保持垂直的同时朝向摄像机(大卢恩),过程也相似。
只要在最开始将广告牌的法线方向的 y 值(竖直方向上)设为 0 。
这样再按照上述的固定法线的方式计算就可以保证广告牌始终垂直啦。
2、实现
整体代码在此部分最后展示
在 Unity 中创建一个 Shader,创建一个材质,将创建的 Shader 赋给创建的材质。
以下为 Shader 的写作过程:
声明变量:
Properties { //透明纹理 _MainTex ("Main Tex", 2D) = "white" {} //颜色 _Color ("Color Tint", Color) = (1, 1, 1, 1) //约束垂直方向上的程度(是固定发法线还是固定指向上的方向) _VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1 }
广告牌技术中,我们需要在模型空间下进行计算。
因此需要在 SubShader 的标签中开启 DisableBatching。以取消批处理:
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
Pass 的 Tags 设置前向渲染。Tags { "LightMode"="ForwardBase" }
关闭深度写入,开启混合模式,关闭剔除功能。
ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Cull Off
定义输入、输出结构体:
CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; fixed _VerticalBillboarding; struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
所有的计算都是在模型空间中进行的。顶点着色器是本 Shader 的核心。
在顶点着色器中:
首先使用模型空间的原点作为广告牌的锚点 center,计算视角位置在模型空间下的坐标 viewer。
v2f o;//输出结构体 float3 center = float3(0, 0, 0); float3 viewer = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1));
按照广告牌的原理计算向量。使用 _VerticalBillboarding 控制垂直方向上的约束度。
- 当 _VerticalBillboarding 为1的时候,意味着法线固定。
此时广告牌完全朝向摄像机。
此时法向量就是视角朝向方向。 - 当 _VerticalBillboarding 为0的时候,意味着向上的方向固定为(0,1,0)。
此时广告牌在保持垂直的同时朝向摄像机。
此时法向量在竖直方向上的值,也就是 y 值为 0。
x、z 值与视角方向向量的x、z 值相同。
float3 normalDir = viewer - center; // If _VerticalBillboarding equals 1, we use the desired view dir as the normal dir // Which means the normal dir is fixed // Or if _VerticalBillboarding equals 0, the y of normal is 0 // Which means the up dir is fixed normalDir.y =normalDir.y * _VerticalBillboarding; normalDir = normalize(normalDir);
随后,为了防止视角方向正好是竖直方向,导致法线可能会与向上的方向重合(会导致叉积结果是0),
进行一下判断。
如果重合,那么就让向上的方向为(0, 0, 1)。
// Get the approximate up dir // If normal dir is already towards up, then the up dir is towards front float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0); //计算向右的方向 float3 rightDir = normalize(cross(upDir, normalDir)); upDir = normalize(cross(normalDir, rightDir));
abs() 函数:
取绝对值。
最后计算得到新的顶点位置:
// Use the three vectors to rotate the quad float3 centerOffs = v.vertex.xyz - center; float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z; o.pos = UnityObjectToClipPos(float4(localPos, 1)); o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); return o;
片元着色器很简单:
fixed4 frag (v2f i) : SV_Target { fixed4 c = tex2D (_MainTex, i.uv); c.rgb *= _Color.rgb; return c; }
最终代码如下:
Shader "Unity Shaders Book/Chapter 11/Billboard" { Properties { _MainTex ("Main Tex", 2D) = "white" {} _Color ("Color Tint", Color) = (1, 1, 1, 1) _VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1 } SubShader { // Need to disable batching because of the vertex animation Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"} Pass { Tags { "LightMode"="ForwardBase" } ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Cull Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; fixed _VerticalBillboarding; struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert (a2v v) { v2f o; // Suppose the center in object space is fixed float3 center = float3(0, 0, 0); float3 viewer = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1)); float3 normalDir = viewer - center; // If _VerticalBillboarding equals 1, we use the desired view dir as the normal dir // Which means the normal dir is fixed // Or if _VerticalBillboarding equals 0, the y of normal is 0 // Which means the up dir is fixed normalDir.y =normalDir.y * _VerticalBillboarding; normalDir = normalize(normalDir); // Get the approximate up dir // If normal dir is already towards up, then the up dir is towards front float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0); float3 rightDir = normalize(cross(upDir, normalDir)); upDir = normalize(cross(normalDir, rightDir)); // Use the three vectors to rotate the quad float3 centerOffs = v.vertex.xyz - center; float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z; o.pos = UnityObjectToClipPos(float4(localPos, 1)); o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 c = tex2D (_MainTex, i.uv); c.rgb *= _Color.rgb; return c; } ENDCG } } FallBack "Transparent/VertexLit" }
上面的代码要求平面模型必须满足:
平面模型的顶点在模型空间内是竖直排列的。这样我们偏移得到的才是正确的顶点位置。
3、效果
将大卢恩的贴图拖给材质,调整属性即可得到如下效果:
4、附:大卢恩纹理图片