こういう感じに,目がこっちを常に見てくる(LookAtする)のをテクスチャのUV操作で実現する,というネタです.(黒い丸が黒目,という設定です!心の目で目を見てください!)
なにげにこのブログでUnityネタは初かも.まぁUnityはいろいろな方が書いた記事がめちゃくちゃあるし,あんまり書くことがないんすよね(いつもお世話になっております>各位).
背景としては,後輩さん(@Korechi_)が言っていた「OculusQuestのGuardianを使ってなにかやりたい」というネタをパクって考えていて,Guardianという名前からインスパイアされてのジャストアイデアで,1984のBigBrother風のネタを思いついて実装してみたのがコレです.
#OculusQuest のGuardianでなんかやりたいという@Korechi_ 氏のアイデアをパクって,1984のBigBrother風のを作った✌️
— むぅー777 (@mu_777_) 2019年7月13日
「Guardianは本当にGuardianなのか」的な
わかりにくいけど,壁の目は常にユーザ側を向いてきてキモいw
小ネタのわりに地味にだらだらしてしまったのでこのへんで供養🙏 pic.twitter.com/gOQPZ2BLCx
で,コレをするときに,壁の目がユーザをLookAtさせたいなということで,このUV操作でLookAtをやりました.それのやりかたを忘れないようにここに書いておきます.
方針
- 各Vertexで,そのVertexから見たユーザの位置のベクトル取得
- そのベクトルをメッシュ平面のUV座標に射影
- その射影したベクトルを適当にスケーリング
- そのスケーリングしたベクトル分だけ黒目テクスチャのUVをオフセットする
これを毎フレーム行う,という感じで行きました.
結局やることはいわゆるUVスクロールと一緒です.
方針の具体化
「1. 各Vertexで,そのVertexから見たユーザの位置のベクトル取得」については,各Vertexで行うということを考えてシェーダでやることにします.UVスクロールもシェーダでやられてますね.
このへんとか
では実際に,シェーダで「そのVertexから見たユーザの位置のベクトル」をどう作るかですが,ここでは,「Origin座標系からVertex座標系に変換する同次変換行列を作って,その変換行列でOrigin座標系で記述されたユーザの位置ベクトルをVertex座標系に変換する」 という感じでいきます.文面にしたら一見ややこしげですが,結局は大したことは言ってません.
もうちょっと具体的に書くと, は,Vertex座標系で表現されたベクトルをOrigin座標系に変換できる同次変換行列で,つまり,Origin座標系で表現されたユーザ位置 と,Vertex座標系で表現されたユーザ位置 とは,以下のような関係になります. $$ v^{ori}_{user} = ^{ori}T^{ver} v^{ver}_{user} $$
は,Unityの座標系で取得できるユーザ位置まんまなので,あとは の逆がわかれば が求められます,
あとは のxy成分(=テクスチャの平面の成分)を取り出して,適当にスケーリングし,その分UVを移動させるだけです.
実装
この記事一番上のgifのができるソースをここに置いてます.以下ではこれをベースに説明します.
事前準備
動かす対象のテクスチャを用意します.自分はめんどかったのでパワポで作りましたw こういうしょぼい画ならパワポは意外とサクッとかけて楽です.
スクリプト側
スクリプト側は単純で,Start関数でメッシュを作って,Updateでシェーダにユーザ位置(ここではカメラ位置)を送っているだけです.メッシュに関しては,別に自前で作る必要はないのですが,自分への忘備録も兼ねて作ってみてます.
長くないので全体をここに貼ってしまいます.
このスクリプトをアタッチするGameObjectのMeshRendererには,前述のテクスチャと後述のシェーダを入れたマテリアルをつけてください.
あと,メッシュを作り終えたあとに行っている処理は,
_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に変更しておく処理です.
シェーダ側
こちらも別にそこまで難しいわけではないのですが,まずはシェーダのコードを貼ります.
キモは,vert関数の中の o.uvOffset
を計算するところまでですね.
方針の具体化 のところで説明した通り, Origin座標系からVertex座標系に変換する同次変換行列 を, Vertex座標系からOriginx座標系に変換する同次変換行列 を求めて,その逆として算出 します.
まず, は,Origin座標系で表現されたVertexの位置 姿勢 を使って以下のように表せられ,その逆である は以下のようにして求められます. $$ ^{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.vertex
なので簡単に取得できます.
あとは,Origin座標系で表現されたVertexの姿勢 ですが,これは,Origin座標系で表現されたVertexの,Forward/Up/Rightベクトル から求めることができます.
このとき,以下のように は求まります.
$$
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ベクトルである を,この回転行列で回して見ると,以下のようになってわかりやすいかなぁと思います.
$$
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}
$$
ということで, を求める材料は揃いました.
回転行列の逆は転置と同じなので,まずそれを求めているのがシェーダ内の下の部分です.
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つを並べたものの転置が であり,コード内の texRori
となります.
さらにここから, を求めているのが以下の部分で,
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
を使ってまず, を求めて,それを使って, である texTori
を作ります.
ここまできたら,あとは以下のように を 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隅全てで内側を向くからです.
おまけ:目の感じにするには
Twitterに上げたようにちゃんと目のようにするために,まぶたと白目のテクスチャも作っておきそれは動かさず,黒目テクスチャだけをこの記事で紹介した方法で動かすことで,目っぽくなっている.
もうちょっと具体的には,まぶたと白目のテクスチャが白ではないところはまぶたと白目のテクスチャを,白いところは黒目テクスチャを描画する,という感じで実現した
PC側で実行すると,目の動きのところがわかりやすい pic.twitter.com/QQCFxPIOyG
— むぅー777 (@mu_777_) July 14, 2019
最後に
Shader Forgeトライしたほうがよかったかなぁ~~
こういう計算はベタに書くほうがたぶん楽なんだけど,もっとおしゃんな感じにしようとするとべた書きでそこまでの力はない...