0%

【光栅化渲染器】(二)框架搭建

上一节完成了环境配置和测试,这一节开始搭建一个渲染管线的框架,之后就都在此框架上加入各种功能和算法。

1 统一管理全局变量

首先我们使用一个头文件 Global.h 来统一管理我们用到的所有头文件、全局变量和类声明,为了之后使用方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#pragma once
#ifndef GLOBEL_H
#define GLOBEL_H

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <cstdlib>
#include <cmath>
#include <vector>

// MVP变换矩阵
glm::mat4 ModelMatrix;
glm::mat4 ViewMatrix;
glm::mat4 ProjectMatrix;
// 视口变换矩阵
glm::mat4 ViewPortMatrix;
// 法线变换矩阵
glm::mat3 NormalMatrix;

const float PI = 3.14159265359;
// 环境光
const glm::vec3 Ambient = glm::vec3(0.5, 0.5, 0.5);

// 相机类
class Camera;
// 渲染管线类
class Draw;
// 材质类
class Material;

// 平行光
class DirectionLight;
// 聚光灯
class SpotLight;
// 点光源
class PointLight;

#endif

2 FrameBuffer 类

和光线追踪器中一样,我们使用一个 unsigned char 数组来存放像素,但这次我们使用 RGBA 四个通道。为了保证整个代码结构清晰,我们封装一个 FrameBuffer 类来管理颜色缓冲,后续还可以加入深度缓冲。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*
* FrameBuffer 类,管理颜色缓冲和深度缓冲
*/
#pragma once
#include "Global.h"

class FrameBuffer {
public:
int Width, Height;
std::vector<unsigned char> colorBuffer;
~FrameBuffer() = default;

FrameBuffer(const int& w = 800, const int& h = 600) {
Width = w;
Height = h;
//RGBA四个通道,数组大小为宽*高*4
colorBuffer.resize(w * h * 4, 0);
}

// 重设缓冲区大小
void Resize(const int& w, const int& h) {
Width = w;
Height = h;
colorBuffer.resize(w * h * 4, 0);
}

// 使用给定的颜色清空颜色缓冲,不要使用vector下标访问,会很慢,直接使用指针操作
void ClearColorBuffer(const glm::vec4& color) {
unsigned char* p = colorBuffer.data();
for (int i = 0; i < Width * Height * 4; i += 4) {
*(p + i) = (unsigned char)color.r;
*(p + i + 1) = (unsigned char)color.g;
*(p + i + 2) = (unsigned char)color.b;
*(p + i + 3) = (unsigned char)color.a;
}
}

// 将颜色写入对应位置
void WritePoint(const int& x, const int& y, const glm::vec4& color) {
if (x < 0 || x >= Width || y < 0 || y >= Height)
return;
int xy = (y * Width + x);
unsigned char* p = colorBuffer.data();
*(p + xy * 4) = (unsigned char)color.r;
*(p + xy * 4 + 1) = (unsigned char)color.g;
*(p + xy * 4 + 2) = (unsigned char)color.b;
*(p + xy * 4 + 3) = (unsigned char)color.a;
}
};

3 管理顶点数据

顶点是我们光栅化渲染器的输入数据,所以理所应当用一个类来管理。一个顶点包含的数据有:模型坐标、顶点颜色、顶点法线、纹理坐标。注意位置坐标使用的是四维齐次坐标,在模型空间和世界空间的坐标都是 x, y, z 加上恒为 1 的 w,所以要用四维向量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
* 顶点Vertex类和
*/
#pragma once
#ifndef VERTEX_H
#define VERTEX_H

#include "Global.h"

class Vertex {
public:
glm::vec4 position;
glm::vec4 color;
glm::vec2 texcoord;
glm::vec3 normal;

Vertex() = default;
~Vertex() = default;

Vertex(
const glm::vec4& _pos,
const glm::vec4& _color,
const glm::vec2& _tex,
const glm::vec3& _normal
) :
position(_pos), color(_color), texcoord(_tex), normal(_normal) {}

Vertex(
const glm::vec3& _pos,
const glm::vec4& _color = glm::vec4(0, 0, 0, 0),
const glm::vec2& _tex = glm::vec2(0, 0),
const glm::vec3& _normal = glm::vec3(0, 0, 1)
) :
position(_pos, 1.0f), color(_color), texcoord(_tex), normal(_normal) {}

Vertex(const Vertex& v) :position(v.position), color(v.color), texcoord(v.texcoord), normal(v.normal) {}
};

#endif

4 渲染管线思路

输入输出都定义完了,接下来开始一步一步实现渲染管线。通常的 OpenGL 渲染管线流程如下:

  1. 输入顶点数据和图元类型(点、直线、三角形等基本图元)
  2. 顶点着色器对顶点进行处理,将坐标变换到世界坐标,计算纹理坐标和顶点颜色等,输出到中间结构体(v2f)
  3. 对 v2f 进行图元装配过程,也就是为每个三角形指定顶点数据与索引
  4. 将顶点变换到摄像机的观察空间
  5. 进行投影,将顶点变换到裁剪空间
  6. 进行裁剪和面剔除工作,将看不见的图元进行裁剪,剔除背向面,减少后续计算量
  7. 执行齐次除法,将顶点变换到 NDC(标准设备坐标)
  8. 执行视口变换,最终将顶点转换到屏幕坐标(从三维变成二维)
  9. 光栅化,计算图形在屏幕上最终覆盖的像素点
  10. 用顶点数据插值,在像素点位置生成新的 v2f
  11. 逐像素运行片元着色器,进行纹理采样、光照计算等,输出该点最终颜色值(RGBA)
  12. 执行透明度测试->模板测试->深度测试,丢弃掉一些片元
  13. 执行混合操作

5 V2F 类

上述过程中,一个重要的结构体是 v2f,因此首先我们需要定义一个 v2f 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#pragma once
#ifndef V2F_H
#define V2F_H

#include "Global.h"
#include "math.h"

class V2F {
public:
glm::vec4 worldPos;
glm::vec4 windowPos;
glm::vec4 color;
glm::vec2 texcoord;
glm::vec3 normal;

glm::mat3 TBN;

float Z;

V2F() = default;
~V2F() = default;
V2F(
const glm::vec4& _wPos,
const glm::vec4& _pPos,
const glm::vec4& _color,
const glm::vec2& _tex,
const glm::vec3& _normal,
const glm::mat3& _tbn
) :
worldPos(_wPos), windowPos(_pPos), color(_color), texcoord(_tex), normal(_normal), TBN(_tbn) {}
V2F(const V2F& v) :
worldPos(v.worldPos), windowPos(v.windowPos), color(v.color), texcoord(v.texcoord), normal(v.normal), TBN(v.TBN), Z(v.Z) {}

//两个顶点之间的插值
static V2F lerp(const V2F& v1, const V2F& v2, const float& factor) {
V2F result;
result.windowPos = Lerp(v1.windowPos, v2.windowPos, factor);
result.worldPos = Lerp(v1.worldPos, v2.worldPos, factor);
result.color = Lerp(v1.color, v2.color, factor);
result.normal = Lerp(v1.normal, v2.normal, factor);
result.texcoord = Lerp(v1.texcoord, v2.texcoord, factor);

result.TBN = v1.TBN;

result.Z = Lerp(v1.Z, v2.Z, factor);

return result;
}
};

#endif

V2F 中一个重要的操作就是插值,为此我们新建一个 math.h 文件来存放需要用到的数学操作,首先是线性插值函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#pragma once

#ifndef MATH_H
#define MATH_H

#include"Global.h"

//插值
glm::vec4 Lerp(const glm::vec4& v1, const glm::vec4& v2, float factor) {
return (1.0f - factor) * v1 + factor * v2;
}
glm::vec3 Lerp(const glm::vec3& v1, const glm::vec3& v2, float factor) {
return (1.0f - factor) * v1 + factor * v2;
}
glm::vec2 Lerp(const glm::vec2& v1, const glm::vec2& v2, float factor) {
return (1.0f - factor) * v1 + factor * v2;
}

float Lerp(const float& f1, const float& f2, float factor) {
return (1.0f - factor) * f1 + factor * f2;
}

#endif

6 Shader 类

有了上面这些准备,现在我们可以用顶点着色器来对顶点进行处理了。先从简单的情况开始,我们首先渲染一个二维图形,因为画二维图形不需要三维到二维的变换,所以三个变换矩阵都置为单位矩阵,实际上等于没做变换直接输出。片元着色器也是直接将调用点的颜色进行输出即可。我们同样封装一个 Shader 类来管理顶点和片元着色器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/*
* 着色器 Shader 类
*/
#pragma once
#ifndef SHADER_H
#define SHADER_H

#include "V2F.h"
#include "Vertex.h"

class Shader {
public:
Shader()
{
ModelMatrix = glm::mat4(1.0f);
ViewMatrix = glm::mat4(1.0f);
ProjectMatrix = glm::mat4(1.0f);
};

virtual ~Shader() = default;

private:
glm::mat4 ModelMatrix;
glm::mat4 ViewMatrix;
glm::mat4 ProjectMatrix;

public:
// 顶点着色器
virtual V2F VertexShader(const Vertex& a2v) {
V2F o;
// 变换到世界空间
o.worldPos = ModelMatrix * a2v.position;
// 变换到裁剪空间
o.windowPos = ProjectMatrix * ViewMatrix * o.worldPos;
// 法线变换
o.normal = glm::normalize(NormalMatrix * a2v.normal);
o.texcoord = a2v.texcoord;
o.color = a2v.color;
return o;
}
// 片元着色器
virtual glm::vec4 FragmentShader(const V2F& v) {
return v.color;
}

void setModelMatrix(const glm::mat4& model) {
ModelMatrix = model;
}
void setViewMatrix(const glm::mat4& view) {
ViewMatrix = view;
}
void setProjectMatrix(const glm::mat4& project) {
ProjectMatrix = project;
}
};

#endif

7 视口变换

现在我们可以在主函数中定义是三个顶点,由于 OpenGL 的 NDC 的坐标范围是[-1,1),而我们目前并没有做任何的坐标变换,因此我们直接定义 NDC 下的顶点坐标:

1
2
3
4
5
6
7
8
9
10
Shader shader;
FrameBuffer FrontBuffer(SCR_WIDTH, SCR_HEIGHT);

Vertex V1(glm::vec3(-0.5, -0.5, 0), glm::vec4(255, 0, 0, 0));
Vertex V2(glm::vec3(0.5, -0.5, 0), glm::vec4(0, 255, 0, 0));
Vertex V3(glm::vec3(0, 0.5, 0), glm::vec4(0, 0, 255, 0));

V2F o1 = shader.VertexShader(V1);
V2F o2 = shader.VertexShader(V2);
V2F o3 = shader.VertexShader(V3);

现在我们得到了处理后的顶点,但还在 NDC 当中,需要进一步转化为屏幕空间中的坐标,即需要进行视口变换。视口变换做的操作是将 X, Y 坐标从 [-1,1) 映射到屏幕坐标 [0,w) 和 [0,h) 上,同时将原点从屏幕中间移到左下角。注意,在 OpenGL 中,左下角是原点,右上角是 (w,h) ,而 DirectX 中左上角是原点。在我们的 math.h 中定义视口变换矩阵,该矩阵是一个缩放+平移的矩阵:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// glm 的矩阵是行矩阵,而我们一般使用的是列矩阵,所以存放的时候要转置
// 行矩阵做变换是右乘 v * M ,列矩阵是左乘 M * v

//视口变换矩阵 ox oy是左下角的坐标 从[-1,1]的 NDC 变换到屏幕坐标 [0,0],[w,h]
// Vp = [ w/2 , 0 , 0 , ox+w/2 ,
// 0 , h/2 , 0 , oy+h/2 ,
// 0 , 0 , 1 , 0 ,
// 0 , 0 , 0 , 1 ]
glm::mat4 GetViewPortMatrix(int ox, int oy, int width, int height) {

glm::mat4 result = glm::mat4(1.0f);
result[0][0] = width / 2.0f;
result[3][0] = ox + (width / 2.0f);
result[1][1] = height / 2.0f;
result[3][1] = oy + (height / 2.0f);
return result;
}

然后修改主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取视口变换矩阵
glm::mat4 ViewPortMatrix = GetViewPortMatrix(0, 0, SCR_WIDTH, SCR_HEIGHT);

Shader shader;
FrameBuffer FrontBuffer(SCR_WIDTH, SCR_HEIGHT);

Vertex V1(glm::vec3(-0.5, -0.5, 0), glm::vec4(255, 0, 0, 0));
Vertex V2(glm::vec3(0.5, -0.5, 0), glm::vec4(0, 255, 0, 0));
Vertex V3(glm::vec3(0, 0.5, 0), glm::vec4(0, 0, 255, 0));

V2F o1 = shader.VertexShader(V1);
V2F o2 = shader.VertexShader(V2);
V2F o3 = shader.VertexShader(V3);

// 视口变换
o1.windowPos = ViewPortMatrix * o1.windowPos;
o2.windowPos = ViewPortMatrix * o2.windowPos;
o3.windowPos = ViewPortMatrix * o3.windowPos;

8 光栅化

接下来需要计算我们的三角形覆盖了哪些屏幕像素,这里使用经典的扫描线算法。其思想很简单,从三角形最上面的点开始往下逐步画横线,两个交点之间的区域就是覆盖的区域。

image-20220519162253946

朝向下侧的三角形原理也是一样的,只不过是对称过来了。有了这两种三角形,不难发现任意三角形都能最多分为一个平顶和一个平底三角形,如下图:

image-20220519162338364

于是我们得到一般三角形的光栅化方法:

  1. 根据三个顶点的 y 坐标判定是否有两个相等,有则判断是平底还是平顶三角形,直接画
  2. 找到 y 值在中间的点,划分出上下两个三角形,画两个

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/*
* 扫描线算法
*/
void ScanLineTriangle(const V2F& v1, const V2F& v2, const V2F& v3) {

std::vector<V2F> arr = { v1, v2, v3 };
//对顶点根据 y 坐标排序,arr[0] 在最下面 arr[2]在最上面
if (arr[0].windowPos.y > arr[1].windowPos.y) {
V2F tmp = arr[0];
arr[0] = arr[1];
arr[1] = tmp;
}
if (arr[1].windowPos.y > arr[2].windowPos.y) {
V2F tmp = arr[1];
arr[1] = arr[2];
arr[2] = tmp;
}
if (arr[0].windowPos.y > arr[1].windowPos.y) {
V2F tmp = arr[0];
arr[0] = arr[1];
arr[1] = tmp;
}

//中间跟上面的 y 相等,是底三角形
if (equal(arr[1].windowPos.y, arr[2].windowPos.y)) {
DownTriangle(arr[1], arr[2], arr[0]);
}//否则是顶三角形
else if (equal(arr[1].windowPos.y, arr[0].windowPos.y)) {
UpTriangle(arr[1], arr[0], arr[2]);
}
// 其他情况划分为两个三角形
else {
float weight = (arr[2].windowPos.y - arr[1].windowPos.y) / (arr[2].windowPos.y - arr[0].windowPos.y);
V2F newEdge = V2F::lerp(arr[2], arr[0], weight);
UpTriangle(arr[1], newEdge, arr[2]);
DownTriangle(arr[1], newEdge, arr[0]);
}
}

void UpTriangle(const V2F& v1, const V2F& v2, const V2F& v3) {
// 分为左、右、上三个顶点
V2F left, right, top;
left = v1.windowPos.x > v2.windowPos.x ? v2 : v1;
right = v1.windowPos.x > v2.windowPos.x ? v1 : v2;
top = v3;
// 对左顶点 x 坐标取整
left.windowPos.x = int(left.windowPos.x);
// y 的垂直跨度,用于插值系数的计算
int dy = top.windowPos.y - left.windowPos.y;
//从上往下插值
int nowY = top.windowPos.y;
for (int i = dy; i >= 0; --i) {
float weight = 0;
if (dy != 0) {
weight = float(i) / dy;
}
V2F newLeft = V2F::lerp(left, top, weight);
V2F newRight = V2F::lerp(right, top, weight);
newLeft.windowPos.x = int(newLeft.windowPos.x);
newRight.windowPos.x = int(newRight.windowPos.x + 0.5);
newLeft.windowPos.y = newRight.windowPos.y = nowY;
ScanLine(newLeft, newRight);
nowY--;
}
}

void DownTriangle(const V2F& v1, const V2F& v2, const V2F& v3) {
V2F left, right, bottom;
left = v1.windowPos.x > v2.windowPos.x ? v2 : v1;
right = v1.windowPos.x > v2.windowPos.x ? v1 : v2;
bottom = v3;

int dy = left.windowPos.y - bottom.windowPos.y;
//从上往下插值
int nowY = left.windowPos.y;
for (int i = 0; i < dy; ++i) {
float weight = 0;
if (dy != 0) {
weight = float(i) / dy;
}
V2F newLeft = V2F::lerp(left, bottom, weight);
V2F newRight = V2F::lerp(right, bottom, weight);
newLeft.windowPos.x = int(newLeft.windowPos.x);
newRight.windowPos.x = int(newRight.windowPos.x + 0.5);
newLeft.windowPos.y = newRight.windowPos.y = nowY;
ScanLine(newLeft, newRight);
nowY--;
}
}

void ScanLine(const V2F& left, const V2F& right) {
int length = right.windowPos.x - left.windowPos.x;
for (int i = 0; i < length; ++i) {
V2F v = V2F::lerp(left, right, (float)i / length);
v.windowPos.x = left.windowPos.x + i;
v.windowPos.y = left.windowPos.y;

FrontBuffer.WritePoint(v.windowPos.x, v.windowPos.y, shader.FragmentShader(v));
}
}

然后修改上一节的主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "Global.h"
#include "FrameBuffer.h"
#include "Shader.h"

Shader shader;
FrameBuffer FrontBuffer;

unsigned int SCR_WIDTH = 800;
unsigned int SCR_HEIGHT = 600;

int main()
{
// 获取视口变换矩阵
glm::mat4 ViewPortMatrix = GetViewPortMatrix(0, 0, SCR_WIDTH, SCR_HEIGHT);

FrontBuffer.Resize(SCR_WIDTH, SCR_HEIGHT);

Vertex V1(glm::vec3(-0.5, -0.5, 0), glm::vec4(255, 0, 0, 255));
Vertex V2(glm::vec3(0.5, -0.5, 0), glm::vec4(0, 255, 0, 255));
Vertex V3(glm::vec3(0, 0.5, 0), glm::vec4(0, 0, 255, 255));

V2F o1 = shader.VertexShader(V1);
V2F o2 = shader.VertexShader(V2);
V2F o3 = shader.VertexShader(V3);

// 视口变换
o1.windowPos = ViewPortMatrix * o1.windowPos;
o2.windowPos = ViewPortMatrix * o2.windowPos;
o3.windowPos = ViewPortMatrix * o3.windowPos;

// 渲染并写出图片
FrontBuffer.ClearColorBuffer(glm::vec4(0, 0, 0, 255));
ScanLineTriangle(o1, o2, o3);

std::string filepath = "D:\\TechStack\\ComputerGraphics\\RenderEngine\\Results\\test.png";
stbi_write_png(filepath.c_str(), SCR_WIDTH, SCR_HEIGHT, 4, FrontBuffer.colorBuffer.data(), 0);
return 0;
}

得到的效果如下:

test

9 Draw 类

我们成功渲染出了二维图形,不过目前我们的渲染流程全都写在主函数中,需要一定的封装,于是我们定义一个 Draw 类来封装渲染过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#pragma once
#ifndef DRAW_H
#define DRAW_H

#include "Global.h"
#include "FrameBuffer.h"
#include "Shader.h"
#include "V2F.h"

class Draw {
private:
int Width;
int Height;
FrameBuffer* FrontBuffer;
Shader* shader;
glm::mat4 ViewPortMatrix;

public:
Draw(const int& w, const int& h) :
Width(w), Height(h), FrontBuffer(nullptr), shader(nullptr) {}
~Draw() {
if (FrontBuffer)
delete FrontBuffer;
if (shader)
delete shader;
FrontBuffer = nullptr;
shader = nullptr;
}

void setModelMatrix(const glm::mat4& model) {
shader->setModelMatrix(model);
}

void setViewMatrix(const glm::mat4& view) {
shader->setViewMatrix(view);
}

void setProjectMatrix(const glm::mat4& project) {
shader->setProjectMatrix(project);
}

// 初始化,设定帧缓冲区和 Shder
void Init() {
if (FrontBuffer)
delete FrontBuffer;
if (shader)
delete shader;
ViewPortMatrix = GetViewPortMatrix(0, 0, Width, Height);
FrontBuffer = new FrameBuffer(Width, Height);
shader = new Shader();
}

void Resize(const int& w, const int& h) {
Width = w;
Height = h;
FrontBuffer->Resize(w, h);
ViewPortMatrix = GetViewPortMatrix(0, 0, w, h);
}

void ClearBuffer(const glm::vec4& color) {
FrontBuffer->ClearColorBuffer(color);
}

void show(std::string& filepath) {
stbi_write_png(filepath.c_str(), Width, Height, 4, FrontBuffer->colorBuffer.data(), 0);
}

void DrawTriangle(const Vertex& v1, const Vertex& v2, const Vertex& v3) {
V2F o1 = shader->VertexShader(v1);
V2F o2 = shader->VertexShader(v2);
V2F o3 = shader->VertexShader(v3);
// 视口变换
o1.windowPos = ViewPortMatrix * o1.windowPos;
o2.windowPos = ViewPortMatrix * o2.windowPos;
o3.windowPos = ViewPortMatrix * o3.windowPos;
ScanLineTriangle(o1, o2, o3);
}

/*
****************** 扫描线算法 *******************
*/
void ScanLine(const V2F& left, const V2F& right) {
int length = right.windowPos.x - left.windowPos.x;
for (int i = 0; i < length; ++i) {
V2F v = V2F::lerp(left, right, (float)i / length);
v.windowPos.x = left.windowPos.x + i;
v.windowPos.y = left.windowPos.y;

FrontBuffer->WritePoint(v.windowPos.x, v.windowPos.y, shader->FragmentShader(v));
}
}

void UpTriangle(const V2F& v1, const V2F& v2, const V2F& v3) {
// 分为左、右、上三个顶点
V2F left, right, top;
left = v1.windowPos.x > v2.windowPos.x ? v2 : v1;
right = v1.windowPos.x > v2.windowPos.x ? v1 : v2;
top = v3;
// 对左顶点 x 坐标取整
left.windowPos.x = int(left.windowPos.x);
// y 的垂直跨度,用于插值系数的计算
int dy = top.windowPos.y - left.windowPos.y;
//从上往下插值
int nowY = top.windowPos.y;
for (int i = dy; i >= 0; --i) {
float weight = 0;
if (dy != 0) {
weight = float(i) / dy;
}
V2F newLeft = V2F::lerp(left, top, weight);
V2F newRight = V2F::lerp(right, top, weight);
newLeft.windowPos.x = int(newLeft.windowPos.x);
newRight.windowPos.x = int(newRight.windowPos.x + 0.5);
newLeft.windowPos.y = newRight.windowPos.y = nowY;
ScanLine(newLeft, newRight);
nowY--;
}
}

void DownTriangle(const V2F& v1, const V2F& v2, const V2F& v3) {
V2F left, right, bottom;
left = v1.windowPos.x > v2.windowPos.x ? v2 : v1;
right = v1.windowPos.x > v2.windowPos.x ? v1 : v2;
bottom = v3;

int dy = left.windowPos.y - bottom.windowPos.y;
//从上往下插值
int nowY = left.windowPos.y;
for (int i = 0; i < dy; ++i) {
float weight = 0;
if (dy != 0) {
weight = float(i) / dy;
}
V2F newLeft = V2F::lerp(left, bottom, weight);
V2F newRight = V2F::lerp(right, bottom, weight);
newLeft.windowPos.x = int(newLeft.windowPos.x);
newRight.windowPos.x = int(newRight.windowPos.x + 0.5);
newLeft.windowPos.y = newRight.windowPos.y = nowY;
ScanLine(newLeft, newRight);
nowY--;
}
}

void ScanLineTriangle(const V2F& v1, const V2F& v2, const V2F& v3) {

std::vector<V2F> arr = { v1, v2, v3 };
//对顶点根据 y 坐标排序,arr[0] 在最下面 arr[2]在最上面
if (arr[0].windowPos.y > arr[1].windowPos.y) {
V2F tmp = arr[0];
arr[0] = arr[1];
arr[1] = tmp;
}
if (arr[1].windowPos.y > arr[2].windowPos.y) {
V2F tmp = arr[1];
arr[1] = arr[2];
arr[2] = tmp;
}
if (arr[0].windowPos.y > arr[1].windowPos.y) {
V2F tmp = arr[0];
arr[0] = arr[1];
arr[1] = tmp;
}

//中间跟上面的 y 相等,是底三角形
if (equal(arr[1].windowPos.y, arr[2].windowPos.y)) {
DownTriangle(arr[1], arr[2], arr[0]);
}//否则是顶三角形
else if (equal(arr[1].windowPos.y, arr[0].windowPos.y)) {
UpTriangle(arr[1], arr[0], arr[2]);
}
// 其他情况划分为两个三角形
else {
float weight = (arr[2].windowPos.y - arr[1].windowPos.y) / (arr[2].windowPos.y - arr[0].windowPos.y);
V2F newEdge = V2F::lerp(arr[2], arr[0], weight);
UpTriangle(arr[1], newEdge, arr[2]);
DownTriangle(arr[1], newEdge, arr[0]);
}
}

};

#endif

然后主函数就变得非常简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "Global.h"
#include "Draw.h"

unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
const std::string OUT_PATH = "D:\\TechStack\\ComputerGraphics\\RenderEngine\\Results\\";
const std::string FILE_NAME = "test.png";

int main()
{
Draw dw(SCR_WIDTH, SCR_HEIGHT);
// 初始化渲染器
dw.Init();
// 三角形三个顶点
Vertex V1(glm::vec3(-0.5, -0.5, 0), glm::vec4(255, 0, 0, 128));
Vertex V2(glm::vec3(0.5, -0.5, 0), glm::vec4(0, 255, 0, 128));
Vertex V3(glm::vec3(0, 0.5, 0), glm::vec4(0, 0, 255, 128));
// 设定背景并画三角形,这次有透明效果
dw.ClearBuffer(glm::vec4(0, 0, 128, 128));
dw.DrawTriangle(V1, V2, V3);

// 写出图片
std::string filepath = OUT_PATH + FILE_NAME;
dw.show(filepath);

return 0;
}

最终的效果:

test

到此为止整个渲染器的框架就搭建完成,之后只需要在这个框架上添加算法和各种功能即可。

---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。