1. 程式人生 > >【Ray Tracing in One Weekend 超詳解】 光線追蹤1-5

【Ray Tracing in One Weekend 超詳解】 光線追蹤1-5

 

一天一篇,今天來學習第7章 (散射)漫反射材質

Chapter7: Diffuse Materials

 Preface

從這一章開始,我們將通過光線追蹤製作一些逼真的材質。

我們將從漫射(磨砂)材料開始。 

 先看效果

  

 正文

不發光的漫射物體僅僅呈現其周圍的顏色,但是它們用它們自己的固有顏色來調和這些色彩。

從漫反射表面反射的光方向是隨機的,比如:如果我們將三條光線傳送到一個漫反射表面,它們將各自具有不同的隨機行為:

引用書上的圖:

  

                      diagram 7-1

它們也可能被吸收而不是被反射。 表面越暗,光線越可能被吸收。 (這就是為什麼它是黑的!)

任何隨機化方向的演算法都會產生看起來很粗糙的表面。 最簡單的方法之一是理想的漫反射表面。 

 原文還提到了Lambertian發射面

 

我們來看一下,如何實現上述功能

圖說一切:

   

 

                          diagram 7-2

 

 圖解

先簡述一下各個原件:左黃球是以eye為中心的一個單位圓,右黃球是一個和左黃球一樣的圓,至於怎麼生成的,後續說

左黃球上有兩個隨機點,藍紫色的s1,紅紫色的s2,對應於右黃球上為s1' 和s2' 

紅色為視線;深綠色為反射線;三個黑球為漫反射球體,黑色只是用顏色來區分各個原件的功能,並不是黑色的漫反射球(畫完才發現,黑球都把光線吸收了。。。。==!)

 實現過程

步驟一:從eye發出一條視線,交球面於p點,之後我們確定隨機反射方向

將右邊的黃色圓部分放大:

引用書中一張圖:

    

                                    diagram 7-3

 n為P點的單位法向量,方向向外,下面那個點是碰撞點P,找一個和點P相切的單位圓

而這個圓的圓心o的位置就等於p+n: eye->P),因為我們的原點就是eye,所以根據向量就可以得出位置資訊

基於eye的向量和位置體系,其實方便了我們利用向量運算代替位置運算,更直觀。這個自己理解下就好,不是重點。

步驟二:then, we pick a random point s from the unit randius sphere.

當我們找到這個s點之後,我們將沿著p->s的方向進行反射,但是我們如何找這個random point呢?

這個時候我們就需要用到我們的diagram 7-2了,回去看一眼那個藍紫色點s1,做一個平行四邊形,對應到s1',他們是等價的(向量只用方向和大小進行定義,不規定起始位置,所以我們能說它們等價)。

我們先在原點單位球中找一個隨機點,構成一個eye->s的向量s1然後,將s1的起點移動到o處,即s1',也就是說s1'就是我們要求的隨機點,因為直接求隨機點s1'的位置並不好求,所以,只能這樣,其實想是很好想,但是要描述清楚就應該是這麼描述。

步驟三:最後我們得到反射線的方向dir = s1' - p,s1' = o + s1, o = p + n

然後,我們來求s1:

#include <random>
#define stds std::
using namespace rt;

stds mt19937 mt;
stds uniform_real_distribution<rtvar> rtrand;

const rtvec random_unit_sphere()
{
    rtvec p;
    do
    {
        p = 2.0*rtvec(rtrand(mt), rtrand(mt), rtrand(mt)) - rtvec(1, 1, 1);
    } while (dot(p, p) >= 1.0);      //rejection method
    return p;
}

關於隨機數生成,在上一篇講過了,應該是靠後講的

rtrand生成的是0~1的隨機數,然後乘以2再減去1,得到的p的每一個分量均位於-1~1,其實它的範圍是一個正方體,而我們要求的是球內隨機點。

所以我們採用書中所述的rejection方法,拒絕非法點:如果基於原點eye找一個隨機點(x,y,z)

如果x*x+y*y+z*z>=1,那麼它不符合我們的需要,我們重新找。

  

最後,我們通過上面的程式碼就得到了一個球內隨機點。

上述就是diagram 7-2中基於藍紫色點進行反射的深綠色光線的反射過程

當然,還有基於紅紫色的反射線,前半部分就和上面一樣,所以也沒有畫平行四邊形,關於後續反射

步驟四:將當前碰撞點P作為eye,以反射方向向量dir為視線方向進行步驟一

直到沒有碰撞,為止

而且,光線沒經過一次反射強度就會衰減,我們也是這麼做的,我們採用的是每反射一次,衰減一半。

#define LOWPRECISION

#include <fstream>
#include "intersect.h"
#include "sphere.h"        
#include "intersections.h"
#include "camera.h"
#include <random>
#define stds std::
using namespace rt;

stds mt19937 mt;
stds uniform_real_distribution<rtvar> rtrand;

const rtvec random_unit_sphere()
{
    rtvec p;
    do
    {
        p = 2.0*rtvec(rtrand(mt), rtrand(mt), rtrand(mt)) - rtvec(1, 1, 1);
    } while (dot(p, p) >= 1.0);
    return p;
}

rtvec lerp(const ray& sight, const intersect* world)
{
    hitInfo rec;
    if (world->hit(sight, 0., intersect::inf(), rec))        //如果沒有有效碰撞點
    {
        rtvec target = rec._p + rec._n + random_unit_sphere();    //隨機點s的最後位置
        return 0.5*lerp(ray{ rec._p,target - rec._p }, world);    //強度衰減,新建eye繼續發射視線
    }
    else
    {
        rtvec dirUnit = sight.direction().ret_unitization();
        rtvar t = 0.5*(dirUnit.y() + 1.);
        return (1. - t)*rtvec(1., 1., 1.) + t*rtvec(0.5, 0.7, 1.0);
    }
}

void build_7_1()
{
    stds ofstream file("graph7-1.ppm");
    size_t W = 400, H = 200, sample = 100;

    if (file.is_open())
    {
        file << "P3\n" << W << " " << H << "\n255\n" << stds endl;
                
        intersect** list = new intersect*[2];
        list[0] = new sphere(rtvec(0, 0, -1), 0.5);
        list[1] = new sphere(rtvec(0, -100.5, -1), 100);
        intersect* world = new intersections(list, 2);

        camera cma;

        for (int y = H - 1; y >= 0; --y)
            for (int x = 0; x < W; ++x)
            {
                rtvec color;
                for (int cnt = 0; cnt < sample; ++cnt)
                {
                    lvgm::vec2<rtvar> para{ 
                        (rtrand(mt) + x) / W,
                        (rtrand(mt) + y) / H };
                    color += lerp(cma.get_ray(para), world);
                }
                color /= sample;
                int r = int(255.99 * color.r());
                int g = int(255.99 * color.g());
                int b = int(255.99 * color.b());
                file << r << " " << g << " " << b << stds endl;
            }
        stds cout << "complished" << stds endl;
        file.close();

        if (list[0])delete list[0];
        if (list[1])delete list[1];
        if (list)delete[] list;
        if (world)delete world;
    }
    else
        stds cerr << "open file error" << stds endl;
}

int main()
{
    build_7_1();
}

 

 效果圖如下:

 

注意球體下的陰影。 這張照片非常暗,但是我們的球體在光線每次反射時只吸收了一半的能量,因此它們是50%的反射器。

在現實生活中, 這些球體應該是淺灰色的。 其原因在於幾乎所有影象觀看者都假設影象是“伽馬校正的”,這意味著這些0到1的值在被儲存為位元組之前做了一些變換。這種做法有很多好處,但就我們的目的而言,今天不講這個,瞭解即可。

如果我們對我們日常的視覺做一個近似,我們可以使用“gamma 2”,即只是簡單的平方根:

 

這樣就會得到下圖:

看起來更好些。

 

感謝您的閱讀,生活愉快~