当前位置: 首页 > news >正文

【Games101】C++ 基础软光栅化器:光栅化三角形(Cmake 配置 Eigen OpenCV 库)

相关代码已上传至 github ,链接:MZRHnbnb/Software-Rasterizer

如对图形学基础的齐次坐标和矩阵变换不了解和不熟悉,可查看《Games101》或者前往阅读我对此推导和解析的博客,链接:【线性代数】图形学基础:齐次坐标与矩阵变换-CSDN博客

配置环境

这里我利用了Eigen 库来进行点和向量的表示并计算,比较方便;利用 OpenCV 库来显示图像。安装好 Eigen 和 OpenCV 后,我们可以用Cmake进行配置(Cmake也要先上网自行下载)。在VS2022下载的时候应该就已经内置好了插件,但是如果是使用vscode还是需要下载相关插件启用 Cmake 。但我个人感觉 VS2022 更好用,因为 vscode 我怎么写这个 CMakeLists.txt 这个 OpenCV 都一直报错。

要自行创建一个名为 CMakeLists.txt 的文件,一个字也不能改否则 Cmake 无法识别。在文件里写要配置的库和要编译的代码文件,我个人的 CMakeLists.txt 内容在 GitHub 仓库可查看,大家可自行查看借鉴或者自行编写。

实现思路

配置好环境后,我们要用到的头文件基本如下。

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense>//Eigen头文件 #include <opencv2/opencv.hpp>//OpenCV头文件

既然要画出一个三角形,我们可以要先输入三角形的三个坐标点,用于后续计算。输入的坐标都是三维坐标,但为了方便计算,我们最好是以齐次坐标方式存储。在这里我们存储坐标点可以利用 Eigen 库Vector3f类存储三维坐标 (x,y,z) 和Vector4f类来存储齐次坐标 (x,y,z,w) ,利用一个 vector 数组存储好三角形的三个顶点。

//输入 std::vector<Eigen::Vector3f> s;//存(x,y,z) for(int i=1;i<=3;++i){ float x,y,z; std::cin>>x>>y>>z; s.push_back({x,y,z}); } //转齐次坐标 std::vector<Eigen::Vector4f> qs;//存(x,y,z,w)齐次坐标 for (int i = 0; i < 3; ++i) { qs.push_back({ s[i].x(),s[i].y(),s[i].z(),1 }); }

接下来为了方便,我个人先暂且不写 View 变换,先默认摄像机位于原点,上方是 Y 轴方向,始终朝向 -Z 方向。大家如有需要可自行编写 View 变换。下面直接可以写 P 变换,即透视投影

在 Games101 有十分具体的对于透视投影矩阵的推导,这里就不再赘述了。在代码使用中,我们需要给相机 camera 的视锥体定义好四个参数:

1. FOV :视场角,控制镜头能看到多大的角度,如 45 度 , 90 度等。

2. Aspect :宽高比,即渲染窗口的宽度和高度之比。

3. near :指镜头最近能看到多远。

4. far :指镜头最远能看到多远。

由此可得到的透视投影矩阵如下(右手系且相机朝向 -Z )

在对三角形的每一个点进行透视投影矩阵变换后,要记得进行归一化的透视除法,获得 NDC 坐标,随后再进行下一步处理。

//透视投影 float fov, aspect, near, far; std::cin >> fov >> aspect >> near >> far; fov = fov * 3.14159f / 180.0f;//转为弧度制利用cmath的tan函数计算 Eigen::Matrix<float, 4, 4> P;//Eigen矩阵 P.setZero();//初始化为0 //输入矩阵里的数字 P(0, 0) = 1.0f / (aspect * tan(fov * 0.5f)); P(1, 1) = 1.0f / tan(fov * 0.5); P(2, 2) = -(near + far) / (far - near); P(2, 3) = -2 * near * far / (far - near); P(3, 2) = -1; //对三角形每个点乘上透视投影矩阵 for (int i = 0; i < 3; ++i) { qs[i] = P * qs[i]; //透视除法,每个xyz都要除以对应w值 qs[i].x() /= qs[i].w(); qs[i].y() /= qs[i].w(); qs[i].z() /= qs[i].w(); }

做完透视投影后,下一步是视口变换,将我们处理好的,x 和 y 都在 [-1,1] 区间的坐标,映射到我们屏幕上的坐标。这里其实可以利用 OpenCV 库函数的功能直接获取个人设备上的分辨率,如1920×1080 和 2560×1440 等等。但是这里为了方便我直接定义屏幕长为 800 ,宽为 600。这一步比较简单,只需要对每个三角形变换后的 x 和 y 进行如下操作即可:

x=(1+x)÷2×长

y=(1-y)÷2×宽(OpenCV 中 y 轴朝下)

//视口变换 int width = 800; int hight = 600; cv::Mat image(hight, width, CV_8UC3, cv::Scalar(0, 0, 0));//顺便创建好显示的屏幕 for (int i = 0; i < 3; ++i) { qs[i].x() = (1.0f + qs[i].x()) * 0.5f * width; qs[i].y() = (1.0f - qs[i].y()) * 0.5f * hight; }

做完这些变换之后,接下来就是光栅化三角形了。我们现在需要做的就是,判断一个像素的中心是否在三角形内,如果是就涂上红色,否则不管。判断一个像素是否在三角形内应该用的是叉积计算判断,我们可以将其封装成一个 bool 类型函数。

bool chek(const std::vector<Eigen::Vector4f>& arr, float x, float y) { std::vector<float> z(arr.size());//用于记录每个点计算叉积的z坐标 //按顺序遍历每个点算叉积,设其z坐标都为0,方便判断 for (int i = 0; i < arr.size(); ++i) { Eigen::Vector3f a(arr[(i + 1) % arr.size()].x() - arr[i].x(), arr[(i + 1) % arr.size()].y() - arr[i].y(), 0); Eigen::Vector3f b(x-arr[i].x(), y-arr[i].y(), 0); Eigen::Vector3f ans = a.cross(b); z[i] = ans.z(); } //只有在三个点算的叉积都同向,这个像素点就是在三角形内,否则不在 return (z[0] <= 0 && z[1] <= 0 && z[2] <= 0) || (z[0] >= 0 && z[1] >= 0 && z[2] >= 0); }

随后为了性能问题,我们不能遍历一整个屏幕的像素,而是通过三角形的三个顶点坐标,计算其包围盒,遍历包围盒内的像素点。

//计算包围盒 float minx = 1e9, miny = 1e9, maxx = -1e9, maxy = -1e9; for (int i = 0; i < 3; ++i) { minx = std::min(minx, qs[i].x()); miny = std::min(miny, qs[i].y()); maxx = std::max(maxx, qs[i].x()); maxy = std::max(maxy, qs[i].y()); } //检查像素是否在三角形内,遍历包围盒 for (int i = miny - 1; i <= maxy + 1; ++i) { for (int j = minx - 1; j <= maxx + 1; ++j) { if (j<0 || j>width - 1 || i<0 || i>hight - 1) continue;//超出屏幕坐标点范围不用算 //对xy都加0.5找到像素中心 float rx = j + 0.5f; float ry = i + 0.5f; //通过刚刚的函数判断是否在三角形内,是则给像素设置为红色 if (chek(qs, rx, ry)) { image.at<cv::Vec3b>(i, j) = cv::Vec3b(0, 0, 255); } } } //最后输出屏幕的图像 cv::imshow("666", image); cv::waitKey(0);

至此一个简易的光栅化一个彩色三角形的软光栅化器就完成了。我们可以尝试如下输入

-0.5 -0.5 -2 0.5 -0.5 -2 0 0.5 -2 45 1.333 0.1 100

预期可以得到

但目前尚未加入 V 变换因为把相机固定在了原点并朝向 -Z ,而且 Z-Buffer 深度缓冲也尚未实现,但我们可以先实现出来这个简易版,后续需要再自行加入新功能。

新增:Z_Buffer深度缓冲

目前最近的一次更新中我优化了一下我的一些变量命名,让它们看起来更加直观,而且加入了Z_Buffer 深度缓冲,可以实现输入多个三角形,并指定其 RGB 颜色,渲染出来其遮挡关系,并对整个代码进行了一个比较大的重构,具体可自行查看 GitHub 仓库。

Z_Buffer 深度缓冲实际上就是利用一个二维数组,维护一个一整个屏幕内每一个像素的深度值,只显示最小深度的颜色,实现类似 Games101 提到的“画家算法”。

计算深度值,我们需要计算三角形内每个像素的重心坐标,从而通过线性插值计算出深度值。重心坐标其实就是三角形内每个像素点点相对于三个顶点的一个比例,对此我们可以利用三角形的面积通过比例计算,如下图

α = S_2 / S

β = S_3 / S

γ = S_1 / S

我们可以利用 Vector3f 来存储重心坐标,用二维叉积就能算出三角形面积,封装成如下函数

//计算重心坐标 Eigen::Vector3f calculate_bary(const std::vector<Eigen::Vector4f> &triangle,float x, float y) { Eigen::Vector3f ans; float temp[5] = { 0 }; Eigen::Vector2f ab(triangle[1].x() - triangle[0].x(), triangle[1].y() - triangle[0].y()); Eigen::Vector2f ac(triangle[2].x() - triangle[0].x(), triangle[2].y() - triangle[0].y()); float whole_triangle = ac.cross(ab); for (int i = 0; i < triangle.size(); ++i) { Eigen::Vector2f a(triangle[(i + 1) % triangle.size()].x() - triangle[i].x(), triangle[(i + 1) % triangle.size()].y() - triangle[i].y()); Eigen::Vector2f b(x - triangle[i].x(), y - triangle[i].y()); temp[i] = b.cross(a) / whole_triangle; } //注意三角形的对应关系,不要弄混 ans.x() = temp[1]; ans.y() = temp[2]; ans.z() = temp[0]; return ans; }

得到重心坐标后,我们就可以进行深度值的计算了。这里我们直接对 NDC 坐标里的 Z 值进行线性插值计算,可以得出精确的深度值,所以我们封装好的函数之后,在后续光栅化再调用即可。

//计算Z-Buffer深度缓冲 float calculate_Z(const std::vector<Eigen::Vector4f> &triangle,Eigen::Vector3f bary) { //进行线性插值计算 float Z_depth = bary.x() * triangle[0].z() + bary.y() * triangle[1].z() + bary.z() * triangle[2].z(); return Z_depth; }

随后我们在光栅化阶段再进行深度缓冲计算,另外,由于重心坐标的特性,如果一个像素点的重心坐标每一个值的范围都在 [0,1] ,则在三角形内,反之不在。可据此判断像素点是否在三角形内,不再用叉积法,过于冗余,因为重心是我们在深度缓冲时候必算的,顺手就可以判断一个像素点是否在三角形内。

bool chek_point_inbary(Eigen::Vector3f bary) { return bary.x() >= 0 && bary.x() <= 1 && bary.y() >= 0 && bary.y() <= 1 && bary.z() >= 0 && bary.z() <= 1; }

随后进行光栅化,为了方便我们先封装好一个光栅化一个三角形的函数,要记得引用传入 Z_Buffer 二维数组来进行深度缓冲。实际上逻辑和原本都差不多,不过是加入一个深度缓冲计算而已。

//光栅化 void Paint_Triangle(const std::vector<Eigen::Vector4f> &triangle,std::vector<std::vector<float>> &Z_Buffer, cv::Mat &image,int width,int hight,int R,int G,int B) { //计算包围盒 float min_x = 1e9, min_y = 1e9, max_x = -1e9, max_y = -1e9; for (int i = 0; i < 3; ++i) { min_x = std::min(min_x, triangle[i].x()); min_y = std::min(min_y, triangle[i].y()); max_x = std::max(max_x, triangle[i].x()); max_y = std::max(max_y, triangle[i].y()); } for (int i = min_y - 1; i <= max_y + 1; ++i) { for (int j = min_x - 1; j <= max_x + 1; ++j) { if (j<0 || j>width - 1 || i<0 || i>hight - 1) continue; float rx = j + 0.5f; float ry = i + 0.5f; Eigen::Vector3f bary = calculate_bary(triangle, rx, ry);//计算重心坐标 float Z_depth = calculate_Z(triangle, bary);//计算深度值 //只有点在三角形内,且当前深度值比Z_Buffer深度值小时才对像素涂色 if (chek_point_inbary(bary) && Z_depth <= Z_Buffer[i][j]) { Z_Buffer[i][j] = Z_depth;//更新当前最小的深度,即不被遮挡的那个深度 image.at<cv::Vec3b>(i, j) = cv::Vec3b(B, G, R);//OpenCV采用的是BGR } } } }

然后在 main 函数调用,光栅化我们输入的每一个三角形。

//Z_Buffer深度缓冲二维数组 std::vector<std::vector<float>> Z_Buffer(hight + 5, std::vector<float>(width + 5, 1e9)); //光栅化每一个三角形 for (int i = 0; i < triangle_cnt; ++i) { //输入三角形的颜色 int R, G, B; std::cin >> R >> G >> B; Paint_Triangle(triangle_point_NDC[i], Z_Buffer, image, width, hight, R, G, B); }

目前对于深度缓冲的修改就完成了,现在可以很好的处理三角形之间的遮挡关系。而现在的输入数据要改成:首先输入三角形数量,然后输入每个三角形的三个顶点,输入 Fov , Aspect , near , far ,最后输入每个三角形的颜色 RGB 值。如下示例数据

2 -2.0 1.5 -4 2.0 -1.5 -8 -2.0 -1.5 -6 2.0 1.5 -4 -2.0 -1.5 -8 2.0 -1.5 -6 45 1.33 0.1 50 255 0 0 0 0 255

预期则可得到

做完深度缓冲,下一步就可以加入适当的光照模型或者加入抗锯齿功能了。如有兴趣可通过链接继续阅读:【Games101】C++软光栅化器:Blinn Phong光照模型及简易MSAA抗锯齿实现(Cmake 配置 Eigen OpenCV 库)-CSDN博客

http://www.cnnetsun.cn/news/3908146.html

相关文章:

  • 从AI直播智能体实践,拆解智能体工作流构建的核心工程逻辑
  • LensWalk:基于主动视觉的视频理解新范式与工程实践
  • 5分钟掌握RPG Maker解密工具:游戏资源提取与修改终极指南
  • Fast-GitHub终极指南:国内开发者必备的GitHub加速神器
  • 探秘全国建设管理信息网站:数字化转型浪潮下的行业变革与未来展望
  • 09 LOD 与大型数字孪生模型管理
  • 5分钟掌握专业EPUB电子书制作:免费开源在线编辑器终极指南
  • Nintendo Switch破解终极指南:大气层系统1.7.1完整安装与优化教程
  • 矩阵系统,ai数字人生成,声音克隆
  • 通达信数据接口终极指南:3步免费获取专业金融数据
  • 如何解决多输入法词库兼容性问题:开源深蓝词库转换工具技术深度解析
  • 铜官山区建设局网站如何助力城市更新与民生幸福 深度解析官方平台的服务功能与便民指南
  • Unity游戏开发:构建模块化通关失败处理系统
  • 微软 Edge 停用 MV2 扩展程序平台,uBlock Origin 等广告拦截器将无法使用
  • windows快捷菜单(简化菜单/新版菜单/IExplorerCommand)开发指南
  • 又一次面试整理
  • IP组播完全入门指南(从零到HCIP)
  • 戴尔网站建设的目标:如何打造符合企业长期发展的专业数字门户
  • SpringBoot图书馆座位管理系统设计与实现
  • 宣城网站建设jidela 揭秘:如何让本地中小企业的官网不再是“摆设”,真正带来业务增长?
  • RAG与LoRA技术融合:构建希腊语多领域专家模型的实战指南
  • 跨境电商多语种本地化实战指南与避坑策略
  • Windows系统kernelbase.dll丢失问题的安全修复方案
  • Python agent-guard 包详解:功能、安装、语法与案例
  • 旧版快捷菜单(IContextMenu)实现
  • 释放你的音乐自由:3分钟学会使用ncmdumpGUI解密网易云音乐NCM文件
  • Qt QProcess执行Linux管道命令的三种解决方案与实战指南
  • 大模型移动端部署实战:llama.cpp量化与Android手机本地运行指南
  • 如何打造专业高效的医疗科技网站建设方案并实现流量与转化的双重突破
  • [具身智能-796]:直流电机的信号死区?并图示