全网最简单的软渲染器

news2024/11/8 9:16:11

在这里插入图片描述

引言

  • 本文实现了一个包含矩阵变化、光栅化、面剔除、深度测试等功能的软渲染器。
  • 所谓软渲染器就是使用 CPU 渲染 3D 模型的程序。
  • 因此请记住我们的最终目的:将3D模型显示在屏幕上 。
  • 本文分为两个部分:预备知识、渲染器核心实现。预备知识概述了简化的渲染管线,渲染器核心实现将讨论核心算法,软渲染器完整的源代码请参见文末附录。

软光栅渲染效果预览

  • 如果您迫不及待体验下图中的渲染效果,可以直接转到附录中的工程代码构建运行。

正视效果图

在这里插入图片描述

侧视效果图

在这里插入图片描述

整体效果图

在这里插入图片描述

一、预备知识

  • 从计算机中二进制的模型数据到屏幕上五彩缤纷的渲染画面,我们需要知道这之间发生了什么。
  • 首先我们需要了解模型数据是怎样表示模型的,其次是模型如何进行移动、旋转之类的变换,最后是如何将模型显示在屏幕的一个个像素上。
  • 如果您已熟悉标题内容,可以选择直接查看核心代码和工程源文件。
  • 如果您不了解标题内容,请您参阅:GAMES101现代计算机图形学入门。
  • 如果您对渲染管线的工作流程觉得一知半解,请您参阅:渲染管线流程概述。

1.1 模型文件

  • 一个简单的立方体3D模型如下图所示:

在这里插入图片描述

  • 模型是什么?模型是以特定格式存储的文本文件,常见的存储格式有:OBJ、FBX、GLTF等。
  • 上图中模型包含了一个立方体网格和一个贴图,我们将一个固定的贴图贴在立方体不同的面上,就组成了这样一个生动地立方体模型。
  • 本文使用OBJ格式的模型文件,一个典型的OBJ文件信息如下:
# Blender v2.83.5 OBJ File: ''
# www.blender.org
mtllib Cube.mtl
o Cube
v 1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 1.000000
vt 0.625000 0.500000
vt 0.875000 0.500000
vt 0.875000 0.750000
vt 0.625000 0.750000
vt 0.375000 0.750000
vn 0.0000 1.0000 0.0000
vn 0.0000 0.0000 1.0000
vn -1.0000 0.0000 0.0000
vn 0.0000 -1.0000 0.0000
vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
usemtl Material
s off
f 1/1/1 5/2/1 7/3/1 3/4/1
f 4/5/2 3/4/2 7/6/2 8/7/2
f 8/8/3 7/9/3 5/10/3 6/11/3
f 6/12/4 2/13/4 4/5/4 8/14/4
  • 文件的前四行是说明数据,以v、vt或vn开头的行表示模型数据。
  • v开头的行表示一个顶点的位置信息,其后的3个浮点数表示顶点坐标的x、y、z值。
  • vt开头的行表示一个纹理坐标,其后紧跟的2个浮点数表示纹理坐标的u、v值。
  • vn开头的行表示一个法向量,其后紧跟的3个浮点数表示向量的x、y、z值。在计算机图形学中,法向量常用于表示三维模型的表面法线方向,是计算光照、阴影、碰撞检测等操作的重要参数
  • f 开头的行表示一个面,一个面可以包含多个顶点。上文文件中每个面f包含了4个顶点,每个顶点表示为:顶点位置索引 / 顶点纹理索引 / 顶点法向量索引。4个顶点组成的两个三角形表示出一个四边形。
  • v、vt和vn各自表示各自的信息,不具有顺序对应关系。当我们需要v、vt和vn时,我们只需要说明它是第几个,即提供它的索引信息。
  • 为了简洁的展现OBJ文件的格式,上文示例OBJ文件经过裁剪,原始文件见文末附录:完整的OBJ文件。

1.2 变换

  • 模型数据是位于局部坐标系下的,我们需要对其进行变换,变换都以矩阵的形式实现。
  • 下表列举了各种变换,下图展示了通过矩阵实现坐标变换。
变换名称意义
模型变换将模型从局部坐标系移动到世界坐标系
观察变换按照摄像机位置将世界空间转换到观察空间
投影变换按照视口参数将观察空间转换为标准投影立方体
视口变换按照屏幕大小将标准投影立方体转换为屏幕视口
  • 请参阅:计算机图形学笔记:3-5
    在这里插入图片描述

1.2.1 模型变换

  • 模型创作者们通常使用3D建模软件创作模型,他们并不关心模型的使用者会将模型放于游戏世界的何处,他们的工作是在三维空间的原点附近创作出炫酷的3D模型。
  • 我们称模型创作者建立的模型位于对象空间中,对象空间中的坐标系为局部坐标系,即OBJ文件中表示的信息都是在局部坐标系下。
  • 模型的使用者是游戏创作者,他们负责把模型放到宏大世界中的每一个角落,以使得整个游戏世界充实而丰富。我们称宏大的游戏世界为世界空间,世界空间中的坐标系为世界坐标系。
  • 在我们渲染3D模型时,如果直接渲染会导致所有模型位于一处,即坐标系原点。这是因为我们使用模型在局部坐标系下的位置坐标作为其在世界坐标系下的位置坐标,我们并不希望出现这样的画面,我们希望的是模型分散于世界空间中各个角落。
  • 因此我们需要对模型的坐标进行变换,使得变换后模型的位置移动到我们期望的位置,而这个变换是通过矩阵实现的,这个矩阵称为模型矩阵。模型矩阵负责将模型从局部坐标系变换到空间坐标系,它可以表示模型的缩放、旋转、平移。
  • 请参阅:模型矩阵原理推导
    在这里插入图片描述

1.2.2 观察变换

  • 在现实世界中我们使用眼睛观察整个世界,我们看到物体只因其处于我们的视野范围内,而游戏世界也同理,我们需要在游戏世界中使用一双"眼睛"来表示玩家的可见范围,这就是摄像机。
  • 摄像机具有位置和方向属性,位置表示了摄像机位于世界空间何处,方向表示摄像机指向哪个方向。在确定了摄像机的摆放状态后,我们对世界空间中的所有物体进行观察变换,将摄像机变换到标准位置,这将极大利于后面各种的计算。
  • 请参阅:观察矩阵原理推导
    在这里插入图片描述

1.2.3 投影变换

  • 我们不需要将整个游戏世界渲染在屏幕上,我们只需要将摄像机视野中的画面渲染在屏幕上,怎样得到摄像机视野中的画面呢?我们需要先定义摄像机的成像方式,这涉及两种不同的投影原理。
  • 我们人眼看世界会产生 “近大远小” 的效果,这种根据物体与摄像机之间距离进行投影的方式称为 “透视投影”。我们使用一个视锥来表示透视投影下的视野范围,视锥可参数化为:远近屏幕的距离n和f、垂直的可视角度fov、长宽比aspect。
  • 视锥的近平面表示要呈现画面的大小,通过 从透视到正交 变换将所有平面压缩为和近平面一样大,远处的物体因此会缩小。
  • 根据视锥的参数,我们可以推出 从透视到正交 的变换矩阵,此变换可将视锥的远平面压为和近平面一样大,即把视锥挤成一个立方体。
  • 把视锥挤成立方体后,再进行正交投影,即可将空间中的所有物体压缩到一个【-1,1】三次方的立方体中。
  • 请参阅:投影矩阵原理推导
    在这里插入图片描述

1.2.4 视口变换

  • 模型变换是为了进行模型的缩放、旋转和平移,观察变换是为了将摄像机移动到标准位置便于计算,投影变换是为了定义视野、成像方式并压缩物体到标准立方体。这些操纵模型数据的方式都不是我们的最终目的,我们的最终目的是将模型显示到屏幕上。
  • 经过最后的正交投影后,视野内的所有物体都被压缩到了【-1,1】三次方的立方体中,我们现在想要把它呈现在屏幕上。如作者电脑的分辨率为1920 * 1080,那么我们就需要将【-1,1】三次方的立方体中的内容映射到1920 * 1080的屏幕上,这需要进行视口变换。
  • 我们定义显示渲染画面的窗口为视口,依据视口的大小对标准立方体进行拉伸的变换被称为:视口变换。
  • 请参阅:视口矩阵原理推导
    在这里插入图片描述

1.3 小结

  • 通过上述过程我们了解了3D模型文件的格式、变换方法和流程。
  • 3D模型文件中定义了许多顶点信息和索引,我们通过模型变换将顶点位置、法向量等信息转变到世界空间中,使得我们的模型可以以多种姿态出现在世界空间的所有位置。
  • 摄像机定义了我们在什么位置、往什么方向看,而投影变换根据摄像机的视野锥体参数,返回给我们一个标准立方体。
  • 根据视口的大小,我们对标准立方体进行视口变换,得到了一个前后平面为视口大小的立方体。
  • 经过所有变换后需要显示的内容都位于一个立方体中,它的远近平面大小等于视口大小、远近平面上坐标的z值分别为-1和1。
  • 你可以选择忽略z值,这样立方体直接变成一个平面,这样就可以呈现一幅画面了。当然忽略z值会导致前后遮挡关系的错乱,如果你进行简单的深度测试,即仅保留最前面的画面,那么就可以得到完美画面了!
  • 经过上述过程,我们已经可以得到一幅完美的画面了,那么我们为什么还要继续?因为得到画面是理论上的,即理论上我们可以得到完美的画面,但是现实我们还没有把它展示出来,这涉及到现代设备的成像原理。
    在这里插入图片描述

1.4 光栅化

  • 屏幕是由离散的像素组成的,比如1920 * 1080表示横向分布1920个像素点,而纵向分布1080个像素点。每个像素可以显示出不同的颜色,上百万个像素点呈现出了您现在所看到的画面。
  • 当我们的模型数据经过上述所有过程后,虽然模型顶点信息可能发生变换,它依旧还是模型格式数据,即模型的每个面由三角形或四边形数据表示。我们无法直接提供给屏幕硬件这些数据,它无法识别这其中的含义,我们只能告诉屏幕硬件在某个像素点应该显示什么颜色。 我们现在拥有的并不比1.1中所具有的多,我们仅拥有变换后的v、vn和无需变换的vt、f 。
  • 什么时候需要渲染一个像素点?当像素点位于模型的某个三角形内时我们需要渲染像素点,当然这个"在三角形内"不用考虑z值,可以想象将三角形投影到近平面上,我们只需要检查近平面上的像素点是否在这个投影的三角形内即可。
  • 像素点可能数以百万,因此一个个像素点去检查是非常低效的,因为一个三角形只可能覆盖一块区域,因此我们检查模型中每一个三角形包含了哪些像素会更高效。这个将顶点数据转换为片元的过程就是光栅化,片元不仅包含某一像素,还包含这个像素所具有的世界坐标系位置、颜色、纹理坐标等。
  • 我们只具有顶点的数据,而顶点几乎不可能刚好在某个像素上,那么我们如何得到像素点的z坐标、颜色、纹理坐标等信息呢?这需要使用插值算法,而对于三角形图元来说,常用重心坐标进行插值计算。
  • 请参阅:三角形重心坐标插值原理
    在这里插入图片描述

1.5 深度测试

  • 当进行光栅化后,我们得到了一系列片段,片段都位于像素点的位置处,并且片段中记录了这个像素点经过插值后得到的坐标z值、纹理坐标、颜色值等信息。这样的片段会很多,因为一个像素可能被很多个三角形包含,因此一个像素点对应的片段常常有很多个,为了实现现实中的遮挡关系,我们必须仅留下最前面的片段,这就是深度测试。
  • 在深度测试中我们需要指定片段的丢弃方式,比如设定当一个片段的深度大于其他同位置处的片段时,我们就将这个片段进行丢弃,这就是深度测试。

二、渲染器核心实现

2.1 读取模型文件

// 网格类
struct Mesh
{
	// 存储顶点的位置数据、纹理数据、法向量数据、颜色数据,以及图元索引
	std::vector<Vector3<float>> positionBuffer;
	std::vector<Vector2<float>> uvBuffer;
	std::vector<Vector3<float>> normalBuffer;
	std::vector<Vector3<float>> colorBuffer;
	std::vector<Vector3<int>> indexBuffer;

	// 将s按照splitchar分隔为多个string存储在vec中
	void stringSplit(std::string s, char splitchar, std::vector<std::string>& vec)
	{
		if (vec.size() > 0)
			vec.clear();
		int length = s.length();
		int start = s[0] == splitchar ? 1 : 0;
		for (int i = 0; i < length; ++i)
		{
			if (s[i] == splitchar)
			{
				vec.push_back(s.substr(start, i - start));
				start = i + 1;
			}
			else if (i == length - 1)
				vec.push_back(s.substr(start, i + 1 - start));
		}
	}

	// 读取path地址中的obj文件
	void readObjFile(std::string path)
	{
		std::ifstream in(path);
		std::string txt = "";

		if (in)
		{
			while (std::getline(in, txt))
			{
				if (txt[0] == 'v' && txt[1] == ' ')
				{
					std::vector<std::string> num;
					txt.erase(0, 2);
					stringSplit(txt, ' ', num);
					Vector3<float> pos;
					pos = Vector3<float>((float)atof(num[0].c_str()), (float)atof(num[1].c_str()), (float)atof(num[2].c_str()));
					this->positionBuffer.push_back(pos);
				}
				else if (txt[0] == 'v' && txt[1] == 'n')
				{
					std::vector<std::string> num;
					txt.erase(0, 3);
					stringSplit(txt, ' ', num);
					Vector3<float> n = Vector3<float>((float)atof(num[0].c_str()), (float)atof(num[1].c_str()), (float)atof(num[2].c_str()), 0.0);
					this->normalBuffer.push_back(n);
				}
				else if (txt[0] == 'v' && txt[1] == 't')
				{
					std::vector<std::string> num;
					txt.erase(0, 3);
					stringSplit(txt, ' ', num);
					this->uvBuffer.push_back(Vector2<float>((float)atof(num[0].c_str()), (float)atof(num[1].c_str())));
				}
				else if (txt[0] == 'f' && txt[1] == ' ')
				{
					std::vector<std::string> num;
					txt.erase(0, 2);
					stringSplit(txt, ' ', num);
					for (int i = 0; i < num.size(); ++i)
					{
						std::vector<std::string> threeIndex;
						stringSplit(num[i], '/', threeIndex);
						Vector3<int> indexes = { atoi(threeIndex[0].c_str()) - 1, atoi(threeIndex[1].c_str()) - 1, atoi(threeIndex[2].c_str()) - 1 };
						this->indexBuffer.push_back(indexes);
					}
				}
			}
		}
		else
			std::cout << "no file" << std::endl;
	}

	// 打印网格中保存的数据信息
	void print()
	{
		std::cout << "Mesh data:" << std::endl;
		for (int i = 0; i < positionBuffer.size(); ++i)
		{
			std::cout << "v ";
			positionBuffer[i].print();
			std::cout << std::endl;
		}
		std::cout << std::endl;
		for (int i = 0; i < uvBuffer.size(); ++i)
		{
			std::cout << "vt ";
			uvBuffer[i].print();
			std::cout << std::endl;
		}
		std::cout << std::endl;
		for (int i = 0; i < normalBuffer.size(); ++i)
		{
			std::cout << "vn ";
			normalBuffer[i].print();
			std::cout << std::endl;
		}
		std::cout << std::endl;
		for (int i = 0; i <= indexBuffer.size() - 3; i += 3)
		{
			std::cout << "f ";
			indexBuffer[i].print();
			std::cout << " ";
			indexBuffer[i + 1].print();
			std::cout << " ";
			indexBuffer[i + 2].print();
			std::cout << std::endl;
		}
		std::cout << "end" << std::endl;
	}
};

2.2 变换矩阵的构造

// 矩阵类
template<typename T>
struct Matrix4
{
	Matrix4() {  }
	// 可变参数的构造方法
	Matrix4(const std::initializer_list<float>& list)
	{
		auto begin = list.begin();
		auto end = list.end();
		int i = 0, j = 0;
		while (begin != end)
		{
			data[i][j++] = *begin;
			if (j > 3)
			{
				++i;
				j = 0;
			}
			++begin;
		}
	}

	T data[4][4] = {};

	void Identity()
	{
		for (int i = 0; i < 4; ++i)
			data[i][i] = 1;
	}

	// 矩阵乘法
	Matrix4<T> operator * (const Matrix4<T>& right) const
	{
		Matrix4 res;
		for (int i = 0; i < 4; ++i)
		{
			for (int j = 0; j < 4; ++j)
			{
				for (int k = 0; k < 4; ++k)
				{
					res.data[i][j] += this->data[i][k] * right.data[k][j];
				}
			}
		}
		return res;
	}

	// 矩阵乘以顶点
	Vector3<T> operator * (const Vector3<T>& v) const
	{
		float x = v.x * data[0][0] + v.y * data[0][1] + v.z * data[0][2] + v.w * data[0][3];
		float y = v.x * data[1][0] + v.y * data[1][1] + v.z * data[1][2] + v.w * data[1][3];
		float z = v.x * data[2][0] + v.y * data[2][1] + v.z * data[2][2] + v.w * data[2][3];
		float w = v.x * data[3][0] + v.y * data[3][1] + v.z * data[3][2] + v.w * data[3][3];
		Vector3<float> returnValue(x, y, z, w);
		return returnValue;
	}

	// 获取model矩阵移动向量trs后的转换矩阵
	static Matrix4<T> get_model_matrix_translation(const Matrix4& model, const Vector3<float>& trs)
	{
		Matrix4 trsModel;
		trsModel.Identity();
		for (int i = 0; i < 3; i++)
			trsModel.data[i][3] = trs[i];
		return trsModel * model;
	}

	// 获取model矩阵缩放scale向量后的转换矩阵
	static Matrix4 get_model_matrix_scale(const Matrix4& model, const Vector3<float>& scale)
	{
		Matrix4 scaModel;
		scaModel.Identity();
		for (int i = 0; i < 3; i++)
			scaModel.data[i][i] = scale[i];
		return scaModel * model;
	}

	// 获取model矩阵绕X旋转rotation_angle后的转换矩阵
	static Matrix4 get_model_matrix_rotateX(const Matrix4& model, float rotation_angle)
	{
		rotation_angle = rotation_angle / 180 * PI;
		Matrix4 rotateModel;
		rotateModel.Identity();
		rotateModel.data[1][1] = cos(rotation_angle);
		rotateModel.data[1][2] = -sin(rotation_angle);
		rotateModel.data[2][1] = -rotateModel.data[1][2];
		rotateModel.data[2][2] = rotateModel.data[1][1];
		return rotateModel * model;
	}

	// 获取model矩阵绕Y轴旋转rotation_angle后的转换矩阵
	static Matrix4 get_model_matrix_rotateY(const Matrix4& model, float rotation_angle)
	{
		rotation_angle = rotation_angle / 180 * PI;
		Matrix4 rotateModel;
		rotateModel.Identity();
		rotateModel.data[0][0] = cos(rotation_angle);
		rotateModel.data[0][2] = sin(rotation_angle);
		rotateModel.data[2][0] = -rotateModel.data[0][2];
		rotateModel.data[2][2] = rotateModel.data[0][0];
		return rotateModel * model;
	}

	// 获取model矩阵绕Z轴旋转rotation_angle后的转换矩阵
	static Matrix4 get_model_matrix_rotateZ(const Matrix4& model, float rotation_angle)
	{
		rotation_angle = rotation_angle / 180 * PI;
		Matrix4 rotateModel;
		rotateModel.Identity();
		rotateModel.data[0][0] = cos(rotation_angle);
		rotateModel.data[0][1] = -sin(rotation_angle);
		rotateModel.data[1][0] = -rotateModel.data[0][1];
		rotateModel.data[1][1] = rotateModel.data[0][0];
		return rotateModel * model;
	}

	// 获取model矩阵绕axisT旋转轴rotation_angle后的转换矩阵(认为axisT是过原点的向量)
	static Matrix4 get_model_matrix_Rotate(const Matrix4& model,
		const Vector3<float>& axisT, float rotation_angle)
	{
		rotation_angle = rotation_angle / 180 * PI;

		Vector3<float> axis = axisT;
		axis.normalize();

		Matrix4 rotateModel;
		rotateModel.Identity();
		rotateModel = { (1 - cos(rotation_angle)) * (axis.x * axis.x) + cos(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.x * axis.y) - axis.z * sin(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.x * axis.z) + axis.y * sin(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.x * axis.y) + axis.z * sin(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.y * axis.y) + cos(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.y * axis.z) - axis.x * sin(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.x * axis.z) - axis.y * sin(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.y * axis.z) + axis.x * sin(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.z * axis.z) + cos(rotation_angle) };
		return rotateModel * model;
	}

	// 根据摄像机位置eye_pos、朝向front、上向量up获得观察矩阵
	static Matrix4 get_view_matrix(const Vector3<float>& eye_pos, const Vector3<float>& front,
		const Vector3<float>& up)
	{
		Matrix4 view;

		Matrix4 translate;
		translate = { 1, 0, 0, -eye_pos[0], 0, 1, 0, -eye_pos[1], 0, 0, 1,
			-eye_pos[2], 0, 0, 0, 1 };

		Matrix4 rotate;
		Vector3<float> gxt = Vector3<float>::Cross(front, up);

		rotate = { gxt[0], gxt[1], gxt[2], 0,
			up[0], up[1], up[2], 0,
			-front[0], -front[1], -front[2], 0,
			0, 0, 0, 1 };

		return rotate * translate;
	}

	// 根据摄影角度eye_fov、宽高比aspect_ratio、近平面距离zNear、远平面距离zFar获取投影矩阵
	static Matrix4 get_projection_matrix(float eye_fov, float aspect_ratio,
		float zNear, float zFar)
	{
		Matrix4 projection;
		float f, n, l, r, b, t, fov;
		fov = eye_fov / 180 * PI;
		n = -zNear; 
		f = zFar;
		t = tan(fov / 2) * zNear;
		b = -t;
		r = t * aspect_ratio;
		l = -r;

		// 从透视到正交 矩阵
		Matrix4 pertoorth;
		pertoorth = { n, 0, 0, 0,
			0, n, 0, 0,
			0, 0, n + f, -n * f,
			0, 0, 1, 0 };

		// 正交矩阵的位移部分
		Matrix4 orth1;
		orth1 = { 1, 0, 0, -(r + l) / 2,
			0, 1, 0, -(t + b) / 2,
			0, 0, 1, -(n + f) / 2,
			0, 0, 0, 1 };
		
		// 正交矩阵的缩放部分
		Matrix4 orth2;
		orth2 = { 2 / (r - l), 0, 0, 0,
			0, 2 / (t - b), 0, 0,
			0, 0, 2 / (n - f), 0,
			0, 0, 0, 1 };
		projection = orth2 * orth1 * pertoorth;
		return projection;
	}

	// 获取视口变换矩阵
	static Matrix4 get_viewport_matrix(float width, float height)
	{
		Matrix4 viewport;
		viewport.Identity();
		viewport.data[0][0] = width / 2;
		viewport.data[1][1] = height / 2;
		viewport.data[0][3] = width / 2;
		viewport.data[1][3] = height / 2;
		return viewport;
	}
	
	void Print()
	{
		std::cout << "-----------------Matrix Begin--------------" << std::endl;
		for (int i = 0; i < 4; ++i)
		{
			for (int j = 0; j < 4; ++j)
			{
				std::cout << "[" << data[i][j] << "]   ";
			}
			std::cout << std::endl;
		}
		std::cout << "-----------------Matrix End----------------" << std::endl;
	}
};

2.3 数学方法类

struct Myth
{
	// 将x限定在mi和ma中返回
	static float clampe(float x, float mi, float ma)
	{
		if (x < mi)x = mi;
		if (x > mi)x = ma;
		return x;
	}

	// 获取二维坐标p在三角形v123中的重心坐标
	template<typename T>
	static Vector3<T> centerOfGravity(const Vector3<T>& v1, const Vector3<T>& v2,
		const Vector3<T>& v3, const Vector2<int>& p)
	{
		if ((-(v1.x - v2.x) * (v3.y - v2.y) + (v1.y - v2.y) * (v3.x - v2.x)) == 0)
			return Vector3<T>(1, 0, 0);
		if (-(v2.x - v3.x) * (v1.y - v3.y) + (v2.y - v3.y) * (v1.x - v3.x) == 0)
			return Vector3<T>(1, 0, 0);
		float alpha = (-(p.x - v2.x) * (v3.y - v2.y) + (p.y - v2.y) * (v3.x - v2.x)) / (-(v1.x - v2.x) * (v3.y - v2.y) + (v1.y - v2.y) * (v3.x - v2.x));
		float beta = (-(p.x - v3.x) * (v1.y - v3.y) + (p.y - v3.y) * (v1.x - v3.x)) / (-(v2.x - v3.x) * (v1.y - v3.y) + (v2.y - v3.y) * (v1.x - v3.x));
		float gamma = 1 - alpha - beta;
		return Vector3<T>(alpha, beta, gamma);
	}

	// 获取三角形v123包围盒的左上角二维坐标
	template<typename T>
	static Vector2<float> get_leftTop(const Vector3<T>& v0, const Vector3<T>& v1, const Vector3<T>& v2)
	{
		return Vector2<float>(min(v0.x, min(v1.x, v2.x)), max(v0.y, max(v1.y, v2.y)));
	}

	// 获取三角形v123包围盒的右下角二维坐标
	template<typename T>
	static Vector2<float> get_rightBottom(const Vector3<T>& v0, const Vector3<T>& v1, const Vector3<T>& v2)
	{
		return Vector2<float>(max(v0.x, max(v1.x, v2.x)), min(v0.y, min(v1.y, v2.y)));
	}

	// 判断二维顶点pos是否在三角形pos123中
	static bool isInTriangle(const Vector3<float>& pos, const Vector3<float>& pos0,
		const Vector3<float>& pos1, const Vector3<float>& pos2)
	{

		// 三次叉乘
		Vector3<float> res1 = Vector3<float>::Cross((pos - pos0), (pos1 - pos0));

		Vector3<float> res2 = Vector3<float>::Cross((pos - pos1), (pos2 - pos1));

		Vector3<float> res3 = Vector3<float>::Cross((pos - pos2), (pos0 - pos2));

		// 要求叉积同向
		if (res1.z * res2.z > 0 && res1.z * res3.z > 0 && res2.z * res3.z > 0)
			return true;
		else
			return false;
	}

	static bool isInNDC(const Vector3<float>& pos)
	{
		return ((abs(pos.x) > 1) + (abs(pos.y) > 1) + (abs(pos.z) > 1)) != 3;
	}
};

2.4 图元

// 顶点数据类
struct VertexData
{
	VertexData() {}
	VertexData(Vector3<float> pos, Vector2<float> texCoor = Vector2<float>(0, 0), Vector3<float> nor = Vector3<float>(0, 0, 0, 1), Vector3<float> colr = Vector3<float>(255, 255, 255, 1)) :
		position(pos), uv(texCoor), normal(nor), color(colr) {}
	Vector3<float> position;
	Vector2<float> uv;
	Vector3<float> normal;
	Vector3<float> color;
};

// 三角形类
struct Triangle
{
	VertexData vertex[3];
	// 根据三角形顶点叉乘获得三角形的法向量,用来进行面剔除
	Vector3<float> getNormal()
	{
		Vector3<float> v1 = vertex[1].position - vertex[0].position;
		Vector3<float> v2 = vertex[2].position - vertex[1].position;
		return Vector3<float>::Cross(v2, v1);
	}
};


2.5 渲染管线

// 片段类:存储片段位置以及片段的各种信息
struct Pixel
{
	Pixel(VertexData data, Vector2<int> posi) :
		verdata(data), pos(posi) {}
	VertexData verdata;
	Vector2<int> pos;
};

// 摄像机类
class Camera
{
public:
	Camera() {}
	Camera(Vector3<float> posT, Vector3<float> frontT,
		Vector3<float> upT) :pos(posT), front(frontT), up(upT) {}
	Vector3<float> pos;
	Vector3<float> front;
	Vector3<float> up;
};

// 渲染管线类
class Render
{
public:
	// 定义视口的大小
	int width, height;
	Render(int screenWidth, int screenHeight) :width(screenWidth), height(screenHeight) {}

	// in_triangle存储从Mesh网格中提取的三角形
	std::vector<Triangle> in_triangle;
	// 提取Mesh中的三角形到in_triangle中
	void assemblingElements(const Mesh& mesh)
	{
		for (int i = 0; i <= mesh.indexBuffer.size() - 3; i += 3)
		{
			Triangle trian;
			for (int j = 0; j < 3; ++j)
			{
				trian.vertex[j].position = mesh.positionBuffer[mesh.indexBuffer[i + j].x];
				trian.vertex[j].uv = mesh.uvBuffer[mesh.indexBuffer[i + j].y];
				trian.vertex[j].normal = mesh.normalBuffer[mesh.indexBuffer[i + j].z];
				trian.vertex[j].color = mesh.colorBuffer[mesh.indexBuffer[i + j].x];
			}
			in_triangle.push_back(trian);
		}
	}

	// 存储顶点着色器需要使用的转换矩阵
	Matrix4<float> model, view, projection, viewport;
	void setMatrix(Matrix4<float> modelT, Matrix4<float> viewT,
		Matrix4<float> projectionT, Matrix4<float> viewportT)
	{
		model = modelT;
		view = viewT;
		projection = projectionT;
		viewport = viewportT;
	}

	// out_triangle存储in_triangle处理后得到的三角形数据
	std::vector<Triangle> out_triangle;
	Camera camera;

	// isBackCulling属性设定是否开启面剔除
	bool isBackCulling;
	// 背面剔除算法
	void backfaceCulling()
	{
		std::vector<Triangle>::iterator it = out_triangle.begin();
		while (it != out_triangle.end())
		{
			// 在NDC空间中,使用front点乘三角形法向量来判断面是否可见
			// 点乘为负说明夹角大于90°,则面不可见
			Vector3<float> v = camera.front;
			Vector3<float> n = (*it).getNormal();
			float value = v * n;
			if (value < 0)
				it = out_triangle.erase(it);
			else
				++it;
		}
	}
	
	// isViewClippping属性设定是否开启裁剪操作
	bool isViewClipping;
	// 裁剪算法
	void viewFrustumClipping()
	{
		std::vector<Triangle>::iterator it = out_triangle.begin();
		while (it != out_triangle.end())
		{
			// index判断三角形是否在NDC中
			int index = 0;
			// 判断三角形每个顶点是否在NDC中
			for (int j = 0; j < 3; ++j)
				index += Myth::isInNDC((*it).vertex[j].position);
			// 当三个顶点都不在NDC中时剔除这个三角形
			if (!index)
				it = out_triangle.erase(it);
			else
				++it;
		}
	}

	// 顶点着色器
	void vertexShader()
	{
		// 渲染管线从顶点着色器开始启动
		// 清除out_triangle中的内容,重新对原始三角形数据in_triangle进行转换
		out_triangle.clear();
		for (int i = 0; i < in_triangle.size(); ++i)
		{
			Triangle trans;
			trans = in_triangle[i];
			for (int j = 0; j < 3; ++j)
			{
				// 将三角形都转换到NDC空间
				trans.vertex[j].position = projection * view * model * in_triangle[i].vertex[j].position;
				// 投影后顶点w分量不为1,需要标准化
				trans.vertex[j].position.standard();
			}
			out_triangle.push_back(trans);
		}

		// 如果开启裁剪操作则进行裁剪
		if(isViewClipping)
			viewFrustumClipping();
		// 如果开启背面剔除则进行剔除
		if(isBackCulling)
			backfaceCulling();
			
		// 进行最后的视口变换
		for (int i = 0; i < out_triangle.size(); i++)
			for (int j = 0; j < 3; ++j)
				out_triangle[i].vertex[j].position =
				viewport * out_triangle[i].vertex[j].position;
	}

	// 定义像素vector存储所有片段信息
	std::vector<Pixel> pixels;
	// 定义二维顶点vector2之间的比较方法
	class map_key_comp
	{
	public:
		bool operator()(const Vector2<int>& lhs, const Vector2<int>& rhs)const
		{
			return lhs.x < rhs.x || (lhs.x == rhs.x && lhs.y < rhs.y);
		}
	};
	// 定义map存储每个像素的深度缓存
	std::map<Vector2<int>, float, map_key_comp> zBuffer;
	
	// isTestZ表示是否进行深度测试
	bool isTestZ;
	// 光栅化算法
	void rasterization()
	{
		// 清楚片段缓冲和深度缓冲信息
		pixels.clear();
		zBuffer.clear();
		
		// 遍历经过裁剪和剔除的所有三角形
		for (int i = 0; i < out_triangle.size(); ++i)
		{
			// 取出三角形顶点的位置
			Vector3<float> posArr[3] = {
				out_triangle[i].vertex[0].position ,
				out_triangle[i].vertex[1].position ,
				out_triangle[i].vertex[2].position };
		
			// 获取三角形的包围盒
			Vector2<float> leftTop = Myth::get_leftTop(posArr[0],posArr[1], posArr[2]);
			Vector2<float> rightBottom = Myth::get_rightBottom(posArr[0],posArr[1], posArr[2]);

			// 遍历包围盒中每一个像素
			for (int x = leftTop.x; x <= rightBottom.x; ++x)
			{
				for (int y = leftTop.y; y >= rightBottom.y; --y)
				{
					// 判断像素点xy是否在三角形内
					const Vector2<int> pixPos(x, y);
					bool isInTriangle = Myth::isInTriangle(Vector3<float>(x, y, 0), posArr[0], posArr[1], posArr[2]);
					// 如果像素点xy在三角形内
					if (isInTriangle)
					{
						// 获取像素点xy的重心坐标
						Vector3<float> abg = Myth::centerOfGravity(
							posArr[0], posArr[1], posArr[2], pixPos);
						float z = posArr[0].z * abg.x +
							posArr[1].z * abg.y + posArr[2].z * abg.z;
						
						// 如果存在像素点xy的深度信息
						if (zBuffer.count(pixPos))
						{
							// 则当且仅当新的像素点在更前面时更新深度缓冲zBuffer
							if (z <= zBuffer[pixPos])
								;
							else
								zBuffer[pixPos] = z;
						}
						else	// 如果不存在像素点xy的深度信息则直接记录深度
							zBuffer[pixPos] = z;
						
						// 差值像素点的各种属性
						Vector3<float> color = out_triangle[i].vertex[0].color * abg.x +
							out_triangle[i].vertex[1].color * abg.y + out_triangle[i].vertex[2].color * abg.z;
						Vector3<float> normal = out_triangle[i].vertex[0].normal * abg.x +
							out_triangle[i].vertex[1].normal * abg.y + out_triangle[i].vertex[2].normal * abg.z;
						Vector2<float> uv = out_triangle[i].vertex[0].uv * abg.x +
							out_triangle[i].vertex[1].uv * abg.y + out_triangle[i].vertex[2].uv * abg.z;
						// 创建像素xy在此三角形下的pixel数据压入pixels缓冲
						VertexData verdata(Vector3<float>(0, 0, z), uv, normal, color);
						pixels.push_back(Pixel(verdata, Vector2<int>(x, y)));
					}
				}
			}
		}
	}

	// 深度测试
	void testZ()
	{
		std::vector<Pixel>::iterator it = pixels.begin();
		while (it != pixels.end())
		{	// 如果片段深度大于其对应像素点的最小深度记录,则丢弃这个片段
			if ((*it).verdata.position.z < zBuffer[(*it).pos])
				it = pixels.erase(it);
			else
				++it;
		}
	}

	// 渲染管线:顶点着色器-光栅化-深度测试
	void renderingPipeline()
	{
		vertexShader();
		rasterization();
		if(isTestZ)
			testZ();
	}
};

三、附录

  • 工程使用EasyX进行绘制,可以下载EasyX安装其库。
  • ScreenWindow负责读取render的pixel片段缓冲进行绘制,如果您对哪里有疑问,欢迎评论!
  • 本系列还将继续完善此软光栅,如果您觉得本文有哪里欠缺,请一定不吝赐教,期待您的评论!

3.1 完整工程源代码

  • 按键控制摄像机移动,而摄像机移动方向和物体移动方向相反,因此你按D键物体会向左移动。
  • 请修改工程代码main函数中OBJ文件路径,您可以将3.2中OBJ文件复制保存到本地.txt文件中,使用.txt的文件地址替换OBJ文件路径即可。
#include <map>
#include <time.h>
#include <conio.h>
#include <vector>
#include <Windows.h>
#include <fstream>
#include <string>
#include <iostream>
#include <graphics.h>

#define PI 3.1415926535
#define DEBUG 1
const int screenWidth = 800, screenHeight = 600;

template<typename T>
struct Vector3
{
	T x, y, z, w;

	Vector3<T>() :x(0), y(0), z(0), w(1) {}
	Vector3<T>(T vx, T vy, T vz) : x(vx), y(vy), z(vz), w(1) {}
	Vector3<T>(T vx, T vy, T vz, T vw) :x(vx), y(vy), z(vz), w(vw) {}
	
	Vector3<T> operator*(const T right)const
	{
		return Vector3<T>(x * right, y * right, z * right);
	}
	T operator*(const Vector3<T> right)const
	{
		return this->x * right.x + this->y * right.y + this->z * right.z;
	}
	T operator[](int index)const
	{
		return index == 0 ? x : (index == 1 ? y : (index == 2 ? z : w));
	}
	Vector3<T> operator+(const Vector3<T>& right)const
	{
		return Vector3<T>(this->x + right.x, this->y + right.y, this->z + right.z);
	}
	Vector3<T> operator-()const
	{
		return Vector3<T>(-x, -y, -z);
	}
	Vector3<T> operator-(const Vector3<T>& right)const
	{
		return Vector3<T>(this->x - right.x, this->y - right.y, this->z - right.z);
	}
	static Vector3<T> Dot(const Vector3<T>& left, const Vector3<T>& right)
	{
		return left.x * right.x + left.y * right.y + left.z * right.z;
	}
	static Vector3<T> Cross(const Vector3<T>& left, const Vector3<T>& right)
	{
		return Vector3<T>(
			left.y * right.z - left.z * right.y,
			left.z * right.x - left.x * right.z,
			left.x * right.y - left.y * right.x
		);
	}

	float getLength()
	{
		return sqrt(x * x + y * y + z * z);
	}
	void normalize()
	{
		float length = getLength();
		if(length==0)
			return;
		x /= length;
		y /= length;
		z /= length;
	}
	void standard()
	{
		if (!w)
			return;
		x /= w;
		y /= w;
		z /= w;
		w = 1;
	}
	void print()
	{
		std::cout << "[" << x << "," << y << "," << z << "]";
	}
};
typedef Vector3<float> Color;

template<typename T>
struct Vector2
{
	Vector2<T>() :x(0), y(0) {}
	Vector2<T>(T vx, T vy) : x(vx), y(vy) {}
	T x, y;
	Vector2<T> operator+(const Vector2& right)
	{
		return Vector2(x + right.x, y + right.y);
	}
	Vector2<T> operator*(float value)
	{
		return Vector2(x * value, y * value);
	}
	void print()
	{
		std::cout << "[" << x << "," << y << "]";
	}
};

struct Mesh
{
	std::vector<Vector3<float>> positionBuffer;
	std::vector<Vector2<float>> uvBuffer;
	std::vector<Vector3<float>> normalBuffer;
	std::vector<Vector3<float>> colorBuffer;
	std::vector<Vector3<int>> indexBuffer;

	void stringSplit(std::string s, char splitchar, std::vector<std::string>& vec)
	{
		if (vec.size() > 0)
			vec.clear();
		int length = s.length();
		int start = s[0] == splitchar ? 1 : 0;
		for (int i = 0; i < length; ++i)
		{
			if (s[i] == splitchar)
			{
				vec.push_back(s.substr(start, i - start));
				start = i + 1;
			}
			else if (i == length - 1)
				vec.push_back(s.substr(start, i + 1 - start));
		}
	}

	void readObjFile(std::string path)
	{
		std::ifstream in(path);
		std::string txt = "";

		if (in)
		{
			while (std::getline(in, txt))
			{
				if (txt[0] == 'v' && txt[1] == ' ')
				{
					std::vector<std::string> num;
					txt.erase(0, 2);
					stringSplit(txt, ' ', num);
					Vector3<float> pos;
					pos = Vector3<float>((float)atof(num[0].c_str()), (float)atof(num[1].c_str()), (float)atof(num[2].c_str()));
					this->positionBuffer.push_back(pos);
				}
				else if (txt[0] == 'v' && txt[1] == 'n')
				{
					std::vector<std::string> num;
					txt.erase(0, 3);
					stringSplit(txt, ' ', num);
					Vector3<float> n = Vector3<float>((float)atof(num[0].c_str()), (float)atof(num[1].c_str()), (float)atof(num[2].c_str()), 0.0);
					this->normalBuffer.push_back(n);
				}
				else if (txt[0] == 'v' && txt[1] == 't')
				{
					std::vector<std::string> num;
					txt.erase(0, 3);
					stringSplit(txt, ' ', num);
					this->uvBuffer.push_back(Vector2<float>((float)atof(num[0].c_str()), (float)atof(num[1].c_str())));
				}
				else if (txt[0] == 'f' && txt[1] == ' ')
				{
					std::vector<std::string> num;
					txt.erase(0, 2);
					stringSplit(txt, ' ', num);
					for (int i = 0; i < num.size(); ++i)
					{
						std::vector<std::string> threeIndex;
						stringSplit(num[i], '/', threeIndex);
						Vector3<int> indexes = { atoi(threeIndex[0].c_str()) - 1, atoi(threeIndex[1].c_str()) - 1, atoi(threeIndex[2].c_str()) - 1 };
						this->indexBuffer.push_back(indexes);
					}
				}
			}
		}
		else
			std::cout << "no file" << std::endl;
	}

	void print()
	{
		std::cout << "Mesh data:" << std::endl;
		for (int i = 0; i < positionBuffer.size(); ++i)
		{
			std::cout << "v ";
			positionBuffer[i].print();
			std::cout << std::endl;
		}
		std::cout << std::endl;
		for (int i = 0; i < uvBuffer.size(); ++i)
		{
			std::cout << "vt ";
			uvBuffer[i].print();
			std::cout << std::endl;
		}
		std::cout << std::endl;
		for (int i = 0; i < normalBuffer.size(); ++i)
		{
			std::cout << "vn ";
			normalBuffer[i].print();
			std::cout << std::endl;
		}
		std::cout << std::endl;
		for (int i = 0; i <= indexBuffer.size() - 3; i += 3)
		{
			std::cout << "f ";
			indexBuffer[i].print();
			std::cout << " ";
			indexBuffer[i + 1].print();
			std::cout << " ";
			indexBuffer[i + 2].print();
			std::cout << std::endl;
		}
		std::cout << "end" << std::endl;
	}
};

template<typename T>
struct Matrix4
{
	Matrix4() {  }
	Matrix4(const std::initializer_list<float>& list)
	{
		auto begin = list.begin();
		auto end = list.end();
		int i = 0, j = 0;
		while (begin != end)
		{
			data[i][j++] = *begin;
			if (j > 3)
			{
				++i;
				j = 0;
			}
			++begin;
		}
	}

	T data[4][4] = {};

	void Identity()
	{
		for (int i = 0; i < 4; ++i)
			data[i][i] = 1;
	}

	Matrix4<T> operator * (const Matrix4<T>& right) const
	{
		Matrix4 res;
		for (int i = 0; i < 4; ++i)
		{
			for (int j = 0; j < 4; ++j)
			{
				for (int k = 0; k < 4; ++k)
				{
					res.data[i][j] += this->data[i][k] * right.data[k][j];
				}
			}
		}
		return res;
	}

	Vector3<T> operator * (const Vector3<T>& v) const
	{
		float x = v.x * data[0][0] + v.y * data[0][1] + v.z * data[0][2] + v.w * data[0][3];
		float y = v.x * data[1][0] + v.y * data[1][1] + v.z * data[1][2] + v.w * data[1][3];
		float z = v.x * data[2][0] + v.y * data[2][1] + v.z * data[2][2] + v.w * data[2][3];
		float w = v.x * data[3][0] + v.y * data[3][1] + v.z * data[3][2] + v.w * data[3][3];
		Vector3<float> returnValue(x, y, z, w);
		return returnValue;
	}

	static Matrix4<T> get_model_matrix_translation(const Matrix4& model, const Vector3<float>& trs)
	{
		Matrix4 trsModel;
		trsModel.Identity();
		for (int i = 0; i < 3; i++)
			trsModel.data[i][3] = trs[i];
		return trsModel * model;
	}
	static Matrix4 get_model_matrix_scale(const Matrix4& model, const Vector3<float>& scale)
	{
		Matrix4 scaModel;
		scaModel.Identity();
		for (int i = 0; i < 3; i++)
			scaModel.data[i][i] = scale[i];
		return scaModel * model;
	}
	static Matrix4 get_model_matrix_rotateX(const Matrix4& model, float rotation_angle)
	{
		rotation_angle = rotation_angle / 180 * PI;
		Matrix4 rotateModel;
		rotateModel.Identity();
		rotateModel.data[1][1] = cos(rotation_angle);
		rotateModel.data[1][2] = -sin(rotation_angle);
		rotateModel.data[2][1] = -rotateModel.data[1][2];
		rotateModel.data[2][2] = rotateModel.data[1][1];
		return rotateModel * model;
	}
	static Matrix4 get_model_matrix_rotateY(const Matrix4& model, float rotation_angle)
	{
		rotation_angle = rotation_angle / 180 * PI;
		Matrix4 rotateModel;
		rotateModel.Identity();
		rotateModel.data[0][0] = cos(rotation_angle);
		rotateModel.data[0][2] = sin(rotation_angle);
		rotateModel.data[2][0] = -rotateModel.data[0][2];
		rotateModel.data[2][2] = rotateModel.data[0][0];
		return rotateModel * model;
	}
	static Matrix4 get_model_matrix_rotateZ(const Matrix4& model, float rotation_angle)
	{
		rotation_angle = rotation_angle / 180 * PI;
		Matrix4 rotateModel;
		rotateModel.Identity();
		rotateModel.data[0][0] = cos(rotation_angle);
		rotateModel.data[0][1] = -sin(rotation_angle);
		rotateModel.data[1][0] = -rotateModel.data[0][1];
		rotateModel.data[1][1] = rotateModel.data[0][0];
		return rotateModel * model;
	}
	static Matrix4 get_model_matrix_Rotate(const Matrix4& model,
		const Vector3<float>& axisT, float rotation_angle)
	{
		rotation_angle = rotation_angle / 180 * PI;

		Vector3<float> axis = axisT;
		axis.normalize();

		Matrix4 rotateModel;
		rotateModel.Identity();
		rotateModel = { (1 - cos(rotation_angle)) * (axis.x * axis.x) + cos(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.x * axis.y) - axis.z * sin(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.x * axis.z) + axis.y * sin(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.x * axis.y) + axis.z * sin(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.y * axis.y) + cos(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.y * axis.z) - axis.x * sin(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.x * axis.z) - axis.y * sin(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.y * axis.z) + axis.x * sin(rotation_angle),
			(1 - cos(rotation_angle)) * (axis.z * axis.z) + cos(rotation_angle) };
		return rotateModel * model;
	}

	static Matrix4 get_view_matrix(const Vector3<float>& eye_pos, const Vector3<float>& front,
		const Vector3<float>& up)
	{
		Matrix4 view;

		Matrix4 translate;
		translate = { 1, 0, 0, -eye_pos[0], 0, 1, 0, -eye_pos[1], 0, 0, 1,
			-eye_pos[2], 0, 0, 0, 1 };

		Matrix4 rotate;
		Vector3<float> gxt = Vector3<float>::Cross(front, up);

		rotate = { gxt[0], gxt[1], gxt[2], 0,
			up[0], up[1], up[2], 0,
			-front[0], -front[1], -front[2], 0,
			0, 0, 0, 1 };

		return rotate * translate;
	}
	static Matrix4 get_projection_matrix(float eye_fov, float aspect_ratio,
		float zNear, float zFar)
	{
		Matrix4 projection;
		float f, n, l, r, b, t, fov;
		fov = eye_fov / 180 * PI;
		n = -zNear; 
		f = zFar;
		t = tan(fov / 2) * zNear;
		b = -t;
		r = t * aspect_ratio;
		l = -r;

		Matrix4 pertoorth;
		pertoorth = { n, 0, 0, 0,
			0, n, 0, 0,
			0, 0, n + f, -n * f,
			0, 0, 1, 0 };

		Matrix4 orth1;
		orth1 = { 1, 0, 0, -(r + l) / 2,
			0, 1, 0, -(t + b) / 2,
			0, 0, 1, -(n + f) / 2,
			0, 0, 0, 1 };

		Matrix4 orth2;
		orth2 = { 2 / (r - l), 0, 0, 0,
			0, 2 / (t - b), 0, 0,
			0, 0, 2 / (n - f), 0,
			0, 0, 0, 1 };
		projection = orth2 * orth1 * pertoorth;
		return projection;
	}

	static Matrix4 get_viewport_matrix(float width, float height)
	{
		Matrix4 viewport;
		viewport.Identity();
		viewport.data[0][0] = width / 2;
		viewport.data[1][1] = height / 2;
		viewport.data[0][3] = width / 2;
		viewport.data[1][3] = height / 2;
		return viewport;
	}
	void Print()
	{
		std::cout << "-----------------Matrix Begin--------------" << std::endl;
		for (int i = 0; i < 4; ++i)
		{
			for (int j = 0; j < 4; ++j)
			{
				std::cout << "[" << data[i][j] << "]   ";
			}
			std::cout << std::endl;
		}
		std::cout << "-----------------Matrix End----------------" << std::endl;
	}
};

struct VertexData
{
	VertexData() {}
	VertexData(Vector3<float> pos, Vector2<float> texCoor = Vector2<float>(0, 0), Vector3<float> nor = Vector3<float>(0, 0, 0, 1), Vector3<float> colr = Vector3<float>(255, 255, 255, 1)) :
		position(pos), uv(texCoor), normal(nor), color(colr) {}
	Vector3<float> position;
	Vector2<float> uv;
	Vector3<float> normal;
	Vector3<float> color;
};

struct Triangle
{
	VertexData vertex[3];
	Vector3<float> getNormal()
	{
		Vector3<float> v1 = vertex[1].position - vertex[0].position;
		Vector3<float> v2 = vertex[2].position - vertex[1].position;
		return Vector3<float>::Cross(v2, v1);
	}
};

struct Pixel
{
	Pixel(VertexData data, Vector2<int> posi) :
		verdata(data), pos(posi) {}
	VertexData verdata;
	Vector2<int> pos;
};

struct Myth
{
	static float clampe(float x, float mi, float ma)
	{
		if (x < mi)x = mi;
		if (x > mi)x = ma;
		return x;
	}

	template<typename T>
	static Vector3<T> centerOfGravity(const Vector3<T>& v1, const Vector3<T>& v2,
		const Vector3<T>& v3, const Vector2<int>& p)
	{
		if ((-(v1.x - v2.x) * (v3.y - v2.y) + (v1.y - v2.y) * (v3.x - v2.x)) == 0)
			return Vector3<T>(1, 0, 0);
		if (-(v2.x - v3.x) * (v1.y - v3.y) + (v2.y - v3.y) * (v1.x - v3.x) == 0)
			return Vector3<T>(1, 0, 0);
		float alpha = (-(p.x - v2.x) * (v3.y - v2.y) + (p.y - v2.y) * (v3.x - v2.x)) / (-(v1.x - v2.x) * (v3.y - v2.y) + (v1.y - v2.y) * (v3.x - v2.x));
		float beta = (-(p.x - v3.x) * (v1.y - v3.y) + (p.y - v3.y) * (v1.x - v3.x)) / (-(v2.x - v3.x) * (v1.y - v3.y) + (v2.y - v3.y) * (v1.x - v3.x));
		float gamma = 1 - alpha - beta;
		return Vector3<T>(alpha, beta, gamma);
	}

	template<typename T>
	static Vector2<float> get_leftTop(const Vector3<T>& v0, const Vector3<T>& v1, const Vector3<T>& v2)
	{
		return Vector2<float>(min(v0.x, min(v1.x, v2.x)), max(v0.y, max(v1.y, v2.y)));
	}

	template<typename T>
	static Vector2<float> get_rightBottom(const Vector3<T>& v0, const Vector3<T>& v1, const Vector3<T>& v2)
	{
		return Vector2<float>(max(v0.x, max(v1.x, v2.x)), min(v0.y, min(v1.y, v2.y)));
	}

	static bool isInTriangle(const Vector3<float>& pos, const Vector3<float>& pos0,
		const Vector3<float>& pos1, const Vector3<float>& pos2)
	{

		// 三次叉乘
		Vector3<float> res1 = Vector3<float>::Cross((pos - pos0), (pos1 - pos0));

		Vector3<float> res2 = Vector3<float>::Cross((pos - pos1), (pos2 - pos1));

		Vector3<float> res3 = Vector3<float>::Cross((pos - pos2), (pos0 - pos2));

		// 要求叉积同向
		if (res1.z * res2.z > 0 && res1.z * res3.z > 0 && res2.z * res3.z > 0)
			return true;
		else
			return false;
	}

	static bool isInNDC(const Vector3<float>& pos)
	{
		return ((abs(pos.x) > 1) + (abs(pos.y) > 1) + (abs(pos.z) > 1)) != 3;
	}
};
class Camera
{
public:
	Camera() {}
	Camera(Vector3<float> posT, Vector3<float> frontT,
		Vector3<float> upT) :pos(posT), front(frontT), up(upT) {}
	Vector3<float> pos;
	Vector3<float> front;
	Vector3<float> up;
};
class Render
{
public:
	int width, height;
	Render(int screenWidth, int screenHeight) :width(screenWidth), height(screenHeight) {}

	std::vector<Triangle> in_triangle;
	void assemblingElements(const Mesh& mesh)
	{
		for (int i = 0; i <= mesh.indexBuffer.size() - 3; i += 3)
		{
			Triangle trian;
			for (int j = 0; j < 3; ++j)
			{
				trian.vertex[j].position = mesh.positionBuffer[mesh.indexBuffer[i + j].x];
				trian.vertex[j].uv = mesh.uvBuffer[mesh.indexBuffer[i + j].y];
				trian.vertex[j].normal = mesh.normalBuffer[mesh.indexBuffer[i + j].z];
				trian.vertex[j].color = mesh.colorBuffer[mesh.indexBuffer[i + j].x];
			}
			in_triangle.push_back(trian);
		}
	}


	Matrix4<float> model, view, projection, viewport;
	void setMatrix(Matrix4<float> modelT, Matrix4<float> viewT,
		Matrix4<float> projectionT, Matrix4<float> viewportT)
	{
		model = modelT;
		view = viewT;
		projection = projectionT;
		viewport = viewportT;
	}

	std::vector<Triangle> out_triangle;
	Camera camera;
	bool isBackCulling;
	void backfaceCulling()
	{
#if DEBUG // 调试宏,如果需要调试可以在程序开头#define DEBUG 1 ,觉得碍眼可以删除
		std::cout << std::endl << "-----------------backfaceCulling Begin-----------------" << std::endl;
#endif
		std::vector<Triangle>::iterator it = out_triangle.begin();
		while (it != out_triangle.end())
		{
			Vector3<float> v = camera.front;
			Vector3<float> n = (*it).getNormal();
			float value = v * n;
			if (value < 0)
				it = out_triangle.erase(it);
			else
				++it;
		}
#if DEBUG
		std::cout << std::endl << "out:" << out_triangle.size() << std::endl;
		for (int i = 0; i < out_triangle.size(); ++i)
		{
			std::cout << "Triangle " << i << " :" << std::endl;
			out_triangle[i].vertex[0].position.print();
			std::cout  << std::endl;
			out_triangle[i].vertex[1].position.print();
			std::cout << std::endl;
			out_triangle[i].vertex[2].position.print();
			std::cout << std::endl;
		}
		std::cout << std::endl << "-----------------backfaceCulling End-----------------" << std::endl;
#endif
	}

	bool isViewClipping;
	void viewFrustumClipping()
	{
#if DEBUG
		std::cout << std::endl << "-----------------viewFrustumClipping Begin-----------------" << std::endl;
#endif
		std::vector<Triangle>::iterator it = out_triangle.begin();
		while (it != out_triangle.end())
		{
			int index = 0;
			for (int j = 0; j < 3; ++j)
				index += Myth::isInNDC((*it).vertex[j].position);
			if (!index)
				it = out_triangle.erase(it);
			else
				++it;
		}
#if DEBUG
		std::cout << std::endl << "out:" << out_triangle.size() << std::endl;
		for (int i = 0; i < out_triangle.size(); ++i)
		{
			std::cout << "Triangle " << i << " :" << std::endl;
			out_triangle[i].vertex[0].position.print();
			std::cout << std::endl;
			out_triangle[i].vertex[1].position.print();
			std::cout << std::endl;
			out_triangle[i].vertex[2].position.print();
			std::cout << std::endl;
		}
		std::cout << std::endl << "-----------------viewFrustumClipping End-----------------" << std::endl;
#endif
	}

	void vertexShader()
	{
#if DEBUG
		std::cout << std::endl << "-----------------vertexShader Begin-----------------" << std::endl;
		std::cout << std::endl << "in:" << in_triangle.size() << std::endl;
		for (int i = 0; i < in_triangle.size(); ++i)
		{
			std::cout << "Triangle " << i << " :" << std::endl;
			in_triangle[i].vertex[0].position.print();
			std::cout << std::endl;
			in_triangle[i].vertex[1].position.print();
			std::cout << std::endl;
			in_triangle[i].vertex[2].position.print();
			std::cout << std::endl;
		}
#endif
		out_triangle.clear();
		for (int i = 0; i < in_triangle.size(); ++i)
		{
			Triangle trans;
			trans = in_triangle[i];
			for (int j = 0; j < 3; ++j)
			{
				trans.vertex[j].position = projection * view * model * in_triangle[i].vertex[j].position;
				trans.vertex[j].position.standard();
			}
				
			out_triangle.push_back(trans);
		}


#if DEBUG
		std::cout << std::endl << "out:" << out_triangle.size() << std::endl;
		for (int i = 0; i < out_triangle.size(); ++i)
		{
			std::cout << "Triangle " << i << " :" << std::endl;
			out_triangle[i].vertex[0].position.print();
			std::cout << std::endl;
			out_triangle[i].vertex[1].position.print();
			std::cout << std::endl;
			out_triangle[i].vertex[2].position.print();
			std::cout << std::endl;
		}
		std::cout << std::endl << "-----------------vertexShader endmodel-----------------" << std::endl;
#endif
		if(isViewClipping)
			viewFrustumClipping();
		if(isBackCulling)
			backfaceCulling();
		for (int i = 0; i < out_triangle.size(); i++)
			for (int j = 0; j < 3; ++j)
				out_triangle[i].vertex[j].position =
				viewport * out_triangle[i].vertex[j].position;
#if DEBUG
		std::cout << std::endl << "out:" << out_triangle.size() << std::endl;
		for (int i = 0; i < out_triangle.size(); ++i)
		{
			std::cout << "Triangle " << i << " :" << std::endl;
			out_triangle[i].vertex[0].position.print();
			std::cout << std::endl;
			out_triangle[i].vertex[1].position.print();
			std::cout << std::endl;
			out_triangle[i].vertex[2].position.print();
			std::cout << std::endl;
		}
		std::cout << std::endl << "-----------------vertexShader end-----------------" << std::endl;
#endif
	}

	std::vector<Pixel> pixels;
	class map_key_comp
	{
	public:
		bool operator()(const Vector2<int>& lhs, const Vector2<int>& rhs)const
		{
			return lhs.x < rhs.x || (lhs.x == rhs.x && lhs.y < rhs.y);
		}
	};
	std::map<Vector2<int>, float, map_key_comp> zBuffer;
	
	bool isTestZ;
	void rasterization()
	{
#if DEBUG
		std::cout << std::endl << "-----------------rasterization Begin-----------------" << std::endl;
		std::cout << out_triangle.size() << std::endl;
#endif
		pixels.clear();
		zBuffer.clear();
		
		for (int i = 0; i < out_triangle.size(); ++i)
		{
			//if (i==2 || i == 3 || i==4 || i==5 || i==8 || i==9  )
				//continue;
			Vector3<float> posArr[3] = {
				out_triangle[i].vertex[0].position ,
				out_triangle[i].vertex[1].position ,
				out_triangle[i].vertex[2].position };

			Vector2<float> leftTop = Myth::get_leftTop(posArr[0],posArr[1], posArr[2]);
			Vector2<float> rightBottom = Myth::get_rightBottom(posArr[0],posArr[1], posArr[2]);

			for (int x = leftTop.x; x <= rightBottom.x; ++x)
			{
				for (int y = leftTop.y; y >= rightBottom.y; --y)
				{
					const Vector2<int> pixPos(x, y);
					bool isInTriangle = Myth::isInTriangle(Vector3<float>(x, y, 0), posArr[0], posArr[1], posArr[2]);
					if (isInTriangle)
					{
						Vector3<float> abg = Myth::centerOfGravity(
							posArr[0], posArr[1], posArr[2], pixPos);
						float z = posArr[0].z * abg.x +
							posArr[1].z * abg.y + posArr[2].z * abg.z;

						if (zBuffer.count(pixPos))
						{
							if (z <= zBuffer[pixPos])
								;
							else
								zBuffer[pixPos] = z;
						}
						else
							zBuffer[pixPos] = z;
						
						Vector3<float> color = out_triangle[i].vertex[0].color * abg.x +
							out_triangle[i].vertex[1].color * abg.y + out_triangle[i].vertex[2].color * abg.z;
						Vector3<float> normal = out_triangle[i].vertex[0].normal * abg.x +
							out_triangle[i].vertex[1].normal * abg.y + out_triangle[i].vertex[2].normal * abg.z;
						Vector2<float> uv = out_triangle[i].vertex[0].uv * abg.x +
							out_triangle[i].vertex[1].uv * abg.y + out_triangle[i].vertex[2].uv * abg.z;

						VertexData verdata(Vector3<float>(0, 0, z), uv, normal, color);
						pixels.push_back(Pixel(verdata, Vector2<int>(x, y)));
					}
				}
			}
		}
#if DEBUG
		std::cout << std::endl << "-----------------rasterization End-----------------" << std::endl;
		std::cout << pixels.size() << std::endl;
		std::cout << zBuffer.size() << std::endl;
#endif
	}

	void testZ()
	{
#if DEBUG
		std::cout << std::endl << "-----------------testZ Begin-----------------" << std::endl;
#endif
		std::vector<Pixel>::iterator it = pixels.begin();
		while (it != pixels.end())
		{
			if ((*it).verdata.position.z < zBuffer[(*it).pos])
				it = pixels.erase(it);
			else
				++it;
		}
#if DEBUG
		std::cout << pixels.size() << std::endl;
		std::cout << std::endl << "-----------------testZ End-----------------" << std::endl;
#endif
	}

	void renderingPipeline()
	{
		vertexShader();
		rasterization();
		if(isTestZ)
			testZ();
	}
};

struct ScreenWindow
{
	const int width, height;
	Color clearBack;
	bool isClearBack = false;
	std::vector<Pixel> pixels;

	ScreenWindow(int screenWidth, int screenHeight) :width(screenWidth), height(screenHeight)
	{
		initgraph(screenWidth, screenHeight);

	}
	~ScreenWindow()
	{
		closegraph();
	}

	void update(const std::vector<Pixel>& input, bool clearBackColor = false, Color color = Color(1, 1, 1, 1))
	{
		this->pixels = input;
		if (clearBackColor)
		{
			isClearBack = true;
			clearBack = color;
			setbkcolor(RGB(clearBack.x, clearBack.y, clearBack.z));
		}

	}
	void show()
	{
		setorigin(0, 600);
		setaspectratio(1, -1);
		if (isClearBack)
			cleardevice();
		BeginBatchDraw();
		for (int i = 0; i < pixels.size(); ++i)
		{
			//putpixel(pixels[i].pos.x, pixels[i].pos.y, RGB(255, 0, 0));
			putpixel(pixels[i].pos.x, pixels[i].pos.y, RGB(pixels[i].verdata.color.x, pixels[i].verdata.color.y, pixels[i].verdata.color.z));
		}
		FlushBatchDraw();
	}

};


int main()
{
	initgraph(screenWidth, screenHeight, SHOWCONSOLE);
	// OBJ文件地址
	std::string meshlLocation = "C:\\Users\\32156\\source\\repos\\SoftRender\\OBJ\\Cube.txt";
	Mesh mesh;
	mesh.readObjFile(meshlLocation);
	mesh.colorBuffer = {
		Color(255,255,255),
		Color(255,0,0),
		Color(0,255,0),
		Color(0,0,255),
		Color(255,255,255),
		Color(255,0,0),
		Color(0,255,0),
		Color(0,0,255),
	};

	Vector3<float> scale(0.5, 0.5, 0.5), position(0, 0, -2);
	Matrix4<float> model;
	model.Identity();
	model = Matrix4<float>::get_model_matrix_scale(model, scale);
	model = Matrix4<float>::get_model_matrix_translation(model, position);
	model.Print();

	float fov = 90, aspecet = 1, n = 1, f = 2;
	Matrix4<float> projection;
	projection.Identity();
	projection = Matrix4<float>::get_projection_matrix(fov, aspecet, n, f);
	projection.Print();

	Matrix4<float> viewport = Matrix4<float>::get_viewport_matrix(screenWidth, screenHeight);

	ScreenWindow window(screenWidth, screenHeight);

	Vector3<float> cameraPos(0.0f, 0.0f, 0.0f), cameraFront(0, 0, -1), cameraUp(0, 1, 0);
	cameraPos.normalize(); cameraFront.normalize(); cameraUp.normalize();
	Matrix4<float> view = Matrix4<float>::get_view_matrix(cameraPos, cameraFront, cameraUp);

	Render render(screenWidth, screenHeight);
	render.assemblingElements(mesh);

	render.camera = Camera(cameraPos, cameraFront, cameraUp);
	render.isViewClipping = true;
	render.isBackCulling = true;
	render.isTestZ = true;
	render.model = model;
	render.view = view;
	render.projection = projection;
	render.viewport = viewport;

#if DEBUG
	std::cout << "While out:" << std::endl;
	mesh.print();
	std::cout << std::endl;
	render.model.Print();
	std::cout << std::endl;
	render.view.Print();
	std::cout << std::endl;
	render.projection.Print();
	std::cout << std::endl;
	render.viewport.Print();
	std::cout << std::endl;
#endif
	clock_t begin = clock();
	while (true)
	{
		char key = _getch();
		switch (key)
		{
		case 'w':
			cameraPos.y += 0.1;
			break;
		case 's':
			cameraPos.y -= 0.1;
			break;
		case 'a':
			cameraPos.x += 0.1;
			break;
		case 'd':
			cameraPos.x -= 0.1;
			break;
		default:
			break;
		}
		clock_t now = clock();
		float deletime = static_cast<float>(now - begin) / CLOCKS_PER_SEC * 1000;
		begin = clock();
		int fps = 1.0 / deletime;
		std::cout << "FPS:" << fps << std::endl;

		cameraFront.normalize();
		render.view = Matrix4<float>::get_view_matrix(cameraPos, cameraFront, cameraUp);
		cameraPos.print();
		std::cout << std::endl;
		render.view.Print();
		std::cout << std::endl;

		render.renderingPipeline();

		window.update(render.pixels, true, Color(0, 0, 0));
		window.show();
	}
}

3.2 完整的OBJ文件

# Blender v2.83.5 OBJ File: ''
# www.blender.org
mtllib Cube.mtl
o Cube
v 1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 1.000000 -1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 1.000000
vt 0.625000 0.500000
vt 0.875000 0.500000
vt 0.875000 0.750000
vt 0.625000 0.750000
vt 0.375000 0.750000
vt 0.625000 1.000000
vt 0.375000 1.000000
vt 0.375000 0.000000
vt 0.625000 0.000000
vt 0.625000 0.250000
vt 0.375000 0.250000
vt 0.125000 0.500000
vt 0.375000 0.500000
vt 0.125000 0.750000
vn 0.0000 1.0000 0.0000
vn 0.0000 0.0000 1.0000
vn -1.0000 0.0000 0.0000
vn 0.0000 -1.0000 0.0000
vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
usemtl Material
s off
f 1/1/1 5/2/1 7/3/1 3/4/1
f 4/5/2 3/4/2 7/6/2 8/7/2
f 8/8/3 7/9/3 5/10/3 6/11/3
f 6/12/4 2/13/4 4/5/4 8/14/4
f 2/13/5 1/1/5 3/4/5 4/5/5
f 6/11/6 5/10/6 1/1/6 2/13/6

3.3 参考资料

  • GAMES101现代计算机图形学入门
  • 入门渲染管线的详细讲解
  • 从零开始写一个软渲染器
  • OpenGL中文学习网(旧版)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/636099.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

可以在手机上使用的提醒事项软件有哪些

随着科技的进步&#xff0c;越来越多的人选择使用各种手机软件来提高自己的效率&#xff0c;高效率的手机软件已经成为人们生活和工作中不可或缺的一部分。其中&#xff0c;提醒事项软件是一种非常实用的工具&#xff0c;可以督促用户按时完成任务&#xff0c;有效防止遗忘。 …

chatgpt赋能python:如何利用Python在网上赚钱:一份SEO指南

如何利用Python在网上赚钱&#xff1a;一份SEO指南 随着互联网的快速发展和Python成为一种越来越受欢迎的编程语言&#xff0c;越来越多的人开始利用Python在网上赚取额外的收入。本篇文章将介绍一些Python工具和技术&#xff0c;以及SEO最佳实践&#xff0c;帮助你利用Python…

Spring Boot 回顾

Spring Boot 的发展是很快的&#xff0c;也使用了很多年。但是在工作中&#xff0c;还是发现了很多公司还没有怎么使用 Spring Boot&#xff0c;依旧是在使用 Spring MVC。于是决定复习总结一下。框架的进步和飞跃很重要&#xff0c;但是也不能一味追求新鲜事物&#xff0c;历史…

Yolo算法的演进—YoloCS有效降低特征图空间复杂度(附论文下载)

点击蓝字 关注我们 关注并星标 从此不迷路 计算机视觉研究院 公众号ID&#xff5c;计算机视觉研究院 学习群&#xff5c;扫码在主页获取加入方式 论文地址&#xff1a;YOLOCS: Object Detection based on Dense Channel Compression for Feature Spatial Solidification (arxiv…

【6.12 代随_55day】判断子序列、不同的子序列

判断子序列、不同的子序列 判断子序列动态规划方法图解步骤代码 不同的子序列1.方法图解步骤代码 判断子序列 力扣连接&#xff1a;392. 判断子序列&#xff08;简单&#xff09; 动态规划方法 &#xff08;这道题也可以用双指针的思路来实现&#xff0c;时间复杂度也是O(n)…

Linux变卡cpu占用高,已装curl、wget但提示命令找不到(挖矿病毒)

现象 服务器变卡&#xff0c;top cpu占用非常高&#xff0c;但是进程占用只有1% 下载安装htop查看 看样子中了挖矿病毒 解决方式 修改/etc/hosts 把dev.fugglesoft.me 指向127.0.0.1 F9 -> enter杀掉进程 又多出来一个执行sh的&#xff0c;修改病毒sh&#xff0c;vi进去随便…

Portraiture5.0滤镜插件中文升级版本下载及功能介绍

专注人像修图&#xff0c;打造完美摄影作品&#xff0c;Portrait5.0是款最新版的人脸图像后期处理软件。它可以帮助用户快速编辑修图&#xff0c;提供全方面的人物美白效果&#xff0c;让用户可以更轻松地修图&#xff0c;操作简单&#xff0c;方便快捷&#xff0c;非常不错。新…

Cilium 如何处理 L7 流量

还记得在 使用 Cilium 增强 Kubernetes 网络安全 示例中&#xff0c;我们通过设置网络策略限制钛战机 tiefighter 访问死星 deathstar 的 /v1/exhaust-port 端点&#xff0c;但放行着陆请求 /v1/request-landing。在提起 Cilium 时&#xff0c;都说其是使用 eBPF 技术推动的用于…

心法利器[85] | 算法技术和职业规划

心法利器 本栏目主要和大家一起讨论近期自己学习的心得和体会&#xff0c;与大家一起成长。具体介绍&#xff1a;仓颉专项&#xff1a;飞机大炮我都会&#xff0c;利器心法我还有。 2022年新一版的文章合集已经发布&#xff0c;累计已经60w字了&#xff0c;获取方式看这里&…

花3万买的大学申请文书,竟和ChatGPT写的一样?

正值高考毕业季&#xff0c;留学出国又成热门话题。眼下&#xff0c;选学校、写申请书是不少学生头等大事。在AI如火如荼的今年&#xff0c;这个老行当却有了新变化。 当学生纷纷用AI写申请书&#xff0c;留学机构开始缩减业务&#xff0c;中介用AI写文书“糊弄”学生&#xf…

哈夫曼树和哈夫曼编码

一.哈夫曼树 1.哈夫曼树 哈夫曼树是一种用于编码的树形结构。它是通过将频率最低的字符反复组合形成的二叉树&#xff0c;使得出现频率高的字符具有较短的二进制编码&#xff0c;而出现频率低的字符具有较长的编码。 在哈夫曼树中&#xff0c;每个叶子节点都代表一个字符&am…

chatgpt赋能python:Python图形填充颜色教程

Python图形填充颜色教程 Python是一种简单易学、高效的编程语言&#xff0c;广泛应用于数据分析、机器学习、Web开发等领域。其中&#xff0c;图形处理是Python编程领域的一个重要方面。在很多情况下&#xff0c;我们需要填充图形颜色来增加图形的美观程度和可读性。本文将介绍…

使用OpenCV和MediaPipe实现姿态识别!

大家好&#xff0c;我是小F&#xff5e; MediaPipe是一款由Google开发并开源的数据流处理机器学习应用开发框架。 它是一个基于图的数据处理管线&#xff0c;用于构建使用了多种形式的数据源&#xff0c;如视频、音频、传感器数据以及任何时间序列数据。 MediaPipe通过将各个感…

表示学习(Representation Learning) Part1--Pretext Text

文章目录 Representation LearningInferring structure&#xff08;推断结构&#xff09; Transformation predictionRotation predictionRelative transformation prediction ReconstructionDenoising AutoencodersContext encodersColorizationSplit-brain encoders Instance…

屏幕录像视频录制编辑软件TechSmith Camtasia 2023 for Mac 简体中文版

TechSmith Camtasia for Mac 中文版 是一款专业的屏幕录像视频录制编辑软件&#xff0c;非常容易就可以获得精彩的截屏视频。创建引人注目的培训&#xff0c;演示和演示视频。Camtasia 屏幕录制软件简化&#xff0c;直观&#xff0c;让您看起来像专业人士。利用Camtasia&#x…

SpringMVC 学习总结

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 1. 什么是 Spring MVC 1.1 Spring、Spring MV…

Dockerfile创建镜像

一、Docker镜像的创建 创建镜像有三种方法&#xff0c;分别为【基于已有镜像创建】、【基于本地模板创建】以及【基于Dockerfile创建】。 1.1 基于现有镜像创建 &#xff08;1&#xff09;首先启动一个镜像&#xff0c;在容器里做修改docker run -it centos:7 /bin/bash …

旧手机卖掉之前我们需要做这几个操作

随着科技的不断进步&#xff0c;人们使用的电子产品也在不断地迭代更新。当我们不再使用旧手机时&#xff0c;卖掉它可以省下一笔开支&#xff0c;但也需要注意保护个人隐私数据。因此&#xff0c;在售卖二手手机之前&#xff0c;正确清除旧手机中的历史数据变得至关重要。 首先…

Java网络开发(Tomcat)——从同步到异步 从jsp 到 js + axios + vue 实现 数据分页显示 数据增删改查

目录 引出一些固定的东西1.固定的响应格式2.name 变成 v-model 进行双向绑定3.下拉框选中--:value"type.id"4.vue导包固定写法5.script固定写法6.axios的get请求7.axios的post请求8.前端美化&#xff1a; 数据分页显示1.后端改成resp响应2.前端的修改要点&#xff08…

揭秘报表新玩法!标配插件不再单调,如何用柱形图插件让你的报表瞬间高大上!

摘要&#xff1a;本文由葡萄城技术团队于CSDN原创并首发。转载请注明出处&#xff1a;葡萄城官网&#xff0c;葡萄城为开发者提供专业的开发工具、解决方案和服务&#xff0c;赋能开发者。 前言 图表作为一款用于可视化数据的工具&#xff0c;可以帮助我们更好的分析和理解数…