我覺得這東西對做遊戲很有用,所以找了一下,最後找到 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]
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
在 Update()內頻繁做重複的費時計算會拖累系統,所以我讓 xOrg 和 yOrg 的計算在有必要時才做,也就是在 inspector 中拖曳 Seed 時才會重算。
code可以在這裡下載。
沒有留言:
張貼留言