「天才けんけんぱ」:Unityとセンサーの連携(3/4)
本記事は次に示す4つのページから構成されています。
「天才けんけんぱ」:Unityとセンサーの連携(1/4)
「天才けんけんぱ」:Unityとセンサーの連携(2/4)
「天才けんけんぱ」:Unityとセンサーの連携(3/4)
「天才けんけんぱ」:Unityとセンサーの連携(4/4)
1.3 演出のポイント
ここでは、「天才けんけんぱ」というアトラクションにおいて、応用できそうな点をピックアップして解説していきます。
本アトラクションでは、川辺のステージを作り、その川に住む生き物や植物も楽しさを演出するために一役買っています(図7参照)。ここでは、その演出の中で「群シミュレーション」と「Shaderを用いた“揺れる”動き」の二点を解説したいと思います。
図7.川辺には、オタマジャクシやカエル、トンボなどが生息して、足に反応して動きを変える
一つ目の「群シミュレーション」は、魚や鳥などの群れの動きをシミュレーションするためのアルゴリズムです。本アトラクションでは、オタマジャクシや魚を群れとなって行動させています。これにより、本当の川に住む生き物のようなリアルな動きを演出することができます。また、人の足が近づくと、一斉に逃げていくような動作を追加することで、“けんけんぱ”とは別の楽しさを子供に与えることができます。
二つ目の「Shaderを用いた“揺れる”動き」は、川の左右に配置した植物の風に揺れる動きと、人が踏んだときに草葉が揺れる動きについてです。この草葉の動きは、植物の3Dモデルを左右に周期的に波打たせることで、その動きを表現しています。このような動きを行わせるにはShaderを用いて行うことができます。
1.3.1 群シミュレーション
魚や鳥などの群(Flock)をなして行動する動きをシミュレーションするための手法に、Boidsアルゴリズム[6]があります。このアルゴリズムを用いることで、図8のように、オタマジャクシの群れの動きをシミュレーションすることができます。
図8.おたまじゃくしの群シミュレーション
上の図のような動きを生み出すためには、とても複雑なことをやるのではないかと思うかもしれません。しかし、Boidsアルゴリズムはとてもシンプルです。実際、オタマジャクシに「ある3つのルール」を適用するだけで、上のような複雑な群れの動きを作り出すことができます。ここにBoidsアルゴリズムの面白さと奥深さがあります。
Boidsアルゴリズムを説明する際に、「エージェント」という言葉が出てきます。エージェントとは、群れを構成している個体のことで、上の例では、オタマジャクシを指します。
Boidsアルゴリズムは、エージェントに対して動きのルールを適用することで、群れの動きをシミュレーションします。先ほど述べた通り、エージェントに適用するルールは3つで、次のようになります。
1.分離:エージェントは互いに一定の距離をとる
2.整列:エージェントは群れ全体の方向へ整列する
3.結合:エージェントは群れ全体の重心へ向かう
「集結」のルールで、エージェントが集まり、「整列」のルールで、群全体の進む方向を合わせます。「整列」と「集結」だけではエージェントが密集してしまうので、「分離」の制約を設け、エージェント同士の間に適度な距離を作ります。
Boidsアルゴリズムの説明は以上です。それでは、実際にUnityで実装していきます。ここで説明に用いる実装は、出来るだけ単純化しているため、オリジナルのものとは少し違っています。詳細については、原著論文[6]や他の解説記事などを参考にして下さい。
1.3.1.1 UnityでのBoidsアルゴリズムの実装
まずはじめに、エージェントをPrefabとして作成します。ここでは、図9に示すような直方体を用いることにします。適宜、魚や鳥などの3Dモデルに変更することも、もちろん可能です。図9に示す通り、Rigidbodyを追加して、物理的な動きを制御できるようにします。ここでは、エージェントの動きを平面上に制約するため、y軸方向には動けないように、Freeze Positonのyにチェックを入れています。また、回転軸もy軸上だけで回転するように、Freeze Rotationのyにチェックを入れます
図9.エージェント(群れで動く個体)をPrefabとして用意する
また、上のPrefabにはAgent.csというスクリプトを追加しています。Agent.csが行うことは、エージェントの動く方向に回転するだけです。ファイルの中身はUpdate関数に次の一行を追加しただけになります。
void Update () {
transform.rotation = Quaternion.LookRotation(rigidbody.velocity);
}
続いて、郡れ全体を管理するためのオブジェクトを作成します。図10に示すとおり、FlockManagerという名前のオブジェクトに、後ほど作成するFlockManager.csというスクリプトを追加します。このFlockManager.csがエージェントを配置し、3つのルールに従って、各エージェントを動かす指示を与えます。
図10.FlockManagerが群れ全体の動きの指示を出す
FlockManager.csは、次に示す3つのメンバー変数を持ちます。numAgentsはエージェントの数を、agentPrefabはエージェントのPrefabをそれぞれ指定します。_agentListは各エージェントをリストで保持します。
public int numAgents = 20; public GameObject agentPrefab; private List<GameObject> _agentList = new List<GameObject>();
FlockManager.csは、初期化処理として、エージェントを生成し、ランダムに配置します。そのためのコードは次に示すようになります。
void Start () { for( int i = 0; i < numAgents; i++ ) { _agentList.Add( GameObject.Instantiate(agentPrefab) as GameObject ); _agentList[i].transform.position = new Vector3( Random.Range(-1f, 1f), 0, Random.Range(-1f, 1f) ); } }
FlockManager.csの毎フレームの処理として、先ほど述べた3つのルールを適用してエージェントを動かします。そのため、Update関数は次のようになります。ruleAlignmentは「整列」、ruleCohesionは「結合」、_ruleSeparationは「分離」のためのルールを記述した関数です。
void Update () {
_ruleAlignment();
_ruleCohesion();
_ruleSeparation();
}
それでは、3つのルールを一つずつ解説していきます。はじめに_ruleAlignment「整列」についてです。ここで行うことは、群れ全体の平均的な移動速度を求め(コード中では、averageVeclocity)、各エージェントにその平均的速度を設定します。
private void _rule_Alignment() { Vector3 averageVelocity = Vector3.zero; foreach (GameObject agent in _agentList) averageVelocity += agent.rigidbody.velocity; averageVelocity /= _agentList.Count; foreach (GameObject agent in _agentList) _setVelocity( agent, averageVelocity ); }
ここでは、_setVelocityという関数を用いています。これは、現在のエージェントの速度を、希望する速度(targetV)にいきなり設定するのではなく、現在のエージェントの速度から徐々に希望する速度に近づけるために使用します。
private void _setVelocity( GameObject agent, Vector3 targetV) { float RATIO = .001f; agent.rigidbody.velocity = agent.rigidbody.velocity * (1f - RATIO) + targetV * RATIO; }
続いて、_ruleCohesion、「結合」についてです。「結合」のルールで行うことは、群れの中心位置(コード中ではflockCenter)を求めて、その中心に向かう速度を設定することです。コードは次のようになります。
private void _rule_Cohesion() { Vector3 flockCenter = Vector3.zero; foreach(GameObject agent in _agentList ) { flockCenter += agent.transform.position; } flockCenter /= _agentList.Count; foreach(GameObject agent in _agentList ) { Vector3 dir = (flockCenter - agent.transform.position).normalized; _setVelocity( agent, dir ); } }
最後に、_ruleSeparation、「分離」についてです。ここで行うことは、群れの中で最も近いエージェントを探し出して、そのエージェントとの距離がある一定以内であれば(コード中ではSEPARATIOIN_DIST)、そのエージェントから遠ざかる方向へ移動させます。コードは次のようになります。
private void _rule_Separation() { float SEPARATIOIN_DIST = 1.0f; foreach (GameObject a1 in _agentList) { float minDis = float.MaxValue; Vector3 minDiff = Vector3.zero; foreach (GameObject a2 in _agentList) { if (a1 == a2) continue; Vector3 diff = a1.transform.position - a2.transform.position; if( minDis > diff.sqrMagnitude ) { minDis = diff.sqrMagnitude; minDiff = diff; } } if( minDis < SEPARATIOIN_DIST ) { float ratio = minDis / SEPARATIOIN_DIST; _setVelocity( a1, a1.rigidbody.velocity.magnitude * minDiff.normalized ); } } }
以上で実装についての説明は終わりです。実際に実行すると、図11のように、エージェントが群れをつくって動き出します。ここで説明したBoidsアルゴリズムは出来るだけ単純な実装を心掛けています。実際には、他にもパラメータを設定して、動きの調整が必要になるでしょう。
図11.UnityでのBoidsアルゴリズム:群れをなして動きだす
1.3.2 Shaderを用いた“揺れる”動き
ここでは、草花の風に揺れる動きをUnityでどのように表現するか、ということについて解説します。具体的には図12に示すような、風で周期的に揺れ動く葉っぱの表現を対象とします。「天才けんけんぱ」では、通常の“揺れ”に加えて、人が踏んだときだけ揺れの度合いを強めることで、踏まれたときのアクションも追加して実装しています。
図12.風に揺れ動く葉っぱの動きをShaderを用いて表現する
上の図のような周期的な動きをUnityで実装する方法はいくつかありますが、ここではUnityの「Surface Shader」を用いる方法を見ていきます。Shaderとは、GPU上で計算されるプロセスをカスタマイズできる機能です。Shaderを用いることでレンダリング(描画)方法を細かくカスタマイズすることができ、多彩な表現を可能とします。
Shaderを使ってできることは、簡単に言うと、「色の塗り方」と「3Dモデルの頂点の場所」を自分好みに変更できることです。今回の場合、「3Dモデルの頂点の場所」を周期的に動かすことで、風に揺れ動く草葉を表現したいと思います。具体的には、草の3Dモデルは図12に示すように、板状の面にテクスチャが貼られた形になります。この3Dモデルの各頂点(バーテックス)を時間とともに、場所に応じて位置を変更します。
図12.茂った草の3Dモデル:右図は使用したテクスチャ
それでは、GrassWave.shaderという名前のSahderを作成します。GrassWave.shaderの中身を後ほど示すコードの通り記述すれば、マテリアルのシェーダを、図13のようにドロップダウンメニューから選ぶことができます。
図13.GrassWaveシェーダを適用する
Shader "GrassWave" { Properties { _Color ("Main Color r:ampl g:speed b:time", Color) = (1,1,1,1) _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {} _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5 _WaveCycle ("Wave Cycle", Range(0.0,5.0) ) = 1.0 _WaveAmount ("Wave Amount", Range(0,0.1) ) = 0.02 } SubShader { Tags { "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" } LOD 200 Lighting Off Cull Off CGPROGRAM #pragma surface surf Lambert alphatest:_Cutoff vertex:vert sampler2D _MainTex; float4 _Color; float _WaveCycle; float _WaveAmount; struct Input { float2 uv_MainTex; fixed4 color : COLOR; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = _Color * tex2D(_MainTex, float2(IN.uv_MainTex)); o.Albedo = c.rgb; o.Alpha = c.a; } void vert (inout appdata_full v) { float4 p = v.vertex; float sy = p.y; p.x += sin((_Time.y * _WaveCycle)) * _WaveAmount * sy; p.z += cos((_Time.y * _WaveCycle)) * _WaveAmount * sy; v.vertex = p; } ENDCG } }
ここで大切なポイントはvertいう関数です(このvertという名前は変更してかまいません。その場合、「#pragma … vertex:vert」の部分を適宜変更します。)。このvertという関数内で頂点の位置を調整することができます。上のコードでは、v.vertexに3Dモデルの各頂点の座標値が格納されています。そして、このv.vertexの値を変更することで、各頂点の位置を変更することができます。
上のコードでは、各頂点のx座標とz座標の位置をsin・cos関数で円運動するように動かせています。ここでTime.yはビルトイン変数であり、シェーダ内でアニメーションをするために使用することができます。WaveCycleとWaveAmountは、自分で定義したプロパティです。WaveCycleは動きの周期スピード、_WaveAmountは動きの量を調整するための変数です。
これをUnityで実行すると、草のモデルは風で揺れるような周期的な動きをします。また、周期スピードと動きの量を調整することで、人に踏まれた場合の動きを追加することができます。
以上で「Shaderを用いた“揺れる”動き」の説明は終わります。ここではShaderの詳細な説明は省略しましたが、是非時間をとっってShaderについて勉強することをおすすめします。Shaderが使えるようになると、多彩な表現が可能になります。Shaderについて体系的に学びたい場合は、『Unity Shaders and Effects Cookbook[7]』や『The Cg tutorial[8]』などの書籍を参考にして下さい。