Preface
為了得到更好的紋理,很多人采用各種形式的柏林噪聲(該命名來自於發明人 Ken Perlin)
柏林噪聲是一種比較模糊的白噪聲的東西:(引用書中一張圖)
柏林噪聲是用來生成一些看似雜亂無章其實有些變換規律的圖形(更加貼近自然),比如海水、地形、霧等
當然這里面的原理涉及分形幾何等相關的知識
例如,2D柏林噪聲可以生成
以及一些網上的總結:
還有一些其他的圖
是不是看起來自然多了
那么今天,我們就來領略一下隨機技術帶來的自然之美~
Chapter 4:Perlin Noise
柏林噪聲有2個關鍵的特點:
第一,輸入相同的3D點,總能返回相同的隨機值
第二,簡單快捷,使用一些hack的方法,達到快速近似的效果。
關於隨機數:
許多人在他們的程序中使用“隨機數產生器”,以使得物體的運動行為更加自然,或者用來生成紋理。隨機數產生器在一些情況下很有用,比如用在模擬自然物體的地方,如地形,海水等。
自然物體通常是分形的,有各種各樣的層次細節,比如山的輪廓,通過高度區分就有高山(mountain,高度變化大)、山丘(hill,高度變化適中)、巨石(高度變化小) 、石頭(高度變化很小)等。另外,比如草地、海浪、跑動的螞蟻、搖晃的樹枝、風、大理石的花紋等等,這些都呈現出了或大或小的細節變化。Perlin噪聲函數通過噪聲函數來模擬這些自然景觀。
要構造一個Perlin函數,首先需要一個噪聲函數和一個插值函數。
我們第一步當然是構建一個Perlin的類
class Perlin { public: inline rtvar noise(const rtvec& p)const; inline static rtvar* randomvalue() { return _randomvalue; } inline static int* perm_x() { return _perm_x; } inline static int* perm_y() { return _perm_y; } inline static int* perm_z() { return _perm_z; } public: static rtvar* perlin_generate(); static int* perlin_generate_perm(); static void permute(int* p, int n);
private: static rtvar* _randomvalue; static int* _perm_x; static int* _perm_y; static int* _perm_z; };
我們來介紹一下,第一個public包含的是和該類相關的成員函數
第二個public是我們的隨機數生成函數,它們按理說應該和此類無關,但是放在類外,擔心污染命名空間,所以暫時列為靜態函數成員,畢竟它們和Perlin類有很大關系,最后的時候,再把所有的功能性全局函數封裝到3D泛型庫里面
類數據成員:分別是Perlin隨機函數生成的隨機序列以及三個方向的輔助隨機分量序列
我們如下設置這三個隨機函數
rtvar * Perlin::perlin_generate() { rtvar* p = new rtvar[256]; for (int i = 0; i < 256; ++i) p[i] = lvgm::rand01(); return p; } int* Perlin::perlin_generate_perm() { int * p = new int[256]; for (int i = 0; i < 256; ++i) p[i] = i; permute(p, 256); return p; } void Perlin::permute(int * p, int n) { for (int i = n - 1; i > 0; --i) { int target = int(lvgm::rand01() * (i + 1)); stds swap(p[i], p[target]); } }
然后用它們初始化靜態數據成員
rtvar* Perlin::_randomvalue = Perlin::perlin_generate(); int* Perlin::_perm_x = Perlin::perlin_generate_perm(); int* Perlin::_perm_y = Perlin::perlin_generate_perm(); int* Perlin::_perm_z = Perlin::perlin_generate_perm();
其中,總隨機序列由第一種方法生成,序列中的每一個元素均為0~1的隨機數
分量的隨機序列由第二種方法生成,即,初始序列為1-255,之后遍歷整個序列,當前位置和一個隨機生成的位置進行交換,已達到序列隨機化
隨機函數講完了,我們來看一下產生噪聲值的函數
u,v,w是插值時候用的,目前暫時不用
參數p為空間某點的位置(未經歸一化或單位化)
上面的函數也很好懂,就不細說了
我們暫時先不管插值函數,我們先用這個試一下效果
class noise_texture :public texture { public: noise_texture() { } virtual rtvec value(rtvar u, rtvar v, const rtvec& p)const override; private: Perlin _noise; }; rtvec noise_texture::value(rtvar u, rtvar v, const rtvec& p)const { return rtvec(1, 1, 1) * _noise.noise(p); }
就是把原來的噪聲值騰個地方,轉個手,沒什么變化
然后主函數中
相機參數依然是:(以后默認是這個)
得到的效果是這樣的:
其實還有個中間產品,之前把noise中的最后一行寫成了
return _randomvalue[_perm_x[i] ^ _perm_y[i] ^ _perm_z[i]];
結果得到了下圖(未做到完全隨機)
感覺這個手誤形成圖也挺好看的
第一個圖片看起來有點生硬粗糙,不是很光滑,所以,我們采用線性插值光滑一下
rtvar Perlin::trilinear_interp(rtvar c[2][2][2], rtvar u, rtvar v, rtvar w) { rtvar accumulate = 0; for (int i = 0; i < 2; ++i) for (int j = 0; j < 2; ++j) for (int k = 0; k < 2; ++k) accumulate += (i * u + (1 - i)*(1 - u))* (j * v + (1 - j)*(1 - v))* (k * w + (1 - k)*(1 - w))* c[i][j][k]; return accumulate; }
我們把插值函數加入到noise函數中,當然你也可以嘗試其他的插值函數
inline rtvar Perlin::noise(const rtvec& p)const { int i = floor(p.x()); int j = floor(p.y()); int k = floor(p.z()); rtvar u = p.x() - i; rtvar v = p.y() - j; rtvar w = p.z() - k; rtvar list[2][2][2]; for (int a = 0; a < 2; ++a) for (int b = 0; b < 2; ++b) for (int c = 0; c < 2; ++c) list[a][b][c] = _randomvalue[_perm_x[(i + a) & 255] ^ _perm_y[(j + b) & 255] ^ _perm_z[(k + c) & 255]]; return trilinear_interp(list, u, v, w); }
我們同時采用了隨機生成函數和插值函數
我們還可以再嘗試一下利用Hermit Cubic來進行舍入插值
它的頻率依舊有點低,我們可以對參數施加一定的縮放比例,加速它的變化
也就是圖像中的顏色更迭的太慢(I think)
class noise_texture :public texture { public: noise_texture() { } noise_texture(const rtvar scale); virtual rtvec value(rtvar u, rtvar v, const rtvec& p)const override; private: Perlin _noise; rtvar _scale; }; noise_texture::noise_texture(const rtvar scale) :_scale(scale) { } rtvec noise_texture::value(rtvar u, rtvar v, const rtvec& p)const { return rtvec(1, 1, 1) * _noise.noise(_scale * p); }
下面是scale 為 15 的圖像
看起來密集多了,顏色變換頻率也快了
下面是scale 為 1.5 的圖像
顯然,上面的圖像格點還是很明晰的,可能是因為最小值和最大值總是精確地落在整數x / y / z上。 Ken Perlin采用了另一種技巧,將隨機單位向量(而不僅僅是浮點數)放在格點上,並使用點積來移動格子的最小值和最大值。
所以,首先我們需要將隨機浮點數更改為隨機向量,試一試新的方法
下面是書上的代碼,你運行之后打不開圖像文件,因為里面是錯的,我們邊看邊數說哪里錯了
我們需要把數據成員_randomvalue改為static rtvec*
所以初始化語句也要改
rtvec * Perlin::_randomvalue = Perlin::perlin_generate(); int * Perlin::_perm_x = Perlin::perlin_generate_perm(); int * Perlin::_perm_y = Perlin::perlin_generate_perm(); int * Perlin::_perm_z = Perlin::perlin_generate_perm();
rtvec * Perlin::perlin_generate() { rtvec * p = new rtvec[256]; for (int i = 0; i < 256; ++i) p[i] = rtvec(-1 + 2 * lvgm::rand01(), -1 + 2 * lvgm::rand01(), -1 + 2 * lvgm::rand01()).ret_unitization(); return p; }
且看上面這段代碼, -1 + 2*lvgm::rand01(),返回的區間為-1~1
inline rtvar Perlin::noise(const rtvec& p)const { int i = floor(p.x()); int j = floor(p.y()); int k = floor(p.z()); rtvar u = p.x() - i; rtvar v = p.y() - j; rtvar w = p.z() - k; rtvec list[2][2][2]; for (int a = 0; a < 2; ++a) for (int b = 0; b < 2; ++b) for (int c = 0; c < 2; ++c) { list[a][b][c] = _randomvalue[_perm_x[(i + a) & 255], _perm_y[(j + b) & 255], _perm_z[(k + c) & 255]]; #ifdef listtest if (list[a][b][c].x() < 0)stds cout << "list.x < 0 " << stds endl; if (list[a][b][c].y() < 0)stds cout << "list.y < 0 " << stds endl; if (list[a][b][c].z() < 0)stds cout << "list.z < 0 " << stds endl; #endif } return perlin_interp(list, u, v, w);
上述測試部分可能會輸出信息,因為list中有負值,然后Perlin向量插值就可能會是負值
rtvar Perlin::perlin_interp(rtvec list[2][2][2], rtvar u, rtvar v, rtvar w) { rtvar uu = u*u*(3 - 2 * u); rtvar vv = v*v*(3 - 2 * v); rtvar ww = w*w*(3 - 2 * w); rtvar accumulate = 0; for (int i = 0; i < 2; ++i) for (int j = 0; j < 2; ++j) for (int k = 0; k < 2; ++k) { rtvec weight(u - i, v - j, w - k); accumulate += (i*uu + (1 - i) * (1 - uu))* (j*vv + (1 - j) * (1 - vv))* (k*ww + (1 - k) * (1 - ww))* lvgm::dot(list[i][j][k], weight); #ifdef accumulatetest if (accumulate < 0)stds cout << "accumulate < 0 " << stds endl; #endif } return (accumulate); }
****************************** 為什么是“錯”的 ***************************************
如果noise返回一個負值,那么
rtvec noise_texture::value(rtvar u, rtvar v, const rtvec& p)const { return rtvec(1., 1., 1.) *_noise.noise(p); }
它返回的就是一個負值
bool lambertian::scatter(const ray& rIn, const hitInfo& info, rtvec& attenuation, ray& scattered)const { rtvec target = info._p + info._n + lvgm::random_unit_sphere(); scattered = ray{ info._p, target - info._p }; attenuation = _albedo->value(0.,0.,info._p); return true; }
scatter傳出去的attenuation就是負值
主函數中
錯誤信息會輸出成功,lerp函數返回含有負值的向量
gamma校正負值開根號為出現無窮
你的圖像文件數據讀取會報錯!
****************************** 插曲結束 ***************************************
那么如果把隨機生成函數改為
rtvec * Perlin::perlin_generate() { rtvec * p = new rtvec[256]; for (int i = 0; i < 256; ++i) p[i] = rtvec(abs(-1 + 2 * lvgm::rand01()), abs(-1 + 2 * lvgm::rand01()), abs(-1 + 2 * lvgm::rand01())).ret_unitization(); return p; }
還不行,因為noise會返回負值,那么我們把Perlin插值的返回值改為正值即可
rtvar Perlin::perlin_interp(rtvec list[2][2][2], rtvar u, rtvar v, rtvar w) { rtvar uu = u*u*(3 - 2 * u); rtvar vv = v*v*(3 - 2 * v); rtvar ww = w*w*(3 - 2 * w); rtvar accumulate = 0; for (int i = 0; i < 2; ++i) for (int j = 0; j < 2; ++j) for (int k = 0; k < 2; ++k) { rtvec weight(u - i, v - j, w - k); accumulate += (i*uu + (1 - i) * (1 - uu))* (j*vv + (1 - j) * (1 - vv))* (k*ww + (1 - k) * (1 - ww))* lvgm::dot(list[i][j][k], weight); #ifdef accumulatetest if (accumulate < 0)stds cout << "accumulate < 0 " << stds endl; #endif } return abs(accumulate); //!!! }
那么我們的圖片將是這個樣子的
如果我們不改動隨機數生成器,只保證noise函數最后的返回值為正值
那么也是上面那個圖
改動間還得到了如下圖:
上面兩個圖是因為noise函數中list的值每次取得都和z有關,所以造成了上述線條現象
不小心重新抄寫的時候將^寫成了逗號,不過改為^也是錯,因為不管怎么選擇下標,該數組中的元素值始終都是-1~1
真正的解法是value取值的時候對noise的返回值做處理
下面是Perlin.hpp

/// Perlin.hpp // ----------------------------------------------------- // [author] lv // [begin ] 2019.1 // [brief ] the Perlin-class for the ray-tracing project // from the 《ray tracing the next week》 // ----------------------------------------------------- #pragma once namespace rt { class Perlin { public: inline rtvar noise(const rtvec& p)const; inline rtvar turb(const rtvec& p, int depth) const; inline rtvec* randomvalue()const { return _randomvalue; } inline int* perm_x()const { return _perm_x; } inline int* perm_y()const { return _perm_y; } inline int* perm_z()const { return _perm_z; } public: static rtvec * perlin_generate(); static void permute(int * p, int n); static int * perlin_generate_perm(); static rtvar perlin_interp(rtvec list[2][2][2], rtvar u, rtvar v, rtvar w); private: static rtvec * _randomvalue; static int * _perm_x; static int * _perm_y; static int * _perm_z; }; rtvec * Perlin::_randomvalue = Perlin::perlin_generate(); int * Perlin::_perm_x = Perlin::perlin_generate_perm(); int * Perlin::_perm_y = Perlin::perlin_generate_perm(); int * Perlin::_perm_z = Perlin::perlin_generate_perm(); rtvec * Perlin::perlin_generate() { rtvec * p = new rtvec[256]; for (int i = 0; i < 256; ++i) p[i] = rtvec(-1 + 2 * lvgm::rand01(), -1 + 2 * lvgm::rand01(), -1 + 2 * lvgm::rand01()).ret_unitization(); return p; } int* Perlin::perlin_generate_perm() { int * p = new int[256]; for (int i = 0; i < 256; ++i) p[i] = i; permute(p, 256); return p; } void Perlin::permute(int* p, int n) { for (int i = n - 1; i; i--) { int tar = int(lvgm::rand01() * (i + 1)); stds swap(p[i], p[tar]); } } rtvar Perlin::turb(const rtvec& p, int depth = 7) const { rtvar accumulate = 0; rtvec t = p; rtvar weight = 1.0; for (int i = 0; i < depth; i++) { accumulate += weight*noise(t); weight *= 0.5; t *= 2; } return abs(accumulate); } inline rtvar Perlin::noise(const rtvec& p)const { int i = floor(p.x()); int j = floor(p.y()); int k = floor(p.z()); rtvar u = p.x() - i; rtvar v = p.y() - j; rtvar w = p.z() - k; rtvec list[2][2][2]; for (int a = 0; a < 2; ++a) for (int b = 0; b < 2; ++b) for (int c = 0; c < 2; ++c) { list[a][b][c] = _randomvalue[_perm_x[(i + a) & 255] ^ _perm_y[(j + b) & 255] ^ _perm_z[(k + c) & 255]]; #ifdef listtest if (list[a][b][c].x() < 0)stds cout << "list.x < 0 " << stds endl; if (list[a][b][c].y() < 0)stds cout << "list.y < 0 " << stds endl; if (list[a][b][c].z() < 0)stds cout << "list.z < 0 " << stds endl; #endif } return perlin_interp(list, u, v, w); } rtvar Perlin::perlin_interp(rtvec list[2][2][2], rtvar u, rtvar v, rtvar w) { #ifdef uvwtest if (u < 0)stds cout << "u < 0 " << stds endl; if (v < 0)stds cout << "v < 0 " << stds endl; if (w < 0)stds cout << "w < 0 " << stds endl; if (u > 1)stds cout << "u > 1 " << stds endl; if (v > 1)stds cout << "v > 1 " << stds endl; if (w > 1)stds cout << "w > 1 " << stds endl; #endif rtvar uu = u*u*(3 - 2 * u); rtvar vv = u*v*(3 - 2 * v); rtvar ww = u*w*(3 - 2 * w); rtvar accumulate = 0; for (int i = 0; i < 2; ++i) for (int j = 0; j < 2; ++j) for (int k = 0; k < 2; ++k) { rtvec weight(u - i, v - j, w - k); accumulate += (i*uu + (1 - i) * (1 - uu))* (j*vv + (1 - j) * (1 - vv))* (k*ww + (1 - k) * (1 - ww))* lvgm::dot(list[i][j][k], weight); #ifdef accumulatetest if (accumulate < 0)stds cout << "accumulate < 0 " << stds endl; #endif } return accumulate; } }
以及noise_texture.hpp中的value函數,如下:
方可解決noise中返回為負的情況,_scale 為 5 的時候做出的圖如下:
同樣,我們可以將光線追蹤提高圖片質量的慣用伎倆——采樣,用在噪聲值生成上面,即:使用具有多個相加頻率的復合噪聲。 這通常稱為turbulence
用turb函數來代替noise函數,編者在turb返回的時候取了絕對值,而noise中的負值任由不管,不知為何。。
得到如下圖:
既然,編者已經將turb返回值取了絕對值,我們大可試一下之前的value函數
_scale 為 5 時候
看着有點密集,和書上的不太像,把_scale調為3,得到如下圖,看着差不多了
程序紋理的入門是大理石紋理, 基本思想是使顏色與正弦函數成比例,並使用turbulence來調整相位,能使條紋起伏
y = sin(wx + φ)
_scale就是w值,我實在調不出來書上的紋理
我把_scale的值調成6.3,結果如下:
_scale 值越大,圖像上的正弦曲線波動幅度越小
如果誰調整出來書上的_scale值了,請於下方評論區留言
我們不妨把value函數中的turb改為原來的noise
則得到下面這幅圖
可以看到比較明顯的格塊狀,所以turb還是好一點
今天就到這兒了,感謝您的閱讀,生活愉快~