ひつじTips

技術系いろいろつまみ食います。

[Unity]ShaderでテクスチャのUVを操作して目の注視(LookAt)っぽいことをしてみる

f:id:mu-777:20190713135728g:plain
カメラの位置が移動すると,その方向に黒い丸(黒目)が向く

こういう感じに,目がこっちを常に見てくる(LookAtする)のをテクスチャのUV操作で実現する,というネタです.(黒い丸が黒目,という設定です!心の目で目を見てください!)
なにげにこのブログでUnityネタは初かも.まぁUnityはいろいろな方が書いた記事がめちゃくちゃあるし,あんまり書くことがないんすよね(いつもお世話になっております>各位).

背景としては,後輩さん(@Korechi_)が言っていた「OculusQuestのGuardianを使ってなにかやりたい」というネタをパクって考えていて,Guardianという名前からインスパイアされてのジャストアイデアで,1984のBigBrother風のネタを思いついて実装してみたのがコレです.

で,コレをするときに,壁の目がユーザをLookAtさせたいなということで,このUV操作でLookAtをやりました.それのやりかたを忘れないようにここに書いておきます.

方針

f:id:mu-777:20190714033952p:plain
大雑把な方針
  1. 各Vertexで,そのVertexから見たユーザの位置のベクトル取得
  2. そのベクトルをメッシュ平面のUV座標に射影
  3. その射影したベクトルを適当にスケーリング
  4. そのスケーリングしたベクトル分だけ黒目テクスチャのUVをオフセットする

これを毎フレーム行う,という感じで行きました.

結局やることはいわゆるUVスクロールと一緒です.

f:id:mu-777:20190712221725p:plain
このネタを思いついたときに,自分の作業メモ用Slackに殴り書いたメモ.
結局最後までこの方針で行った

方針の具体化

「1. 各Vertexで,そのVertexから見たユーザの位置のベクトル取得」については,各Vertexで行うということを考えてシェーダでやることにします.UVスクロールもシェーダでやられてますね.

このへんとか

qiita.com

nn-hokuson.hatenablog.com

では実際に,シェーダで「そのVertexから見たユーザの位置のベクトル」をどう作るかですが,ここでは,「Origin座標系からVertex座標系に変換する同次変換行列を作って,その変換行列でOrigin座標系で記述されたユーザの位置ベクトルをVertex座標系に変換する」 という感じでいきます.文面にしたら一見ややこしげですが,結局は大したことは言ってません.

f:id:mu-777:20190714042326p:plain
座標変換のイメージ

もうちょっと具体的に書くと,^{ori}T^{ver} は,Vertex座標系で表現されたベクトルをOrigin座標系に変換できる同次変換行列で,つまり,Origin座標系で表現されたユーザ位置  v^{ori}_{user} と,Vertex座標系で表現されたユーザ位置 v^{ver}_{user} とは,以下のような関係になります. {} $$ v^{ori}_{user} = ^{ori}T^{ver} v^{ver}_{user} $$

 v^{ori}_{user} は,Unityの座標系で取得できるユーザ位置まんまなので,あとは ^{ori}T^{ver} の逆がわかれば v^{ver}_{user} が求められます,

あとは v^{ver}_{user} のxy成分(=テクスチャの平面の成分)を取り出して,適当にスケーリングし,その分UVを移動させるだけです.

実装

この記事一番上のgifのができるソースをここに置いてます.以下ではこれをベースに説明します.

github.com

事前準備

動かす対象のテクスチャを用意します.自分はめんどかったのでパワポで作りましたw こういうしょぼい画ならパワポは意外とサクッとかけて楽です.

f:id:mu-777:20190714124207p:plain
今回用意したテクスチャ

スクリプト

スクリプト側は単純で,Start関数でメッシュを作って,Updateでシェーダにユーザ位置(ここではカメラ位置)を送っているだけです.メッシュに関しては,別に自前で作る必要はないのですが,自分への忘備録も兼ねて作ってみてます.

長くないので全体をここに貼ってしまいます.


このスクリプトをアタッチするGameObjectのMeshRendererには,前述のテクスチャと後述のシェーダを入れたマテリアルをつけてください.

f:id:mu-777:20190714123521p:plain
マテリアルをつけておく

あと,メッシュを作り終えたあとに行っている処理は,

_meshRenderer.material.SetVector("_UpDirectionOnTex", Vector3.up);

ここは,後々使用する,テクスチャにとってのUpベクトルをシェーダに入れておく処理,

_meshRenderer.material.SetVector("_UVOffsetScale", Vector3.one * 0.15f);

ここは, 方針の具体化 のところでも言っていた,適当なスケーリング用のスケールをシェーダに入れておく処理,

_meshRenderer.material.GetTexture("_MainTex").wrapMode = TextureWrapMode.Clamp;

ここは,地味にハマったのですが,UnityではテクスチャのWrapModeがデフォルトではRepeatになっていて,そのままだと下のように黒目が無限に出てくるので,それをClampに変更しておく処理です.

f:id:mu-777:20190714043645g:plain
上にカメラを移動したときに,黒目が下から繰り返し出てくる様子

シェーダ側

こちらも別にそこまで難しいわけではないのですが,まずはシェーダのコードを貼ります.


キモは,vert関数の中の o.uvOffset を計算するところまでですね.

方針の具体化 のところで説明した通り, Origin座標系からVertex座標系に変換する同次変換行列 ^{ver}T^{ori} を, Vertex座標系からOriginx座標系に変換する同次変換行列 ^{ori}T^{ver} を求めて,その逆として算出 します.

まず,^{ori}T^{ver} は,Origin座標系で表現されたVertexの位置  v_{ver}^{ori} 姿勢 R_{ver}^{ori} を使って以下のように表せられ,その逆である ^{ver}T^{ori} は以下のようにして求められます. {} $$ ^{ori}T^{ver}= \begin{bmatrix} R_{ver}^{ori} & v_{ver}^{ori} \\ \begin{matrix} 0 & 0 & 0 \end{matrix} & 1 \\ \end{bmatrix} $$

$$ ^{ver}T^{ori}= \begin{bmatrix} (R_{ver}^{ori})^{-1} & - (R_{ver}^{ori})^{-1} v_{ver}^{ori} \\ \begin{matrix} 0 & 0 & 0 \end{matrix} & 1 \\ \end{bmatrix} $$

詳しくは,このへんを見てください.(このページにはいつもお世話になっております>熊谷先生) www.mech.tohoku-gakuin.ac.jp


Origin座標系で表現されたVertexの位置  v_{ver}^{ori} は,バーテックスシェーダでは引数の中に入っているv.vertex なので簡単に取得できます.

あとは,Origin座標系で表現されたVertexの姿勢 R_{ver}^{ori} ですが,これは,Origin座標系で表現されたVertexの,Forward/Up/Rightベクトル から求めることができます.

f:id:mu-777:20190714130359p:plain
Origin座標系で表現されたVertexのForward/Up/Rightベクトル

このとき,以下のように R_{ver}^{ori} は求まります.

$$ R_{ver}^{ori}= \begin{bmatrix} & & \\ v_{ver\_x}^{ori} & v_{ver\_y}^{ori} & v_{ver\_z}^{ori}\\ & &
\end{bmatrix}= \begin{bmatrix} x_{ver\_x}^{ori} & x_{ver\_y}^{ori} & x_{ver\_z}^{ori} \\ y_{ver\_x}^{ori} & y_{ver\_y}^{ori} & y_{ver\_z}^{ori} \\ z_{ver\_x}^{ori} & z_{ver\_y}^{ori} & z_{ver\_z}^{ori}
\end{bmatrix} $$

このへんは例えば,Origin座標系でのRightベクトルである  v_{ori\_x}^{ori} = \begin{bmatrix} 1 \\ 0 \\ 0 \end{bmatrix} を,この回転行列で回して見ると,以下のようになってわかりやすいかなぁと思います.

$$ R_{ver}^{ori} v_{ori\_x}^{ori} = \begin{bmatrix} x_{ver\_x}^{ori} & x_{ver\_y}^{ori} & x_{ver\_z}^{ori} \\ y_{ver\_x}^{ori} & y_{ver\_y}^{ori} & y_{ver\_z}^{ori} \\ z_{ver\_x}^{ori} & z_{ver\_y}^{ori} & z_{ver\_z}^{ori}
\end{bmatrix} \begin{bmatrix} 1 \\ 0 \\ 0 \end{bmatrix} = \begin{bmatrix} x_{ver\_x}^{ori} \\ y_{ver\_x}^{ori} \\ z_{ver\_x}^{ori} \end{bmatrix} = v_{ver\_x}^{ori} $$


ということで, ^{ver}T^{ori} を求める材料は揃いました.

回転行列の逆は転置と同じなので,まずそれを求めているのがシェーダ内の下の部分です.

float3 upvec = normalize(_UpDirectionOnTex);
float3 rightvec = normalize(cross(upvec, v.normal));
float3x3 texRori = float3x3(
   rightvec.x, rightvec.y, rightvec.z,
   upvec.x, upvec.y, upvec.z,
   v.normal.x, v.normal.y, v.normal.z);

Origin座標系で表現されたVertexのUpベクトルはスクリプトから入れてもらった _UpDirectionOnTex を使って,Forwardベクトルには法線ベクトルを使います.そうすれば,Rightベクトルはベクトルの外積で求められます.その3つを並べたものの転置が  (R_{ver}^{ori})^{-1} であり,コード内の texRori となります.


さらにここから, ^{ver}T^{ori} を求めているのが以下の部分で,

float3 texPori = mul(texRori, v.vertex);
float4x4 texTori = float4x4(
   rightvec.x, rightvec.y, rightvec.z, -texPori.x,
   upvec.x, upvec.y, upvec.z, -texPori.y,
   v.normal.x, v.normal.y, v.normal.z, -texPori.z,
   0, 0, 0, 1);

texRori を使ってまず, (R_{ver}^{ori})^{-1} v_{ver}^{ori} を求めて,それを使って, ^{ver}T^{ori} である texTori を作ります.


ここまできたら,あとは以下のように  v_{user}^{ori} を Vertex座標系で表現して(vInTexCoord),それをスケーリングして実際のUVのオフセット量(o.uvOffset)にして,フラグメントシェーダに渡します.

float3 vInTexCoord = mul(texTori, float4(_UserPosition, 1)).xyz;
o.uvOffset = float2(-vInTexCoord.x*_UVOffsetScale.x, vInTexCoord.y*_UVOffsetScale.y);


実際にUVを動かすのはフラグメントシェーダ側で,オフセット量だけズラしてテクスチャの値を取得します.

fixed4 col = tex2D(_MainTex, i.uv - i.uvOffset); 

実際にUVのオフセット量を計算しているのはバーテックスシェーダでVertexごとに行っているだけですが,フラグメントシェーダ側では線形補間されるので,画的に崩れたりすることは普通はないかと思います.

ただし,この四角のメッシュがユーザ位置(をテクスチャ平面に射影した点)がメッシュ内に内包される場合は,テクスチャの黒目よりも小さい黒目になります.これはUVオフセットがメッシュの4隅全てで内側を向くからです.

f:id:mu-777:20190714142339p:plain
ユーザ位置がメッシュに内包される場合,黒目が小さくなるイメージ(伝われ!)

おまけ:目の感じにするには

Twitterに上げたようにちゃんと目のようにするために,まぶたと白目のテクスチャも作っておきそれは動かさず,黒目テクスチャだけをこの記事で紹介した方法で動かすことで,目っぽくなっている.

もうちょっと具体的には,まぶたと白目のテクスチャが白ではないところはまぶたと白目のテクスチャを,白いところは黒目テクスチャを描画する,という感じで実現した

f:id:mu-777:20190714142646p:plainf:id:mu-777:20190714124207p:plain
目を作るためのテクスチャセット.左は動かず,右だけをこの記事で説明したように動かすことで,目っぽくしてる

最後に

Shader Forgeトライしたほうがよかったかなぁ~~

こういう計算はベタに書くほうがたぶん楽なんだけど,もっとおしゃんな感じにしようとするとべた書きでそこまでの力はない...