Unityで学ぶシーングラフ入門の入門モドキ

Posted: 2015年12月14日 カテゴリー: プログラミングメモ, Unity, Unity メモ
タグ:, , ,

はじめに

この記事は Unity 2 Advent Calendar 2015 の15日目の記事です。

14日目はShiroKuroさん(@taku_nishimu)の「UGUIでアニメーションを使ってNoCodingで画面遷移を作る」でした。

 

Unibookに今度こそ何か書こうと思ってましたが、思い出した頃には締め切りを過ぎていたので代わりにまだ空きがあるAdvent Calenderに書く事を急遽思いつきました。

 

・・・そしてUnityの記事のフリして実はゲームプログラム系記事です。Unityなネタおもいつきませんでしたあぁぁぁ!!!

 

シーングラフって?

簡単に言ってしまえばゲームオブジェクト(transform)の親子関係の事です。今回の記事では「シーングラフ」はこの構造を指します。

scenegraph0

親のtransform情報に変更が有れば全ての子供に影響が有り、子供の情報が変われば親には影響が無く、その子供の全ての子供に影響がでます。

 

例を上げますと、人型キャラの上半身には胴体が有り、その子供として腕がくっ付いていてその子供に~と続けていく構造で、腕からさらに先にある親指が動いても親である上半身に影響を与えませんが、胴体が動いたら指の位置もそれに合わせて基準が動く構造の事です。

 

多分文章で説明するよりもゲームオブジェクトに親子関係を持たせて適当に動かした方が意味が解りやすいとは思います。

 

この記事ではシーングラフの基本の基本に当たる構造の実装をUnityを使って雑に説明していきます。

 

[注意事項]

筆者はUnityの内部実装がどうなっているのかを知らない為、今回紹介するUnityを例にした解説や、実装方法は一般的な解説と実装方法をUnityを使う側からの想像で当てはめています。

なので、ここで紹介した物 = Unityの仕様/実装方法、では無いのでご注意ください。

 

言い訳

えー、サンプルはありません!!メンドクs・・・急遽書く事を決めた為時間が取れませんでした!決してHearthStoneやったり、Catanやってたとかじゃありませんよ?

 

座標空間のおさらい

シーングラフの実装に入る前に、まずは座標系のおさらいを軽くします。

3dを扱う際、段階的に違う座標系で計算を行います。座標系の種類は5つあります:

  • Local Space
  • World Space
  • Camera Space
  • Projection (Normalized Device) Space
  • Screen Space

 

それぞれについて軽く説明します。

Local space

オブジェクト自身を変形する座標です。”Model Space”と言う時も有ります。UnityではtransformのlocalPosition, localRotation, localScaleにあたります。エディター上でtransformの値を弄る時はLocal Spaceの設定を行っています。

World space

local spaceの情報に外的要因(親のオブジェクト等)を加えた変形を行った後の座標系です。Unityではtransformのposition, scale, rotationにあたります。

Camera space

world space内のオブジェクトにカメラの情報(view行列)を適用させ、変形した後の座標系です。viewの中にはカメラの位置や向きなどが入っています。

Projection space

camera space内のオブジェクトにカメラの投影行列(projection matrix)を適用させ変形した後の座標です。投影の種類はUnityのカメラ設定でも見るOrthogonalとPerspectiveです。

Screen space

最終的に画面に描画すると時に使用する座標になります。dxライブラリやxna等のライブラリでで2dゲームを作る際にはスクリーン座標で直接する事が多いです。unityではUIを作る際にスクリーン座標と同じように設定をすることもあります、多分。

 

座標系の説明は以上です。直接は関係無いので個々に行われる変換はスルーしました、。シーングラフの実装ではlocalとworldを扱います。

 

シーングラフ

上記の通り、シーングラフはオブジェクトに親子関係を持たせるので、木構造を使用します。

scenegraph1

この全体の仕組みをシーングラフと呼び、各オブジェクトをシーンノードと呼びます。

unity内でのルート(最初のノード)はシーンファイル内のゲームオブジェクトを配置している場所(hierarchy)と同じだと思ってください。

scenegraph2

シーンノード実装

それでは実装の説明に入ります。

まずクラスを宣言します。今回はシーングラフの解説がメインなので、コンポーネントに分けず、このクラス内にtransform情報を宣言します。まずはシーンノードクラスの宣言です:

// -- ノード自体の情報 -- //
public string Name { get; protected set; }
public bool IsActive { get; set; }

// -- Transform系の情報 -- //

// このノードがくっ付いてる親のシーンノード。
protected SceneNode m_parent;

// このノードにくっ付いている子供(シーンノード一覧)。
protected List<SceneNode> m_childs;

// ノードの位置情報
public Vector3 Translation { get; protected set; }

// ノードの回転情報
public Matrix4x4 Rotation { get; protected set; }

// ノードのスケール情報
public Vector3 Scale { get; protected set; }

// ノードのLocal Space内の変形情報
public Matrix4x4 LocalTransform { get; protected set; }

// ノードのWorld Space内の変形情報
public Matrix4x4 WorldTransform { get; protected set; }


public SceneNode(string name, SceneNode parent = null)
{
    this.Name = name;
    m_parent = parent;
    m_childs = new List<SceneNode>();

    LocalTransform = Matrix4x4.identity;
    WorldTransform = Matrix4x4.identity;

    Translation = Vector3.zero;
    Scale = Vector3.one;
    Rotation = Matrix4x4.identity;

    IsActive = true;
}

protected virtual void OnAdded()
{
}

protected virtual void OnRemoved()
{
}

クラスの内部にはまずGameObjectに有る様な名前とActiveを持たせています。その後にある宣言はTransformに有る内容になります。OnAdded()メソッドはノードが別のノードにアタッチされた時に呼ばれ、OnRemoved()はノードが切り離された時に呼ばれます。この記事ではOnAdded()は中身を特に実装しませんが、何か処理を足したくなったり、継承したクラスで使いたくなった時の為に乗せておいています。

 

宣言は終わりましたので、まずはノードを追加、削除、全削除等の構造的な処理を実装して行きます。 最初にノードを子供として追加する処理を実装します。やる事は簡単で、子供として追加するノードの親を自身にし、子供一覧に追加した後にOnAdded()を呼びます。

public virtual bool AddChild(SceneNode toAdd)
{
    toAdd.m_parent = this;
    m_childs.Add(toAdd);
    toAdd.OnAdded();
    return true;
}

次に子供を切り離す処理を実装します。これは単純にListクラスのRemove()を呼んでいます。

public virtual bool RemoveChild(SceneNode toRemove)
{
    toRemove.OnRemoved();
    return m_childs.Remove(toRemove);
}

次に子供とその子供を全て切り離して消していく処理を実装します。実装は再帰で構造の底まで行き、そこから全ての子供を消していっています。

public void RemoveAllChidren()
{
    int childCount = m_childs.Count;
    for (int i = 0; i < childCount; ++i)
    {
        m_childs[i].RemoveAllChidren();
    }

    for (int i = 0; i < childCount; ++i)
    {
        m_childs[i].OnRemoved();
    }

    m_childs.Clear();
}

RemoveAllChildren()内で全ての子供のRemoveAllChildren()を呼んで行き、その後に通常のRemove()同様OnRemoved()で切り離された時に処理を呼び、後はリストを空にしていっています。

 

子供を消す処理を作りましたので、今度はノードの終了処理を呼びます。これも今回は単純に全ての子供に再帰的に処理を呼び出し、上で実装したRemoveAllChidren()を呼び出して行きます。

public virtual bool Shutdown()
{
    // 全ての子供の終了処理を呼ぶ
    int count = m_childs.Count;
    for (int i = 0; i < count; ++i)
    {
        m_childs[i].Shutdown();
    }

    // 子供を全て消していく
    RemoveAllChidren();
    return true;
}

これで追加、削除、全削除、おまけに終了処理の実装が出来ました。

 

前準備が長くなりましたが、これから変形情報の更新処理を実装して行きます。

 

最初に行う事はLocalTransformの行列を計算します。

やる事としましては、位置、回転、スケールを一つの行列に組み合わせます。準備段階として位置とスケールのVector3をMatrix4x4に変換する処理をまず実装します。


public static Matrix4x4 CreateTranslationMatrix(Vector3 translation)

{

Matrix4x4 ret = Matrix4x4.identity; ret.SetRow(3, new Vector4(translation.x, translation.y, translation.z, 1.0f));

return ret;

}

public static Matrix4x4 CreateScaleMatrix(Vector3 scale)

{

Matrix4x4 ret = Matrix4x4.identity; ret.m00 = scale.x; ret.m11 = scale.y; ret.m22 = scale.z; ret.m33 = 1.0f; return ret;

}

両方のメソッドで1.0fになっている所はwの値で、デフォルトにしてあります(Identityで初期化しているので必要ないはずですが、念の為)。

 

準備出来ましたので、更新処理でLocalTransformを計算します。

public virtual void Update(float delta)
{
    if (!IsActive)
    {
        return;
    }

    // 各行列を作る
    Matrix4x4 translation = CreateTranslationMatrix( Translation );
    Matrix4x4 scale = CreateScaleMatrix(Scale);
    Matrix4x4 rotation = this.Rotation;

    // [移動 x 回転 x スケール]の順番でなければいけない
    LocalTransform = Translation * rotation * scale;

    // ...続く

これでLocalTransformを計算出来ました。これが自身の情報ですので、これを基準にしてWorldTransformを計算して行きます。

 

補足:行列のおさらい

上記で行列の計算を行っておりますので、次に進む前に行列に関しての補足情報です。

行列の掛け算を行う際、計算の順番により結果が変わりますので、計算順を間違えない事が大事になります。

[a x b != b x a]

これを踏まえたうえで移動、回転、スケール情報を一つの行列に組み合わせる時も計算順を考える必要があります。

変形用行列を作る際に行う計算順は

[移動 x  回転 x スケール]

になります。

ちなみに、Unityの行列は行優先(Column-major)らしいので、もし列優先(row-major)の場合は下記になります。

[スケール x  回転 x 移動]

 

Worldの計算

では本題に戻ってWorldの計算に入ります。Worldの値の計算は結構単純で

[World = Local(自身) * 親のWorld]

で計算出来ます。Worldは親から受けた影響を自身に与え、その結果がまた子供のWorldの計算に使われていくイメージです。もし親がいない場合(自身がシーングラフのルートの場合)、自身のLocalがWorldになります。

if (m_parent != null)
{
    // ここで親の影響を受ける
    WorldTransform = LocalTransform * m_parent.WorldTransform;
}
else
{
    // 親が居ないから自身に値を設定
    WorldTransform = LocalTransform;
}

これでLocalもWorldも計算出来ましたので、後は子供の更新処理を呼んで行きます。子供も更新処理の中で同じ計算を行って生きますので、これにより親の変形情報がツリー上の全ての子供に影響を与えるデータ構造を実装出来ます。

    // 再起処理で子供を全て更新する
    int childCount = m_childs.Count;
    for (int i = 0; i < childCount; ++i)
    {
        m_childs[i].Update(delta);
    }

    // ...BVH update

} // end of Update()

ノードの実装基本的な実装が出来ました!

 

シーングラフ実装

では次にルートノードを保持するシーングラフクラスを作成していきます。シーングラフクラスはイメージ的には記事の最初の方に有った、Unityのヒエラルキー部分に当たります。ゲームオブジェクトの子供でなく、ヒエラルキーの直下に置いてあるオブジェクトが、ルートにアタッチされているノードと言うイメージです。

scenegraph3

今回のシーングラフの実装自体は凄く簡単で、中にルートのノードを保持させ、それを主に操作していきます。

public class Scenegraph
{
    public const string _ROOT_NODE_NAME = "Root";
    SceneNode m_root;

    public Scenegraph()
    {
        m_root = new SceneNode(_ROOT_NODE_NAME, null);
    }

    public SceneNode Root
    {
        get { return m_root; }
    }

次に更新処理を実装します。今回はノードのUpdate()を呼ぶだけです。

public virtual void Update(float delta)
{
    if (m_root != null)
    {
        m_root.Update(delta);
    }
}

次に終了処理を実装します。今回はノードの終了処理だけを呼び出しています。

public void Shutdown()
{
    m_root.Shutdown();
    m_root = null;
}

次はノードを追加する処理を実装します。今回はアタッチする親を指定出来る様にしますが、もしその親がルート以下に居なかった場合は、追加をしない様にします。ですので、まず先に指定したノードを検索する処理をノード内に実装します。

public SceneNode FindChild(SceneNode toFind)
{
    // 自身を確認
    if (this == toFind)
    {
        return this;
    }

    // 次は子供を確認
    int childCount = m_childs.Count;
    for (int i = 0; i < childCount; ++i)
    {		
        // 子供を検索していく
        SceneNode node = m_childs[i].FindChild(toFind);
		
        // 見つかったら返す
        if (node != null)
        {
            return node;
        }
	}
	
	// 見つからなかった
	return null;
}

検索する処理は今までと同じ様に全ての子供を再起的に検索していき、指定したノードが居たら返す様にしています。

 

ノードを見つける機能を実装しましたので、ノードを追加する処理をシーングラフ内に実装します。

public bool AddNode(SceneNode toAdd, SceneNode parent = null)
{
    if (parent == null)
    {
        parent = m_root;
    }

    SceneNode targetNode = m_root.FindChild(parent);
    if (targetNode == null)
    {
        return false;
    }

    return targetNode.AddChild(toAdd);
}

実装は以上になります。後はシーングラフを作って、ノードをくっつけて行き、更新処理を呼んで行くイメージです。

 

おまけ

シーンノードのUpdate()の最後に “//… BVH update” の一文が有りますが、このシーングラフの構造はBounding Volume Hierarchy(BVH) を実装する時に流用出来ます。シーングラフは更新の影響がツリーの一番上から一番下に流れていきますが、BVHを実装する時はツリーの一番下から上に影響を与えていく(大きさを合体させていく)ので、変形の更新処理後に挟むと、実装するには調度言い場所になります。

 

終わり

だらだらと書いて解りづらかったとは思いますが、Transformの親子関係の内部構造を妄想するヒントになったり(する人は居ないとは思いますが・・・)、自前のライブラリを作ってる人の参考にでもなれば幸いです。ざっくりと説明しましたので、何か質問が有ればコメントをください。

 

明日は日本語が出来るUnity使いが皆お世話になっているであろうテラシュールウェアの椿さん(@tsubaki_t1)です!!

 

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中