Catlike學習筆記(1.4)-使用Unity構建分形

又兩個星期沒寫文章了,主要是沉迷 Screeps 這個遊戲,真的是太好玩了導致我這兩個禮拜 Github 小綠點幾乎天天刷。其實想開一個新坑大概把自己寫 AI 的心路歷程記錄下,不過覺得因爲要消耗太多時間暫時決定先不開,準備把過程中遇到的有趣的算法問題記錄下就好了。言歸正傳今天來到「構建分形」 這篇文章。比較簡單主要介紹遞歸的思想。我們就迅速一些,因爲我還要繼續沉迷 Screeps,因爲還要繼續學習嗯。。。再貼一次「原文鏈接」吧。。

PART 1 概述

分形」這種東西,隨便了解一下大概就能想到作者要用遞歸的方法來完成。所以這一篇教程性質的文章主要是講在 Unity 裏使用遞歸完成一些事情。鑑於大家應該上大學的時候隨隨便便上個課就差不多瞭解遞歸這種基本概念,因此我們就進展快一些~大概要完成以下事情:

  • 使用遞歸生成一大堆立方體和球體
  • 整理一下使其變成分形
  • 美化一下

PART 2 遞歸生成

首先我們要在 MonoBehaviour 裏面生成一個立方體,需要如下代碼。

public class Fractal : MonoBehaviour
{
    public Mesh Mesh;
    public Material Material;

    // Use this for initialization
    void Start ()
    {
        gameObject.AddComponent<MeshFilter>().mesh = Mesh;
        gameObject.AddComponent<MeshRenderer>().material = Material;
    }
}

非常簡單,然後在場景裏新建一個 GameObject 再掛上這個腳本,拖一些默認的 mesh 和 material 上去就好了,運行發現 OK 完美成功。那麼說好的遞歸呢?非常簡單,我們只需要在Start()裏面創建一個新的 GameObject 再給他掛上這個 MonoBehaviour 就好。當然要記得限制遞歸的次數不然要爆炸~每次遞歸都記得調整子物體的位置和大小,最後設置一下遞歸深度這樣就 OK 了,代碼如下

public class Fractal : MonoBehaviour
{
    public Mesh Mesh;
    public Material Material;

    public int Depth;

    // Use this for initialization
    void Start ()
    {
        gameObject.AddComponent<MeshFilter>().mesh = Mesh;
        gameObject.AddComponent<MeshRenderer>().material = Material;
        if (Depth > 0)
        {
            new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this);
        }

    }

    public void Initialize(Fractal parent)
    {
        Mesh = parent.Mesh;
        Material = parent.Material;
        Depth = parent.Depth - 1;
        transform.SetParent(parent.transform);
    }
}

那麼這樣就可以生成一大堆疊在一起的立方體了。。接下來的目標就是對這段代碼修修補補讓這些立方體組成看起來像是分形的樣子。

PART 3 分形

首先我們嘗試讓每個立方體在除了底面的每一面生成一個比他小一半的立方體。首先需要讓Initialize()接收位置和方向以及大小的參數。

public void Initialize(Fractal parent, float size, Vector3 pos, Vector3 rot)
{
    Mesh = parent.Mesh;
    Material = parent.Material;
    Depth = parent.Depth - 1;
    Size = size;
    transform.SetParent(parent.transform);
    transform.localPosition = pos;
    transform.localEulerAngles = rot;
    transform.localScale = Vector3.one * size;
}

非常簡單,然後在每個立方體執行Start()的時候初始化 5 個小立方體,之所以我們需要設置小立方體的朝向是爲了小立方體朝着其 Z 軸方向 (0, 0, 1) 生長,而不用考慮每次遞歸的時候的生長方向。代碼如下

private void Start ()
{
    gameObject.AddComponent<MeshFilter>().mesh = Mesh;
    gameObject.AddComponent<MeshRenderer>().material = Material;
    if (Depth <= 0) return;
    var posOffset = Size + Size / 2f;
    new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Size, Vector3.left * posOffset, new Vector3(0, -90, 0));
    new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Size, Vector3.right * posOffset, new Vector3(0, 90, 0));
    new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Size, Vector3.up * posOffset, new Vector3(-90, 0, 0));
    new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Size, Vector3.down * posOffset, new Vector3(90, 0, 0));
    new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Size, Vector3.forward * posOffset, new Vector3(0, 0, 0));
}

最後在場景裏設置下Scale = (0.5, 0.5, 0.5)size = 0.5f Depth = 4,再設置初始物體 Z 向上,即(-90, 0, 0)運行一下效果如圖所示還不錯~

picture

嗯感覺還不錯~再多設置一下變成 6 呢?我的 Macbook Pro 風扇開始呼呼的轉。。。

picture

接下來稍微重構下代碼~之前的太醜了。我們把五行長得差不多的創建子物體的代碼提取一下關鍵參數,完整版如下:

public class Fractal : MonoBehaviour
{
    public Mesh Mesh;
    public Material Material;

    public int Depth;
    public float Size;

    private readonly Vector3[] _positions = {Vector3.left, Vector3.right, Vector3.up, Vector3.down, Vector3.forward};
    private readonly Vector3[] _rotations = {Vector3.down, Vector3.up, Vector3.left, Vector3.right, Vector3.zero};

    // Use this for initialization
    private void Start ()
    {
        gameObject.AddComponent<MeshFilter>().mesh = Mesh;
        gameObject.AddComponent<MeshRenderer>().material = Material;
        if (Depth <= 0) return;
        var posOffset = Size + Size / 2f;
        for (int i = 0; i < 5; i++)
        {
            new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Size, _positions[i] * posOffset, _rotations[i] * 90);
        }
    }

    public void Initialize(Fractal parent, float size, Vector3 pos, Vector3 rot)
    {
        Mesh = parent.Mesh;
        Material = parent.Material;
        Depth = parent.Depth - 1;
        Size = size;
        transform.SetParent(parent.transform);
        transform.localPosition = pos;
        transform.localEulerAngles = rot;
        transform.localScale = Vector3.one * size;
    }
}

PART 4 美化

感覺作者寫的美化一點都不美~不過我們還是按照教程順手做一些換個 Mesh 啊隨機旋轉啦,生成機率之類的事情吧也算是有個交代。

隨機 Mesh

這個非常簡單了我們把 Mesh 這個字段擴充成一個數組。然後在初始化MeshFilter的地方從裏面隨機一個出來,像下面這樣。然後在拖一些 Mesh 進去。

public class Fractal : MonoBehaviour
{
    public Mesh[] Mesh;
    public Material Material;
    ...
    private void Start ()
    {
        gameObject.AddComponent<MeshFilter>().mesh = Mesh[Random.Range(0, Mesh.Length)];
        gameObject.AddComponent<MeshRenderer>().material = Material;
        ...
    }
    ...
}

這樣就可以了~運行起來每次都不太一樣。。圖就不截了變化並不大大家應該可以想象出來~

生成機率

也很簡單,添加一個機率然後在生成的地方每次隨機一下,隨到了就生成。。

public class Fractal : MonoBehaviour
{
    ...
    public float Probability;
    ...
    private void Start ()
    {
        ...
        for (int i = 0; i < 5; i++)
        {
            if (Random.Range(0, 1f) <= Probability)
            {
                new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Size, _positions[i] * posOffset, _rotations[i] * 90);
            }
        }
    }

    public void Initialize(Fractal parent, float size, Vector3 pos, Vector3 rot)
    {
        ...
        Probability = parent.Probability;
        ...
    }
}

把機率調成 0.75 以後生成效果如下圖(跟上一條隨機 Mesh 一起展示了)

picture

旋轉起來吧

接下來要做的就是讓這些東西全部動起來。。。並且以隨機的速度。。嗯我已經可以想像出來大概是怎樣的鬼畜場景了,嘗試實現一下的話首先就是加一個最大旋轉速度。然後在Update()裏面隨機好速度然後做一次旋轉就好了~

public class Fractal : MonoBehaviour
{
    ...
    public float MaxRotateSpeed;
    ...

    private void Start ()
    {
        ...
    }

    private void Update()
    {
        var rotationSpeed = Random.Range(-MaxRotateSpeed, MaxRotateSpeed);
        transform.Rotate(0f, rotationSpeed * Time.deltaTime, 0f);
    }

    ...
}

運行一下發現似乎總是在原地抖動的樣子。。。一定是我們速度變換的頻率太高了所以最終結果會趨近於原地不動,稍微限制一下加點隨機。。

public class Fractal : MonoBehaviour
{
    ...
    public float MaxRotateSpeed;
    public float RotateSpeedChangeRate;
    private float RotateSpeed;
    ...

    private void Update()
    {
        Random.InitState(Depth * (int)Mathf.Ceil(Time.time * 100));
        if (Random.Range(0, 1f) <= RotateSpeedChangeRate)
        {
            RotateSpeed = Random.Range(-MaxRotateSpeed, MaxRotateSpeed);
        }
        transform.Rotate(0f, 0f,  RotateSpeed * Time.deltaTime);
    }

    public void Initialize(Fractal parent, float size, Vector3 pos, Vector3 rot)
    {
        ...
        MaxRotateSpeed = parent.MaxRotateSpeed;
        RotateSpeedChangeRate = parent.RotateSpeedChangeRate;
        ...
    }
}

哇畫面真的是太詭異了。。。

animation

PART 5 總結

好的這一篇文章就這樣成功的 水過去了 完成了~這一篇大概上就是遞歸的使用方法吧~其實沒怎麼看原文自己摸索的時候還是要稍微花幾分鐘的,不過還是非常簡單啊大家隨便看看應該就可以瞭解的很透徹了~感興的同學的歡迎 follow 我的「Github」下載「項目工程」準備繼續去玩 Screeps 嘍~


原文鏈接:https://snatix.com/2018/07/07/022-constructing-a-fractal/

本文由 sNatic 發佈於『大喵的新窩』 轉載請保留本申明

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章