您好,登錄后才能下訂單哦!
第三篇來了~今天去參加了 Unite 2018 Berlin,感覺就是。。。。非常困。。。回來以后稍微睡了下清醒了覺得是時候認真學習下了,不過講的很多東西都是還沒有發布或者只有 Preview 的版本,按照 Unity 的習慣肯定 Bug 多到令人發指,最近不太想折騰所以就先繼續寫文章把。。按照慣例奉上『原文鏈接』
首先大概介紹一下什么是『Catlike教程』,大家自行訪問一下就會發現是這位『大神』寫的一個 Unity 系列教程,里面由淺至深的以一個個有趣的小課題來引導大家學習 Unity 的方方面面~回想自己畢業三年都在做 Unity 游戲開發,然而看了大神的教程以后發現自己欠缺的東西非常多~真正對引擎的掌握程度非常低只是在不停的拼 UI 寫業務邏輯。做這個系列呢也是希望自己可以堅持把大神的教程學完讓自己變得更厲害~就醬。。
那么言歸正傳我們本期節目的最終目標是實現作者配圖中的看起來很屌的圖形,像是這樣的。。。
對比上一篇文章的函數圖像,大概有以下幾個關鍵點需要實現。
首先我們的目標是可以通過一個滑桿來控制「上一篇」中的曲線顯示的函數,因此先復制之前的代碼改改名字比如 Graph4DController.cs 再修改類名與文件名一致。然后我們的關鍵是需要修改這一行
var pos = new Vector3(x, Calc(x), 0);
使其變成根據滑桿中的 int 值選擇 delegate 中的某個函數,如下所示,代碼中主要修改的地方用注釋稍微解釋了下。
// 新的 deleagate
public delegate float Function(float x, float t);
// 記得修改類名與文件名一致否則不能掛在 gameobject 上
public class Graph4DController : MonoBehaviour
{
[Range(10, 100), SerializeField] private int _resolution;
[SerializeField] private GameObject _cube;
// 添加新的滑桿
[Range(0, 1), SerializeField] private int _function;
// 一個 delegate 數組用于保存我們接下來使用的兩個函數
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;
// 此處修改調用方法
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;
}
}
于是我們實現了如下的效果~
不過作者在原文中還添加了 Enum 然后可以不用滑桿而是改用一個下拉菜單來改變要顯示的函數圖像。最終效果沒什么不同就不再贅述了感興趣的同學可以自行找到『原文鏈接』查看更詳細的步驟~
那么接下來開始要真正的繪制一個3D曲面了~那么首先是創建更多的小方塊~我們在初始化的地方改成一個二維的 List 來保存所有的小方塊
private void Start()
{
...
for (int i = 0; i < _resolution; i++)
{
_points.Add(new List<Transform>());
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 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;
}
}
}
最后再稍微修改下兩個函數的參數就完成了從 2D 到 3D 的跳躍~如圖所示
不過我們并不應該滿足于此,感覺這樣其實并沒有充分利用 Z 軸啊,完全就是復制了很多條曲線排在一起。所以我們新建兩個這樣的函數。
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
可以很明顯的看出是兩個完全一樣的正弦波分別沿 x 軸和 Z 軸傳播并且直接疊加,那么第二個。。。反正很復雜語言解釋不清楚大概就是 3 個波疊加起來的,大家可以一行一行注釋掉看看效果就知道了~
那么如何畫出一個波紋呢,首先波紋是由原點也就是(0, 0)
點開始均勻擴散的,那么可能是一個從原點向周圍擴散的正弦波。那么直覺上來說這個函數可能長這樣。。
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 除以 1 + 2 * Mathf.PI * d
試一試,之所以加1是為了防止在距離原點過于近的時候結果趨近于無窮大。所以現在代碼變成了這樣~
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;
}
跑起來看一下會發現。。。emmmm
所以我們再加上一些參數比如_velocity
傳播速度,frequency
水波頻率,_amplitude
振幅,_attenuation
衰減。代碼如下。(這些參數并不是數值越大就直觀意義上越大,雖然這樣不太好但是懶得整理了。。。大家大概意思理解就好)
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;
}
然后將這些參數調整到合適的值,就完成一個完美的水波了~如圖所示
顯然我們不能滿足于此,傳入 x 和 z 來計算出唯一的 y 導致了無法有兩個點擁有相同的 x 和 z,這極大的限制了我們的發揮~比如說畫出一個球體之類的。所以我們接下來的目標是畫出真正的三維圖形~
在開始之前,我們首先要放棄傳入 x 和 z 來計算 y 的設想,所以應該把所有的函數的返回值改成 Vector3,并且為了區分我們將函數的參數變成 u,v,t。
public delegate Vector3 Function(float u, float v, float t);
public enum GraphFunctionName {
Sine,
MultiSine,
Sine2D,
MultiSine2D,
Ripple,
}
public class Graph4DController : 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<List<Transform>> _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<List<Transform>>();
_step = 2f / _resolution;
var scale = Vector3.one * _step;
for (int i = 0; i < _resolution; i++)
{
_points.Add(new List<Transform>());
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);
}
}
那么如何組成一個圓柱體呢,首先我們知道圓柱體可以認為是由許多個圓環組成的,那么如何構成一個圓環呢?我們知道 u 的取值范圍是[-1, 1],將 u PI 即可獲得 [-PI, PI] 即剛好一個圓周的弧度,對應的坐標即是`(x = sin(PI u), z = cos(PI * u))`,按照以上思路我們完成以下代碼。然后每一個點的縱座標 y 就直接取 v 的值即可形成「每個水平的圓周上有100個點,共100個圓縱向排列組成的圓柱體」了好吧感覺表述的不是特別清楚寫出來跑跑看就知道了。。。
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);
}
運行一下發現果然是一個圓柱體,如果想要控制圓柱體的半徑和高直接在 x 和 z 乘以 R,y 乘以 H 即可,如下圖所示。代碼就不貼了大家都會自己乘~
那么如何讓這個圓柱體動起來呢~比如說隨便對 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 的系數可以看到很多有趣的現象哦~懶得自己寫的可以打開我的「Github Repo」直接運行時修改 FactorU 和 FactorV 的值查看結果~最終我們可以達到類似這樣的效果
我們在圓柱體的基礎上稍加修改就可以獲得一個球體,首先,球體跟圓柱體一樣也可以認為是很多半徑不同的圓環組成的,那么圓環的半徑呈現怎樣的變化呢,我們想象球體沿經線切開后,可以觀察到一圈緯線的半徑和緯線的縱座標分別對應Cos(PI / 2 * v)
和Sin(PI / 2 * v)
,按照這個思路我們嘗試寫出如下代碼。
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);
}
運行一下發現完全沒有問題~如圖所示。。。
所以想要讓球體動起來我們可以使用同樣地思路對 r 的計算進行一點點魔改,比如說這樣的一個參數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);
...
}
調一些奇怪的參數。。。然后就出現了一坨嚅動的,。。球體。。。
那么想象下一個圓環體和球體到底有什么區別呢,針對每左半條或者右半條經線圈,如果直接變成一個環,那么球體不就變成圓環了么。。。那么怎么變成圓環呢,我們之前提到
一圈緯線的半徑和緯線的縱座標分別對應
Cos(PI / 2 * v)
和`Sin(PI / 2 * v)
所以我們把半個周期的 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);
}
運行一下發現還是球體啊。。這是為什么呢,仔細觀察發現似乎小方塊比以前稀疏了,是因為半條經線被擴展到整個周期以后變成了一整圈經線,所以和對面的那半條完全重疊了。。所以怎么解決這個問題呢?就是擴大緯線圈讓相對的兩個半條經線不會相互重疊甚至完全分離就可以了。所以這樣修改下試試
private Vector3 Torus(float u, float v, float t)
{
var r = _radius * Mathf.Cos(Mathf.PI * v) + _radius2;
...
}
這里之所以是加一個_radius2
在最外面是為了達到「無論 v 如何變化都可以是的半徑無條件增加 _radius2」的效果。。。運行下會發現嗯果然沒問題了。。
所以最后也順便讓它動起來吧。。。
好吧這篇真的好長,而且寫的好累并且在公式功能壞掉的情況下又很難講清楚~大家把「Github Repo」下載下來自己運行稍微修改下就很容易理解了~總之我們把簡單的圖像擴展到了三維的圖形的過程還是很有趣的~雖然不知道暫時有什么用處不過對于培養數學思維也還是挺有幫助的~好吧希望下一篇早日更新~就醬。。。
原文鏈接:https://snatix.com/2018/06/20/021-mathematical-surfaces/
本文由 sNatic 發布于『大喵的新窩』 轉載請保留本申明
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。