Unity
UnrealEngine

[UE4] マテリアルに3軸方向からテクスチャを貼ってUVの境目を目立たなくする

適当に背景素材を作っていると、テクスチャの入り方がおかしくなる場合が多々あります。

例えば、 Sphere 形状のメッシュにテクスチャを貼ると、次の様に UV の境目が目立ちます。
normal_tilling.png
また、UV(0, 0), UV(1, 1) の点へテクスチャが吸い込まれてるような感じになり、半径が大きな中央部分のテクスチャも伸びいます。
(繋ぎ目が目立つように半端な値でタイリングしています)

この原因は UV の座標値が次のようになっている為です。
uv_color.png

テクスチャの UV 座標

uv_coord.png
waifu2x で 2^2 倍に拡大した。

今回は、どんな形状でもそれとなくいい感じにテクスチャが入るマテリアルを作成していきます。

3軸方向からテクスチャを貼るマテリアルを作成する

複数の方向からテクスチャを貼ることで上記の課題の解決を試みます。
今回利用するテクスチャはスターターコンテンツに含まれる次のアセットです。

textures.PNG

まずは、3軸方向からテクスチャを貼るための情報を生成します。
各 XYZ 軸の両面から、直交する座標を UV としてテクスチャを貼っていきます。
X 軸方向に対しては YZ を UV とする感じです。
また、各軸に対するテクスチャを貼る部分と UV の境目をブレンドするマスク値を生成します。

uv_and_mask.PNG

3軸方向の UV の生成

UV の値はピクセル座標の xyz から、2チャンネルずつ組合せて、3軸方向の UV 値を生成します。

  1. ピクセル座標を WorldPosition から取得し、ObjectPosition を引くことで、オブジェクトの移動に対応します。
  2. ピクセル座標を TransformVector で WorldSpace から LocalSpace へ変更し、オブジェクトの回転へ対応します。
  3. LocalSpace へ変更すると長さが正規化されるので、ObjectScale を掛けてオブジェクトの拡縮へ対応しています。
  • Mask(R, B): xz 座標を UV 座標とする
  • Mask(R, G): xy 座標を UV 座標とする
  • Mask(G, B): yz 座標を UV 座標とする

3軸方向のマスク値の生成

マスクの値は、頂点の法線の向きを基準に生成します。

  1. 法線情報を VertexNormalWS から取得し、LocalSpace へ変更することでオブジェクトの回転へ対応します。
  2. 法線の値は、-1 ~ 1 の値を取るので、Abs で絶対値へ変換し、各軸の両面へマスクが掛かるようにします。
  3. Power マスク境界のブレンドを調整できるようにします。
  4. 各チャンネルの値を平準化し、Vector を単一チャンネルへ分解します。

マスク値を可視化すると次のようなサイコロ状になり、3軸6方向にマスクが掛かるのが分かります。
mask_color.PNG

  • R: X 座標に対するマスク値(直交する YZ 座標の UV に対するマスク)
  • G: Y 座標に対するマスク値(直交する XZ 座標の UV に対するマスク)
  • B: Z 座標に対するマスク値(直交する XY 座標の UV に対するマスク)

テクスチャをサンプリングしてマスクしてブレンド

それぞれの UV でサンプリングした値を Multiply でマスクし、Add でブレンドします。
各UVに対し、直交する軸のマスク値を利用します。
UV(R, B) * Mask(G) + UV(R, G) * Mask(B) + UV(G, B) * Mask(R)

ベースカラーグラフ

(アルファからラフネスを取っています)
triplar_basecolor.PNG

ノーマルマップグラフ

triplar_normal.PNG

レンダリング結果を確認する

こんな感じで、テクスチャを均一に貼ることができるました。
ブレンドしているところはよく見れば分かりますが、オブジェクトを回転して目立たないようにするなりしましょう。
triplaner_texture.PNG

サンプル数が多くて重くない?

各ピクセルで6回のサンプリングを行うので、場合によっては問題になる可能性があります。

マスク軸のどれかが1に近ければ、マスクもブレンドも行う必要はなく、最も大きいマスク値に直交する UV 座標で1回サンプリングを行うだけで良いかもしれません。

これは CustomNode でシェーダーコードを直接書くことで実装できます。

ExpressionCode
/** 引数定義
 * Tex: TextureObject
 * UV3: float3 (UV座標に使う vector, UV の VertexInterpolator からとってくる)
 * Mask3: float3 (マスク座標に使う vector, Mask の VertexInterpolator からとってくる)
 * Threshold: float (しきい値, 0.999 とか)
 * OutputType: float4
 */

float4 ret;

[branch] if (Mask3.x >= Threshold) {
  ret = Texture2DSample(Tex, TexSampler, float2(UV3.y, UV3.z));
}
else [branch] if (Mask3.y >= Threshold) {
  ret = Texture2DSample(Tex, TexSampler, float2(UV3.x, UV3.z));
}
else [branch] if (Mask3.z >= Threshold) {
  ret = Texture2DSample(Tex, TexSampler, float2(UV3.x, UV3.y));
}
else {
  float4 sample_x = Texture2DSample(Tex, TexSampler, float2(UV3.y, UV3.z));
  float4 sample_y = Texture2DSample(Tex, TexSampler, float2(UV3.x, UV3.z));
  float4 sample_z = Texture2DSample(Tex, TexSampler, float2(UV3.z, UV3.y));
  ret = lerp(lerp(sample_x, sample_y, Mask3.y), sample_z, Mask3.z);
}

return ret;  // BaseColor のリターン

// return UnpackNormalMap(ret);  // NormalMap は UnpackNormalMap() して返す

こんな感じでサンプリング数を減らして軽量化できるかもしれません。

CustomeNode は Output を追加できないので、結局、BaseColor と NormalMap で最小でも 2回のサンプリングが行われてしまう。