軟光柵-uraster程式碼閱讀(入門極品)
程式碼連結:https://github.com/Steve132/uraster
所有的程式碼都在uraster.hpp
中。程式碼非常簡單,適合初學者學習軟光柵的實現。整個程式碼,在理解渲染管線基本流程的基礎上,很容易理解,因此首先對渲染管線的基本流程進行介紹。
渲染管線流程介紹
詳細內容可以參見:games101第5節課,和第6節課。
課程地址見:http://games-cn.org/intro-graphics/
上圖取自https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/。
此處重點關注:頂點資料、頂點著色器、光柵化、片段著色器。
作為輸入的資料往往是三維頂點資料,這些頂點資料經過vertex shader,進行模型變換,檢視變換,透鏡變換,頂點的座標會變成規範化後的座標(-1,1)。結合最終影像的寬高就能夠確定之前的三維上的點在二維上對應的位置。由於影像是由畫素組成,是離散的。因此將連續的模型繪製到影像上的時候,需要進行光柵化的過程。光柵化之後,對每個光柵化的位置進行著色。
接下來重點介紹下如何進行光柵化。很容易想到可以利用取樣的方式進行。如下圖所示(from games 101):
for (int i=0; i<width; ++i)
for (int j=0; j<height; ++j)
buffer[i,j] = f(i,j)
那麼現在的重點就是,如何判斷一個點是否位於三角形內部。
判斷點是否在三角形內(1)
假如一個點在P1P2,P2P0,P0P1向量的同一邊,那麼點就在三角形內部,否則在三角形外部。
判斷點是否在三角形內(2)
利用三角形的重心進行判斷,三角形的重心是三角形三條中線的交點。如果有個一點滿足如下性質:
\(aP_0 + bP_1 + cP_2 = P_c, \ a + b + c = 1, \ a \ge 0,b\ge 0, c\ge 0\)
那麼點Pc就在三角形內部。
uraster程式碼瀏覽
主要包括如下組成成分:
// 資料結構,型別
class Framebuffer; // 用來儲存繪製的結果,內部就是個vector
struct BarycentricTransform; // functor 用來輔助計算重心座標
// 函式
void run_vertex_shader(...); // 用來執行外部傳入進來的頂點著色器函式,用來進行模型檢視投影變換,以及顏色設定
void rasterize_triangle(...); // 用來對三角形進行光柵化操作
void rasterize(...); // 光柵化的總入口
void draw(...); // 渲染操作的總入口
是不是很簡單,作為入門材料實在是太合適了。從入口開始往後看,先看一下draw:
/// \param fb 用來存繪製結果
/// \param vertexbuffer_b 頂點儲存空間開始的位置
/// \param vertexbuffer_e 頂點儲存空間結束的位置
/// \param indexbuffer_b 索引儲存空間開始的位置
/// \param indexbuffer_e 索引儲存空間結束的位置
/// \param vcache_b vcache_e 儲存頂點著色器之後的結果
/// \param vertex_shader 頂點著色器functor
/// \param fragment_shader 片段著色器functor
template<class PixelOut,class VertexVsOut,class VertexVsIn,class VertShader, class FragShader>
void draw(Framebuffer<PixelOut>& fb,
const VertexVsIn* vertexbuffer_b,const VertexVsIn* vertexbuffer_e,
const std::size_t* indexbuffer_b,const std::size_t* indexbuffer_e,
VertexVsOut* vcache_b,VertexVsOut* vcache_e,
VertShader vertex_shader,
FragShader fragment_shader)
{
std::unique_ptr<VertexVsOut[]> vc;
// 確保vcache_b的輸出大小和頂點儲存空間的大小一致
if(vcache_b==NULL || (vcache_e-vcache_b) != (vertexbuffer_e-vertexbuffer_b))
{
vcache_b=new VertexVsOut[(vertexbuffer_e-vertexbuffer_b)];
vc.reset(vcache_b);
}
// 執行頂點著色器
run_vertex_shader(vertexbuffer_b,vertexbuffer_e,vcache_b,vertex_shader);
// 執行光柵化
rasterize(fb,indexbuffer_b,indexbuffer_e,vcache_b,fragment_shader);
}
主要看一下三角形光柵化的過程:
/// \brief 輸入三角形的三個頂點,進行光柵化,並對每個位置利用片段著色器進行著色
template<class PixelOut,class VertexVsOut,class FragShader>
void rasterize_triangle(Framebuffer<PixelOut>& fb,const std::array<VertexVsOut,3>& verts,FragShader fragment_shader)
{
std::array<Eigen::Vector4f,3> points{{verts[0].position(),verts[1].position(),verts[2].position()}};
// 除以w項,將齊次座標系轉換成標準化的座標系(-1,1)
std::array<Eigen::Vector4f,3> epoints{{points[0]/points[0][3],points[1]/points[1][3],points[2]/points[2][3]}};
// 獲取xy位置
auto ss1=epoints[0].head<2>().array(),ss2=epoints[1].head<2>().array(),ss3=epoints[2].head<2>().array();
// 計算xy平面上的包圍矩形
Eigen::Array2f bb_ul=ss1.min(ss2).min(ss3);
Eigen::Array2f bb_lr=ss1.max(ss2).max(ss3);
Eigen::Array2i isz(fb.width,fb.height);
//將座標對映為影像大小 (-1.0,1.0)->(0,imgdim)
Eigen::Array2i ibb_ul=((bb_ul*0.5f+0.5f)*isz.cast<float>()).cast<int>();
Eigen::Array2i ibb_lr=((bb_lr*0.5f+0.5f)*isz.cast<float>()).cast<int>();
ibb_lr+=1; //add one pixel of coverage
//clamp the bounding box to the framebuffer size if necessary (this is clipping. Not quite how the GPU actually does it but same effect sorta).
// 在結合影像區域,限定包圍矩形
ibb_ul=ibb_ul.max(Eigen::Array2i(0,0));
ibb_lr=ibb_lr.min(isz);
// 初始化重心座標計算類
BarycentricTransform bt(ss1.matrix(),ss2.matrix(),ss3.matrix());
//for all the pixels in the bounding box
for(int y=ibb_ul[1];y<ibb_lr[1];y++)
for(int x=ibb_ul[0];x<ibb_lr[0];x++)
{
// 轉換成-1到1的範圍
Eigen::Vector2f ssc(x,y);
ssc.array()/=isz.cast<float>(); //move pixel to relative coordinates
ssc.array()-=0.5f;
ssc.array()*=2.0f;
//Compute barycentric coordinates of the pixel center
// 計算重心座標
Eigen::Vector3f bary=bt(ssc);
//if the pixel has valid barycentric coordinates, the pixel is in the triangle
// 重心座標需要在0到1的範圍內,點踩在三角形的範圍內
if((bary.array() < 1.0f).all() && (bary.array() > 0.0f).all())
{
// 計算這個點的深度資訊
float d=bary[0]*epoints[0][2]+bary[1]*epoints[1][2]+bary[2]*epoints[2][2];
//Reference the current pixel at that coordinate
PixelOut& po=fb(x,y);
// if the interpolated depth passes the depth test
// 進行深度測試
if(po.depth() < d && d < 1.0)
{
// 計算當前點
//interpolate varying parameters
VertexVsOut v=verts[0];
v*=bary[0];
VertexVsOut vt=verts[1];
vt*=bary[1];
v+=vt;
vt=verts[2];
vt*=bary[2];
v+=vt;
//call the fragment shader
po=fragment_shader(v);
po.depth()=d; //write the depth buffer
}
}
}
}
接著來看一下使用的時候需要注意什麼:
struct XXXVertVsOut // 以下幾個函式是必須的
{
const Eigen::Vector4f& position() const;
BunnyVertVsOut& operator+=(const BunnyVertVsOut& tp);
BunnyVertVsOut& operator*(const float& f);
};
class XXXPixel
{
public:
float& depth(); // 該函式也是必須的
};
放上示例程式跑出來的結果: