2DテクスチャをUVを使わずに投影するTri-Planar Texture Mapping

## 概要

Shadertoyの[この作品](https://www.shadertoy.com/view/ldyGWm)を見ていたら知った手法。
通常の2Dテクスチャを3Dモデルの位置と法線から投影するように貼り付ける方法です。

詳細については以下の記事をご覧ください。
今回はこれを理解するために自分なりのメモとして書きました。

- [Use Tri-Planar Texture Mapping for Better Terrain](https://gamedevelopment.tutsplus.com/articles/use-tri-planar-texture-mapping-for-better-terrain--gamedev-13821)

実行すると以下のように、通常の2Dテクスチャを立体的に貼り付けることができます。
（ちなみに下の例はレイマーチによって描き出されたCubeに対して、2Dのテクスチャを貼り付けているところです）
![Tri-Planar Texture Mappingサンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/994028bc-b738-e592-eb15-ae7d0c8c1a19.gif)
- [Tri-Planar Texture Mapping](https://www.shadertoy.com/view/wtjGWy)

## 解説

冒頭でも書いたように[こちらのShadertoyの作品](https://www.shadertoy.com/view/ldyGWm)で使用されているものを参考にしました。
言及されている記事のURLがすでになくなっていたので検索して見つけた以下の記事を参考に実装しています。

- [Use Tri-Planar Texture Mapping for Better Terrain](https://gamedevelopment.tutsplus.com/articles/use-tri-planar-texture-mapping-for-better-terrain--gamedev-13821)

まずはコードを。

```glsl:Tri-Planar_Texture_Mapping
vec3 tex3D(sampler2D tex, vec3 p, vec3 n)
{
    vec3 blending = abs(n);
    blending = normalize(max(blending, 0.00001));
    
    // normalized total value to 1.0
    float b = (blending.x + blending.y + blending.z);
    blending /= b;
    
    vec4 xaxis = texture(tex, p.yz);
    vec4 yaxis = texture(tex, p.xz);
    vec4 zaxis = texture(tex, p.xy);
    
    // blend the results of the 3 planar projections.
    return (xaxis * blending.x + yaxis * blending.y + zaxis * blending.z).rgb;
}
```

コード自体は長くないですね。
なにをやっているかのイメージは、参考にさせてもらった記事から引用すると以下のようになります。

![triPlanar-angles.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/2bc93eee-e4b3-0fd0-4e4e-3db4b5909180.png)
※ 対象位置に対して3方から2Dテクスチャをプロジェクションしているようなイメージです。

では順を追って見ていきましょう。

### 法線を調整

まず冒頭で行っている部分ですが、（おそらく）法線を使える形に調整しているものと思われます。
該当コードは以下。

```glsl:法線の調整
vec3 blending = abs(n);
blending = normalize(max(blending, 0.00001));
```

`n`が法線です。これを`abs`することによってマイナスをなくします。（テクスチャの値調整に利用するのでマイナスはいらないってことですね）
そして次の行の`max`ですが、これはゼロベクトルによる処理をなくすためかなと。
`0.00001`との`max`を取ることでゼロの値を少しだけ底上げしています。
そしてさらにそれを`normalize`で正規化します。


### 合計値を1.0になるように調整

さて次の部分は何をしているかというと、（利用箇所を見ると分かりますが）法線の値の合計を`1.0`になるように調整しています。

```glsl:合計値を1.0に調整
float b = (blending.x + blending.y + blending.z);
blending /= b;
```

上で調整したベクトルをさらにその合計で割っています。
つまり、調整されたあとのベクトルは`xyz`成分の合計が必ず`1.0`になる、ということです。

-----------------

例えば、xyzの値が`(0.25, 0.43, 0.86)`という単位ベクトルがあったとして、今のままだと合計すると`1.54`になり`1.0`を超えてしまいます。（あくまで**ベクトルの長さが1.0**の成分のため）

なので、xyz成分すべてをこの`1.54`で割ることで合計が`1.0`になるように調整している、というわけですね。

-----------------

次のところで利用しますが、このベクトルを用いて3方のテクスチャを合成するため`1.0`を超えないように調整している、というわけですね。



### 3軸分のテクセルを合成する

最後の部分はテクセルの合成です。

```glsl:3軸分のテクセルを合成する
vec4 xaxis = texture(tex, p.yz);
vec4 yaxis = texture(tex, p.xz);
vec4 zaxis = texture(tex, p.xy);
    
// blend the results of the 3 planar projections.
return (xaxis * blending.x + yaxis * blending.y + zaxis * blending.z).rgb;
```

最初の部分に注目すると、変数名が軸の名前になっているのが分かるかと思います。
そしてその変数には、**評価している点（`p`）の軸に投影した位置**を利用してテクセルフェッチした値が入っています。

例えば`xaxis`に着目すると、テクセルは`p.yz`を利用してフェッチしています。（`x`を無視しているのが分かりますね）
これは`x`軸を無視した`yz`の値です。（`p.x = 0;`とした、と考えてもいいと思います）
要は`YZ`平面上でのテクスチャの位置をサンプリングしようとしている、というわけです。

それ以外の軸に対するテクセルフェッチも同様の考え方です。
これで**3方（3平面）のテクセル**を取得することができました。


#### 取得したテクセルを合成する

最後の工程は前述の3方（3平面）のテクセルを適切に合成することです。

以下の部分ですね。

```glsl:テクセルの合成
// blend the results of the 3 planar projections.
return (xaxis * blending.x + yaxis * blending.y + zaxis * blending.z).rgb;
```

合計値を`1.0`に調整すると書いたのがここの計算の目的のためです。
3つのテクセルを合成するため、合計値が`1.0`にならないと適切に合成できませんよね。
そのために合計が`1.0`になるように調整されていたわけです。

そして計算自体はとてもシンプルです。
`blending`ベクトルは、法線がどの平面に、**より向いているか**を示すものです。
つまり仮に、真上を向いている法線の場合は`yaxis`のテクセルを100%採用する、というわけですね。
しかしながら、だいたいの場合において法線は色々な方向を向いているので、その`xyz`の値を、**どの平面に向いているかの割合**と考えて、それを元にテクセルを合成している、というわけなのですね。

ちなみに、xyz =` (1, 0, 0)`、`(0, 1, 0)`、`(0, 0, 1)`と法線を無視してそれぞれの平面に対するテクセルを強制的にフェッチしてみると以下のような結果になります。なんとなくイメージが湧くのではないでしょうか。

**(1, 0, 0)の投影**↓
![(1, 0, 0)の投影](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/21155b13-66cd-45fd-1fbd-51a4ed579e50.png)

**(0, 1, 0)の投影**↓
![(0, 1, 0)の投影](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/4348282d-4ee3-e2a2-e6ff-e114021eb151.png)

**(0, 0, 1)の投影**↓
![(0, 0, 1)の投影](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/65dbaf2b-896e-b585-c546-e602d5da003b.png)



## まとめ

意外とシンプルなコードながら、Shadertoyで絵を書いていくのに使えるテクニックだなーと思っています。
それに、実際のコンテンツ制作時にも意外と役に立つかもしれませんね。