C# Tutorialを触ってみる1.2 - Constructing a Fractal - ①


Constructing a Fractal

Unity C# Tutorialsの続きです。
今回はConstructing a Fractalを進めていきます。
http://catlikecoding.com/unity/tutorials/constructing-a-fractal/

フラクタルライクなものを作成するみたいですね。フラクタルについては以下参照。
https://ja.wikipedia.org/wiki/%E3%83%95%E3%83%A9%E3%82%AF%E3%82%BF%E3%83%AB

関係の無い話ですが、フラクタルって聞いたら、フラクタル構造の野菜であるロマネスコを浮かべますよね。一度食べてみたい。(本当にどうでもいい)

1.How to Make a Fractal

フラクタルを作るにあたり、オブジェクトを一つ一つ作成し、親子を設定するのは非常に大変です。ですので今回はスクリプトでそれらを自動生成します。

まず新しいシーンを作成し、そこにFractalという名前の空のオブジェクトと、同じ名前のマテリアルとスクリプトを作成します。マテリアルにはSpecularを使用しています。

2.Shoewing Something

次はオブジェクトからMeshFilterとMeshRendererを持つオブジェクトを作成します。
スクリプトに以下のコードを記述し、FractalのオブジェクトにAdd Componentから追加します。

using UnityEngine;
using System.Collections;

public class Fractal : MonoBehaviour {

    public Mesh mesh;
    public Material material;

    private void Start () {
        gameObject.AddComponent<MeshFilter>().mesh = mesh;
        gameObject.AddComponent<MeshRenderer>().material = material;
    }
}

Startの中でオブジェクトにメッシュ(ポリゴン)とマテリアルを与えています。
mesh と materialはpublicで記載されており、Inspectorから操作することが可能です。
meshは今回はUnity標準のCubeを使用するので、Inspectorから右側にあるドット(点)をクリックし、Cubeを選択します。

Materialには1で作成したマテリアルをドラッグしてください。

この状態で実行すると、Inspectorが以下のように変わり、オブジェクトにMesh(cube)とMesh Rendererが追加されていることがわかります。
ゲーム画面にCubeのオブジェクトが実行中のみ作成されます。

3.Making Children

次に子のオブジェクトを作成します。本来以下のコードを用いて作成しますが、これには問題があります。

new GameObject("Fractal Child").AddComponent<Fractal>();

これをStart内に記述すると、作成したオブジェクトからStartが呼ばれ再帰的に作成されるため、メモリーがオーバーフローするなどの問題が発生します。
これを回避するために、maximum depthを設定します。子供を作っていくたびにdepthを加算していきます。

public int maxDepth;

private int depth;

private void Start () {
    gameObject.AddComponent<MeshFilter>().mesh = mesh;
    gameObject.AddComponent<MeshRenderer>().material = material;
    if (depth < maxDepth) {
        new GameObject("Fractal Child").AddComponent<Fractal>();
    }
}

これを実行すると、Fractal Childが作成されます。が、MaxDepthが4にもかかわらず、1つしか作成されません。
作成された子のInspectorを確認すると分かりますが、Max Depthが0になっています。
また、MeshやMeshRendererも2で設定したものが反映されていません。

これは新しく動的に作成されたオブジェクトに、親と同じ設定が反映されていないからですね。
ですので、オブジェクトを作成した段階で初期化を行う必要があります。
初期化関数を作成し、それを子作成時に呼びます。

private void Start () {
    gameObject.AddComponent<MeshFilter>().mesh = mesh;
    gameObject.AddComponent<MeshRenderer>().material = material;
    if (depth < maxDepth) {
        new GameObject("Fractal Child").
            AddComponent<Fractal>().Initialize(this);
    }
}

private void Initialize (Fractal parent) {
    mesh = parent.mesh;
    material = parent.material;
    maxDepth = parent.maxDepth;
    depth = parent.depth + 1;
    transform.parent = parent.transform;
}

また、作成された子オブジェクトの親は、子を作成したオブジェクトにしたいので

transform.parent = parent.transform;

として、親を登録します。

4.Shaping Children

子オブジェクトが4つ出来ましたが、全て重なっているため1つに見えます。
これを見えるようにするために、オブジェクトを移動させます。またサイズも親より小さくしていきます。

サイズのスケールはpublicでchildScaleという名前の変数を用意し、ここではInspectorから0.5と設定します。
また、先程作成した初期化の中にもスケーリングの処理を入れます。
今後パラメータを追加するに度に、初期化に記述することを忘れないようにしましょう。

移動に関しては、親オブジェクトに触れるように、親のオブジェクトのサイズと自分のサイズの半分だけ上へ移動します。

public float childScale;

private void Initialize (Fractal parent) {
    mesh = parent.mesh;
    material = parent.material;
    maxDepth = parent.maxDepth;
    depth = parent.depth + 1;
    childScale = parent.childScale;
    transform.parent = parent.transform;
    transform.localScale = Vector3.one * childScale;
    transform.localPosition = Vector3.up * (0.5f + 0.5f * childScale);
}

以下の部分でスケーリングとポジショニングを行っています。

    transform.localScale = Vector3.one * childScale;
    transform.localPosition = Vector3.up * (0.5f + 0.5f * childScale);

直感的に見てみると、どうして同じ値を入れているのにそれぞれ別の作成されるのか疑問に思うかもしれません。
これはlocalScaleとlocalPositionを使用しているからですね。
localで見るということは、一個上の親を基準として動かしている、と考えるとわかりやすいかもしれません。

5.Making Multiple Children

無事タワーのようなオブジェクトを作成できましたが、まだまだフラクタルとは呼べませんね。
4では縦方向にオブジェクトを追加しましたが、今回は横方向にもオブジェクトを作成してみます。

private void Start () {
    gameObject.AddComponent<MeshFilter>().mesh = mesh;
    gameObject.AddComponent<MeshRenderer>().material = material;
    if (depth < maxDepth) {
        new GameObject("Fractal Child").AddComponent<Fractal>().
            Initialize(this, Vector3.up);
        new GameObject("Fractal Child").AddComponent<Fractal>().
            Initialize(this, Vector3.right);
    }
}

private void Initialize (Fractal parent, Vector3 direction) {
    …
    transform.localPosition = direction * (0.5f + 0.5f * childScale);
}

ここまで来たところで、一旦これらの処理がどのように行われているのか見てみましょう。
本来これらのオブジェクト作成は一瞬で出来てしまう為、その様子を観測できません。
そこでコルーチンを使用して、ゆっくり見てみます。

コルーチンについては以下のサイトがわかりやすく説明しています。
http://qiita.com/kazz4423/items/73219068684e87adc87d

private void Start () {
    gameObject.AddComponent<MeshFilter>().mesh = mesh;
    gameObject.AddComponent<MeshRenderer>().material = material;
    if (depth < maxDepth) {
        StartCoroutine(CreateChildren());
    }
}

private IEnumerator CreateChildren () {
    yield return new WaitForSeconds(0.5f);
    new GameObject("Fractal Child").
        AddComponent<Fractal>().Initialize(this, Vector3.up);
    yield return new WaitForSeconds(0.5f);
    new GameObject("Fractal Child").
        AddComponent<Fractal>().Initialize(this, Vector3.right);
}

新たにCreateChildrenという関数が作られています。
IEnumeratorはコルーチンのインターフェースという意味らしいです。
内部にオブジェクトを作成する処理が書かれていますが、間に

    yield return new WaitForSeconds(0.5f);

とありますね。これにより処理を一旦終了させ、0.5秒後に再開する、という仕組みです。
実行してみるとオブジェクトが生成される様子が確認できると思います。

次にオブジェクトをもう一方向追加してみましょう

private IEnumerator CreateChildren () {
    yield return new WaitForSeconds(0.5f);
    new GameObject("Fractal Child").
        AddComponent<Fractal>().Initialize(this, Vector3.up);
    yield return new WaitForSeconds(0.5f);
    new GameObject("Fractal Child").
        AddComponent<Fractal>().Initialize(this, Vector3.right);
    yield return new WaitForSeconds(0.5f);
    new GameObject("Fractal Child").
        AddComponent<Fractal>().Initialize(this, Vector3.left);
}

ぱっとみうまく出来ているように見えまが、全て上向きで作成されてしまっています。また、一部が他のキューブにめり込んでしまいます。
overdraw visionを使用すると全体の様子がよくわかります。
overdraw visionはScene viewの左上から選択できます。

右側に作成されたオブジェクトは右側を上として作成したいので、そのオブジェクトをローカルで回転します。

private IEnumerator CreateChildren () {
    yield return new WaitForSeconds(0.5f);
    new GameObject("Fractal Child").AddComponent<Fractal>().
        Initialize(this, Vector3.up, Quaternion.identity);
    yield return new WaitForSeconds(0.5f);
    new GameObject("Fractal Child").AddComponent<Fractal>().
        Initialize(this, Vector3.right, Quaternion.Euler(0f, 0f, -90f));
    yield return new WaitForSeconds(0.5f);
    new GameObject("Fractal Child").AddComponent<Fractal>().
        Initialize(this, Vector3.left, Quaternion.Euler(0f, 0f, 90f));
}

private void Initialize (Fractal parent,
                         Vector3 direction,
                         Quaternion orientation) {
    …
    transform.localRotation = orientation;
}

しかしよく見ると、これでもまたキューブが重なり消えてしまっているところがあります。
これはスケールを0.5にしているからですね。スケールを減らすか、CubeをSphereにすることで解決できます。

長くなりましたので、一旦ここで切ります。
次回はこの続きをやっていきます。