2016年5月25日 星期三

Shader.Find() 初學易犯錯

Unity 的外觀由 materials 構成。我們看到的物體上的塗色,材質,和各式光影效果,都是 material 造成的。而每個 material 的底層,實際上是個 shader。可以在 Inspector 內換掉:




要在 script  換掉 shader,就用 Shader.Find():

someGameObject.GetComponent<Renderer>().material = new Material(Shader.Find("MyShader"));

但我之前遇過一個怪現象,在PC上測試時,上面的 code 可以跑,但編譯成 android 後不行。原來 Unity 有段話我沒注意到(http://docs.unity3d.com/ScriptReference/Shader.Find.html):

Note that a shader might be not included into the player build if nothing
references it! In that case, Shader.Find will work only in the editor,
and will result in pink "missing shader" materials in the player build.
Because of that, it is advisable to use shader references instead of
finding them by name. To make sure a shader is included into the game
build, do either of: 
1) reference it from some of the materials used in
your scene, 2) add it under "Always Included Shaders" list in
ProjectSettings/Graphics or 3) put shader or something that references it
(e.g. a Material) into a "Resources" folder.  

也就是在 Unity Editor 中可以用的 Shaders,不是每個都會自動放進 player build。解決辦法有三種。
第一種比較直覺,在 scene 中有使用的話,就會自動納入 player build。但要是物件是透過 script 產生的,這方法行不通。

第二種是透過 Edit->Project Settings -> Graphics 加入:



第三種方法,先把要用的 shaders 放在 Resources 目錄(Assets\Resources),script 再去載入,
Shader myShader = Resources.Load ("MyShader") as Shader;
someGameObject.GetComponent<Renderer>().material = new Material(myShader);







2016年5月24日 星期二

C# script template in Unity

新建 C# script 時,Unity 會很貼心幫你補上Update()、Start()、註解:

 1 using UnityEngine;
 2 using System.Collections;
 3 
 4 public class MainPlayer : MonoBehaviour {
 5 
 6     // Use this for initialization
 7     void Start () {
 8 
 9     }
10 
11     // Update is called once per frame
12     void Update () {
13 
14     }
15 }


剛開始很方便,但不必超過5次,你就會覺得很煩,因為你每次都要砍掉註解,或是根本不需要 Start()/Update()。有這種困擾的話,可以去改一下 template:

$(UNITY_FOLDER)\Editor\Data\Resources\ScriptTemplates\81-C# Script-NewBehaviourScript.cs.txt

我自己有在用 doxygen, 加上懶得一一補上 import,所以常用的 libs 都先寫了,看起來像這樣:
 1 namespace ThisGame
 2 {
 3   using UnityEngine;
 4   using UnityEngine.UI;
 5   using System.Collections;
 6   using System.Collections.Generic;
 7   using System;
 8   using Random = UnityEngine.Random;
 9 
10   using UnityEngine.Events;
11   using UnityEngine.EventSystems;
12 
13   /**
14    * @brief #SCRIPTNAME#
15    *
16    *
17    */
18   public class #SCRIPTNAME# : MonoBehaviour
19   {
20 
21   }
22 }


2016年5月21日 星期六

2D ellipse mesh in Unity

這篇文章講的是在 Unity 中以 script 建立 2D 橢圓。


Project view 中是有一個 Circle Sprite,

但那是一個 sprite,實際上仍是矩形,裡面放一個圓形的圖案而已,不是我要的。

一個真正的 2D ellipse mesh 它的外圍形狀是橢圓的,我們在實做時,把多個頂點繞著圓心一圈組成。示意圖配合公式,像是這樣:

只要在code裡面產生很多三角形(紅色那些三角形),繞一圈,就可以組成一個橢圓。


Unity 裡面,每個物體都會由多個 Mesh 組成,而每個 Mesh 再由多個三角形組成。每個三角形有三個頂點,每個頂點上有一些屬性,像是座標、UV、法線向量...。不過這篇文章只示範產生座標的方法,而法線讓 unity 自動計算,至於 UV 這個用來配置材質的東西,以後再說。


在兜橢圓時,三角形的數量越多,看起來就會越圓。底下這個是5個三角形組成的橢圓:

是不像橢圓,但比較好解釋,因為上面的藍色線的方向是有意義的。Unity 中的三角形,它的三個頂點在連起來時,順序必須是順時針方向,否則看不到。如果你畫了三角形卻看不到,很可能Camera 照到它的背面了,可以去三角形的背面看看。


可動的code:
 1 using UnityEngine;
 2 using UnityEngine.UI;
 3 using System.Collections;
 4 using System.Collections.Generic;
 5 using System;
 6 using Random = UnityEngine.Random;
 7 
 8 using UnityEngine.Events;
 9 using UnityEngine.EventSystems;
10 
11 
12 public class test : MonoBehaviour
13 {
14   void Start ()
15   {
16     Ellipse2D ellipse1 = new Ellipse2D (0, 0, 0, 1, 1);
17     Ellipse2D ellipse2 = new Ellipse2D (4, 0, 0, 2, 3);
18     Ellipse2D ellipse3 = new Ellipse2D (2, 5, 0, 3, 2);
19   }
20 }
21 
22 
23 public class Ellipse2D
24 {
25   public GameObject gameObject;
26 
27   public Ellipse2D (
28     float   x,
29     float   y,
30     float   z,
31     float   semiMajor,
32     float   semiMinor
33   )
34   {
35     gameObject = new GameObject ("Ellipse2D");
36 
37     Vector3 center = new Vector3 (x, y, z);
38     gameObject.transform.localPosition = center;
39 
40     Renderer renderer = gameObject.AddComponent<MeshRenderer> ();
41     renderer.material = new Material (Shader.Find ("Diffuse"));
42 
43     // 建立 mesh. 一個 mesh 上可以有多個三角形
44     Mesh mesh = new Mesh ();
45 
46     // 定義每個 ellipse 上由 30 個三角形構成
47     int triangleCount = 30; // 10
48 
49     float dTh = (2 * Mathf.PI / triangleCount);
50 
51     // 存放頂點
52     Vector3[] vertices = new Vector3[triangleCount * 3];
53 
54     // 每個 for index 會計算一個三角形中的三個頂點座標
55     for (int index = 0; index < triangleCount; index++) {
56 
57       // triangle vertex 1
58       vertices [index * 3] = new Vector3 (0, 0, 0);
59 
60       // triangle vertex 2
61       float th1 = dTh * (index + 1);
62       vertices [index * 3 + 1] = new Vector3 (semiMajor * Mathf.Cos (th1), semiMinor * Mathf.Sin (th1), 0);
63 
64       // triangle vertex 3
65       float th2 = dTh * (index);
66       vertices [index * 3 + 2] = new Vector3 (semiMajor * Mathf.Cos (th2), semiMinor * Mathf.Sin (th2), 0);
67     }
68 
69     // 蒐集所有頂點給 mesh
70     mesh.vertices = vertices;
71 
72     // 指定三角形的繞法
73     int[] triangles = new int[triangleCount * 3];
74     for (int i = 0; i < triangleCount * 3; i++) {
75       triangles [i] = i;
76     }
77 
78     mesh.triangles = triangles;
79 
80     // 自動計算法線
81     mesh.RecalculateNormals ();
82     mesh.RecalculateBounds ();
83     mesh.Optimize ();
84 
85     // 最後對物件添上 MeshFilter component, 指定 mesh 給他。
86     MeshFilter mf = gameObject.AddComponent<MeshFilter> ();
87     mf.mesh = mesh;
88   }
89 }
90 

圖看起來像這樣:




Reference:
http://docs.unity3d.com/ScriptReference/Mesh.html












2016年5月19日 星期四

Perlin Noise in Unity3D 01 - basic use

有玩過 Minecraft 的人,會注意到,它的地圖產生起點是透過一個 seed 來的。每張 Minecraft 地圖,都有無盡的延伸,一個地圖的 seed 會產生固定的地圖,不同的 seed 也會產生不同的地圖。這就是說,一個簡單的 seed,就能把無盡的複雜度記錄起來。利用這個特性,網路上有很多 Minecraft map 的分享。你只要給別人seed (字串或數字),他們就能在自己的 Minecraft 裡重現一樣的地圖。

我覺得這東西對做遊戲很有用,所以找了一下,最後找到 Perlin Noise。Perlin Noise 背後的原理和實作可以見 Hugo Elias 的 "Perlin Noise" 文章。但如果你用 Unity,那它裡面就內建  Perlin Noise 了,叫 Mathf.PerlinNoise()

底下的內容簡介 Unity 的 PerlinNoise() 的基本用法。

首先測試一下來自官網上的範例:

 1 namespace TestPerlinNoise
 2 {
 3   using UnityEngine;
 4   using UnityEngine.UI;
 5   using System.Collections;
 6   using System.Collections.Generic;
 7   using System;
 8   using Random = UnityEngine.Random;
 9 
10   using UnityEngine.Events;
11   using UnityEngine.EventSystems;
12 
13   public class Test_Official_Sample_01 : MonoBehaviour
14   {
15     public int pixWidth;
16     public int pixHeight;
17     public float xOrg;
18     public float yOrg;
19     public float scale = 1.0F;
20     private Texture2D noiseTex;
21     private Color[] pix;
22     private Renderer rend;
23 
24     void Start ()
25     {
26       rend = GetComponent<Renderer> ();
27       noiseTex = new Texture2D (pixWidth, pixHeight);
28       pix = new Color[noiseTex.width * noiseTex.height];
29       rend.material.mainTexture = noiseTex;
30     }
31 
32     void CalcNoise ()
33     {
34       float y = 0.0F;
35       while (y < noiseTex.height) {
36         float x = 0.0F;
37         while (x < noiseTex.width) {
38           float xCoord = xOrg + (x / noiseTex.width) * scale;
39           float yCoord = yOrg + (y / noiseTex.height) * scale;
40           float sample = Mathf.PerlinNoise (xCoord, yCoord);
41           pix [(int)(y * noiseTex.width + x)] = new Color (sample, sample, sample);
42           x++;
43         }
44         y++;
45       }
46       noiseTex.SetPixels (pix);
47       noiseTex.Apply ();
48     }
49 
50     void Update ()
51     {
52       CalcNoise ();
53     }
54   }
55 }
56 
注意,如果照貼官網上的 code,會報錯誤,說 float 不能用在 int,實際上就是少一個 (int) 轉型,上面已在 line 41 中修正。

這段code會去撈 Renderer component,所以只要是看得到的任何物件都能套上來用。最簡單就是在 scene 裡建一個 Plane (3D Object->Plane):


再把這個 script 拖給它,成為 script component。不過在進入 Play Mode 前,先去 inspector 中調整 2 個參數,這樣 code 裡的 Texture 才有寬和高:


進入 Play Mode。圖看起來類似這樣:



有明有暗,但不確定看到什麼。再試幾種不同的 scale 看看:


scale = 1.0



scale = 2.0



scale = 5.0


很像在上空拍到的土地,有高低起伏。可以看出 Perlin Noise 的效果了。

回頭仔細看 Mathf.PerlinNoise() 這個 API:
public static float PerlinNoise(float x, float y);

我們往裡面填 x, y, 它傳回介於 0 和 1 之間的值。傳回值比較好理解,拿到後,看要放大幾倍就乘上多少。x, y  這兩個輸入值就比較不直覺了,我們知道那是採樣座標,不過該填多少?

同樣是拿剛剛的例子當說明:
35       while (y < noiseTex.height) {
36         float x = 0.0F;
37         while (x < noiseTex.width) {
38           float xCoord = xOrg + (x / noiseTex.width) * scale;
39           float yCoord = yOrg + (y / noiseTex.height) * scale;
40           float sample = Mathf.PerlinNoise (xCoord, yCoord);
41           pix [(int)(y * noiseTex.width + x)] = new Color (sample, sample, sample);
42           x++;
43         }
44         y++;
45       }

以 xOrg = yOrg = 0,  width = height = 100, scale = 2 來說,while loop 內的兩個輸入值的範圍是這樣的:
xCoord = [0, 2.0]
yCoord = [0, 2.0]

把 scale 換成 5,兩個輸入值的範圍:
xCoord = [0, 5.0]
yCoord = [0, 5.0]

再比對圖,可以這樣想像:如果 (x, y) 的輸入值的範圍大一些(0~5),就是視野較大;值的範圍小(0~2),就是視野較小。有了這個概念,比較能掌握該輸入的範圍。譬如,假設我們要模擬衛星拍攝山嶺的照片,就不可能挑 [0, 1] 當輸入範圍,而是要挑類似 [0, 10] 當輸入範圍。


範例中還有一組 (xOrg, yOrg) 參數:

這相當於空拍時的位移。可以拿它當做 map 的 seed,因為不同的 (xOrg, yOrg),看起來的地圖就不同。
但要注意,餵給 Mathf.PerlinNoise() 的值不能太大,否則會有奇怪的問題,像把 (xOrg, yOrg)  設定成 (0, 2000000),就會開始出現精度不足的問題了:

也就是說 PerlinNoise() 內的參數,不只起終點範圍要挑對,就連大小都要留意。我們不能隨便給個類似 seed = 4535838512378634 這樣的大數字就丟進去給 PerlinNoise() 裡的 x 或 y 用,而是要轉換,轉成PerlinNoise() 可接受的合理範圍,譬如 50000.0 之類的。

我試了一下,湊出一組簡易的公式:
xOrg = (seed * 15013 + 4585787) % 50000;
yOrg = (seed * 8627 + 196051) % 50000;

有取餘數,無論如何一定不會超過 50000 了。

修改過後的code:
 1 namespace TestPerlinNoise
 2 {
 3   using UnityEngine;
 4   using UnityEngine.UI;
 5   using System.Collections;
 6   using System.Collections.Generic;
 7   using System;
 8   using Random = UnityEngine.Random;
 9 
10   using UnityEngine.Events;
11   using UnityEngine.EventSystems;
12 
13   public class Test_by_seed: MonoBehaviour
14   {
15     public int pixWidth;
16     public int pixHeight;
17     public float scale = 1.0F;
18     private Texture2D noiseTex;
19     private Color[] pix;
20     private Renderer rend;
21 
22     public long seed = 0;
23     private long lastSeed = 1;
24     private float lastXOrg;
25     private float lastYOrg;
26 
27     void Start ()
28     {
29       rend = GetComponent<Renderer> ();
30       noiseTex = new Texture2D (pixWidth, pixHeight);
31       pix = new Color[noiseTex.width * noiseTex.height];
32       rend.material.mainTexture = noiseTex;
33     }
34 
35     void CalcNoise (long seed )
36     {
37       float xOrg;
38       float yOrg;
39 
40       if (seed != lastSeed) {
41         xOrg = (seed * 15013 + 4585787) % 50000;
42         yOrg = (seed * 8627 + 196051) % 50000;
43       } else {
44         xOrg = lastXOrg;
45         yOrg = lastYOrg;
46       }
47       lastXOrg = xOrg;
48       lastYOrg = yOrg;
49       lastSeed = seed;
50 
51       float y = 0.0F;
52       while (y < noiseTex.height) {
53         float x = 0.0F;
54         while (x < noiseTex.width) {
55           float xCoord = xOrg + (x / noiseTex.width) * scale;
56           float yCoord = yOrg + (y / noiseTex.height) * scale;
57           float sample = Mathf.PerlinNoise (xCoord, yCoord);
58           pix [(int)(y * noiseTex.width + x)] = new Color (sample, sample, sample);
59           x++;
60         }
61         y++;
62       }
63       noiseTex.SetPixels (pix);
64       noiseTex.Apply ();
65     }
66 
67     void Update ()
68     {
69       CalcNoise (seed);
70     }
71   }
72 }
73 
在 inspector 上,會看到 xOrg 和 yOrg 合併成一個值叫 seed:


在 Update()內頻繁做重複的費時計算會拖累系統,所以我讓 xOrg 和 yOrg 的計算在有必要時才做,也就是在 inspector 中拖曳 Seed 時才會重算。

code可以在這裡下載。