Redpoll's 60
第5章 変換のオブジェクト化

$§$5-6 Transformクラスの実装 2


階層関係にある複数のオブジェクトについて前節で得られた結論は次のようなものであった。

あるオブジェクトに(平行移動、回転、スケールによる)変化が発生した場合には、そのオブジェクトのローカル行列とワールド行列、及び そのオブジェクトのすべての子オブジェクトのワールド行列 に対して更新処理が必要となる (上の階層のオブジェクトに対しては更新処理は必要ない)。

階層構造に含まれるすべてのオブジェクトのローカル行列やワールド行列の更新を毎フレーム行うことは冗長であるため、必要なときに必要な分だけ更新が行われるようにすることが望ましい。
ここで対象としているオブジェクトは階層構造を持っており、その階層構造は一般的には木構造に属するものである。木構造はいわゆるリンクの作成に適しており、あるオブジェクトが自身の親あるいは子への参照を通じて同じ処理を反復させていくことで、木構造に含まれる一定範囲のオブジェクト(あるいはすべてのオブジェクト)に目的の処理を行わせることが可能になる。
今問題としている、階層構造に含まれるオブジェクトのローカル行列やワールド行列の更新 についても、木構造のデータ処理の基本である再帰処理によって実装する。
ただし、実際の実装は次節で行う。本節で行うことは その実装の準備として、木構造の単純なモデルを用いた再帰処理プログラムの作成である。
このモデルは、親子関係にある複数のオブジェクトの中で、ある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_childrenSystem.Collecions.GenericListインスタンスである)。
冒頭でも述べたが最終的な実装では、あるオブジェクトに何らかの変化が発生した場合には、そのオブジェクトのローカル行列とワールド行列、及び そのオブジェクトのすべての子オブジェクトのワールド行列 に対して更新処理を行う。この Recursiveクラスでは、何らかの変化が発生した場合の更新処理を単純化して、単にメッセージを出力するだけの処理にしている。
(以下の文章では便宜上、Recursiveクラスのインスタンスも単に「オブジェクト」と表現する)
具体的にはオブジェクトに何らかの変化が発生した場合、そのオブジェクトについては「Update Local」と「Update World」の2つのメッセージが出力され、そのオブジェクトの(下階層のすべての)子オブジェクトについては「Update World」だけが出力される。そして、先程も述べたが更新処理は必要なときに必要な分だけ行われるようにする必要がある。5行目、6行目のbool型メンバ変数 m_UPDATE_LOCALm_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();

図1
図2

(以下の文章においても Recursiveクラスのインスタンスについては単に「オブジェクト」という表現を用いる)
1行目~5行目において5つのオブジェクトObj1~Obj5を生成している。
7行目~11行目はそれらのオブジェクトの間における親子関係の設定である (注1)。この5つのオブジェクトには図1に示されるような親子関係が設定される (Obj1が一番上の親であり、Obj5が一番下の子である)。
12行目の SomeChange() はオブジェクトに対する「何らかの変化」を簡易化したメソッドである。このプログラムではObj3SomeChange() をコールしているが、これは Obj3に「何らかの変化」が発生したことを意味する。図2では、この Obj3の変化によってメッセージ出力が必要となるオブジェクトを赤く表示している。
SomeChange()の具体的な内容は m_UPDATE_LOCALtrueにして、NotifyChain()をコールするというものである (上記Recursiveクラス 24行目)。NotifyChain()では m_UPDATE_WORLDtrueにして、子オブジェクトの NotifyChain()をコールする (Recursiveクラス 30行目)。NotifyChain()は再帰関数なので一番下の階層の子オブジェクトまでこのメソッドの呼び出しが再帰的に行われることになる。
上で述べたように、オブジェクトに変化が発生した場合には、その時点ではメッセージを出力せずに、まずは メッセージ出力が必要となるオブジェクトに対してフラグを立てる処理を行う。変化が発生したオブジェクトの場合は「Update Local」「Update World」の2つのメッセージが出力されるので、この2つのメッセージ用のフラグを立てる。変化が発生したオブジェクトの(すべての)子オブジェクトに対しては「Update World」だけが出力されるのでこのメッセージ用のフラグを立てる。
今回の例でいえば、12行目の Obj3.SomeChange() によって Obj3に変化が発生するが、SomeChange()の中では m_UPDATE_LOCALが、さらに NotifyChain()の中では m_UPDATE_WORLDtrueになるので Obj3に対しては必要な2つのフラグは立てられているわけである。また、Obj3の子オブジェクトである Obj4、Obj5に対しても再帰関数NotifyChain()の呼び出しが行われ、その中で m_UPDATE_WORLDtrueになるので、これらの子オブジェクトに対しても必要なフラグが立てられることになる。
図3
14行目の Obj1.UpdateChain() の実行により、一番上の親オブジェクトであるObj1から一番下の子オブジェクトであるObj5まで再帰的にこのメソッドが実行される。UpdateChain()の内容は、m_UPDATE_WORLD あるいは m_UPDATE_LOCALtrueになっているオブジェクトのメッセージを出力するというものである (Recursiveクラス 40行目)。今回の実行結果(図3)では、Obj3については「Update Local」、「Update World」の2つが出力され、Obj4、Obj5については「Update World」のみが出力されている。

このプログラムでは Obj3に変化が発生するという設定であったが、プログラムの12行目の Obj3.SomeChange()Obj4.SomeChange() に変えて実行した場合は、次に示される結果になる。

図4
図5

図4は Obj4の変化によってメッセージ出力が必要となるオブジェクトを赤く表示したものである。
図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();

図6
今回使用するオブジェクトは Obj1~Obj10の10個のオブジェクトで、1行目から10行目はそれらの生成コードである。12行目から23行目において、この10個のオブジェクトの間に親子関係を設定している。ここで設定される階層構造は図6に示されるとおりである。
簡単に説明すると Obj1の1階層下には3つの子オブジェクトObj2、Obj3、Obj4があり、それらの子オブジェクト自身も子オブジェクトを持っている。例えば、Obj2は Obj5を子オブジェクトとして持っているが、さらにその Obj5も Obj8、Obj9を子オブジェクトとして持っている。
25行目では Obj2.SomeChange() と記述されているが、これは Obj2に何らかの変化が発生したことを意味している。Obj2に変化が発生した場合には、Obj2、及び そのすべての子オブジェクトにメッセージ出力のためのフラグを立てる処理が再帰的に行われる。そして、27行目の Obj1.UpdateChain() によって階層構造の一番上の親オブジェクトから一番下の階層まで、すべてのオブジェクトを1つ1つ再帰的にたどっていき、メッセージ出力のフラグの立っているオブジェクトについてはメッセージを出力する。
以下の図7は今回の Obj2の変化によってメッセージを出力することになるオブジェクトを赤く表示したものであり、図8はこのプログラムの実行結果である。

図7
図8

図8に示されるように、変化の発生したオブジェクトである Obj2の出力メッセージは「Update Local」「Update World」の2つであり、Obj2の子オブジェクトである Obj5、Obj8、Obj9の出力メッセージは「Update World」のみとなっている。
この結果は Obj2に変化があった場合の実行結果であるが、以下では他のオブジェクトに変化があった場合についていくつか見てみよう (つまり 25行目の SomeChange() を実行するオブジェクトを変えた場合)。

Obj3に変化があった場合 (25行目を Obj3.SomeChange() とした場合)。

図9
図10

Obj4に変化があった場合 (25行目を Obj4.SomeChange() とした場合)。

図11
図12

Obj5に変化があった場合 (25行目を Obj5.SomeChange() とした場合)。

図13
図14




本節では、あるオブジェクトに変化が生じた際の、そこから発生する連鎖的な変化について非常に簡単なモデルを用いて見てきた。しかし、階層構造にある3Dオブジェクトの連鎖的な変化についても、本節で見てきたものと同様の過程をたどるのである。
そのことについては次節で見ていく。














© 2020-2024 Redpoll's 60 (All rights reserved)