《Raytracing In One Weekend》學習筆記01 Chapter 1、2、3、4、5、6、7、8

根據師兄推薦,打算從 Peter Shirley 的《Ray Tracing in OneWeekend》等系列圖書入門光線追蹤,學習過程中記錄了一些經驗總結筆記。這些筆記中包含了學習過程中遇到的一些知識理解以及編程相關的問題,如今記錄下來,總結經驗,加深印象。

Chapter 1. Overview

第一章介紹了作者將會使用c++進行編碼,可以使用輕量級的IDE,甚至包括Codeblocks,這裏我使用了Clion(通過教育網郵箱登錄可以免費使用),比較輕量,而且代碼管理也很方便。安裝好Clion後記得配置一下C++環境。
作者喜歡在敲代碼的過程中學習這些知識,我也很贊同。但是當一些代碼提供可用時,雖然不好理解,推薦大家可以毫不留情的測試使用,這也是學習的一個過程。

Chapter 2. Output an Image

這一章主要教大家如何從無到有輸入一幅rgb圖像。圖像採用ppm格式(wiki百科上有具體介紹,很詳細,推薦看一下) 存儲,構造過程和輸出過程都十分簡單。
比較有用的一點是,這裏推薦了另外一種輸出圖像的方式(任意格式的圖像),那就是使用stb_image_write第三方庫,方法十分簡單,所用代碼如下:

  • 將stb_image_write.h頭文件源碼文件放到當前目錄下,然後聲明頭文件並編碼輸入:
#include "vec3.h"   // 雙引號應用的是程序目錄的相對路徑中的頭文件
#include <iostream> // 尖括號引用的是編譯器類庫路徑中的頭文件
#define STB_IMAGE_WRITE_IMPLEMENTATION // 使第三方庫stb_image_write成爲可執行的源碼
#include "stb_image_write.h"

using namespace std;

int main() {
    int nx = 200;
    int ny = 100;
    int channels = 3; // 代表rgb三通道,若爲4則代表rgba四通道
    unsigned char *data = new unsigned char[nx*ny*channels]; // 聲明數組,用於存放像素rgb值
    for(int j = ny -1; j >= 0; j--){
        for(int i = 0; i < nx; i++){
            vec3 col(float(i) / float(nx), float(j) / float(ny), 0.2);
            int ir = int(255.99*col[0]);
            int ig = int(255.99*col[1]);
            int ib = int(255.99*col[2]);
            data[(ny - j - 1)*nx*3 + 3 * i] = ir; // 計算出二維圖像中的像素在一維數組中的對應位置,從第一行第一列開始
            data[(ny - j - 1)*nx*3 + 3 * i + 1] = ig;
            data[(ny - j - 1)*nx*3 + 3 * i + 2] = ib;
        }
    }
    stbi_write_png("PNGOutput2.png", nx, ny, channels, data, 0); // 輸出圖像
    return 0;
}

Chapter 3. The vec3 Class

這部分創建了一個vec3向量類,用於創建、表示以及操作三維向量。

  • 新增了一個有意思的點是inline內聯函數,這部分內容c++課本上多有描述,可以解決一些頻繁調用的函數大量消耗棧空間(棧內存)的問題。
  • 此外還重載了c++中的運算符,用於向量的基本數學運算。如果你感覺心裏沒底最好的方式是找課本上的運算符重載實例練習一下,至少有個瞭解。
    難度不大,不用驚慌。
  • 關於c++編程有兩點需要注意:
  • 一是定義類時,創建了.h文件還需要再創建.cpp文件嗎?其實不創建.cpp文件完全不影響編譯,創建.cpp文件的主要目的是實現成員函數聲明和定義的分離,方便代碼管理(個人理解),當前項目相對來說是很輕量的,只需要創建一個.h文件就足夠了(雖然博主也創建了.cpp文件,但是推薦只創建.h文件即可)。
  • 二是可以通過條件編譯實現再同一個項目中管理不同章節的代碼,節省創建新項目的時間。這部分的內容有時間的話博主會另寫一篇總結文章。

Chapter 4. Rays, a Simple Camera, and Background

這部分通過描述了射線的定義並創建了射線ray類,定義一個攝像機,以及創建一個顏色函數color來初步構建了一個光線追蹤器,具有發射光線,計算光線與像素碰撞點的顏色的功能(demo中顯示出了背景的顏色)。個人認爲需要注意的有以下幾點:

  • 射線的數學定義函數需要理解(博主認爲大家應該比較容易理解,需要注意的是初始時只需要起點和方向就能定義一條射線;後續章節中由於涉及到射線與模型相交,所以又引入了參數t,用於表示射線相交後有效部分的長度);
  • 攝像機的位置很有意義(科普向),始終位於(0, 0, 0)原點處,向上與y軸正方向重合,向前朝向z軸的負方向(也就是看向z軸的負方向)。這裏可以給大家普及一下,計算機圖形學中一般是認爲攝像機固定於上述的標準位置,那麼可能有同學問了,現實世界中攝像機是會動的啊。這裏需要解釋一下,此處用到了物理的相對運動知識,也就是說,我們可以把攝像機的運動轉換成模型的運動,對模型進行視圖變換(也叫攝像機變換)從而得到運動後的模型狀態信息,然後再進行渲染操作。變換過程中的數學知識很有趣,有興趣的同學(你可能沒時間看,但是我強烈推薦你在空閒看!),可以觀看閆令琪GAMES101的第四課,視圖變換部分內容(你很有可能會把前面的部分也看完😄)。
    偷了一張圖
  • 射線是由攝像機位置發出的,模擬了由人眼發出的視線,有多少條呢?目前我們是在for循環中規定了一個像素點對應一條,也就是有200*100條(可以想象一下,如果博主沒推錯的話就是對了😄);
  • 射線與某物體(本章節只涉及背景)相交後,需要通過color函數計算一下顏色值,這就是該位置我們看到的顏色;
  • 顏色是通過射線的方向向量單位化後的y分量映射後並進行插值得到的。解釋一下,就是射線有一個方向向量,作者將該向量單位化,所以三個分量的取值範圍都是(-1, 1);然後將對應的y分量從(-1, 1)映射到(0, 1)上(爲啥是開區間?博主暫時沒有考慮;映射的計算過程以y分量爲例,爲:0.5*(y + 1.0)),爲啥插值到(0, 1)區間上?因爲插值參數的範圍需要是(0, 1),這個y分量映射之後就是插值參數t;最後進行插值操作:
// 返回插值後的顏色,從淺藍色到白色之間進行插值;從上到下漸變的顏色同時對應了y從大到小(for循環規定的)的位置特性
return (1.0 - t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0);  
// 特別注意,這個t和第五章中的t不是一回事;這裏的t只代表插值參數,第五章中的t代表射線函數的中的一個變量(注意一下即可)

盜圖王者

生成的背景圖

Chapter 5. Adding a Sphere

這一部分主要是在場景中定義了一個球體,然後判斷射線與球體是否相交,如果相交了,將球體對應的顏色設置爲紅色。你可能對以下幾個問題感到疑惑:

  • 球體是怎麼定義的?事實上,這裏的球體只是在數學計算(射線與球體碰撞)中有所涉及,只是虛構了一個球體,僅有球心和半徑的信息,目前並未創建球體實體(下一章將會創建球體類,充分利用c++面向對象的特點);
  • 射線與球體是怎麼碰撞的? 實際上是通過數學計算進行判斷的。因爲我們知道了球心和半徑,所以可以寫出球面座標公式:
    友善的借用
    Cx/y/z代表了球心座標,R爲球半徑,都是已知的。
    實際上我們並不想用這個方程,我們想用的是球面座標的向量表示方式,即:
    友善的借用
    其中p爲球面上的任意一個點(x, y, z),這個方程的數學含義是,球面上任一點與球心之差構成一個向量,這個向量和自己的點乘結果,是等於方程右側式子的(單純計算推出來的,dot表示點乘);而右側的式子恰恰就是球面座標公式的一部分,它等於R的平方,也就是說,我們得到了球面上任意一點的向量表示方式。
    現在我們想計算球面有沒有和射線發生碰撞,考慮到射線的參數表示爲:p(t)=A+t∗B,p(t)表示射線上的任意一個點(x, y, z),A爲射線起點座標,B爲射線的方向向量,t爲參數變量,可以認爲A、B是已知的,僅t未知。
    所以要想計算球面和射線有沒有相交,直接將射線帶入球面方程即可,如果你思路很清晰的話,會發現代入後僅剩下一個變量,那就是t,帶入後得到了關於t的一元二次方程。博主通過向量的結合率將射線方程代入球面方程後推導了一下,最終得出了下面的方程:
    這個方程不錯
    這就是射線與球面的相交方程,僅含有t這一個未知數,所以可以通過計算相應的一元二次方程的判別式來判斷有沒有解,若有解,有幾個解(1個or2個)。在這一章裏,我們只是判斷了有沒有解的情況,若有解,代表有交點,然後我們將對應位置處的像素賦值爲紅色(問:怎麼找到這個像素的呢?其實閱讀源碼不難發現這裏有這麼一個性質,那就是圖像像素和射線存在一一對應的關係,也就是說,根據像素的u、v座標確定了一條射線,這條射線和模型如果相交,那麼這個像素就要賦值紅色;若未相交,賦值爲計算得到的背景色;所以要賦值的這個像素根本就不需要找,它就是我們最初的圖像上某個具體位置的像素啊);若無解,表示射線與模型沒有交點,射線投向了背景,我們需要將對應位置處的像素賦值爲插值計算出的背景色。
  • 其實這一章還有個重大缺陷,那就是判斷出來的有解的情況,可能對應着球在射線的負方向上,即t<0的情況,很顯然攝像機後面的球是看不見的,下一章對這個缺點進行了改進,排除了t<0的情況。

Chapter 6. Surface Normals and Multiple Objects

這一部分分兩個片段分別介紹了一些內容。
第一個片段介紹了球面法線的定義,以及如何可視化法線(通過顏色標識)。

  • 球面法線定義爲(射線與球面碰撞點 - 球心點),作者個人傾向於對其再進行單位化操作,因爲這會對後續的着色提供方便。
  • 可視化法線:球面上分佈了無數的法線,每一條法線的x/y/z分量都不一定相同(由於我們獲取的法線是進行單位化的,所以每一分量的取值範圍爲(-1, 1)),所以可以利用這個特徵,將每一條法線的x/y/z分量映射到(0, 1)(r/g/b)上 (映射方法很簡單,可以參看第六章color函數的源碼),對應着像素顏色r/g/b的取值範圍,從而表示像素點的顏色,實現法線的可視化。

第二個片段實現了在場景中通過鏈表的形式創建多個物體,用到了c++面向對象的知識。

  • 這裏構造了一個抽象類hittable,可以認爲是一類被射線碰到的物體(這裏作者認爲會與面向對象中的對象混淆,所以換了一種命名方式,最後作者根據這個類中的虛函數hit(用來計算判斷是否發生碰撞)命名了這個抽象類)。
  • 這裏還構造了sphere類,繼承自抽象類hittable,並實現了父類中的虛函數hit的定義(你最好了解一下抽象類虛函數的基礎知識,不用太深入)。這樣的好處就是把不同的碰撞細節定義在了不同的物體類中,如果再添加一個其它類別的物體,比如說立方體(實際這本書並未涉及),我們可以把對應的判斷碰撞方法寫到該類的hit函數裏,充分面向對象。

當前章節中還用到了多態的思想。指的是,聲明hittable類型的鏈表,然後爲該鏈表賦值爲sphere類型的內容。簡單的說,就是一句話:允許將子類類型的指針賦值給父類類型的指針。賦值之後,父對象就可以根據當前賦值給它的子對象的特性以不同的方式運作(摘抄自百度百科)。

關於最後的結果圖:在最後的結果圖中,你可能會問,**爲啥大球是一片均勻的綠色的呢?**我的理解是大球太大了,導致我們看到的球面法線變化值不大,所以映射到顏色後,顯示一片均勻的綠色。至於爲啥是綠色,那是因爲x/y/z對應的r/g/b值中,y分量比較大,所以g值大,顯示綠色(個人理解)。

Chapter 7. Antialiasing

這部分講的是如何實現抗鋸齒。

  • 主要原理很簡單,那就是通過計算周圍像素的顏色平均值來得到當前像素的顏色,比如說在邊界處具有很明顯的前景色和後景色,要想獲得平滑的邊界,可以把前景色和背景色混合得到一個邊界色,作爲過渡,由於像素很小,所以不放大看的話,像素的鋸齒感就消失了。作者在這裏並沒有考慮多層顏色的影響,給出的理由是對畫面效果的提升不大。
  • 實際編程過程中作者隨機採樣(這也是爲啥用到隨機函數的原因)了200個周圍的像素點,最後取了一下平均值得到當前像素的顏色值。這就相當於在計算每一個像素點的顏色值時,在其周圍隨機發射了200條射線,並計算交點顏色,最後取平均。這就與先前章節中介紹的一個像素對應一條射線不一樣了,讀者需要注意。

Chapter 8. Diffuse Materials

Diffuse Materials 漫反射材質 讓模型的視覺效果更逼真。
這部分開頭作者做了一下聲明,那就是作者將幾何形狀和材質分開了,而非一一對應,這會導致一些侷限性。此外,漫反射材質本身不發光,只會反射周圍的環境光,同時會將環境光混合調製成本身的色彩光線在漫反射表面上的反射方向是隨機的,如下圖:
漫反射材質的隨機反射

漫反射材質的隨機反射

當然有些光線也可能會被吸收,表面越黑,吸收光線的能力越強。這也是黑色物體爲啥黑的原因——因爲它吸收光線,極少反射光線。

廢話不多說,我們看看代碼部分有哪些需要注意的。

  • 如何模擬射線的隨機反射?
  • 先搞清楚爲啥要模擬隨機反射:因爲根據漫反射材質隨機反射光線的性質,光線照射到漫反射材質表面上時,會被表面吸收(當前章節不考慮)或隨機反射到一個方向上,而且有可能會反射後再次反射(當然強度會衰減),如果這樣考慮的話,渲染效果將會更加逼真,這是由真實世界中的物理規律決定的。
  • 具體怎麼做才能獲取隨機反射射線呢?作者在實現過程的一個步驟中採用了一個取巧的辦法,叫捨棄法(理解起來很簡單,博主就不多說了,但是博主認爲這個方法有很大缺陷)。整體步驟描述如下:首先我們已經計算得到了射線與物體(這裏主要是sphere)之間的交點,並且知道交點處物體表面的法線(是一個單位向量),所以如果我們在原點(0, 0, 0)處獲取一個隨機點(這個隨機點位於以原點爲球心的單位球體內,實際上demo程序中只計算了x/y/z各分量都爲正的情況),那麼,將該隨機點(其實是以原點爲起點的向量)與已知的交點座標和法線向量相加,就可以得到一個新的隨機點,這個點位於一個球體中,這個球體的半徑爲1,球心座標爲射線與sphere交點加上法線向量。也就是說,我們最終獲得了位於單位半徑球體中的一個隨機點,這個隨機點與射線和sphere交點之差,便是新的反射射線的方向,而交點就是反射射線的起點。至此,一條隨機反射射線便構造出來了。
    絕對原創圖片
構造隨機反射射線
  • 上面提到過,隨機反射射線如果和物體碰撞還可以再次進行反射,但是不會無線反射下去,因爲射線每反射一次,其強度便會衰弱一次(這裏我們用衰弱係數描述),所以最後要麼最後碰撞到背景,要麼因爲強度太弱無法繼續反射。體現在demo程序中便是一個遞歸函數,上述兩種中止情況便是遞歸的返回條件。
  • 最後還需要注意兩個問題:
  • 顏色問題,生成的圖片偏黑,矯正一下,理想的效果應該是淺灰色,爲此我們可以對像素顏色分量分別進行開方處理,變相的增強了顏色的亮度(因爲顏色分量是小數),前後對比效果圖如下👇
  • 在這裏插入圖片描述在這裏插入圖片描述
顏色矯正
  • 編程問題,是浮點類型的t造成的,因爲二進制計算機在判斷相等時,是用範圍來判斷的。舉個例子,對於float數據類型的實數,-0.0000001和0.0000001都被計算機認爲是0,因此我們應該捨去負值(對應射線與球體交點在視線背面的情況),這樣的操作去除了“陰影粉刺”現象(的確是這麼稱呼的。。。),效果圖如下👇
    在這裏插入圖片描述
編程誤差矯正
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章