【Ray Tracing The Next Week 超詳解】 光線追蹤2-4 Perlin noise


 

 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;        
    }

}
Perlin.hpp

 

  以及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還是好一點

 

今天就到這兒了,感謝您的閱讀,生活愉快~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM