文章內使用Unity 2019 LTS
看板是一個遊戲中很常見的功能,讓一個2D平面永遠面向相機,常用在一些看起來重複模型相當多的背景上,例如星空、雲朵,給予玩家這些視覺效果,也可以減少效能的消耗。
Unity完整的API函式庫,可以簡單的幾行(一行?!)程式碼完成這個功能:
using UnityEngine;
public class Billboard : MonoBehaviour
{
// Update is called once per frame
void Update()
{
// 直接呼叫LookAt()面向相機
transform.LookAt(transform.position + Camera.main.transform.rotation * Vector3.forward);
}
}
利用腳本可以簡單的一行把基本功能就做出來,但利用Shader就不一樣了,需要完整的去操控一些渲染的細節,並不是因為Shader的方法原理不一樣,只是Unity API將實作細節隱藏起來。
向量是製作遊戲的好朋友,遊戲中凡是位移、縮放、旋轉動畫、物體運動都與向量有關,想要改變向量位置的方法,如果還記得的話,叫做線性轉換,是線性代數中一個很重要的概念。
如果忘記了,或是第一次接觸想要知道甚麼是線性轉換的話,可以參考這部影片:
十分推薦這位大大的線性代數系列,我有很多觀念都是透過他的影片來重新認識的。
回到主題上,要利用Shader撰寫一個看板功能我們目標就是要找的一個矩陣可以將2D平面的頂點位置一直轉向相機。
既然我們是在一個三維世界,我們則需要一個3x3的矩陣來對應我們頂點向量
矩陣第一列(Column)的處理向量的x分量,第二列變換y分量,第三列變換z分量。我們知道矩陣的每一列都是由一組向量組成的,所以我們要找出三組向量所組成的矩陣。
我們的2D平面要面向相機,所以第一個向量就出來了:
z_axis = camera_position - center
看板永遠指向上方,所以:
y_axis = vector3(0, 1, 0)
最後,X分量的向量,我們可以直接利用叉積(cross product)(註1):
x_axis = cross_product(z_axis, y_axis)
註1: 叉積計算變數的前後順序會影響到答案!
這樣,我們所需要的向量收集全了,以下是看板功能的Shader:
Shader "Learning/Billboard" {
Properties{
_MainTex("Main Tex", 2D) = "white" {}
_Color("Color Tint", Color) = (1, 1, 1, 1)
}
SubShader{
// 兩個標籤
// IgnoreProjector: True的話表示不受Projector元件影響
// DiableBatching: 關閉批次處理是因為頂點會因為會需要對模型空間的頂點做處理,
// Batching會導致資料遺失
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;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_base v) {
v2f o;
float3 center = float3(0, 0, 0);
float3 viewer = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1));
float3 normal = viewer - center;
float3 normalDir = normalize(normal);
float3 upDir = float3(0, 1, 0);
float3 rightDir = normalize(cross(upDir, normalDir));
upDir = normalize(cross(normalDir, rightDir));
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
}
}
}