Catlike學(xué)習(xí)筆記(1.3)-使用Unity畫更復(fù)雜的3D函數(shù)圖像

第三篇來了~今天去參加了 Unite 2018 Berlin,感覺就是。。。。非常困。。?;貋硪院笊晕⑺讼虑逍蚜擞X得是時(shí)候認(rèn)真學(xué)習(xí)下了,不過講的很多東西都是還沒有發(fā)布或者只有 Preview 的版本,按照 Unity 的習(xí)慣肯定 Bug 多到令人發(fā)指,最近不太想折騰所以就先繼續(xù)寫文章把。。按照慣例奉上『原文鏈接

PART 1 概述

首先大概介紹一下什么是『Catlike教程』,大家自行訪問一下就會(huì)發(fā)現(xiàn)是這位『大神』寫的一個(gè) Unity 系列教程,里面由淺至深的以一個(gè)個(gè)有趣的小課題來引導(dǎo)大家學(xué)習(xí) Unity 的方方面面~回想自己畢業(yè)三年都在做 Unity 游戲開發(fā),然而看了大神的教程以后發(fā)現(xiàn)自己欠缺的東西非常多~真正對引擎的掌握程度非常低只是在不停的拼 UI 寫業(yè)務(wù)邏輯。做這個(gè)系列呢也是希望自己可以堅(jiān)持把大神的教程學(xué)完讓自己變得更厲害~就醬。。

那么言歸正傳我們本期節(jié)目的最終目標(biāo)是實(shí)現(xiàn)作者配圖中的看起來很屌的圖形,像是這樣的。。。

Animation

對比上一篇文章的函數(shù)圖像,大概有以下幾個(gè)關(guān)鍵點(diǎn)需要實(shí)現(xiàn)。

  • 支持多函數(shù)疊加

  • 從一條曲線變成一個(gè)曲面

  • 由曲面擴(kuò)展成真正的三維圖形

PART 2 支持多函數(shù)疊加

首先我們的目標(biāo)是可以通過一個(gè)滑桿來控制「上一篇」中的曲線顯示的函數(shù),因此先復(fù)制之前的代碼改改名字比如 Graph3DController.cs 再修改類名與文件名一致。然后我們的關(guān)鍵是需要修改這一行


var pos = new Vector3(x, Calc(x), 0);

使其變成根據(jù)滑桿中的 int 值選擇 delegate 中的某個(gè)函數(shù),如下所示,代碼中主要修改的地方用注釋稍微解釋了下。


// 新的 deleagate

public delegate float Function(float x, float t);

// 記得修改類名與文件名一致否則不能掛在 gameobject 上

public class Graph3DController : MonoBehaviour

{

    [Range(10, 100), SerializeField] private int _resolution;

    [SerializeField] private GameObject _cube;

    // 添加新的滑桿

    [Range(0, 1), SerializeField] private int _function;

    // 一個(gè) delegate 數(shù)組用于保存我們接下來使用的兩個(gè)函數(shù)

    private Function[] _functions;



    ...

    // Use this for initialization

    private void Start()

    {

        // 初始化 _functions

        _functions = new Function[] {SineFunction, MultiSineFunction};      

        ...

    }

    private void Update()

    {

        _startX = -1f;

        for (int i = 0; i < _resolution; i++)

        {

            var x = _startX + i * _step;

            // 此處修改調(diào)用方法

            var pos = new Vector3(x, _functions[_function](x, Time.time), 0);

            var point = _points[i];

            point.transform.localPosition = pos;

        }

    }

    private float SineFunction(float x, float t)

    {

        return Mathf.Sin(Mathf.PI * (x + t));

    }

    private float MultiSineFunction(float x, float t)

    {

        float y = Mathf.Sin(Mathf.PI * (x + t));

        y += Mathf.Sin(2f * Mathf.PI * (x + 2f * t)) / 2f;

        y *= 2f / 3f;

        return y;

    }

}

于是我們實(shí)現(xiàn)了如下的效果~

Animation

不過作者在原文中還添加了 Enum 然后可以不用滑桿而是改用一個(gè)下拉菜單來改變要顯示的函數(shù)圖像。最終效果沒什么不同就不再贅述了感興趣的同學(xué)可以自行找到『原文鏈接』查看更詳細(xì)的步驟~

PART 3 畫出水滴的波紋

那么接下來開始要真正的繪制一個(gè)3D曲面了~那么首先是創(chuàng)建更多的小方塊~我們在初始化的地方改成一個(gè)二維的 List 來保存所有的小方塊


private void Start()

{

    ...

    for (int i = 0; i < _resolution; i++)

    {

        _points.Add(new List());

        for (int j = 0; j < _resolution; j++)

        {

            var point = Instantiate(_cube, transform);

            _points[i].Add(point.transform);

            point.transform.localScale = scale;

            point.SetActive(true);

        }

    }

}

在后續(xù)的遍歷也對該二維數(shù)組進(jìn)行遍歷。


private void Update()

{

    for (int i = 0; i < _points.Count; i++)

    {

        for (int j = 0; j < _points[i].Count; j++)

        {

            var posX = i * _step - 1;

            var posZ = j * _step - 1;

            var pos = new Vector3(posX, _functions[(int) _function](posX, posZ, Time.time), posZ);

            var point = _points[i][j];

            point.localPosition = pos;

        }

    }

}

最后再稍微修改下兩個(gè)函數(shù)的參數(shù)就完成了從 2D 到 3D 的跳躍~如圖所示

Animation

不過我們并不應(yīng)該滿足于此,感覺這樣其實(shí)并沒有充分利用 Z 軸啊,完全就是復(fù)制了很多條曲線排在一起。所以我們新建兩個(gè)這樣的函數(shù)。


private float Sine2DFunction(float x, float z, float t)

{

    float y = Mathf.Sin(Mathf.PI * (x + t));

    y += Mathf.Sin(Mathf.PI * (z + t));

    y *= 0.5f;

    return y;

}

private float MultiSine2DFunction(float x, float z, float t)

{

    float y = 4f * Mathf.Sin(Mathf.PI * (x + z + t * 0.5f));

    y += Mathf.Sin(Mathf.PI * (x + t));

    y += Mathf.Sin(2f * Mathf.PI * (z + 2f * t)) * 0.5f;

    y *= 1f / 5.5f;

    return y;

}

那么Sine2DFunction可以很明顯的看出是兩個(gè)完全一樣的正弦波分別沿 x 軸和 Z 軸傳播并且直接疊加,那么第二個(gè)。。。反正很復(fù)雜語言解釋不清楚大概就是 3 個(gè)波疊加起來的,大家可以一行一行注釋掉看看效果就知道了~

那么如何畫出一個(gè)波紋呢,首先波紋是由原點(diǎn)也就是(0, 0)點(diǎn)開始均勻擴(kuò)散的,那么可能是一個(gè)從原點(diǎn)向周圍擴(kuò)散的正弦波。那么直覺上來說這個(gè)函數(shù)可能長這樣。。


private float Ripple (float x, float z, float t)

{

    float d = Mathf.Sqrt(x * x + z * z);

    float y = Mathf.Sin(Mathf.PI * (d - t));

    return y;

}

運(yùn)行下會(huì)發(fā)現(xiàn)完全不像,主要是因?yàn)樗ㄔ跀U(kuò)散的過程中是要衰減的,正弦波完全不會(huì),因此我們需要加上衰減的控制。既然是衰減的話顯然距離越大衰減的越多嘍所以我們讓 y 除以 1 + 2 * Mathf.PI * d試一試,之所以加1是為了防止在距離原點(diǎn)過于近的時(shí)候結(jié)果趨近于無窮大。所以現(xiàn)在代碼變成了這樣~


private float Ripple(float x, float z, float t)

{

    float d = Mathf.Sqrt(x * x + z * z);

    float y = Mathf.Sin(Mathf.PI * (d - t));

    y = y / (1 + 2 * Mathf.PI * d);

    return y;

}

跑起來看一下會(huì)發(fā)現(xiàn)。。。emmmm

Animation

所以我們再加上一些參數(shù)比如_velocity傳播速度,frequency水波頻率,_amplitude振幅,_attenuation衰減。代碼如下。(這些參數(shù)并不是數(shù)值越大就直觀意義上越大,雖然這樣不太好但是懶得整理了。。。大家大概意思理解就好)


private float Ripple(float x, float z, float t)

{

    float d = Mathf.Sqrt(x * x + z * z);

    float y = Mathf.Sin(_frequency * Mathf.PI * (d - t / _velocity));

    y *= 1 / (_amplitude + _attenuation * 2 * Mathf.PI * d);

    return y;

}

然后將這些參數(shù)調(diào)整到合適的值,就完成一個(gè)完美的水波了~如圖所示

Animation

PART 4 畫出三維圖形

顯然我們不能滿足于此,傳入 x 和 z 來計(jì)算出唯一的 y 導(dǎo)致了無法有兩個(gè)點(diǎn)擁有相同的 x 和 z,這極大的限制了我們的發(fā)揮~比如說畫出一個(gè)球體之類的。所以我們接下來的目標(biāo)是畫出真正的三維圖形~

在開始之前,我們首先要放棄傳入 x 和 z 來計(jì)算 y 的設(shè)想,所以應(yīng)該把所有的函數(shù)的返回值改成 Vector3,并且為了區(qū)分我們將函數(shù)的參數(shù)變成 u,v,t。


public delegate Vector3 Function(float u, float v, float t);

public enum GraphFunctionName {

    Sine,

    MultiSine,

    Sine2D,

    MultiSine2D,

    Ripple,

}

public class Graph3DController : MonoBehaviour

{

    [Range(10, 100), SerializeField] private int _resolution;

    [SerializeField] private GameObject _cube;

    [SerializeField] public GraphFunctionName _function;

    [SerializeField] private float _amplitude = 3;

    [SerializeField] private float _frequency = 4;

    [SerializeField] private float _velocity = 2;

    [SerializeField] private float _attenuation = 6;

    private List> _points;

    private float _step;

    private Function[] _functions;

    // Use this for initialization

    private void Start()

    {

        _functions = new Function[] {SineFunction, MultiSineFunction, Sine2DFunction, MultiSine2DFunction, Ripple};

        _cube.SetActive(false);

        _points = new List>();

        _step = 2f / _resolution;

        var scale = Vector3.one * _step;

        for (int i = 0; i < _resolution; i++)

        {

            _points.Add(new List());

            for (int j = 0; j < _resolution; j++)

            {

                var point = Instantiate(_cube, transform);

                _points[i].Add(point.transform);

                point.transform.localScale = scale;

                point.SetActive(true);

            }

        }

    }

    private void Update()

    {

        for (int i = 0; i < _points.Count; i++)

        {

            for (int j = 0; j < _points[i].Count; j++)

            {

                var u = i * _step - 1;

                var v = j * _step - 1;

                var point = _points[i][j];

                point.localPosition = _functions[(int) _function](u, v, Time.time);

            }

        }

    }

    private Vector3 SineFunction(float u, float v, float t)

    {

        var x = u;

        var y = Mathf.Sin(Mathf.PI * (u + t));

        var z = v;

        return new Vector3(x, y, z);

    }

    private Vector3 MultiSineFunction(float u, float v, float t)

    {

        var x = u;

        float y = Mathf.Sin(Mathf.PI * (u + t));

        y += Mathf.Sin(2f * Mathf.PI * (u + 2f * t)) / 2f;

        y *= 2f / 3f;

        var z = v;

        return new Vector3(x, y, z);

    }

    private Vector3 Sine2DFunction(float u, float v, float t)

    {

        var x = u;

        float y = Mathf.Sin(Mathf.PI * (u + t));

        y += Mathf.Sin(Mathf.PI * (v + t));

        y *= 0.5f;

        var z = v;

        return new Vector3(x, y, z);

    }

    private Vector3 MultiSine2DFunction(float u, float v, float t)

    {

        var x = u;

        float y = 4f * Mathf.Sin(Mathf.PI * (u + v + t * 0.5f));

        y += Mathf.Sin(Mathf.PI * (u + t));

        y += Mathf.Sin(2f * Mathf.PI * (v + 2f * t)) * 0.5f;

        y *= 1f / 5.5f;

        var z = v;

        return new Vector3(x, y, z);

    }

    private Vector3 Ripple(float u, float v, float t)

    {

        var x = u;

        float d = Mathf.Sqrt(u * u + v * v);

        float y = Mathf.Sin(_frequency * Mathf.PI * (d - t / _velocity));

        y *= 1 / (_amplitude + _attenuation * 2 * Mathf.PI * d);

        var z = v;

        return new Vector3(x, y, z);

    }

}

圓柱體

那么如何組成一個(gè)圓柱體呢,首先我們知道圓柱體可以認(rèn)為是由許多個(gè)圓環(huán)組成的,那么如何構(gòu)成一個(gè)圓環(huán)呢?我們知道 u 的取值范圍是[-1, 1],將 u * PI 即可獲得 [-PI, PI] 即剛好一個(gè)圓周的弧度,對應(yīng)的坐標(biāo)即是(x = sin(PI * u), z = cos(PI * u)),按照以上思路我們完成以下代碼。然后每一個(gè)點(diǎn)的縱座標(biāo) y 就直接取 v 的值即可形成「每個(gè)水平的圓周上有100個(gè)點(diǎn),共100個(gè)圓縱向排列組成的圓柱體」了好吧感覺表述的不是特別清楚寫出來跑跑看就知道了。。。


private Vector3 Cylinder(float u, float v, float t)

{

    var x = Mathf.Sin(Mathf.PI * u);

    var y = v;

    var z = Mathf.Cos(Mathf.PI * u);

    return new Vector3(x, y, z);

}

運(yùn)行一下發(fā)現(xiàn)果然是一個(gè)圓柱體,如果想要控制圓柱體的半徑和高直接在 x 和 z 乘以 R,y 乘以 H 即可,如下圖所示。代碼就不貼了大家都會(huì)自己乘~

Animation

那么如何讓這個(gè)圓柱體動(dòng)起來呢~比如說隨便對 R 做一些手腳像下面這樣


private Vector3 InterestingCylinder(float u, float v, float t)

{

    var r = _radius * (0.8f + Mathf.Sin(Mathf.PI * (6f * u + 2f * v + t)) * 0.2f);

    var x = r * Mathf.Sin(Mathf.PI * u);

    var y = _height * v;

    var z = r * Mathf.Cos(Mathf.PI * u);

    return new Vector3(x, y, z);

}

嘗試改變 u 和 v 的系數(shù)可以看到很多有趣的現(xiàn)象哦~懶得自己寫的可以打開我的「Github Repo」直接運(yùn)行時(shí)修改 FactorU 和 FactorV 的值查看結(jié)果~最終我們可以達(dá)到類似這樣的效果

Animation

球體

我們在圓柱體的基礎(chǔ)上稍加修改就可以獲得一個(gè)球體,首先,球體跟圓柱體一樣也可以認(rèn)為是很多半徑不同的圓環(huán)組成的,那么圓環(huán)的半徑呈現(xiàn)怎樣的變化呢,我們想象球體沿經(jīng)線切開后,可以觀察到一圈緯線的半徑和緯線的縱座標(biāo)分別對應(yīng)Cos(PI / 2 * v)Sin(PI / 2 * v),按照這個(gè)思路我們嘗試寫出如下代碼。


private Vector3 Sphere(float u, float v, float t)

{

    var r = _radius * Mathf.Cos(Mathf.PI / 2 * v);

    var x = r * Mathf.Sin(Mathf.PI * u);

    var y = _radius * Mathf.Sin(Mathf.PI / 2 * v);

    var z = r * Mathf.Cos(Mathf.PI * u);

    return new Vector3(x, y, z);

}

運(yùn)行一下發(fā)現(xiàn)完全沒有問題~如圖所示。。。

Animation

所以想要讓球體動(dòng)起來我們可以使用同樣地思路對 r 的計(jì)算進(jìn)行一點(diǎn)點(diǎn)魔改,比如說這樣的一個(gè)參數(shù)factor


private Vector3 InterestingSphere(float u, float v, float t)

{

    var factor = 0.8f + Mathf.Sin(Mathf.PI * (_factorU * u + t)) * 0.1f;

    factor += Mathf.Sin(Mathf.PI * (_factorV * v + t)) * 0.1f;

    var r = factor * _radius * Mathf.Cos(Mathf.PI / 2 * v);

    ...

}



調(diào)一些奇怪的參數(shù)。。。然后就出現(xiàn)了一坨嚅動(dòng)的,。。球體。。。

Animation

圓環(huán)體

那么想象下一個(gè)圓環(huán)體和球體到底有什么區(qū)別呢,針對每左半條或者右半條經(jīng)線圈,如果直接變成一個(gè)環(huán),那么球體不就變成圓環(huán)了么。。。那么怎么變成圓環(huán)呢,我們之前提到

一圈緯線的半徑和緯線的縱座標(biāo)分別對應(yīng)Cos(PI / 2 * v)和`Sin(PI / 2 * v)

所以我們把半個(gè)周期的 cos 和 sin 變成完整周期就可以了,不要除以 2 就好。。于是我們嘗試著寫下如下代碼


private Vector3 Torus(float u, float v, float t)

{

    var r = _radius * Mathf.Cos(Mathf.PI * v);

    var x = r * Mathf.Sin(Mathf.PI * u);

    var y = _radius * Mathf.Sin(Mathf.PI * v);

    var z = r * Mathf.Cos(Mathf.PI * u);

    return new Vector3(x, y, z);

}

運(yùn)行一下發(fā)現(xiàn)還是球體啊。。這是為什么呢,仔細(xì)觀察發(fā)現(xiàn)似乎小方塊比以前稀疏了,是因?yàn)榘霔l經(jīng)線被擴(kuò)展到整個(gè)周期以后變成了一整圈經(jīng)線,所以和對面的那半條完全重疊了。。所以怎么解決這個(gè)問題呢?就是擴(kuò)大緯線圈讓相對的兩個(gè)半條經(jīng)線不會(huì)相互重疊甚至完全分離就可以了。所以這樣修改下試試


private Vector3 Torus(float u, float v, float t)

{

    var r = _radius * Mathf.Cos(Mathf.PI * v) + _radius2;

    ...

}

這里之所以是加一個(gè)_radius2在最外面是為了達(dá)到「無論 v 如何變化都可以是的半徑無條件增加 _radius2」的效果。。。運(yùn)行下會(huì)發(fā)現(xiàn)嗯果然沒問題了。。

Animation

所以最后也順便讓它動(dòng)起來吧。。。

Animation

PART 5 總結(jié)

好吧這篇真的好長,而且寫的好累并且在公式功能壞掉的情況下又很難講清楚~大家把「Github Repo」下載下來自己運(yùn)行稍微修改下就很容易理解了~總之我們把簡單的圖像擴(kuò)展到了三維的圖形的過程還是很有趣的~雖然不知道暫時(shí)有什么用處不過對于培養(yǎng)數(shù)學(xué)思維也還是挺有幫助的~好吧希望下一篇早日更新~就醬。。。


原文鏈接:https://snatix.com/2018/06/20/021-mathematical-surfaces/

本文由 sNatic 發(fā)布于『大喵的新窩』 轉(zhuǎn)載請保留本申明

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容