階層関係にある複数のオブジェクトについて前節で得られた結論は次のようなものであった。
あるオブジェクトに(平行移動、回転、スケールによる)変化が発生した場合には、そのオブジェクトのローカル行列とワールド行列、及び そのオブジェクトのすべての子オブジェクトのワールド行列 に対して更新処理が必要となる (上の階層のオブジェクトに対しては更新処理は必要ない)。
階層構造に含まれるすべてのオブジェクトのローカル行列やワールド行列の更新を毎フレーム行うことは冗長であるため、必要なときに必要な分だけ更新が行われるようにすることが望ましい。
ここで対象としているオブジェクトは階層構造を持っており、その階層構造は一般的には木構造に属するものである。木構造はいわゆるリンクの作成に適しており、あるオブジェクトが自身の親あるいは子への参照を通じて同じ処理を反復させていくことで、木構造に含まれる一定範囲のオブジェクト(あるいはすべてのオブジェクト)に目的の処理を行わせることが可能になる。
今問題としている、階層構造に含まれるオブジェクトのローカル行列やワールド行列の更新 についても、木構造のデータ処理の基本である再帰処理によって実装する。
ただし、実際の実装は次節で行う。本節で行うことは その実装の準備として、木構造の単純なモデルを用いた再帰処理プログラムの作成である。
このモデルは、親子関係にある複数のオブジェクトの中で、ある1つのオブジェクトに変化が発生した場合に起こる連鎖的な変化を単純化したものである。
A) 再帰処理のデータ構造
本節では以下の Recursiveクラスを用いて再帰処理を実装する。
具体的には、この Recursiveクラスのインスタンスを複数生成し、それらの間に親子関係を設定し、ある1つのインスタンスに発生した変化が他のインスタンスにも必要な分だけ起こるようにする。
[class Recursive]
public class Recursive
{
private int m_ID;
private bool m_UPDATE_LOCAL = false;
private bool m_UPDATE_WORLD = false;
private Recursive m_parent = null;
private List<Recursive> m_children = new List<Recursive>();
public Recursive(int id)
{
m_ID = id;
}
public void SetParent(Recursive p)
{
m_parent = p;
m_parent.m_children.Add(this);
}
public void SomeChange()
{
m_UPDATE_LOCAL = true;
NotifyChain();
}
private void NotifyChain()
{
m_UPDATE_WORLD = true;
foreach (var child in m_children)
{
child.NotifyChain();
}
}
public void UpdateChain()
{
UpdateMessage();
foreach(var child in m_children)
{
child.UpdateChain();
}
}
private void UpdateMessage()
{
if (m_UPDATE_WORLD)
{
if (m_UPDATE_LOCAL)
{
Debug.Log("*Update Local : Obj" + m_ID + "*");
m_UPDATE_LOCAL = false;
}
Debug.Log("=Update World : Obj" + m_ID + "=");
m_UPDATE_WORLD = false;
}
}
}
Recursiveクラスのメンバ変数はアクセス修飾子がすべて
private であるため外部からのアクセスはできない。このクラスの唯一のコンストラクタである11行目のコンストラクタでは引数に
int型の値を1つ必要とするが、この
int値はメンバ変数
m_ID にセットされる。
m_IDは Recursiveクラスのインスタンスを識別するためのIDの役割を持っている。
Recursiveクラスの複数のインスタンスの間に親子関係を設定するためには16行目の
SetParent(..) を使用する。 引数には Recursiveクラスのインスタンスをセットする。18行目、19行目が具体的な親子関係の設定である。まず メンバ変数の
m_parent に引数の Recursiveインスタンスを代入するが、これはあるオブジェクトに対する親オブジェクトの設定に相当する。次に その親オブジェクトの
m_children に自身を追加するが、これはそのオブジェクトを親オブジェクト側で子オブジェクトとして設定していることに相当する (
m_childrenは
System.Collecions.Genericの
Listインスタンスである)。
冒頭でも述べたが最終的な実装では、あるオブジェクトに何らかの変化が発生した場合には、そのオブジェクトのローカル行列とワールド行列、及び そのオブジェクトのすべての子オブジェクトのワールド行列 に対して更新処理を行う。この Recursiveクラスでは、何らかの変化が発生した場合の更新処理を単純化して、単にメッセージを出力するだけの処理にしている。
(以下の文章では便宜上、Recursiveクラスのインスタンスも単に「オブジェクト」と表現する)
具体的にはオブジェクトに何らかの変化が発生した場合、そのオブジェクトについては「Update Local」と「Update World」の2つのメッセージが出力され、そのオブジェクトの(下階層のすべての)子オブジェクトについては「Update World」だけが出力される。そして、先程も述べたが更新処理は必要なときに必要な分だけ行われるようにする必要がある。5行目、6行目の
bool型メンバ変数
m_UPDATE_LOCAL、
m_UPDATE_WORLD はそのためのフラグである。オブジェクトに変化が発生するたびに、すべてのオブジェクトのメッセージ出力を行うのではなく、何らかの変化が発生した場合には一旦、必要なオブジェクトに対してメッセージ出力の発生を示すフラグを立てておき、最後に(毎フレームの最後に)一括してフラグの立っているオブジェクトのみをメッセージ出力の対象とする。
28行目のメソッド
NotifyChain() はオブジェクトに何らかの変化が発生した場合に、必要なオブジェクトに対してメッセージ出力の発生を示すフラグを立てるためのメソッドである。38行目のメソッド
UpdateChain() は毎フレームの最後に一括してフラグの立っているオブジェクトのメッセージ出力が行われるようにするためのメソッドである。この2つのメソッドはいわゆる再帰関数であり、自身の処理を実行した後にすべての子オブジェクトに同じ処理を行わせる。これを階層構造の一番下の階層まで再帰的に行っていくことで、(NotifyChainの場合は)そのオブジェクトとそのオブジェクトの(下階層のすべての)子オブジェクトにフラグが立てられ、(UpdateChainの場合は)メッセージが出力されることになる。
では実際に、この Recursiveクラスを使用したプログラムを以下で見ていこう。
# Code1
プログラムの内容はいたって単純である。
[Code1] (実行結果 図3)
Recursive Obj1 = new Recursive(1);
Recursive Obj2 = new Recursive(2);
Recursive Obj3 = new Recursive(3);
Recursive Obj4 = new Recursive(4);
Recursive Obj5 = new Recursive(5);
Obj2.SetParent(Obj1);
Obj3.SetParent(Obj2);
Obj4.SetParent(Obj3);
Obj5.SetParent(Obj4);
Obj3.SomeChange();
Obj1.UpdateChain();
(以下の文章においても Recursiveクラスのインスタンスについては単に「オブジェクト」という表現を用いる)
1行目~5行目において5つのオブジェクトObj
1~Obj
5を生成している。
7行目~11行目はそれらのオブジェクトの間における親子関係の設定である (注1)。この5つのオブジェクトには図1に示されるような親子関係が設定される (Obj
1が一番上の親であり、Obj
5が一番下の子である)。
12行目の
SomeChange() はオブジェクトに対する「何らかの変化」を簡易化したメソッドである。このプログラムではObj
3が
SomeChange() をコールしているが、これは Obj
3に「何らかの変化」が発生したことを意味する。図2では、この Obj
3の変化によってメッセージ出力が必要となるオブジェクトを赤く表示している。
SomeChange()の具体的な内容は
m_UPDATE_LOCAL を
trueにして、
NotifyChain()をコールするというものである (上記Recursiveクラス 24行目)。
NotifyChain()では
m_UPDATE_WORLD を
trueにして、子オブジェクトの
NotifyChain()をコールする (Recursiveクラス 30行目)。
NotifyChain()は再帰関数なので一番下の階層の子オブジェクトまでこのメソッドの呼び出しが再帰的に行われることになる。
上で述べたように、オブジェクトに変化が発生した場合には、その時点ではメッセージを出力せずに、まずは メッセージ出力が必要となるオブジェクトに対してフラグを立てる処理を行う。変化が発生したオブジェクトの場合は「Update Local」「Update World」の2つのメッセージが出力されるので、この2つのメッセージ用のフラグを立てる。変化が発生したオブジェクトの(すべての)子オブジェクトに対しては「Update World」だけが出力されるのでこのメッセージ用のフラグを立てる。
今回の例でいえば、12行目の
Obj3.SomeChange() によって Obj
3に変化が発生するが、
SomeChange()の中では
m_UPDATE_LOCALが、さらに
NotifyChain()の中では
m_UPDATE_WORLDが
trueになるので Obj
3に対しては必要な2つのフラグは立てられているわけである。また、Obj
3の子オブジェクトである Obj
4、Obj
5に対しても再帰関数
NotifyChain()の呼び出しが行われ、その中で
m_UPDATE_WORLDが
trueになるので、これらの子オブジェクトに対しても必要なフラグが立てられることになる。
14行目の
Obj1.UpdateChain() の実行により、一番上の親オブジェクトであるObj
1から一番下の子オブジェクトであるObj
5まで再帰的にこのメソッドが実行される。
UpdateChain()の内容は、
m_UPDATE_WORLD あるいは
m_UPDATE_LOCAL が
trueになっているオブジェクトのメッセージを出力するというものである (Recursiveクラス 40行目)。今回の実行結果(図3)では、Obj
3については「Update Local」、「Update World」の2つが出力され、Obj
4、Obj
5については「Update World」のみが出力されている。
このプログラムでは Obj
3に変化が発生するという設定であったが、プログラムの12行目の
Obj3.SomeChange() を
Obj4.SomeChange() に変えて実行した場合は、次に示される結果になる。
図4は Obj
4の変化によってメッセージ出力が必要となるオブジェクトを赤く表示したものである。
図5はプログラム12行目を
Obj4.SomeChange() に変えて実行した場合の結果である。Obj4に変化が発生しているので、Obj4の出力メッセージは「Update Local」「Update World」の2つであり、子オブジェクトの Obj5は「Update World」のみを出力している。
(注1 : Recursiveクラスの
SetParent(..)にはエラー回避のためのチェックを入れていないので、循環的な親子関係や複数の親の設定など、親子関係の設定が不適切な場合には対応していない。詳しくは次節の
SetParent(..)参照)
# Code2
続いて、オブジェクトの数を増やし、階層構造をやや複雑にした場合で見てみよう。
[Code2] (実行結果 図#)
Recursive Obj1 = new Recursive(1);
Recursive Obj2 = new Recursive(2);
Recursive Obj3 = new Recursive(3);
Recursive Obj4 = new Recursive(4);
Recursive Obj5 = new Recursive(5);
Recursive Obj6 = new Recursive(6);
Recursive Obj7 = new Recursive(7);
Recursive Obj8 = new Recursive(8);
Recursive Obj9 = new Recursive(9);
Recursive Obj10 = new Recursive(10);
Obj2.SetParent(Obj1);
Obj3.SetParent(Obj1);
Obj4.SetParent(Obj1);
Obj5.SetParent(Obj2);
Obj6.SetParent(Obj3);
Obj7.SetParent(Obj4);
Obj8.SetParent(Obj5);
Obj9.SetParent(Obj5);
Obj10.SetParent(Obj6);
Obj2.SomeChange();
Obj1.UpdateChain();
今回使用するオブジェクトは Obj
1~Obj
10の10個のオブジェクトで、1行目から10行目はそれらの生成コードである。12行目から23行目において、この10個のオブジェクトの間に親子関係を設定している。ここで設定される階層構造は図6に示されるとおりである。
簡単に説明すると Obj
1の1階層下には3つの子オブジェクトObj
2、Obj
3、Obj
4があり、それらの子オブジェクト自身も子オブジェクトを持っている。例えば、Obj
2は Obj
5を子オブジェクトとして持っているが、さらにその Obj
5も Obj
8、Obj
9を子オブジェクトとして持っている。
25行目では
Obj2.SomeChange() と記述されているが、これは Obj
2に何らかの変化が発生したことを意味している。Obj
2に変化が発生した場合には、Obj
2、及び そのすべての子オブジェクトにメッセージ出力のためのフラグを立てる処理が再帰的に行われる。そして、27行目の
Obj1.UpdateChain() によって階層構造の一番上の親オブジェクトから一番下の階層まで、すべてのオブジェクトを1つ1つ再帰的にたどっていき、メッセージ出力のフラグの立っているオブジェクトについてはメッセージを出力する。
以下の図7は今回の Obj
2の変化によってメッセージを出力することになるオブジェクトを赤く表示したものであり、図8はこのプログラムの実行結果である。
図8に示されるように、変化の発生したオブジェクトである Obj
2の出力メッセージは「Update Local」「Update World」の2つであり、Obj
2の子オブジェクトである Obj
5、Obj
8、Obj
9の出力メッセージは「Update World」のみとなっている。
この結果は Obj
2に変化があった場合の実行結果であるが、以下では他のオブジェクトに変化があった場合についていくつか見てみよう (つまり 25行目の
SomeChange() を実行するオブジェクトを変えた場合)。
Obj
3に変化があった場合 (25行目を
Obj3.SomeChange() とした場合)。
Obj
4に変化があった場合 (25行目を
Obj4.SomeChange() とした場合)。
Obj
5に変化があった場合 (25行目を
Obj5.SomeChange() とした場合)。
本節では、あるオブジェクトに変化が生じた際の、そこから発生する連鎖的な変化について非常に簡単なモデルを用いて見てきた。しかし、階層構造にある3Dオブジェクトの連鎖的な変化についても、本節で見てきたものと同様の過程をたどるのである。
そのことについては次節で見ていく。