【光柵化】c++光柵化軟渲染器(下)渲染篇


 

引言

  上一篇我們完成了2D渲染器,接下來要實現3D幾何體的繪制。其實3D比2D沒有多太多內容,無非就是多了幾步空間變換和一個視角控制的部分。首先,我們設置一下視角,為之后的三維渲染做准備。

 

空間變換與相機

  這里我們來簡單談談空間變換。它的概念在unity渲染管線那篇文章中有詳細介紹,大概就是要從模型空間變換到世界空間、變換到視角空間、變換到裁剪空間、變換到ndc空間、變換到屏幕坐標。因為這部分屬於圖形學入門的知識,再加上自己太懶不想打太多數學符號,因此直接上幾張自己曾經做過的白板ppt:

 

  好了,相信你對這幾個空間變換有了一定的了解,那么接下來我們就補充一下Matrix類:

 1 void Matrix::lookat(Vector3D camPos,Vector3D tarPos,Vector3D up){
 2     Vector3D xAxis,yAxis,zAxis;
 3     zAxis=-(tarPos-camPos);
 4     zAxis.normalize();
 5     xAxis=up.product(zAxis);
 6     xAxis.normalize();
 7     yAxis=zAxis.product(xAxis);
 8     yAxis.normalize();
 9     normalize();
10     ele[0][0]=xAxis.x;
11     ele[0][1]=xAxis.y;
12     ele[0][2]=xAxis.z;
13     ele[1][0]=yAxis.x;
14     ele[1][1]=yAxis.y;
15     ele[1][2]=yAxis.z;
16     ele[2][0]=zAxis.x;
17     ele[2][1]=zAxis.y;
18     ele[2][2]=zAxis.z;
19     ele[0][3]=-(xAxis.dot(camPos));
20     ele[1][3]=-(yAxis.dot(camPos));
21     ele[2][3]=-(zAxis.dot(camPos));
22 }
23 
24 void Matrix::perspective(float fovy, float aspect, float near, float far){
25     ele[0][0]=1/(aspect*tanf(fovy/2));
26     ele[1][1]=1/(tanf(fovy/2));
27     ele[2][2]=-((far+near)/(far-near));
28     ele[2][3]=-(2*far*near)/(far-near);
29     ele[3][2]=-1;
30 }
31 
32 void Matrix::viewPort(int left,int top,int width,int height){
33     normalize();
34     ele[0][0]=static_cast<float>(width)/2.0f;
35     ele[1][1]=-static_cast<float>(height)/2.0f;
36     ele[0][3]=static_cast<float>(left)+static_cast<float>(width)/2.0f;
37     ele[1][3]=static_cast<float>(top)+static_cast<float>(height)/2.0f;
38 }

 

  其中lookat矩陣是從世界空間轉換到視角空間的矩陣,perspective矩陣是從視角空間轉換到裁剪空間的矩陣,viewport是從ndc空間轉換到屏幕坐標的矩陣,要注意的是中間其實還有一步從裁剪空間坐標轉移到ndc空間的變換,方法很簡單,只需要給x、y、z、w都除以w即可,因此我沒有單寫它的矩陣,但大家千萬不要忘了這一步。

 

  在三維世界中,相機是一個非常重要的物體,它決定了視口的視野。由於一般情況下游戲引擎中的相機以對象的形式呈現,因此我把這里的相機單獨做成了一個類。

 1 #ifndef MAINCAMERA_H
 2 #define MAINCAMERA_H
 3 #include"vector3d.h"
 4  
 5 class maincamera
 6 {
 7 public:
 8     Vector3D pos,goal,up;
 9     float fov,asp,near,far;
10 public:
11     maincamera();
12     maincamera(Vector3D mpos,Vector3D mgoal,Vector3D mup,float mfov,float masp,float mnear,float mfar);
13     void rotateY(float angle);
14 };

 

  首先相機作為一種對象(游戲物體),成員參數必須包括它在世界空間中的位置(pos)和旋轉角度(這里使用的goal和up,分別表示相機看向的點以及相機自身的z軸方向,通過這兩個向量可以唯一確定下來相機的旋轉角度)。此外,在空間變換中我們知道:相機空間向裁剪空間變換時,其裁剪矩陣由相機的張角(fov)、asp(寬高比)、near(近裁剪面距離)、far(遠裁剪面距離)來決定,因此這四個變量也必須要定義。
  成員函數比較簡單,構造函數種可以對相機的transform和屬性參數進行設置,這里定義了一個rotateY函數,作用就是繞着Y軸旋轉angle的角度,每幀調用rotateY就可以起到一種環繞瀏覽的效果。

 1 #include "maincamera.h"
 2 #include "vector4d.h"
 3 #include "matrix.h"
 4 #include "stdio.h"
 5  
 6 maincamera::maincamera()
 7 {
 8     pos=Vector3D(0,4,-8);
 9     goal=Vector3D(0,0,0);
10     up=Vector3D(0,1,0);
11     fov=45*3.14/180.f;
12     asp=1280/767.f;
13     near=1.f;
14     far=50.f;
15 }
16 maincamera::maincamera(Vector3D mpos, Vector3D mgoal, Vector3D mup, float mfov, float masp, float mnear, float mfar)
17 {
18     pos=mpos;
19     goal=mgoal;
20     up=mup;
21     fov=mfov;
22     asp=masp;
23     near=mnear;
24     far=mfar;
25 }
26  
27 void maincamera::rotateY(float angle)
28 {
29     Vector4D pos4(pos.x,pos.y,pos.z,1);
30     Matrix rot;
31     rot.normalize();
32     rot.rotationY(angle);
33     pos4=rot*pos4;
34     pos.setX(pos4.x);
35     pos.setY(pos4.y);
36     pos.setZ(pos4.z);
37 }

 

  現在我們得到了一個相機類。相機需要在執行渲染管線之前被創建出來,因此我們在renderroute類的loop中實例化一個相機類:maincamera *camera=new maincamera; 然后在渲染循環中把相機傳入:pipeline->drawIndex(Pipeline::Fill,camera);
  此時我們需要考慮的事情是:相機數據在管線中的什么位置會被用到。回顧渲染流水線的流程,我們可以知道,相機這個概念最早出現在頂點着色器中。在VS里,我們要將頂點從模型空間變換到裁剪空間,其中從世界空間變換到相機空間、從相機空間變換到裁剪空間這兩大變換都需要相機數據。此時我們更改一下BasicShader類:

 1 #ifndef BASICSHADER_H
 2 #define BASICSHADER_H
 3 #include "shader.h"
 4 #include "polygon.h"
 5 #include "matrix.h"
 6  
 7 class BasicShader: public Shader
 8 {
 9 private:
10     Matrix Mm2w;
11     Matrix Mw2v;
12     Matrix Mv2p;
13 public:
14     BasicShader();
15     ~BasicShader(){}
16     virtual void setCam(Vector3D pos,Vector3D goal,Vector3D up,float fov,float asp,float near,float far);
17     virtual V2F vertexShader(const Vertex &in);
18     virtual Vector4D fragmentShader(const V2F &in);
19 };
20  
21 #endif // BASICSHADER_H

 

  其實就增加了三個變換矩陣:Mm2w是模型轉世界,Mw2v是世界轉相機,Mv2p是相機轉裁剪。此外我們還增加了一個setCam函數,通過相機數據來設置上述的三大矩陣——通過pos,goal,up來構造Mw2v矩陣,通過fov,asp,near,far來構造Mv2p矩陣。因為是要構造矩陣,因此可以在matrix類里補充這兩個構造矩陣的函數。

  根據空間變換的內容,我們可以得到.cpp文件中setCam函數的具體寫法:

 1 #include "basicshader.h"
 2  
 3 BasicShader::BasicShader()
 4 {
 5     Mm2w.normalize();
 6     Mw2v.normalize();
 7     Mv2p.normalize();
 8 }
 9 void BasicShader::setCam(Vector3D pos,Vector3D goal,Vector3D up,float fov,float asp,float near,float far)
10 {
11     Mw2v.lookat(pos,goal,up);
12     Mv2p.perspective(fov,asp,near,far);
13 }
14 V2F BasicShader::vertexShader(const Vertex &in){
15     V2F ret;
16     ret.posM2W=Mm2w*in.position;
17     ret.posV2P=Mw2v*ret.posM2W;
18     ret.posV2P=Mv2p*ret.posV2P;
19     ret.color=in.color;
20     ret.normal=in.normal;
21     ret.texcoord = in.texcoord;
22     return ret;
23 }

 

  在.cpp文件中我們來獲取一下相機參數,這樣可以構造出Mw2v和Mv2p兩個矩陣了。擁有這些矩陣以后,我們便可以在頂點着色器中編寫空間變換的代碼了,如上述代碼所示。之后我們來修改一下渲染管線中的內容:

 1 void Pipeline::drawIndex(RenderMode mode,maincamera *camera)
 2 {
 3     if(m_indices.empty())return;
 4     m_shader->setCam(camera->pos,camera->goal,camera->up,camera->fov,camera->asp,camera->near,camera->far);
 5     for(unsigned int i=0;i<m_indices.size() 3="">vertexShader(vv1),v2=m_shader->vertexShader(vv2),v3=m_shader->vertexShader(vv3);
 6         v1.posV2P/=v1.posV2P.w;
 7         v2.posV2P/=v2.posV2P.w;
 8         v3.posV2P/=v3.posV2P.w;
 9         v1.posV2P=viewPortMatrix*v1.posV2P;
10         v2.posV2P=viewPortMatrix*v2.posV2P;
11         v3.posV2P=viewPortMatrix*v3.posV2P;
12         m_backBuffer->Cover(static_cast<int>(v1.posV2P.x),static_cast<int>(v1.posV2P.y),v1.color);
13         m_backBuffer->Cover(static_cast<int>(v2.posV2P.x),static_cast<int>(v2.posV2P.y),v2.color);
14         m_backBuffer->Cover(static_cast<int>(v3.posV2P.x),static_cast<int>(v3.posV2P.y),v3.color);
15         if(mode==Wire)
16         {
17             bresenham(v1,v2);
18             bresenham(v1,v3);
19             bresenham(v2,v3);
20         }
21         else if(mode==Fill)
22         {
23             edgeWalkingFillRasterization(v1,v2,v3,1);
24             //edgeWalkingFillRasterization(v1,v2,v3,2);
25         }
26     }
27 }

 

  解析一下上面的代碼,在進入管線之后我們要把相機參數給到對應的shader中,然后執行頂點着色器,完成空間變換。此時得到的V2F頂點位於裁剪空間當中,為了得到它們的屏幕坐標,首先要做一下齊次除法(直接除以w,即從裁剪空間變換到ndc空間),然后再乘一下從裁剪空間到屏幕空間中轉化的矩陣viewPortMatrix。乘完之后,就得到了每個點的屏幕坐標。要注意的是,qt會對超出屏幕范圍的點進行取模運算,這不是我們想要的結果,而且對於超出范圍過多的點會導致爆棧,因此我們在光柵化的過程中,我們直接把屏幕之外的片元進行丟棄。

 

  好了,我們的二維轉三維的工作就完成了。接下來我們可以在renderRoute中自由定義一個mesh,裝一下自己喜歡的幾何體形狀,然后在while循環中每幀調用相機繞y軸旋轉的函數:

1 while(!stopped)
2     {
3         pipeline->clearBuffer(Vector4D(0,0,0,1.0f));
4         pipeline->drawIndex(Pipeline::Fill,camera);
5         pipeline->swapBuffer();
6         emit frameOut(pipeline->output());
7         camera->rotateY(0.01f);
8     }

 

  現在距離轉換還差最后一步:深度緩沖。因為我們只能看見離視角最近的內容,看不到遠方的被遮擋的,因此我們可以在framebuffer中定義一個深度緩沖,在每次寫入像素的時候都做一個如下的判斷:

if(current.posV2P.z<=m_backBuffer->depth[xx][yy])

  (PS:深度其實可以用屏幕坐標下的z值來表示)

 

  得到的效果如下:

 

紋理映射

  講道理,紋理映射本是一個很復雜的內容,但是因為前期的框架搭建比較良好+受cpu渲染性能影響沒辦法引入什么高端的紋理映射算法,因此這部分變得比較簡潔。首先話不多說,先定義一個紋理類Texture:

 1 #ifndef TEXTURE_H
 2 #define TEXTURE_H
 3 #include"vector2d.h"
 4 #include"vector4d.h"
 5 #include<QMainWindow>
 6 #include <QString>
 7 
 8 class Textures
 9 {
10 private:
11     int width,height,channel;
12     QImage *pixelBuffer;
13 public:
14     Textures();
15     ~Textures();
16     void loadImage(const QString &path);17     Vector4D sample(const Vector2D &texcoord);
18 };
19 
20 #endif // TEXTURE_H

 

  這些成員大家應該都比較熟悉了。簡單介紹下方法成員,loadImage是從你的磁盤中讀入圖片,sample就是根據傳入的uv坐標,在紋理圖上進行采樣並返回采樣結果。直接上.cpp文件:

 1 #include "textures.h"
 2 #include "QDebug"
 3 
 4 Textures::Textures()
 5 {
 6     width=0;
 7     height=0;
 8     channel=0;
 9     pixelBuffer=nullptr;
10 }
11 
12 void Textures::loadImage(const QString &path)
13 {
14     pixelBuffer=new QImage();
15     pixelBuffer->load(path);
16     width=pixelBuffer->width();
17     height=pixelBuffer->height();
18     channel=3;
19 }
20 
21 Vector4D Textures::sample(const Vector2D &texcoord)
22 {
23     Vector4D result(0.0,0.0,0.0,1.0);
24     unsigned int x = 0, y = 0;
25     double factorU = 0, factorV = 0;
26     if(texcoord.x >= 0.0f && texcoord.x <= 1.0f && texcoord.y >= 0.0f && texcoord.y <= 1.0f)
27     {
28         double trueU = texcoord.x * (width - 1);
29         double trueV = texcoord.y * (height - 1);
30         x = static_cast<unsigned int>(trueU);
31         y = static_cast<unsigned int>(trueV);
32         factorU = trueU - x;
33         factorV = trueV - y;
34     }
35     else
36     {
37         float u = texcoord.x,v = texcoord.y;
38         if(texcoord.x > 1.0f)
39             u = texcoord.x - static_cast<int>(texcoord.x);
40         else if(texcoord.x < 0.0f)
41             u = 1.0f - (static_cast<int>(texcoord.x) - texcoord.x);
42         if(texcoord.y > 1.0f)
43             v = texcoord.y - static_cast<int>(texcoord.y);
44         else if(texcoord.y < 0.0f)
45             v = 1.0f - (static_cast<int>(texcoord.y) - texcoord.y);
46 
47         double trueU = u * (width - 1);
48         double trueV = v * (height - 1);
49         x = static_cast<unsigned int>(trueU);
50         y = static_cast<unsigned int>(trueV);
51         factorU = trueU - x;
52         factorV = trueV - y;
53     }
54     Vector3D texels[4];
55 
56     pixelBuffer->pixelColor(x,y).red();
57     texels[0].x = static_cast<float>(pixelBuffer->pixelColor(x,y).red()) * 1.0f/255;
58     texels[0].y = static_cast<float>(pixelBuffer->pixelColor(x,y).green()) * 1.0f/255;
59     texels[0].z = static_cast<float>(pixelBuffer->pixelColor(x,y).blue()) * 1.0f/255;
75 
76     77     result = texels[0];
78 
79     return result;
80 }

 

  loadImage里讀取圖片的部分是Qt的固定寫法,sample函數主要做了兩部分:修正uv和采樣。修正uv主要指的是如果uv坐標超出了范圍,那么要自動進行循環拓展;采樣部分比較簡單,就是根據uv來對應到圖片中的像素。這里本來寫了雙線性紋理采樣,后來發現效果很不明顯,而且很吃性能,所以就給去掉了。

  那么如何讓頂點知道自己用的是哪張紋理圖呢?這里我們可以給頂點Vertex和V2F類都增加一個成員變量textureID,用來標識紋理,然后在Shader中把紋理存起來,在片元着色器中根據頂點的textureID編號來選擇要采樣的紋理:

 1 BasicShader::BasicShader()
 2 {
 3     Mm2w.normalize();
 4     Mw2v.normalize();
 5     Mv2p.normalize();
 6     tex1->loadImage("D:/ice.jpg");
 7     tex2->loadImage("D:/metal.jpg");
 8     tex3->loadImage("D:/floor.jpg");
 9     tex4->loadImage("D:/wall.jpg");
10 }
11 
12 Vector4D BasicShader::fragmentShader(const V2F &in){
13     Vector4D retColor;
14     if(in.textureID==1)
15         retColor = tex1->sample(in.texcoord);
16     if(in.textureID==2)
17         retColor = tex2->sample(in.texcoord);
18     if(in.textureID==3)
19         retColor = tex3->sample(in.texcoord);
20     if(in.textureID==4)
21         retColor = tex4->sample(in.texcoord);
22     return retColor;
23 }

 

  最后我們可以得到一個這樣的效果:

 

  怎么感覺正方體浮在表面上?仔細觀察你會發現地板的紋理在來回扭動,使得正方體的變化和地板的紋理沒有對應上,就會給人一種正方體浮在表面上的感覺。這個原因很簡單,那就是在投影視角下,uv不再是線性的了。這個問題的解決方式很簡單:在頂點着色器中,我們給texcoord除以一下w,這時候uv才會呈線性,此時我們再進行sample采樣就能得到正確的結果。(在除以w之后,還要把w記錄下來,然后在scanLinePerRow中別忘了再把w乘回來。如果你不在頂點着色器中記錄w,渲染管線會在變換到ndc空間時把w置1,之后你再獲取w就全是1了)

 

  現在紋理正常了。

 

收尾——光照

  最后我們來看一下光照模型。這里我們直接使用簡易的蘭伯特模型(因為性能實在罩不住了,太卡了)我們定義一個light類:

 1 #ifndef LIGHT_H
 2 #define LIGHT_H
 3 #include "vector4d.h"
 4 
 5 class Light
 6 {
 7 public:
 8     int kind;
 9     float intensity;
10     Vector4D m_pos;
11     Vector4D m_col;
12 public:
13     Light(){}
14     Light(Vector4D pos,Vector4D col,int k,float i){kind=k;intensity=i;m_col=col;m_pos=pos;}
15     Light(Light *l){kind=l->kind;intensity=l->intensity;m_col=l->m_col;m_pos=l->m_pos;}
16     ~Light(){}
17 };
18 
19 #endif // LIGHT_H

 

  光照主要就是這幾個屬性:光照類型(已廢棄)、光照強度、光源位置、光源顏色。方法只有個構造函數,.cpp文件直接空白。

  接下來我們需要在renderRoute中把光源初始化出來,然后改寫一下pipeline中drawIndex方法,把光源的指針傳入到渲染管線,然后渲染管線再將光源傳入到shader中,注意這步的傳入並不是用的函數傳參,而是我直接在shader中定義了一個光源成員變量,渲染管線只需要把這個成員變量修改一下即可。(這幾步零零散散的改了好幾處,而且每處都是一兩句代碼,所以我就不放具體修改的代碼了)

  接下來我們開始編寫片元着色器,代碼如下:

 1 Vector4D BasicShader::fragmentShader(const V2F &in){
 2     Vector4D retColor;
 3     if(in.textureID==1)
 4         retColor = tex1->sample(in.texcoord);
 5     if(in.textureID==2)
 6         retColor = tex2->sample(in.texcoord);
 7     if(in.textureID==3)
 8         retColor = tex3->sample(in.texcoord);
 9     if(in.textureID==4)
10         retColor = tex4->sample(in.texcoord);
11     Vector4D n=Vector4D(in.normal.x,in.normal.y,in.normal.z,1);
12     Vector4D l=lights->m_pos-in.posM2W;
13     n.normalize();
14     l.normalize();
15     Vector4D last=Vector4D(lights->m_col.x*retColor.x,lights->m_col.y*retColor.y,lights->m_col.z*retColor.z,1);
16     float tmp=n.dot(l);
17     if(tmp<0)tmp=0;
18     tmp=powf(tmp,0.8f);
19     if(tmp>0.2f)
20         retColor=last*tmp;
21     else
22         retColor=last*0.2f;
23     tmp*=lights->intensity;
24     retColor.w=1;
25     return retColor;
26 }

 

  首先我們獲取到片元的法線,然后獲取光源到片元的向量,接下來便可以使用蘭伯特模型公式:

  Ild = k*I*(N·L)

  其中N表示頂點法向量,L表示從頂點指向光源位置的單位向量(主意指向,不要弄反了)。之后我們再乘一下光照強度,然后再根據效果調整一下強度曲線即可。

 

  以下是最終效果圖:

 

  至此,軟渲染器的制作完成。

 


免責聲明!

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



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