本項目參考自教程《Ray Tracing in One Weekend》,在跑通了所有例子之后,加上了自己的理解寫成筆記,項目使用CPU多線程提速,并增加了GUI進度顯示。
項目鏈接:https://github.com/maijiaquan/ray-tracing-with-imgui
目錄:
《用兩天學(xué)習(xí)光線追蹤》1.項目介紹和ppm圖片輸出
《用兩天學(xué)習(xí)光線追蹤》2.射線、簡單相機和背景輸出
《用兩天學(xué)習(xí)光線追蹤》3.球體和表面法向量
《用兩天學(xué)習(xí)光線追蹤》4.封裝成類
《用兩天學(xué)習(xí)光線追蹤》5.抗鋸齒
《用兩天學(xué)習(xí)光線追蹤》6.漫反射材質(zhì)
《用兩天學(xué)習(xí)光線追蹤》7.反射向量和金屬材質(zhì)
《用兩天學(xué)習(xí)光線追蹤》8.折射向量和電介質(zhì)
《用兩天學(xué)習(xí)光線追蹤》9.可放置相機
《用兩天學(xué)習(xí)光線追蹤》10.散焦模糊
項目介紹
本項目使用了ImGUI的圖形化界面框架,使用官方自帶的一個OpenGL2的例子,目的是用直接繪制的方法,在屏幕上逐像素輸出整張圖片。目前在MacOS(Xcode 10.3)和Windows(Visual Studio 2015)環(huán)境中上能順利運行,其他環(huán)境待測試。
因為大家的主要目的是學(xué)習(xí)光線追蹤,所以環(huán)境搭建、GUI、圖片光柵化輸出、多線程的具體實現(xiàn)等,就不在這里細述,有興趣的朋友們可以看我的代碼實現(xiàn)。每一節(jié)的代碼都會基于上一節(jié)進行增改,最終實現(xiàn)效果如下:

本節(jié)目標(biāo)
在300x150的屏幕上輸出一張插值漸變的圖片,并保存為ppm格式,如下圖所示:

本節(jié)代碼:main1.cpp
多線程
由于用CPU跑光線追蹤很慢,所以用多線程來提速。假設(shè)CPU是雙核的,則理論上能提速一倍。
為了演示一下多線程,這里特地調(diào)整了每個線程的運行速度,并一次性創(chuàng)建了50個線程(假裝CPU有50個核),每個線程繪制完一行之后,跳到下50行繼續(xù)繪制。圖片的寬高為300x150,則每個線程要繪制3行。最下面一行的線程速度最快,越往上速度遞減的話,就會有如下效果:

PPM格式概要和存儲
ppm是一種直接存儲RGB顏色值的文件格式,第一行是p3,表示顏色值用ASCII存。第二行是圖像的寬和高。接下來是每一行按順序存放的顏色值。

本項目會將所有的輸出圖片保存為ppm格式,由于本項目支持實時顯示,關(guān)于如何打開ppm圖片的問題本文不再贅述,網(wǎng)上可找到大量解決辦法。
本節(jié)核心代碼
向量類vec3的具體實現(xiàn):vec3.h
對于像素點的繪制,我直接封裝了一個函數(shù):
void DrawPixel(x, y, r, g, b); //在屏幕上坐標(biāo)為(x,y)的位置上,繪制顏色值為(r,g,b)的一個像素
因為是一個簡單的漸變圖片,所以直接用行和列的下標(biāo)插值的方式來直接賦值RGB,代碼如下:
int nx = 300;
int ny = 150;
void RayTracing()
{
for (int y = ny - 1; y >= 0; y --)
{
for (int x = 0; x < nx; x++)
{
vec3 col(float(x) / float(nx), float(y) / float(ny), 0.8);
int ir = int(255.99 * col[0]);
int ig = int(255.99 * col[1]);
int ib = int(255.99 * col[2]);
DrawPixel(x, y, ir, ig, ib);
}
}
}
如果要寫成多線程,則可以將上面代碼改成下面的樣子:
void RayTracingInOneThread(int k) //繪制一個線程
{
for (int y = ny-k; y >= 0; y -= numThread) //ny為屏幕的高
{
for (int x = 0; x < nx; x++) //nx為屏幕的寬
{
vec3 col(float(x) / float(nx), float(y) / float(ny), 0.8);
int ir = int(255.99 * col[0]);
int ig = int(255.99 * col[1]);
int ib = int(255.99 * col[2]);
DrawPixel(x, y, ir, ig, ib);
}
}
}
void RayTracing()
{
vector<thread> threads; //多線程
for (int k = 0; k < numThread; k++)
{
threads.push_back(thread(RayTracingInOneThread, k));
}
for (auto &thread : threads)
{
thread.join();
}
}
注意:本項目的入口函數(shù)為RayTracing()。由于默認(rèn)使用多線程,入口函數(shù)會分發(fā)到若干個線程里面執(zhí)行,每個線程對應(yīng)的函數(shù)為RayTracingInOneThread(int k)。如果實在無法理解這個函數(shù)的行為,可以粗暴假設(shè)k=1和numThread=1,當(dāng)作是單線程來理解。