Hatena::ブログ(Diary)

土屋つかさのテクノロジーは今か無しか

2018-01-15

Unity Tips(17:ユニティちゃんリップシンク)

下記のサイトを見ながらユニティちゃんモデルでリップシンクさせてみました。

ユニティちゃんが声に合わせて口パクしてくれるリップシンクアセットを作ってみた
http://tips.hecomi.com/entry/2014/07/21/125319

 作業中どうにもうまく動かなくて悩んでいたのですが、ユニティちゃんのバージョンが古い(1.1.0)が原因でした。UnityChan_LipSyncではMTH_DEFに設定されたAIUEOの母音に相当するブレンドシェイプの値を更新することでリップシンクを実現していますが、このブレンドシェイプは1.1.0には搭載されてないのです。検証する際にはバージョンを間違えないようにしてください。

 以下に動作確認までの最小手順を掲載していおきます。

1・プロジェクト新規作成
2・UnityChan_1_2_1.unitypackageをインポート
3・UnityChan_LipSync.unitypackageをインポート
4・\Assets\UnityChan\Prefabs\unitychan_dynamicプレハブをHierarchyに追加
5・\Assets\LipSync\UnityChanLipSync.csをunitychan_dynamicにアタッチ
6・UnityChanLipSync.csのInspectorで"Play Voice Sound/Audio Clip"にファイルを設定(例えば、\Assets\UnityChan\Voice\univ1334)
7・playモードを実行し"Play Voice Sound/Audio Clip"の"play"ボタンをクリックすると、音声が再生され、その音声に合わせてユニティちゃんが口を動かす。

追記
あと、リップシンクの資料を探していると必ずぶつかるのが「oculusがリップシンクプラグインを提供してるっぽいのにリンクが死んでいる問題」なのですが、現在は下記リンク上で提供しているようです(内容は確認してません)

Oculus Lipsync Unity(1.16.0)
https://developer.oculus.com/downloads/package/oculus-lipsync-unity/

2018-01-07

Tsukasa4U開発日記 2

 ANTLR4の勉強をすると決めたはいいものの、どうにも日本語で読めるまとまった資料が見つからなかったため、仕事始め(現在某ゲーム会社さんにフルタイム勤務でお手伝いしてます)から出勤時間に"The ANTLR mega tutorial"を読んでます。

The ANTLR mega tutorial
https://tomassetti.me/antlr-mega-tutorial/

 びっくりするほど長いANTLR4のチュートリアルです。英語をちゃんとは読めないですが、技術記事なので、サンプルコードを見ながらなら何が書いてあるのかは大体わかります。

 また、ANTLR4作者のテレンス・パー自身による解説書も購入しました。もちろん翻訳なんかないので原著です。Unity関係を除くと、英語の技術書を購入したのはもしかしたら10年ぶりくらいかも(その時はRoR3)。

The Definitive ANTLR 4 Reference
https://www.amazon.co.jp/dp/1934356999/ref=cm_sw_r_tw_dp_U_x_nVxuAbJ2V9ENT

 今年はちゃんと英文法から勉強しようかな……(毎年言ってる)。

2018-01-05

Unity Tips(16:Animatorコントローラーのセットアップ)

1からセットアップしたモデルデータにAnimatorコントーローラーアセットを登録する時毎回必要な作業を忘れて焦るのでメモしておきます。

1・モデルのHumanoidセットアップを済ませる
2・AnimtorControllerアセットを作成
3・AnimtorControllerアセットをモデルのAnimatorコンポーネントのControllerにバインド
4・AnimtorControllerアセットを開いてモーションアセットドラッグ&ドロップ
5・モーションアセットインスペクターからRigタブのAnimation TypeをHumanoidでセットアップする

 Animatorウィンドウからモーションのノードダブルクリックすると、RigタブではなくAnimationタブが開かれているため、つい5の作業を忘れてしまいます。これをしないとモーションが正しく動作しないので注意。

2018-01-03

Tsukasa4U開発日記 1

 あけましておめでとうございます。今年もよろしくお願いします。

 2018年は昨年に引き続きUnityの勉強を継続していく予定で、技術書典4にサークル当選したら、シェーダー本の続編を作るつもりでいます。ただ、それはサブ目標で、今年のメインはUnity版司エンジン「Tsukasa4U」の完成です(タイトルは今決めました)。

 昨年初頭にようやくRuby版の司エンジンが安定してきたものの、その後開発が止まってしまっていたのを反省し、今年は年始から開発を再開しています。こちらのブログでも、時々こんな風に開発記録を書ければと思っています。

 実は、去年の中頃にもUnityへの移植を進めていて、それはいったん形になったのですが、あまりにもいびつな設計になってしまったため、没にしました(引き継げるコードは引き継ぐ予定です)。以下これをプレ版と呼称します。

 Ruby版の司エンジンは、Rubyの内部DSLとして実装しており、プレ版もそれに準じてC#の内部DSLとして実装しました。が、元々Rubyのパーサー解析能力とラムダコンテキストに強く依存しているRuby版をそのままC#に移植するのは無理があり、不完全な物しか作れませんでした。

 今回は内部DSL化をすっぱり諦め、Tsukasaスクリプトは、当該のファイルが評価された時点で全文をパースして抽象構文木(AST)化することにします。パースにはANTLR4で生成したジェネレーターを使うつもりです。というわけで、まずはANTLR4の習得からスタートします(道のりが遠いな……)。

 ANTLR4自体は以前もちょっとだけ触っていたので、セットアップ自体は問題なくできる……と思っていたら、なんとANTLRのVS用プラグインが、現時点ではVisualStudio2017に対応していないようで、vs2015のインストールからやる必要がありました……。

 しかしまあ、パーサジェネレーターから作るのであれば、本当に自分の好きなように言語仕様を構築できるわけで、Ruby版よりずっとすっきりした構文になるのではないかと思っています。次回からは、Tsukasaスクリプトの言語仕様について考えていきます。

2017-12-31

3Dリアルタイムレンダリングにおいてベクトルの内積の性質を知っていることがどれだけ重要かについて解説する

 今年も様々な方に有形無形を問わずお世話になりました。感謝の気持ちを込めて大晦日に多少長めの記事を書いてみました。よろしければごらんください。

課題の提示

 まず、こちらの画像をご覧ください。
f:id:t_tutiya:20171231125355p:image
 シーンに球体とポイントライトを一個配置しています。ディレクショナルライトは削除してあるので、ポイントライトに照らされている所のみに色がついています。ポイントライトを動かすと色がつく場所が変わります。
f:id:t_tutiya:20171231125356p:image

f:id:t_tutiya:20171231125357p:image
 このの描画を実現する為の最低限のシェーダーコードは以下になります。

Shader "Unlit/NewUnlitShader"
{
	SubShader
	{
		Pass
		{
			Tags { "LightMode" = "ForwardAdd" }

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"

			struct v2f
			{
				float4 vertex : SV_POSITION; //プロジェクション空間座標
				float4 diffuse :COLOR0; //色
			};

			//頂点シェーダー
			//vertext:頂点のモデル空間座標
			//normal:頂点のモデル空間法線ベクトル
			v2f vert (float4 vertex : POSITION, float4 normal : NORMAL)
			{
				v2f Out;
				//頂点座標をプロジェクション空間へ変換する
				Out.vertex = UnityObjectToClipPos(vertex);

				//モデル空間でのライトの方向ベクトルを取得し、単位ベクトル化して格納する
				float3 LightDir= normalize(ObjSpaceLightDir(vertex));
				//描画色(白)
				float4 Color = float4(1, 1, 1, 0);

				//法線ベクトルとライト方向との内積を取り(0未満なら0)、色と乗算する
				Out.diffuse = Color * max(0, dot(normal, LightDir));
				return Out;
			}

			//フラグメントシェーダー
			fixed4 frag (v2f In) : SV_Target
			{
				//ラスタライザが補間した色の値をそのまま返す
				return In.diffuse;
			}
			ENDCG
		}
	}
}

 シェーダーコードの読み方については解説しません。ShaderLab&HLSLについて詳しく知りたい方は土屋のシェーダー本(https://s-games.booth.pm/items/660001)をよければどうぞ)。

 今回はこのコードの中のこの1行について取り上げます。

//法線ベクトルとライト方向との内積を取り(0未満なら0)、色と乗算する
Out.diffuse = Color * max(0, dot(normal, LightDir));

 シェーダーのサンプルコードを読んでいると、よくこんな感じのコードに出会います。
 ここでのnormalは頂点が持つ法線ベクトル、LightDirは頂点から光源座標へのベクトルが代入されています。どちらもモデル空間座標で、正規化された単位ベクトルです。

 HLSLのリファレンスからmaxやdotの関数の機能をピックアップすると、maxは2つの入力のうち大きい方を返し、dotは2つのベクトル内積を返す関数だとわかります。なので、このコードは「頂点の法線ベクトルと光源方向ベクトル内積を取り(ただし結果が0未満の場合は0)、その値を拡散率として描画色に乗算する」という処理になります。

 拡散率は0.0から1.0までの値を取るので、これを描画色に乗算することによって、「光源方向に対して面が垂直になっている(=面を向けている)ほど白く、角度がつくほど黒く描画する」という事を実現しています。実際の画面を見ると、実際そうなっているのがわかります(※1)。

 ところが、なんで法線ベクトルと光源方向ベクトル内積を取ると拡散率が算出されるのか。これが分からないw

 プログラミングに詳しくない方は意外に思われるかもしれませんが、土屋は数学が苦手なタイプのエンジニアでして、高校時代の数学の時間はほぼ寝て過ごしました。なので、シェーダプログラミングの勉強中にちょくちょく出てくる「ベクトル内積」が、どういう性質を持つ物なのかが良く分からず、ググッてみてもいまいちピンとくる物がありませんでした。

 今回は、この「どうして法線ベクトルと光源方向ベクトル内積を取ると拡散率を算出できるのか」について、解説してみたいと思います。

【注記】上記のような理由で、土屋は数学に明るくなく、以下の解説も重要な要素が抜け落ちてたり、そもそも説明が間違ってたりする所があるかもしれません。その際はどうぞコメントにてご指摘ください。みなさんのコメントを合わせることで、記事の完成度を上げられればと思います。

※1
注意:今回は光源からの距離については無視していて、あくまで光源に対する面の角度のみに注目しています

光と面のなす角度

 通常、ポリゴンの表面の色を決める時には、その表面が光源に対してどのような角度を向いているかが重要になります(※1)。なぜ光源に対する角度が重要なのかと言いますと、光というのは物体表面に対して直行している時に最も反射し、角度が開いていくごとに反射率が落ちるという性質があるからです。
f:id:t_tutiya:20171231125355p:image
 先ほどと同じ画像です。球体の表面のうち、ライトの位置に直行している部分が一番明るく、そこから外れるとどんどん暗くなるのが分かります。
f:id:t_tutiya:20171231125358p:image
 これはライトの方向から球体を見た物。ライトによって明るくなっているのが、球体の半分よりも狭い範囲であることに注目してください。ライトと角度が0度よりも大きくなった場合、もう光は反射しません(そりゃそうだ)。
f:id:t_tutiya:20171231125359p:image
 真上から見るとこんな感じ。
f:id:t_tutiya:20171231130223p:image
 ライトを球体にぐっと近づけました。さっきよりも明るい範囲が小さくなっていますね。ライトの効果を受ける範囲が、サーフェスとの角度に依存していることがよくわかります。
f:id:t_tutiya:20171231125928p:image
 よりわかりやすくするために、見た目をローポリ化してみました(※2)。これだと、面の角度によって色が変化しているのが一目瞭然ですね。

※1
実際にはカメラとの角度も重要なのですが、煩雑になるので今回の記事では省略します

※2
実際には、法線ベクトルの向きをリセットしただけで、使っているポリゴンモデルは同じものです。

角度を算出する方法。

 さて、サーフェスの色を決める時にサーフェスとライトがなす角度、より正確には「ライトが表面にどれだけ直行しているか」が重要であることがわかりました。逆に言えば、「ライトが表面にどれだけ直行しているか」がわかれば、描画色をどれくらい暗くすれば良いかがわかるわけです。しかし、それって、どうやって算出すればいいんでしょうか?

 シェーダプログラミングというのは恐ろしく泥臭い作業でして、乱暴に言えば、全てのポリゴンの表面1ドットごとにシェーダーコードと呼ばれるプログラミングを実行し、その演算を合算することで、1フレーム分の画像を生成しています。想像するだに物凄い計算量ですが、実際にはその1ドットごとに何十回も異なる演算をすることになるので、毎フレームの計算量は膨大な物になります。

 そのため、シェーダプログラミングではシェーダーの処理負荷を下げるために、「しなくていい計算は可能な限りしない」が基本方針になります。

 2つのベクトルをなす角度を計算するのはそこまで難しい作業ではないでしょう(多分。知らない)。しかし、その演算量は可能な限り少なくしたい。そこで登場するのが内積なのです(ふう、やっと出てきたぞ!)。

ベクトル内積

 まず、ベクトル内積というのは下記のようにベクトルの各成分を乗算した上で、その全てを足し合わせる計算の事を言います(以下、3次元ベクトルの場合に限定します)。

a・b = a.x * b.x + a.y * b.y + a.z * b.z; ……

 ベクトル内積は「a・b」のように、2つのベクトルをドットで繋いで表記します。その為、海外では「ドット積」と呼ばれる事が多いようです。HLSLでも、内積の組み込み関数はdotという名前になっています。

 さて、上で説明したように、ベクトル内積は、各成分を乗算して足し合わせた「だけ」の物です。この単純な計算が一体なんの役に立つんでしょうか? これ、正直土屋にもよくわかりません(爆)。ところが、ベクトル内積の性質を利用すると、この単純な計算で、我々が今必要としている情報を得ることができるのです。

 まず内積の性質から。ベクトル内積は、下記の式からも算出できます。

a・b = ||a|| ||b||cosθ; ……

 aとbそれぞれの長さと、2つのベクトルが作る角度θのコサインを掛けると、,汎韻厳覯未出せます(,鉢△同じ結果を返す証明については、ネットでググれば沢山出てくるので省略します)。

 さて、シェーダーで内積を取る時に使用する法線ベクトルやライト方向ベクトルは、正規化されたベクトルです(そうで無いときは、あらかじめ正規化する必要があります)。ベクトルは正規化すると単位ベクトルになります。単位ベクトルとは、長さが1のベクトルです。長さが1のベクトル2つの内積を、先ほどの△亮阿謀てはめてみます。

||a|| ||b||cosθ ……
= 1 * 1 * cosθ
= cosθ

 おわかりのように、正規化されたベクトル同士の内積を取ると、その結果はcosθになります。そして、,鉢△脇韻厳覯未砲覆襪里如↓,鮖箸辰橡\ベクトルとライト方向ベクトルの各要素を乗算し足し合わせると、それは△侶覯漫△垢覆錣繊∨\ベクトルとライト方向ベクトルcosθになるというわけです。どうですか、面白いでしょ!?(驚きの押し売り)

cosθはなんなのか。

 さて、算出できたのはいいけれど、この「cosθ」とはどんな値を取るのでしょうか。高校時代に習った記憶があるでしょうが、cosθは角度に応じて以下のような値を取ります。

0°= 1
90°= 0
180°=-1
270°= 0
360°= 1

 この場合のθは、法線ベクトルとライト方向ベクトルがなす角です。この角度が0ということは、法線ベクトルとライト方向ベクトルが同じ、つまり、ポリゴンの面が完全にライトの方向に向いているという事になります。この時cosθは1になります。そして、角度が徐々に開いていくに従ってcosθの値は0に近づいていき、90度になった時点で0になります(90度より開いた場合にはマイナスになりますが、シェーダーコードではmax関数を使って0未満はすべて0としています)。

 前述した通り、シェーダーでプリミティブ平面の色を決める時には「その平面がライトに対してどれくらいの角度になっているか」が重要になりますが、その角度を算出するのは非常に面倒です。しかし、法線ベクトルとライト方向ベクトル内積(=ベクトル同士の各成分を乗算して足し合わせるという非常にコストの低い処理)を計算するだけで、プリミティブ平面がライト方向に向いている割合を算出出来るので、非常に重宝するのです。

終わりに

 学生時代、数学が人生の役に立たないとは思いませんでしたが、社会人になったら一生使うことはないだろうと正直思っていました。けれど、シェーダプログラミングを習得する(それはつまり、AAAタイトルのグラフィックエンジニアを目指すと同義です)には、ベクトル内積はもちろん、行列とベクトルの乗算、様々な物理現象近似式など、多くの数学知識を習得する必要があります。

 土屋のように高校の数学の時間を寝て過ごしている学生がこれを読んでいたら、せめてベクトルと行列まではちゃんと勉強しておこう。そうじゃないと、今の土屋みたいに、大人になってから凄く苦労しますよw。