實現艾爾登法環中的大盧恩效果

使用頂點動畫中的廣告牌技術(Billboarding),來實現大盧恩在豎直方向上保持始終朝向玩家的效果。

1、廣告牌技術原理

廣告牌技術會根據視角方向來旋轉一個被紋理著色的多邊形,通常用於渲染煙霧、雲朵、火焰等。

使用三個向量確定這個廣告牌的方向:

  • 表面法線(normal)
  • 指向上的方向(up)
  • 指向右的方向( right)

存在兩種廣告牌:

  • 廣告牌完全朝向攝像機。此時廣告牌的法向量就是視角方向
  • 廣告牌在保持垂直的同時朝向攝像機。(如艾爾登法環中的大盧恩)此時廣告牌的向上的方向始終垂直。

構建這三個向量的過程:

  • 通過初始計算得到廣告牌的表面法線(即視角方向) 和指向上的方向。
    兩者往往不垂直,但是兩者其中之一是固定的。
  • 我們假設廣告牌完全朝向攝像機。那麼法線方向是固定的。
    我們根據初始的表面法線和指向上的方向來計算出目標方向的指向右的方向(叉積)
    right = normal * up
  • 對其歸一化後,再由法線方向與指向右的方向計算出正交的指向上的方向即可得到新的 up。
    up = normal * right

image-20220818204447837

  • 如果廣告牌在保持垂直的同時朝向攝像機(大盧恩),過程也相似。
    只要在最開始將廣告牌的法線方向的 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、效果

image-20220827214323954

將大盧恩的貼圖拖給材質,調整屬性即可得到如下效果:

image-20220827214323954

image

4、附:大盧恩紋理圖片

image-20220827214323954
image-20220827214323954
image-20220827214323954