【Unity / URP10.8.1】よく使うシェーダーのテクニックについてメモ (座標まわり)
概要
シェーダーの実装を行う上で、個人的に使うことが多いシェーダーのテクニックについてまとめてみようと思います。
環境
Unity 2020.3.32f1
Universal RP 10.8.1
紹介する手法
- Transform の position を求める
- Transform の forward, right, up を求める
- カメラの前方向ベクトルを求める
- ビューベクトルを求める
- 頂点座標から深度値を求める
- スクリーン座標を求める
- 頂点を画面奥へずらす
- 一定の太さのアウトラインを作る
- 深度値からワールド座標を復元する
手法1. Transform.positionを求める
以下のようなコードで、transform.position (オブジェクトの位置) を取得できます。
float3 worldPosition;
worldPosition.x = UNITY_MATRIX_M[0].w; // ワールド空間X座標 (上から1番目の列のW成分)
worldPosition.y = UNITY_MATRIX_M[1].w; // ワールド空間Y座標 (上から2番目の列のW成分)
worldPosition.z = UNITY_MATRIX_M[2].w; // ワールド空間Z座標 (上から3番目の列のW成分)
transposeを利用して行列を転置させることで、座標取得を以下のように書くことができます。
float3 worldPosition = transpose(UNITY_MATRIX_M)[3].xyz; // ワールド空間X座標
取得できる理由
取得できる理由について
Unityシェーダーのfloat4x4
は行優先な行列です。
UNITY_MATRIX_M
は、オブジェクト空間をワールド空間の座標に変換する行列で、
モデル行列 と呼びます。
モデル行列
モデル行列
これを計算すると、以下のようになります。
transpose(行列の転置)を利用する
モデル行列
UNITY_MATRIX_M
の転置を取ると、行[3]に平行移動成分が入ります。
transpose(UNITY_MATRIX_M)[3].xyz
で平行移動成分が取ることができます。
参考記事
手法 2. Transform の forward, right, up を求める
3DオブジェクトのX,Y,Z軸は、以下のようなコードで取得できます。
float3 axisX = transpose(UNITY_MATRIX_M)[0].xyz; // X軸 : transform.right
float3 axisY = transpose(UNITY_MATRIX_M)[1].xyz; // Y軸 : transform.up
float3 axisZ = transpose(UNITY_MATRIX_M)[2].xyz; // Z軸 : transform.forward
取得できる理由
取得できる理由について
ローカル空間の
ワールド空間における軸となります。
行列の転置を取ることで、軸を簡単に取り出すことができます。
参考記事
参考スライド(53ページ)
手法 3. カメラの前方向のベクトルを取得する
以下のコードで、カメラの前方向ベクトルを取ることができます。
float3 cameraForwardDir = -UNITY_MATRIX_V[2].xyz;
URP内部には、ビューベクトルを取得できる関数GetViewForwardDirが用意されているので、これを利用しても良いでしょう。
float3 GetViewForwardDir()
{
float4x4 viewMat = GetWorldToViewMatrix();
return -viewMat[2].xyz;
}
取得できる理由
取得できる理由について
UNITY_MATRIX_V
は、ワールド空間の座標をカメラ空間の座標に変換するような行列で、
ビュー行列と呼ばれます。
ビュー行列
赤く色を付けた要素が、カメラの前方向ベクトルとなります。
カメラの前方向ベクトルを計算する
カメラ空間では、カメラの前方向ベクトルは
これは、ワールド空間のカメラ前方向ベクトル
ビュー行列を適用するとカメラ空間の前方向ベクトル
数式で書くと、以下のようになります。
ビュー行列の逆行列を用いることで、カメラの前方向ベクトルが求まります。
(補足: 回転行列の転置は、回転行列の逆行列になります。)
ビュー行列の2列目にカメラの前方向ベクトルの要素が格納されているので、以下のシェーダーコードでカメラの前方向ベクトルを取ることができます。
float3 axisZ = -UNITY_MATRIX_V[2].xyz;
参考記事
手法 4.ビューベクトルを求める
描画点からカメラまでの向きベクトル(ビューベクトル)を取得するには、URPの GetWorldSpaceViewDir
関数を使うと良いでしょう。
float3 positionWS = TransformObjectToWorld(v.vertex);
float3 viewDir = GetWorldSpaceViewDir(positionWS);
GetWorldSpaceViewDir は、 OrthographicとPerspective の両方で動作するように実装されています。
GetWorldSpaceViewDirの実装
// Computes the world space view direction (pointing towards the viewer).
float3 GetWorldSpaceViewDir(float3 positionWS)
{
if (IsPerspectiveProjection())
{
// Perspective
return GetCurrentViewPosition() - positionWS;
}
else
{
// Orthographic
return -GetViewForwardDir();
}
}
手法 5. 頂点座標から深度値を求める
Orthographic
Linear01DepthやLinearEyeDepthを用いることで、深度値を求めることができます。
// ニアクリップ面:0 ファークリップ面:1
float depth01 = Linear01Depth(clipZ, _ZBufferParams);
// ニアクリップ面:0 ファークリップ面:f
float eyeDepth = LinearEyeDepth(clipZ, _ZBufferParams);
Perspective
Perspective では、clipZ が 0 ~ 1 の深度値となっていますが、グラフィックスAPIによって向きが異なっています。
#if UNITY_REVERSED_Z
float depth = 1.0 - v.z; // ニアクリップ面:0 ファークリップ面:1
#else
float depth = v.z; // ニアクリップ面:0 ファークリップ面:1
#endif
Orthographic と Perspective の両方で動作させる
URPの、IsPerspectiveProjection() という関数を使うことで、PerspectiveかOrthogprahicsかを判断できるので、
これを利用します。
half4 frag (v2f input) : SV_Target
{
// クリップ空間の頂点座標
float clipZ = input.positionCS.z;
if (IsPerspectiveProjection())
{
// Perspective
// ニアクリップ面:0 ファークリップ面:1
float depth01 = Linear01Depth(clipZ, _ZBufferParams);
// ニアクリップ面:0 ファークリップ面:f
float eyeDepth = LinearEyeDepth(clipZ, _ZBufferParams);
return depth01;
}
else
{
// Orthographic
#if UNITY_REVERSED_Z
// ニアクリップ面:0 ファークリップ面:1
float depth01 = 1.0 - clipZ;
#else
// ニアクリップ面:0 ファークリップ面:1
float depth01 = clipZ;
#endif
return depth01;
}
}
手法 6. スクリーン座標を求める
スクリーン座標を取得したい場合、以下のようなシェーダーを書きます。
- vertシェーダー側で、ComputeScreenPosでクリップ空間座標からスクリーン座標を計算
- fragシェーダーで、スクリーン座標をwで除算
v2f vert (appdata v)
{
v2f o;
o.positionCS = TransformObjectToHClip(v.vertex);
o.screenPos = ComputeScreenPos(o.positionCS);
return o;
}
half4 frag (v2f input) : SV_Target
{
float2 screenPos = input.screenPos.xy / input.screenPos.w;
return float4(screenPos, 0, 1);
}
手法 7. 頂点を画面奥へずらす
画面上の頂点の見た目を変えず、頂点の深度値だけをずらす変えるテクニックになります。
グラフィックスAPIによって、クリップ空間のz値の向きが異なるため注意が必要です。
#if UNITY_REVERSED_Z
o.positionCS.z -= 0.01; // ニアクリップ面:z=1 , ファークリップ面:z=0
#else
o.positionCS.z += 0.01; // ニアクリップ面:z=0 , ファークリップ面:z=1
#endif
// クリップ座標のz値が0~1の範囲外に出ると、表示されなくなるためclampをかける
o.positionCS.z = clamp(o.positionCS.z, 0.001, 0.999);
関連
手法 8. 一定の太さのアウトラインを作る
距離の影響を受けないアウトライン
カメラから離れるほど、W除算によってクリップ空間上の頂点の移動量が小さくなります。
クリップ座標のW値を乗算することで、W除算の影響を打ち消すことができるため、
距離の影響を受けないアウトラインを作ることができます。
v2f vert (appdata v)
{
v2f o;
o.positionCS = TransformObjectToHClip(v.vertex);
float3 normal = TransformObjectToWorldDir(v.normal);
normal = TransformWorldToHClipDir(normal);
#define WIDTH 0.005
// アウトライン用の頂点移動 (クリップ座標のWで乗算)
o.positionCS.xy += normal.xy * WIDTH * o.positionCS.w;
FOVの影響を受けないアウトライン
FOVが大きくなるほど、クリップ空間上の頂点座標の移動量が小さくなります。
unity_CameraProjection._m11
で割ることで、FOVの影響を打ち消すことができるため、
FOVの影響を受けないアウトラインを作ることができます。
v2f vert (appdata v)
{
v2f o;
o.positionCS = TransformObjectToHClip(v.vertex);
float3 normal = TransformObjectToWorldDir(v.normal);
normal = TransformWorldToHClipDir(normal);
#define WIDTH 0.005
// アウトライン用の頂点移動 (unity_CameraProjection._m11で除算)
o.positionCS.xy += normal.xy * WIDTH / unity_CameraProjection._m11;
手法 9. 深度値からワールド座標を復元する
スクリーン座標と深度値が決まると、そのワールド座標を求めることができます。
void ReconstructWorldFromDepth(float2 ScreenPosition, float Depth, float4x4 unity_MatrixInvVP, out float3 Out)
{
// スクリーン座標とDepthからクリップ座標を作成
float4 positionCS = float4(ScreenPosition * 2.0 - 1.0, Depth, 1.0);
#if UNITY_UV_STARTS_AT_TOP
positionCS.y = -positionCS.y;
#endif
// クリップ座標にView Projection変換を適用し、ワールド座標にする
float4 hpositionWS = mul(unity_MatrixInvVP, positionCS);
// 同次座標系を標準座標系に戻す
Out = hpositionWS.xyz / hpositionWS.w;
}
その他 : 空間変換系の便利関数
座標変換系
URPには、座標を変換するための関数が多く用意されています。
float3 TransformObjectToWorld(float3 positionOS)
float3 TransformWorldToObject(float3 positionWS)
float3 TransformWorldToView(float3 positionWS)
float4 TransformObjectToHClip(float3 positionOS)
float4 TransformWorldToHClip(float3 positionWS)
float4 TransformWViewToHClip(float3 positionVS)
float3 TransformTangentToWorld(float3 dirTS, float3x3 tangentToWorld)
float3 TransformWorldToTangent(float3 dirWS, float3x3 tangentToWorld)
float3 TransformTangentToObject(float3 dirTS, float3x3 tangentToWorld)
float3 TransformObjectToTangent(float3 dirOS, float3x3 tangentToWorld)
GetVertexPositionInputs
GetVertexPositionInputs()を使うと、各種空間の座標をまとめて取れます。
VertexPositionInputs positionInputs = GetVertexPositionInputs()
struct VertexPositionInputs
{
float3 positionWS; // World space position
float3 positionVS; // View space position
float4 positionCS; // Homogeneous clip space position
float4 positionNDC;// Homogeneous normalized device coordinates
};
向き変換系
向きを変換するための関数も用意されています。
float3 TransformObjectToWorldDir(float3 dirOS, bool doNormalize)
float3 TransformWorldToObjectDir(float3 dirWS, bool doNormalize)
float3 TransformWorldToViewDir(float3 dirWS, bool doNormalize)
float3 TransformWorldToHClipDir(float3 directionWS, bool doNormalize)
float3 TransformObjectToWorldNormal(float3 normalOS, bool doNormalize)
float3 TransformWorldToObjectNormal(float3 normalWS, bool doNormalize)
Discussion