ugui unity 图片缩放循环_Unity基础系列(四)——构造分形(递归的实现细节)...
目錄 | 1 如何構建分形 2 展示內容 3 構造子節點 4 塑造子節點 5 創建多個子節點 6 更多的子節點,更好的代碼 7 爆炸性生長 8 添加顏色 9、隨機化Mesh 10 使分形不規則 11 旋轉分形 12 添加更多的不確定 |
本文重點:
1、實例化游戲對象
2、了解遞歸
3、使用協程
4、添加隨機性
分形是一個非常有意思的東西,而且大部分時候都很漂亮。在本教程中,我們將編寫一個小的C#腳本,讓它完成一些類似分形的行為。
這里假設你已經能夠了解一些Unity的基本操作,并且能夠創建基本的C#腳本了。如果這些還不熟悉的話,可以再復習一下第一章 時鐘 相關的內容。
這是一篇比較舊的教程,里面提到的漫反射和鏡面材質可能已經不使用與Unity2017了,所以可以忽略這些,但除此之外,這篇教程所展示的內容還是很有意思的。
(創建隨機的3D分形)
1 如何構建分形
在開始構建3D分形之前,先要理解分形的概念。
簡單的來說就是一個粗糙的幾何物體,可以分為若干部分,每個部分都是(或者近似)該物體縮小后的形狀。可以將其應用到Unity中的對象hierarchy中來實現這個效果。比如從某個根對象開始,然后向其中添加較小但在其他方面相同的子對象。
手動完成該操作將會非常麻煩,因此創建腳本來完成。
創建一個新項目和一個新場景。在里面放了一個方向光,把相機移到一個合適的角度,也可以隨意設置。
繼續創建一個用于分形的材質。材質很簡單,僅僅使用specular 著色器與默認設置即可,比起漫反射,這個看起來更舒服一些。
創建一個新的空游戲對象并將其放置在原點。這將是分形的母體。然后創建一個名為Fractal的新C#腳本,并將其添加到對象上。
(工程創建)
2 展示內容
腳本有了,那么分形是什么樣子的呢?這里通過在 Fractal 組件腳本中添加一個公共的Mesh和材料material
來實現它的可配置性。然后插入一個Start方法,在其中添加一個新的MeshFilter組件和一個新的MeshRenderer組件。同時,直接分配對應的網格和材料給它們。
什么是mesh?
按照傳統理解,mesh是圖形硬件用來繪制復雜東西的結構。它是一個3D對象,要么從外部導入到Unity中,這是Unity的默認形狀之一,要么是由代碼生成。mesh需要包含3D空間中的點集合,以及由這些點定義的一組三角形(最基本的2D形狀)。由三角形構成網格所代表的任何表面。
大部分時候,你不會意識到你看到的其實是一堆三角形。
什么是材質?
材質用來定義物體的視覺特性。它們可以是非常簡單(比如一個恒定的顏色),也可以非常復雜。材質一般要包括一個著色器和任何著色器需要的數據。
著色器基本作用是告訴顯卡如何繪制物體的多邊形。標準漫射著色器使用單一的顏色和可選的紋理,結合場景中的光源,來確定多邊形的外觀。這里使用的是稍微復雜的鏡面著色器,同時模擬了一個亮點。
Start函數什么時候調用組件創建之后,處于active狀態,并且在第一次調用它的Update方法之前(如果它有的話),Start方法會被Unity調用。而且只調用一次。
AddComponent 怎么用?
AddComponent方法可以創建特定類型的新組件,并將其附加到游戲對象,返回對其的引用。這就是為什么我們可以立即訪問組件的值。當然也可以使用中間變量。
MeshFilter Filter=gameObject.AddComponent();
filter.mesh=Mesh;
這里展示了一個特殊的語法。因為它是一個通用方法,實際上是可以處理一系列類型的模板。你可以通過在尖括號中傳入參數它來告訴它應該使用什么類型。
現在可以把我們定制的材質分配給fractal組件了。還可以通過單擊屬性旁邊的點并從彈出窗口中選擇Unity默認的立方體來分配Mesh。弄完之后,進入播放模式時,就會顯示一個立方體了。當然,也可以在代碼里手動添加組件。
(運行時可以看到組件了)
3 構造子節點
該如何為這個分形創作子節點呢?最簡單的方法就是在Start函數里創建一個新的Game Object并向其添加一個Fractal組件,試一下。
new 干了什么事情?
new 關鍵字用于構造對象或結構體的新實例。然后調用一個特殊的構造函數方法,該方法與它所屬的類或結構的名字相同。
現在問題是,每一個新的分形實例都會產生另一個分形實例。每一幀都會發生,無窮無盡,導致死循環。如果不手動關閉,運行一段時間,當它把內存耗盡了之后,你的電腦就會死機了。
但大部分時候,無法停止的遞歸算法幾乎會立即消耗完機器的資源,并導致堆棧溢出異常或崩潰。但在這個示例中,相對來說沒那么快,因為它的遞歸的比較慢。
為了防止這種情況發生,需要引入一個最大深度的概念。最開始的分形實例的深度為零。每個它的后代節點都會有一個深度值。比如它的孫節點會有一個2的深度值,以此類推,直到達到最大的深度。
在inspector 窗口中添加一個公共maxDepth整數變量并將其設置為4。再添加一個私有深度整數。然后,只有當我們在最大深度以下時,才創建一個新的子級。
(最大深度)
現在進入播放模式時會如何呢?
只有一個子節點被創造出來了。這是為什么呢?因為我們從來沒有給 depth 值,它總是零。因為零小于4,我們的根分形對象創建了一個子對象。孩子的深度值也是零。又因為,也沒有設置子節點的maxDepth,所以它也是零。因此,該子節點并沒有創造另一個。
除此之外,子節點也沒有分配材質和Mesh。這些引用可以直接從它的父級復制。現在添加一個處理所有必要初始化的新方法。
this是什么意思?
this此關鍵字引用正在調用其方法的當前對象或結構。在引用同一個類的內容時,它一直被隱式地使用。例如,每當我們訪問深度時,我們也可以通過this.depth來完成。通常只在需要傳遞對對象本身的引用時才需要使用此方法,就像對Initialization所做的那樣。那又是為什么要這樣做呢?因為需要調用的是新的子對象的Initialization方法,而不是父對象的初始化方法。
Initialize 調用是否在 Start 之前?
是的。首先創建新的游戲對象。然后創建并添加一個新的分形組件。此時,如果存在其Awake和OnEnable方法,則將調用它們。然后AddComponent方法完成。在此之后,直接調用Initialization。Start的調用要到下一幀才會執行了。
進入游戲模式,如預期的邏輯,這一次會創建四個子孫代。但它們現在還不是真正的孩子,因為它們都出現在層次根節點中。游戲對象之間的父子關系是由它們的轉換層次來定義的。因此,一個孩子需要使它的transform組件的parent等于它的分形父transform 。
(兩種不同的層次結構)
4 塑造子節點
到目前為止,子節點已經被疊加在父節點上了,這意味著仍然只看到一個立方體。現在需要把他們移動到他們的本地空間中,讓它們也能被看到。
每個子節點都應該比它們的父母小,所以我們也必須縮小它們的Scale值。
第一個要解決的是縮放。那么應該縮放多少呢?用一個名為child Scale的新變量來配置它,并在inspector中給它賦值0.5。別忘了把這個值也從父節點傳給子節點。然后用它來設置子節點的local scale。
接下來,該把這些孩子節點搬到哪里去呢?那就直接向上移動吧,這樣它們就能接觸到它們的父節點。假設父節點在所有方向上的大小的單位是1,對于現在正在使用的立方體來說正好合適。向上移動一半,使父節點和子節點正好接觸在一起。因此,我們還需要移動一個額外的距離,距離相當于子節點的一半大小。
(子節點縮放值為0.5,從0.3至0.7)
5 創建多個子節點
現在我們做出來的東西有點像一座塔,還不是真正的分形,要完成分形還需要將它分支化。每個父節點創建多個子節點比較容易。但它們必須朝著不同的方向發展。因此,需要向Initialization方法中添加一個方向參數,并使用它將第二個子節點定位到右邊而不是上面。
…是什么意思?
這意味著我省略了一段沒有改變的代碼。應該清除或更改代碼的位置,或者它的確切位置并不重要。
(每個父節點擁有2個子節點)
這看起來已經有點感覺了!那么光從結果來看你能知道它是按照什么順序來建造的嗎?因為它們都是在幾幀之內創建的,速度太快,無法看到它的創建的過程。如果能放慢這個過程應該會很有意思,因為這樣就能看到它的發生的過程。要如何去完成放慢的過程呢?答案是可以通過協同線創建子節點來實現。
協程可以看做是可以插入暫停語句的方法。當方法調用暫停時,程序的其余部分繼續進行。雖然這個類比不太恰當,太過于簡單化,但我們現在只需要利用這個特點就可以了。
將創建兩個子節點的代碼行移動到一個名為CreateChildren的新方法中。此方法需要將IEnumerator作為返回類型,該類型存在于System.Collection命名空間中。這就是為什么Unity在他們默認的腳本模板中包含它,以及為什么本示例在一開始也包括它的原因。
改變了方法類型之后,調用的方式也要調整,這里不能再用直接調用的方式了,取而代之,要使用Unity的StartCoroutine方法。
然后在創建每個子節點之前添加一個暫停指令。如代碼所示,每半秒鐘內創建一個新的WaitForSecond對象,然后將其返回給Unity。
enumerator是什么?
枚舉是一次遍歷某個集合的概念,就像循環遍歷數組中的所有元素一樣。enumerator(枚舉器)或iterator(迭代器)是為此功能提供接口的對象。System.Collections.IEnumerator描述了這樣的接口。
為什么我們需要用這個呢?因為協程需要用。這也是Unity在默認腳本模板中包含System.Collection的原因,也是本示例將它包括在內的原因。
return 做了什么?
return關鍵字可以表示一個方法中斷或者已經完成,把響應的結果返回給調用者。返回的內容必須與方法的類型匹配。如果它是一個空方法,那么也只需要返回空。
對于一個函數定義為空,可以省略return關鍵。
同樣的,一個方法中可能有多個return語句。在這種情況下,有多個可能的返回點。通常使用if語句來確定使用了哪些return。
yield有什么用?
yield語句被迭代器用來控制協程的生命周期。要使枚舉,就需要跟蹤它的進度。這涉及到一些基本相同的樣模板碼。你真正想要的是只編寫類似于 return firstItem; return secondItem這樣的代碼,直到函數執行結束。yield語句允許你準確地做到這一點。
因此,無論何時使用yield,都會在幕后創建枚舉器對象,以處理繁瑣的部分。這就是為什么我們的CreateChildren方法將IEnumerator作為其返回類型的原因。
順便說一下,你還可以生成另一個迭代器。在這個示例里,另一個迭代器會被完全的處理,所以你其實可以用創造性的方式將它們縫合在一起。
協程怎么工作?
當你在Unity中創建協程時,真正做的其是創建一個迭代器。當你將它傳遞給StartCooutine方法時,它將被存儲,并被要求每幀都要它的下一個Item,直到它完成為止。
yield語句會產生Item。而這中間的部分就是你可以發揮的地方了。
當你自己的代碼繼續運行時,你也可以產生一些特殊的協程,比如WaitForSecond,這樣就可以更好地控制代碼邏輯,但是總的來說都是一個迭代器而已。
現在可以看著它生長了!你能看出來這樣做有什么問題嗎?可能現在還不明顯,現在為每個父節點添加第三個子節點,這一次放在左邊。
(每個父節點3個子節點,正常和overdraw視角)
如果查看overdraw效果?
場景視圖的工具欄有一個下拉列表,默認設置為RGB。它的另一個選擇是 Overdraw 。
其實問題是子節點和他們的父節點有著相同的參考點。這意味著,其父母本身就是右子節點的左子節點。可能有點繞,就是說,父節點和子節點在某些方向上重合了。
為了解決這個問題,需要對子節點進行旋轉,這樣他們的向上方向就會遠離他們的父節點。
我通過向Initialization添加一個方向參數來解決這個問題。它將是一個四元數,用于設置新子節點的local rotation。向上的子節點不需要旋轉,右邊的子節點需要順時針旋轉90度,左邊的子節點需要向相反的方向旋轉。
(旋轉后的效果)
現在子節點已經被旋轉了,但它們生成出來的卻不是分形了。一些最小的子節點最終仍然會消失在根立方體里面。這是因為如果Scale因子為0.5,這個分形將在四個步驟中產生了自相交。你可以通過減少縮放來解決這個問題,也可以使用球體代替立方體。
(子節點縮放為0.5的球體并沒有產生自相交)
6 更多的子節點,更好的代碼
現在的代碼已經有些笨重了。可以通過將方向和方位數據移動到靜態數組來優化。然后,再將CreateChildren簡化為一個短循環,并使用子索引作為Initialization的參數。
數組如何工作?
數組是長度固定的對象,包含一個線性變量序列。在聲明變量時,將方括號放在其類型后面表示需要該類型的數組。所以int myVariable;讓你獲得一個整數,而int[]myVariable;讓你獲得一個整數數組。
訪問數組中的一個條目的方法是將數組索引(而不是位置)放在變量后面的方括號中。MyVariable[0]獲取數組中的第一個條目,myVariable[1]獲取第二個條目,依此類推。
實際上,創建一個數組并將其賦值給變量是使用myVariable=newint[10]完成的;在本例中,該數組創建了一個包含10個條目空間的新數組。或者,您可以通過在花括號中列出它的初始值來隱式地創建一個,比如myVariable={1,2,3};。
for循環怎么工作?
for循環是編寫遍歷某些循環的一種緊湊方式。在本例中,我們使用一個名為i的整數作為迭代器。第一部分聲明迭代器整數,第二部分檢查循環的條件,第三部分增加迭代器。您可以使用while循環來獲得完全相同的結果,但是迭代器代碼不方便分組。
對于(int i=0;i<10;i++){doStuff(I);}
與int i=0;
while(i<10){doStuff(I);i++}效果相同。
順便說一句,i++是i+=1的縮寫,它是i=i+1的縮寫。
現在,讓我們通過簡單地將數據添加到數組中,再引入兩個子元素。一個向前,另一個向后。
(完整的分形,每個父節點擁有5個子節點)
現在有了完整的分形結構。但是根立方體的底部為什么沒有呢?可以這樣想,分形是從某種東西中生長出來的,比如一種植物。雖然我沒有,但如果你想的話,可以添加一個特殊的第六個子節點向下,但只是添加到根節點就好。添加到所有子節點的話又會變成第6個子分形了。
7 爆炸性生長
剛才的示例,我們實際創建了多少個立方體?因為我們總是為每個父節點創建五個子節點,當完全成長的時候,立方體的總數將取決于最大的深度。最大深度為零只產生一個立方體,即初始的根節點。最大深度為一個,產生五個額外的孩子,總共有六個立方體。由于它是分形的,這個圖案重復,我們可以把它寫成函數f(0)=1,f(N)=5×f(n-1)+1。
上述函數產生序列1、6、31、156、781、3906、19531、97656等。你將看到這些數字顯示為Unity游戲視圖中統計數據中的DrawCall的數量。如果啟用了動態批處理,則它將是DrawCall 和 Saved by batching 的總和。
Unity處理四五層的深度還綽綽有余。再高的話,你的幀率將急速下降。
除了數量,持續時間也是一個問題。現在,我們在創建一個新的子節點之前暫停了半秒鐘。這會產生幾秒鐘的同步增長。我們可以通過隨機延遲來更均勻地分配增長。這也導致了一個更不可預測和有機的模式,讓觀察更有意思。
把固定的延遲替換為0.1到0.5之間的隨機范圍。我還增加了最大深度到5,使效果更加明顯。
隨機范圍是如何工作的?
Random是一個實用工具類,它包含一些接口來創建隨機值。它的 Range 方法可用于在一定范圍內生成隨機值。Range方法有兩個版本。可以使用兩個浮點數來調用它,在這種情況下,它會在最小值和最大值之間返回一個浮點數,這兩者都包括在內。或者,可以用兩個整數調用Range,在這種情況下,它返回一個整數,介于最小、排除最大值之間的某個值。這個版本的典型用例是隨機選擇一個索引,比如某某數組[Random.Range(0,omeArray.Length)]。
8 添加顏色
這個分形沒有什么生氣。通過添加一些顏色變化來搞點氣氛。通過從根部的白色插入到最小的子節點的黃色來實現吧。Color.Lerp 接口是一種方便的方式。內插器從0到1,我們通過將當前深度除以最大深度來實現。因為這里不能用整數除法,所以我們首先將深度轉換為浮點數。
Lerp是干什么的?
LERP是線性插值的簡稱。它的典型特征是Lerp(a,b,t),它計算a+(b-a)*t,t在0-1范圍內。有不同類型存在多個版本,包括浮點數、向量和顏色。
(上色了,但是沒有動態批處理)
這看起來有內味了!但另一件事也發生了。動態批處理過去是起作用的,但現在不行了。我們該如何解決這個問題呢?
什么是動態批處理?
動態批處理是由Unity執行的一種drawcall批處理形式。簡而言之,它將共享相同材料的網格組合成更大的網格。這樣做減少了CPU和GPU之間的通信量。
你可以通過 Edit/Projects Settings/Player/, 在 Other Settings 啟用或禁用它。
它只適用于小網格。比如,你會發現它適用于Unity默認的立方體,但不適用于默認的球面。
導致這個結果的問題是,因為調整子節點的材質顏色,Unity默默地創造了一個復制的材質。這其實是必要的,不然一切使用該材質的都將以相同的顏色結束繪制。然而,批處理只有在相同的材質被用于多個物體時才有效。不相等的不檢查也不合并--因為要檢查的話就太耗性能了,而且結果也不一定就滿足合批條件--所以它必須是同一種材質。
那在每個深度都創建一個材質的副本,而不是每個立方體。添加一個新的數組字段來保存材質。然后Start時檢查是否存在數組,如果沒有,則調用一個新的InitializeMaterials方法。在這種方法中,我們將顯式復制我們的材料和改變每一深度的顏色。
null是什么?
非簡單值的變量的默認值為NULL。這意味著變量沒有引用任何內容。試圖從變量中調用或訪問任何為NULL的內容都會導致錯誤。你需要判斷這個值,以確保不會發生這種情況。
你也可以自己將這樣的變量設置為NULL,以便處理你不再需要它所引用的任何內容。注意,當將對對象的引用設置為NULL時,對象并不會自動被銷毀。只有當所有地方都不引用他們的時候,他們才會成為垃圾收集器收集。
還請注意,此方法適用于私有組件字段,但不適用于公共組件字段。這是因為Unity的序列化系統會為它創建一個空數組,而本例中它不會是空數組。
現在,不要將材料引用從父節點傳遞到子節點,而是只傳遞材料數組的引用。如果不這么做的話,每個子節點將被迫創造自己的材料數組,我們就不能解決問題了。
為什么不把 materials 設置為靜態?
之所以不把materials數組設置為靜態,是因為它取決于最大深度,這可能不同于分形和分形之間。同一時間你可以有多個分形但他們可以有不同的最大深度。
(上色了 并且有了動態批處理)
批次合并又回來了,但是已經和之前的不一樣了。但顏色還是沒那么豐富。一個很好的調整是給最深的層次一個完全不同的顏色。這可以揭示分形的模式,可能你這樣也沒注意到吧。
簡單地改變最后的顏色到洋紅之后。此外,調整內插器,使我們仍然看到完全過渡到黃色。當我們在做它的時候,它的平方會帶來一個稍微好一些的轉變。
(有洋紅色的提示了)
再添加第二個顏色級數,例如從白色到青色的紅色提示。我們將使用一個單一的二維數組來容納它們,然后在需要材質時隨機選擇一個。這樣,當我們進入游戲模式時,我們的分形看起來就會有所不同。如果愿意,可以隨意添加第三步。
(隨機顏色)
9、隨機化Mesh
除了顏色,我們還可以隨機選擇使用哪個Mesh。用數組替換公共網格變量,并從其中隨機選擇一個。
如果要在檢查器中的新數組屬性中只放置一個立方體,那么結果將和以前一樣。但是如果加上一個球體,你就會突然得到50%的幾率,形成一個立方體,或者每個分形元素中的一個球體。
隨意填充此數組。我把球體放了兩次,所以它被使用的可能性是立方體的兩倍。你也可以添加其他Mesh,膠囊和圓柱體不太好,因為它們是拉長的。
(隨機選擇立方體和球體)
10 使分形不規則
現在的分形完成的很好,很完整,但是可以通過切斷它的一些分支來使它更加有獨特。通過引入一個新的公共spawnProbability變量來實現。傳遞這個值,然后用它隨機地決定我們是產生一個子節點還是跳過。0的概率意味著根本沒有孩子會生長,而1的概率意味著所有的孩子都會產卵。即使數值略低于一個,也會大大改變我們分形的形狀。
靜態Random.value屬性在0到1之間產生一個隨機值。將它與 spawnProbability 相比較可以告訴我們是否應該創建一個新的子節點。
(70%概率產生的分形效果)
11 旋轉分形
一我們的分形一直是個好孩子,一動不動。但是如果有一點動作是不是會更有趣。添加一個非常簡單的Update方法,它以每秒30度的速度圍繞當前的Y軸旋轉。
有了這個簡單的方法,所有的分形部分現在都在快樂地旋轉。都是以同樣的速度。那么再次隨機化!并使最大速度也可配置。
注意,我們必須在start(而不是Initialization)中初始化我們的旋轉速度,因為根元素也應該旋轉。
(配置速度)
12 添加更多的不確定
我們還能做更多的調整,以微妙的方式打破分形嗎?當然!有很多!其中之一是通過增加一個微妙的旋轉來破壞分形元素的排列。我們稱之為扭曲。
(看起來不錯的扭曲)
另一種選擇是把子節點的比例弄得亂七八糟。或者有時跳過深度。擺造型?那就自己來嘗試下吧!
總結
以上是生活随笔為你收集整理的ugui unity 图片缩放循环_Unity基础系列(四)——构造分形(递归的实现细节)...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python urllib3离线安装_全
- 下一篇: nessus导出报告格式有哪些_高分高能