软光栅个人项目介绍,编写思路及后期整理

软光栅渲染器解释文档,框架,技术细节及待优化点

软光栅项目主要是在学习了计算机图形学相关知识之后,主要是Games101,202,tiny shader等教程,然后借鉴了很多前辈们的思路和框架逻辑编写的。

渲染帧率来说,blinn-phong渲染维持在80帧左右.
三月七
PBR渲染维持在12帧左右

在这里插入图片描述
目前没有多线程和SIMD,后序添加后应该帧率会大幅提高。

渲染结果展示:

Blinn-Phong着色:

莫娜
在这里插入图片描述

泳装符华(改模配布来自B站MMD up主)
在这里插入图片描述
三月七(素材来源同上)
在这里插入图片描述
银狼(素材同上)
在这里插入图片描述
希儿
在这里插入图片描述
旗袍琪亚娜
在这里插入图片描述
符华
在这里插入图片描述

PBR渲染(包括天空盒)结果展示:

漂浮在大道上的左轮手枪
在这里插入图片描述
阳澄湖蟹老板。。。。。。。
在这里插入图片描述
钢铁侠被遗忘的头盔
在这里插入图片描述

软光栅项目的亮点:

  • 无外部依赖,使用了C++标准库和系统原生API,没有外部库的依赖
  • 可编程渲染管线,通过编写着色器shader实现不同效果
  • 类似于UE虚幻的环绕式相机
  • 齐次裁剪(Homogeneous clipping)
  • 透视插值(Perspective correct interpolation)
  • 背面剔除
  • MSAA(MultiSampling Anti-Alising)
  • 深度测试
  • 天空盒
  • 切线空间法线贴图(Tangent space normal mapping)
  • ACES tone mapping
  • Blinn–Phong
  • Physically based rendering(PBR)(SG模型和MR模型)
  • 基于图像的光照Image-based lighting(IBL)

编写思路:

编写思路,重点是写三个类,实现解耦。

场景类,scene,记录了场景中的光照信息,camera和多个需要渲染的模型。
模型类,model,记录了一个模型的mesh信息,贴图信息,位移旋转缩放等。
相机类,camera,记录了相机的位置和朝向。

渲染的时候,先从camera计算出vp矩阵,然后对scene中的每个model。
1.绑定它的mesh和贴图,
2.通过设置uniform的方式把光照传递到shader中
3.从模型的位移旋转缩放信息算出m矩阵来,乘以vp矩阵得到mvp矩阵
4.通过设置uniform的方式把mvp矩阵传递到shader中
5.调用draw命令绘制模型。

实现了模型、相机和光照的解耦。

这里的uniform并不是像opengl一样在gpu中有一个uniform变量区域,而是在管线中使用一个结构体payload在管线的不同位置进行数据传递,比如说从顶点着色器传递到片段着色器。需要的裁剪坐标,世界坐标,uv坐标等,都可以使用这个结构体进行传递。

图形显示界面:

因为软光栅渲染器需要的图形界面功能比较少,只需要提供打开窗口,显示渲染好的界面,接受鼠标键盘事件这些功能就好,所以说并没有选择QT,imgui,glfw等比较成熟的图形界面库,而是选择了直接调用系统API来实现图形界面,win32.h和win32.cpp里面就是windows的API图形界面编程

想要弄懂并自己写一个的话可以直接看微软的官方文档。

微软图形界面官方文档

不过这玩意晦涩难懂,看了就容易忘,官方中文也是机翻。小技巧,可以直接啃英文,不会的去找ChatGPT翻译一下,翻译的其实比浏览器自带的翻译插件好用多了。

基础框架结构其实也挺简单的,首先我们需要知道在win32中窗口的进程工作主要依赖win32消息循环,os会为窗口维护一个消息队列,通过调用PeekMessage()函数我们能从队列中取出队列头部的消息,然后用我们自定义的窗口处理函数WindowProc()对每个消息进行处理,包括各种响应这种事件,鼠标键盘事件,调用相应的回调函数。
在这里插入图片描述

底层数学库和图像库的编写

数学库(math.h,math.cpp)

数学库的编写就是两个主要的类:

向量类

vec2,vec3,vec4,这几个类底层是对应维数的数组,成员函数包括重载了的一堆运算符,比如[],*=,/=之类的,还有向量叉积,点积。
类外重载运算符<<等。进行输出debug显示(因为debug时需要输出颜色分量,颜色color就是一个vec3)

矩阵类,

mat3,mat4,底层是对应大小的二维数组。成员函数包括转置,求逆矩阵,求伴随等。(复习了很多线性代数的知识)。
主要是对运算符重载和函数重载进行一种练习吧。。。。(想起侯捷c++的时候写那些string类的时候)。。。

还有一些(类外)静态函数的编写,
主要是用在MVP变换过程中,比如构成Model矩阵的(当然我们的渲染器里m矩阵直接取得是单位阵,相当于摆放模型的这一步知识,在blender ,3dsmax等仿真建模软件里面就搞定了),旋转(旋转也分为绕x轴,y轴,z轴),平移(主要是在第四列齐次项进行修改),缩放(主要是对主对角线进行k比例系数的一个调节)矩阵。

下面这是绕x轴旋转的,其实如果绕y轴旋转的话就是第二列第二行为1,如果绕z轴旋转的话就是第三列第三行为1.
旋转函数的参数就是角度了,绕哪个轴旋转的角度。

1  0  0  0
0  c -s  0
0  s  c  0
0  0  0  1

V矩阵,也就是用于视图变换的lookat矩阵,这个比较复杂,因为我们这是一个可环绕相机,所以说相机的位置是需要动的。
lookat矩阵主要有3个参数,第一个是eye,也就是视点(摄像机)的位置。第二个是target,也就是目标的位置(看向的物体),第三个是up向量,这是一个三维点,这个表示了相机的头顶的朝向。

以eye为坐标原点建立笛卡尔坐标系,右手系,z轴是向内的一个向量,也是看向负方向(eye-target),则x轴是向右的向量,方向是由up向量和z轴叉乘出来的。然后y轴是z轴和x轴叉乘出来的。

x_axis.x  x_axis.y  x_axis.z  -dot(x_axis,eye)
y_axis.x  y_axis.y  y_axis.z  -dot(y_axis,eye)
z_axis.x  z_axis.y  z_axis.z  -dot(z_axis,eye)
       0         0         0                 1

用于投影变换的正交投影矩阵和透视投影矩阵等。
P矩阵的过程相当复杂,分为正交投影矩阵和透视投影矩阵两种。区分是光是点光源还是平行光。

要先设定一个近平面和远平面,

下面这个是正交投影,这个相当简单,因为就是把长方体平移到原点,之后将长方体的宽和高缩放到2.

2/(r-l)        0         0  -(r+l)/(r-l)
     0    2/(t-b)        0  -(t+b)/(t-b)
     0         0   2/(n-f)  -(f+n)/(n-f)
     0         0         0             1

有点难度的是透视投影,这个需要先把视椎体压成长方形,之后再移动到原点压成标准立方体,执行正交投影变换。

这里牵扯到了视角这个概念,fov,还有宽高比。所以说参数是**视角,宽高比,近平面,远平面。**关于如何推导及其复杂,建议去看games101闫令琪闫神的讲解,很细腻。这里插一句,视椎体的中心点在压缩后,是往远平面积压了。

1/(aspect*tan(fovy/2))              0             0           0
                     0  1/tan(fovy/2)             0           0
                     0              0  -(f+n)/(f-n)  -2fn/(f-n)
                     0              0            -1           0

在编写时该渲染器时,有大量的数学需要用到,除了这里,还有透视插值矫正那。重心坐标计算那里,不过这些都没有写在数学库中,其实是可以封装成一个函数直接调用的。但是这里math文件主要是来处理向量和矩阵了。

图形库(tgaimage.h和tgaimage.cpp)

图形库的使用和tinyshader当中是一样的,由于目标是最小化外部依赖关系,所以说使用TGA当做图像格式。它是支持 RGB/RGBA/黑白格式图像的最简单格式之一。这里像tinyshader 一样,使用的是tgaimage来做TGA文件的解析代码
这里的代码是直接从tga文件的解释介绍文件上扒下来的。
但是如果想知道到底是怎么实现的伙伴可以看这篇文章。
TGA文件解析

模型和贴图加载:

本软光栅渲染器主要是解释.obj模型文件,如果想进行扩展,可以添加新的解析代码,具体就是重载Model类的构造函数而已。
或者可以直接集成上assimp库,可以添加更多不同类型的模型(直接导入pmx模型),不用去在blender中拆分转换(本人blender可以说是完全不会,速成了一下午才导入了mmd模型和纹理拆分)。。。。。。

PS:这里遇到了一个超级大坑,坑的我七零八落险些放弃。。。。
说是起来也很简单,但是由于之前没有接触过各建模软件,所以说中了大坑。。。就是纹理映射的问题,blender中将pmx转换为obj的时候,uv是反着的,需要用uv编辑器翻转v轴。材质才会恢复正常。
这个问题坑就坑在,我渲染的几个模型里面,有几个模型是其他人处理好的,我不知道他经过了这一步。而且又由于自己对于blender完全不熟,根本不知道哪里除了错。还以为是子网格拆分的时候,有的模型顶点拆多了一块,有的模型拆少了一块。导致映射出错。。。。。。由于身边又找不到会blender的,所以只好去闲鱼找人,但是结果一无所获。卡了将近半个多月,渲染的人物模型都及其惊悚。。

左边是blender里面的pmx模型,右边是进行网格拆分后转换成obj文件后用我的渲染器渲染的

这里由衷的对莫娜小姐姐道个歉。。。。。。

这个问题是看贴吧一个老哥的水贴解决的。。百度贴图老哥水贴。并且最后也不是在uv编辑器里重设的uv,而是直接将每一张贴图先顺时针旋转90度,后水平镜像翻转180度修好的。

其实也可以在加载模型时,判断该模型是不是mmd模型,如果是mmd模型,就将uv坐标的v坐标进行反转,也就是1-uv[1]这样进行转换。
在这里插入图片描述
话转回来,说到模型记载,主要是的model里面,写了一个model类,里面包含了对应的模型和贴图信息。

model类的private变量需要是verts,faces,norms,和uvs,也就是顶点,面,法线和纹理数据,这是.obj模型的一些数据,同时还有模型对应的纹理贴图的一些数据。并且还有一些乱七八糟的变量,一般都是bool值,记录这个模型是不是mmd模型,是不是天空盒等

因为涉及到PBR(SG模型和MR模型),所以说贴图的种类比较多:

TGAImage* diffusemap;//漫反射贴图,决定物体表面基本颜色,它通常包含了物体表面的颜色、纹理、阴影等信息,用于模拟物体表面的基本色调。
TGAImage* normalmap;//法线贴图,用于模拟物体表面的凹凸不平,可以在不改变物体形状的情况下增加物体的表面细节。
TGAImage* specularmap;//镜面反射贴图,用于模拟物体表面的高光反射部分,通常用于描述物体表面的镜面反射亮度和大小。
TGAImage* glossinessmap;//光泽度贴图,用于模拟物体表面的光泽程度,与粗糙度是1-关系。
TGAImage* metalnessmap;//金属度贴图,用于模拟物体表面的金属属性,通常用于控制物体表面反射的颜色和强度。
TGAImage* roughnessmap;//粗糙度贴图,用于模拟物体表面的粗糙程度,通常用于调整物体表面的光泽度。
TGAImage* occlusion_map;//遮挡贴图,用于模拟物体表面不同区域之间的遮挡关系,通常用于增加物体表面的阴影效果。
TGAImage* emision_map;//自发光贴图,用于模拟物体表面的自发光效果,通常用于增加物体表面的光源效果。

PBR渲染 (MR模型),至少需要的四张贴图,Albedo,Normal,Metalness,Roughness。其中,Albedo和Normal是一个三通道的值,roughness和metalness是一个单通道的值

obj文件解析类,model构造函数

/*
这个类的构造函数接受三个参数:文件名、一个布尔值is_skybox和另一个布尔值is_from_mmd。
前两个参数用于确定是否加载天空盒纹理以及是否从MikuMikuDance软件中导入模型。
后者会影响纹理坐标的垂直翻转,同时mmd的角色模型我们采取blinn-phong渲染,非mmd角色模型我们采取PBR渲染。
*/
Model::Model(const char* filename, bool is_skybox, bool is_from_mmd)
:is_skybox(is_skybox),is_from_mmd(is_from_mmd),diffusemap(nullptr),normalmap(nullptr),specularmap(nullptr),roughnessmap(nullptr),metalnessmap(nullptr),occlusion_map(nullptr),emision_map(nullptr),environment_map(nullptr)
{
	std::ifstream in;
	in.open(filename, std::ifstream::in);
	if (in.fail())
	{
		std::cout<<"load model failed"<<std::endl;
		return;
	}

	std::string line;//缓冲buffer

	while (!in.eof())	
	{
		std::getline(in, line);
		std::istringstream iss(line.c_str());//istringstream可以根据空格分割string

		char trash;
		if (!line.compare(0, 2, "v ")) //compare相等输出0,不相等输出-1.
		{
			iss >> trash;
			vec3 v;
			for (int i = 0; i < 3; i++)
				iss >> v[i];
		
			verts.push_back(v);
		}
		else if (!line.compare(0, 3, "vn ")) 
		{
			iss >> trash >> trash;//在读取行时,将字符串line和指定的开头字符串进行比较,从而确定这是一个顶点坐标行还是法线行。但是,在处理法线行时,"vn"后面还有一个" "空格,所以需要将iss流多提取一个"trash"变量,以便将流指针移动到接下来的三个浮点数。因此,在读取法线行时需要多提取一个"trash"变量,而在读取顶点坐标行时,"v"后面没有空格,所以不需要多提取。
			vec3 n;
			for (int i = 0; i < 3; i++) 
				iss >> n[i];

			norms.push_back(n);
		}
		else if (!line.compare(0, 3, "vt ")) 
		{
			iss >> trash >> trash;
			vec2 uv;
			for (int i = 0; i < 2; i++) 
				iss >> uv[i];

			if (is_from_mmd)
				uv[1] = 1 - uv[1];

			uvs.push_back(uv);
		}
		else if (!line.compare(0, 2, "f ")) 
		{
			std::vector<int> f;
			int tmp[3];
			iss >> trash;

			while (iss >> tmp[0] >> trash >> tmp[1] >> trash >> tmp[2]) 
			{
				for (int i = 0; i < 3; i++) 
					tmp[i]--; // in wavefront obj all indices start at 1, not zero

				f.push_back(tmp[0]); f.push_back(tmp[1]); f.push_back(tmp[2]);
			}
			faces.push_back(f);
		}
	}

	std::cerr << "# v# " << verts.size() << " f# " << faces.size() << " vt# " << uvs.size() << " vn# " << norms.size() << std::endl;

	create_map(filename);
	
	if (is_skybox)
	{
		environment_map = new cubemap_t();
		load_cubemap(filename);
	}
}

模型的析构函数也比较简单,简单的说就是卸载贴图,指针置零,防止悬空指针的出现。

//卸载模型
Model::~Model() 
{
	//在C++中,delete操作符对空指针进行操作是安全的,不会引发错误。因此,如果你要释放的资源是通过new运算符分配的,你可以直接调用delete删除指针所指向的对象,而无需进行空指针检查。
	delete diffusemap;    diffusemap    = nullptr;
	delete normalmap;     normalmap     = nullptr;
	delete specularmap;   specularmap   = nullptr;
	delete roughnessmap;  roughnessmap  = nullptr;
	delete metalnessmap;  metalnessmap  = nullptr;
	delete occlusion_map; occlusion_map = nullptr;
	delete emision_map;   emision_map   = nullptr;
	
	//但是如果不检查,可能会出现nullptr->的情况出现,所以说要检查
	if (environment_map)
	{
		for (int i = 0; i < 6; i++)
			delete environment_map->faces[i];
		delete environment_map;
	}
	environment_map = nullptr;
}

加载模型贴图

//加载纹理贴图
void Model::create_map(const char* filename)
{
	diffusemap		= nullptr;
	normalmap		= nullptr;
	specularmap		= nullptr;
	roughnessmap	= nullptr;
	metalnessmap	= nullptr;
	occlusion_map	= nullptr;
	emision_map		= nullptr;

	std::string texfile(filename);
	//在字符串中找到最后一个.返回一个长度
	size_t dot = texfile.find_last_of(".");

	//io.h  _access(texfile.substr(0,dot))

	//_access(texfile.data(),0)是一个windows特有的函数,用于检查文件是否存在。如果返回值为-1,则表示文件不存在
	texfile = texfile.substr(0, dot) + std::string("_diffuse.tga");
	if (_access(texfile.data(), 0) != -1)//当文件存在时
	{
		diffusemap = new TGAImage();
		load_texture(filename, "_diffuse.tga", diffusemap);
	}

	//加载法线贴图
	texfile = texfile.substr(0, dot) + std::string("_normal.tga");
	if (_access(texfile.data(), 0) != -1)
	{
		normalmap = new TGAImage();
		load_texture(filename, "_normal.tga", normalmap);
	}

	//加载镜面光贴图
	texfile = texfile.substr(0, dot) + std::string("_spec.tga");
	if (_access(texfile.data(), 0) != -1)
	{
		specularmap = new TGAImage();
		load_texture(filename, "_spec.tga", specularmap);
	}

	//加载粗糙度贴图
	texfile = texfile.substr(0, dot) + std::string("_roughness.tga");
	if (_access(texfile.data(), 0) != -1)
	{
		roughnessmap = new TGAImage();
		load_texture(filename, "_roughness.tga", roughnessmap);
	}

	//加载金属度贴图
	texfile = texfile.substr(0, dot) + std::string("_metalness.tga");
	if (_access(texfile.data(), 0) != -1)
	{
		metalnessmap = new TGAImage();
		load_texture(filename, "_metalness.tga", metalnessmap);
	}

	//加载自发光贴图
	texfile = texfile.substr(0, dot) + std::string("_emission.tga");
	if (_access(texfile.data(), 0) != -1)
	{
		emision_map = new TGAImage();
		load_texture(filename, "_emission.tga", emision_map);
	}

	//加载遮挡贴图
	texfile = texfile.substr(0, dot) + std::string("_occlusion.tga");
	if (_access(texfile.data(), 0) != -1)
	{
		occlusion_map = new TGAImage();
		load_texture(filename, "_occlusion.tga", metalnessmap);
	}
}

加载立方体贴图

void Model::load_cubemap(const char* filename)
{
	environment_map->faces[0] = new TGAImage();
	load_texture(filename, "_right.tga", environment_map->faces[0]);
	environment_map->faces[1] = new TGAImage();
	load_texture(filename, "_left.tga", environment_map->faces[1]);
	environment_map->faces[2] = new TGAImage();
	load_texture(filename, "_top.tga", environment_map->faces[2]);
	environment_map->faces[3] = new TGAImage();
	load_texture(filename, "_bottom.tga", environment_map->faces[3]);
	environment_map->faces[4] = new TGAImage();
	load_texture(filename, "_back.tga", environment_map->faces[4]);
	environment_map->faces[5] = new TGAImage();
	load_texture(filename, "_front.tga", environment_map->faces[5]);
}

纹理采样

普通纹理采样函数(漫反射贴图,金属度贴图等等)
vec3 texture_sample(vec2 uv, TGAImage* image)
{
	//贴图采样里已经通过主轴将纹理坐标值限定到[0,1]里了
	uv[0] = fmod(uv[0], 1);//fmod,求浮点数除法的余数
	uv[1] = fmod(uv[1], 1);

	int uv0 = uv[0] * image->get_width();
	int uv1 = uv[1] * image->get_height();
	TGAColor c = image->get(uv0, uv1);
	vec3 ans;
	/*
	通常在计算机图形学中,TGA图像颜色值映射到三维向量时需要颠倒,因为TGA图像的颜色值通常是按照BGR(蓝绿红)的顺序排列的,而不是RGB(红绿蓝)的顺序。
	而在三维图形学中,通常使用的是RGB的颜色表示方式,因此需要将TGA图像中的颜色值颠倒过来。
	在给定的代码中,将TGA颜色值中的RGB通道映射到了vec3中的BGR通道,因此需要进行2-i的颠倒操作,即将B和R通道颠倒过来。
	*/
	for (int i = 0; i < 3; i++)
		ans[2 - i] = (float)c[i] / 255.f;
	return ans;
}
立方体贴图采样

计算立方体贴图上给定方向的纹理坐标。

具体来说,采用一个vec3类型的方向向量作为输入,并计算出该方向向量在立方体贴图的哪一个面上,以及该面上的纹理坐标。

函数通过找到给定方向向量中长度最大的分量(即所谓的“主轴”)来确定方向向量所在的立方体面,然后将该面映射到UV空间上的规范化纹理坐标。
最后,该函数返回表示立方体贴图面的索引值。

PS:其实learnOpenGL当中对这部分描述相当的详细了。

这样来看,这个函数就不难理解了。

static int cal_cubemap_uv(vec3& direction, vec2& uv)
{
	int face_index = -1;//初始时的面坐标设为-1.

	//变量 ma 表示向量 direction 的三个分量中最大的那个(major axis),那么 sc 和 tc 就分别是 z 和 y 分量
	float ma = 0, sc = 0, tc = 0;//通过主轴来判断时哪个面,之后将该面的纹理坐标使用sc,tc的计算求得。
	float abs_x = fabs(direction[0]), abs_y = fabs(direction[1]), abs_z = fabs(direction[2]);

	if (abs_x > abs_y && abs_x > abs_z)			/* major axis -> x */
	{
		ma = abs_x;

		if (direction.x() > 0)				
		{
			face_index = 0;
			sc = +direction.z();
			tc = +direction.y();
		}
		else								
		{
			face_index = 1;
			sc = -direction.z();
			tc = +direction.y();
		}
	}
	else if (abs_y > abs_z)						
	{
		ma = abs_y;
		if (direction.y() > 0)					
		{
			face_index = 2;
			sc = +direction.x();	//前两轮都是z变负值
			tc = +direction.z();
		}
		else									
		{
			face_index = 3;
			sc = +direction.x();
			tc = -direction.z();
		}
	}
	else										
	{
		ma = abs_z;
		if (direction.z() > 0)					
		{
			face_index = 4;
			sc = -direction.x();		//最后一轮是x变负值
			tc = +direction.y();
		}
		else									
		{
			face_index = 5;
			sc = +direction.x();
			tc = +direction.y();
		}
	}

	uv[0] = (sc / ma + 1.0f) / 2.0f;//除以ma是为了保证得到的值的范围在[-1,1]内
	uv[1] = (tc / ma + 1.0f) / 2.0f;//除以ma是为了保证得到的值的范围在[-1,1]内

	return face_index;
}

这个函数返回的是cube_map的face_index,也就是环境贴图的面序号。同时得到在该面上的uv坐标。
知道这个之后,就将在6张的环境贴图上采样直接简化成了,已知图像,已知uv坐标,来进行采样了,这样其实就是普通的纹理采样了。

创建场景:

通过一个函数指针
void (*build_scene)(Model** model, int& m, IShader** shader_use, IShader** shader_skybox, mat4 perspective, Camera* camera)来指向不同场景函数的入口地址,从而调用该场景。

函数指针其实就是一个指针,指向一个函数的入口地址,一般用于当参数或者是调用函数,比如说回调函数。

该函数的返回值是void,接受以下参数:

Model** model:指向指针的指针,用于传递模型的地址。
int& m:引用类型的整数,用于传递模型数量的变量,模型的子模块数量。
IShader** shader_use:指针数组,用于传递使用的着色器的地址。
IShader** shader_skybox:指针数组,用于传递天空盒着色器的地址。
mat4 perspective:mat4类型的变量,用于传递透视矩阵。
Camera* camera:指向 Camera 对象的指针,用于传递相机对象的地址。

通过函数指针指向不同函数建立场景

void build_fuhua_scene(Model** model, int& m, IShader** shader_use, IShader** shader_skybox, mat4 perspective, Camera* camera);
void build_yayi_scene(Model** model, int& m, IShader** shader_use, IShader** shader_skybox, mat4 perspective, Camera* camera);
void build_qiyana_scene(Model** model, int& m, IShader** shader_use, IShader** shader_skybox, mat4 perspective, Camera* camera);
void build_xier_scene(Model** model, int& m, IShader** shader_use, IShader** shader_skybox, mat4 perspective, Camera* camera);
void build_helmet_scene(Model** model, int& m, IShader** shader_use, IShader** shader_skybox, mat4 perspective, Camera* camera);
void build_gun_scene(Model** model, int& m, IShader** shader_use, IShader** shader_skybox, mat4 perspective, Camera* camera);
void build_yongzhuangfuhua_scene(Model** model, int& m, IShader** shader_use, IShader** shader_skybox, mat4 perspective, Camera* camera);
void build_sanyueqi_scene(Model** model, int& m, IShader** shader_use, IShader** shader_skybox, mat4 perspective, Camera* camera);
void build_yinlang_scene(Model** model, int& m, IShader** shader_use, IShader** shader_skybox, mat4 perspective, Camera* camera);
void build_mona_scene(Model** model, int& m, IShader** shader_use, IShader** shader_skybox, mat4 perspective, Camera* camera);
void build_crab_scene(Model** model, int& m, IShader** shader_use, IShader** shader_skybox, mat4 perspective, Camera* camera);
void build_difa_scene(Model** model, int& m, IShader** shader_use, IShader** shader_skybox, mat4 perspective, Camera* camera);

额,这里我还留了一个接口,给我老婆蒂法,但是由于没有找到合适的模型加上本人blender水平实在差劲到极点。所以说等以后再说吧。

总结:

这些函数内主要是
1.给每个场景创建模型数组,调用解析加载模型的.obj文件。
2.对每个场景设置一些参数,如摄像机参数,场景名称等。
3.选择着色器(选择是Blinn-Phong着色器还是PBR着色器),决定是否需要使用天空盒进行渲染(如果使用天空盒,需要加载IBL环境贴图)。
4.设置着色器参数和一些uniform值,主要是将一些管线需要用到值,比如说投影矩阵和视图矩阵传递加入到payload当中,用来进行传递。
5.如果使用PBR渲染的话,就需要加载IBL贴图(用于PBR渲染的间接光照部分)。

图形渲染管线

主流程详解:

有关于图形渲染管线这个资料是在太多了,知乎文章数不胜数,尤其是还有很多神书,比如RTR3,RTR4。
图形渲染管线主流上是指 原始图形数据经过一个管道各种变换最后再屏幕上输出出来的一个过程,RTR4中分成四个部分,应用程序阶段,几何阶段,光栅化阶段和像素处理阶段。RTR3中将光栅化阶段和像素处理阶段混到了一块来处理。

本光栅化渲染器的渲染管线主要体现在draw_triangle()函数上。

主要看GPU管线,意思是从几何阶段开始,先将顶点数据发送到vertex shader,进行MVP变换,之后将模型顶点从观察空间变换到了裁剪空间,在裁剪空间里,进行齐次裁剪。之后进入光栅化阶段,进行三角形的组装(图元组装)和三角形的遍历(绘制三角形)。
PS:这里把透视除法放到了光栅化阶段里。
在draw_triangle()函数,只到了图元组装阶段。
真正的三角形遍历,也就是绘制三角形阶段在rasterize_singlethread()当中。

void draw_triangles(unsigned char* framebuffer, float* zbuffer, IShader& shader, int nface)
{
	//观察空间
	for (int i = 0; i < 3; i++)
	{
		shader.vertex_shader(nface, i);//顶点着色器
	}
	//此时是在裁剪空间里,在裁剪空间里才要进行齐次裁剪
	//齐次裁剪
	int num_vertex = homo_clipping(shader.payload);

	// 三角形组装和光栅化
	for (int i = 0; i < num_vertex - 2; i++) {
		int index0 = 0;
		int index1 = i + 1;
		int index2 = i + 2;
		//将真正的顶点属性保存到payload当中去(类似于openGL中存到uniform里面去)
		transform_attri(shader.payload, index0, index1, index2);//图元组装
		//光栅化
		rasterize_singlethread(shader.payload.clipcoord_attri, framebuffer,zbuffer,shader);
	}
}

解析下rasterize_singlethread()函数,参数是两个buffer,一个framebuffer,用来进行和bitmap绑定绘制,还有一个zbuffer,用来进行深度测试。三角形的裁剪坐标,还有该模型使用的着色器等等。

void rasterize_singlethread(vec4* clipcoord_attri, unsigned char* framebuffer, float* zbuffer, IShader& shader)
{
	vec3 ndc_pos[3];//标准设备空间坐标,因为是三角形,所以说三个三维坐标
	vec3 screen_pos[3];//屏幕空间坐标
	
	//这个数组是用来保存颜色的
	unsigned char c[3];

	int width  = window->width;
	int height = window->height;
	int is_skybox = shader.payload.model->is_skybox;

	// 透视除法,从裁剪空间变到NDC当中,标准设备空间,也叫做标准立方体。透视除法里的w分量表示远近,用来形成近大远小的一个效果
	for (int i = 0; i < 3; i++)
	{
		ndc_pos[i][0] = clipcoord_attri[i][0] / clipcoord_attri[i].w();
		ndc_pos[i][1] = clipcoord_attri[i][1] / clipcoord_attri[i].w();
		ndc_pos[i][2] = clipcoord_attri[i][2] / clipcoord_attri[i].w();
	}

	// 视口变换
	for (int i = 0; i < 3; i++)
	{
		screen_pos[i][0] = 0.5*((double)width-1)*(ndc_pos[i][0] + 1.0);
		screen_pos[i][1] = 0.5*((double)height-1)*(ndc_pos[i][1] + 1.0);
		screen_pos[i][2] = is_skybox ? 1000: -clipcoord_attri[i].w();	//视图空间Z值,天空盒默认为无穷远
	}
	
	// 背部剔除,天空盒不需要这个
	if (!is_skybox)//非天空盒
	{
		//并且是反面的,那就直接return,因为不用绘制了
		if (is_back_facing(ndc_pos))
			return;
	}

	// 获得需要光栅化的三角形的bounding box
	float xmin = 10000, xmax = -10000, ymin = 10000, ymax = -10000;
	for (int i = 0; i < 3; i++) 
	{
		xmin = float_min(xmin, screen_pos[i][0]);
		xmax = float_max(xmax, screen_pos[i][0]);
		ymin = float_min(ymin, screen_pos[i][1]);
		ymax = float_max(ymax, screen_pos[i][1]);
	}

	//光栅化
	for (int x = (int)xmin; x <= (int)xmax; x++)
	{
		for (int y = (int)ymin; y <= (int)ymax; y++)
		{
			//像素中心的重心坐标
			vec3 barycentric = compute_barycentric2D((float)(x + 0.5), (float)(y + 0.5), screen_pos);

			//alpha,beta,gamma就是用来万恶的插值的源泉
			float alpha = barycentric.x(); float beta = barycentric.y(); float gamma = barycentric.z();

			//当像素在三角形内的时候,我们要对其进行绘制了
			if (is_inside_triangle(alpha, beta, gamma))
			{
				//获取该像素点在framebuffer内的坐标索引
				int index = get_index(x, y);

				//透视插值校正,之前的插值是不正确的,需要进行插值校正。
				float normalizer = 1.0 / ((double)alpha / clipcoord_attri[0].w() + beta / clipcoord_attri[1].w() + gamma / clipcoord_attri[2].w());
				//Z值越大说明距离相机越远
				float z = (alpha * screen_pos[0].z() / clipcoord_attri[0].w() + beta * screen_pos[1].z() / clipcoord_attri[1].w() +  gamma * screen_pos[2].z() / clipcoord_attri[2].w()) * normalizer;

				//进行zbuffer距离判断,深度测试,没通过深度测试的就不绘制了,这使用画家思想进行绘制,注意这里并不是early-z,虽然是在像素处理之前进行的处理,但是
				if (zbuffer[index] > z)
				{
					zbuffer[index] = z;//将物体表面最浅深度写入到深度缓存中
					vec3 color;//屏幕上需要绘制的颜色

					//调用fragment_shader进行片段着色器的着色计算
					if (shader.payload.isPBR)//如果是PBR的话,全局光照模型,需要考虑直接光照和间接光照两部分,处理逻辑和blinnphong shader是不一样的。
					{
#if 1					//std::cout << "dabendan" << std::endl;
						//把父类强制转换成子类,以便于调用子类的函数,这里直接光照的片段着色器不是虚函数,所以说也不用多态。
						PBRShader* pshader = dynamic_cast<PBRShader*>(&shader);
						color = pshader->fragment_shader(alpha, beta, gamma)+pshader->direct_fragment_shader(alpha,beta,gamma);
#else 
						color = shader.fragment_shader(alpha, beta, gamma);
#endif
					}
					else
					{
						color = shader.fragment_shader(alpha, beta, gamma);
					}	
					
					//把颜色值限制在0-255之间
					for (int i = 0; i < 3; i++)
					{
						c[i] = (int)float_clamp(color[i], 0, 255);
					}
					//将颜色值写入到framebuffer中去
					set_color(framebuffer, x, y, c);
				}
			}
		}
	}
}

顶点处理阶段(在vertex shader中),其实blinnPhong shader和PBR大同小异

齐次裁剪(Homogeneous clipping)

https://fabiensanglard.net/polygon_codec/clippingdocument/Clipping.pdf
相机空间后经过投影变换到达裁剪空间进行齐次裁剪,裁剪的平面是
其实就是将x.y.z的值与w的值进行比较,将处于[-w,w]之外的模型裁剪掉。内部的模型顶点保留下来。

//齐次裁剪
//这里应该是齐次裁剪的那个地方,(标准立方体,NDC进行裁剪,大错特错),因为裁剪是在裁剪空间,是NDC空间之前进行的。

//这个枚举类型定义了7个裁剪平面,用于视椎体裁剪,命名为W_PLANE、X_RIGHT、X_LEFT、Y_TOP、Y_BOTTOM、Z_NEAR和Z_FAR。这些裁剪平面用于图形渲染中的视景体裁剪(view frustum clipping)操作。
typedef enum
{
	W_PLANE,//W_PLANE:W平面是一个虚拟的裁剪平面,位于视椎体的远离相机的一侧。它用于深度缓冲区(depth buffer)中的近裁剪,将远离相机的物体剔除
	X_RIGHT,//X_RIGHT 和 X_LEFT:这两个裁剪平面定义了视景体的左右边界,用于剔除相机视野范围之外的物体。
	X_LEFT,
	Y_TOP,//Y_TOP 和 Y_BOTTOM:这两个裁剪平面定义了视景体的上下边界,用于剔除相机视野范围之外的物体。
	Y_BOTTOM,
	Z_NEAR,//Z_NEAR 和 Z_FAR:这两个裁剪平面定义了视景体的近裁剪平面和远裁剪平面,用于剔除相机视野范围之外的物体。
	Z_FAR
} clip_plane;
/*
判断顶点的 w 分量是否小于等于一个极小的阈值 EPSILON(可以认为 w 分量小于等于零)。如果满足条件,则返回 true,表示顶点在裁剪平面内。
判断顶点的 x 分量是否大于等于顶点的 w 分量。如果满足条件,则返回 true,表示顶点在裁剪平面内。
判断顶点的 x 分量是否小于等于顶点的 w 分量的相反数。如果满足条件,则返回 true,表示顶点在裁剪平面内。
判断顶点的 y 分量是否大于等于顶点的 w 分量。如果满足条件,则返回 true,表示顶点在裁剪平面内。
判断顶点的 y 分量是否小于等于顶点的 w 分量的相反数。如果满足条件,则返回 true,表示顶点在裁剪平面内。
判断顶点的 z 分量是否大于等于顶点的 w 分量。如果满足条件,则返回 true,表示顶点在裁剪平面内。
判断顶点的 z 分量是否小于等于顶点的 w 分量的相反数。如果满足条件,则返回 true,表示顶点在裁剪平面内。
*/
static bool is_inside_plane(clip_plane c_plane, vec4 vertex){
	switch (c_plane){
		case W_PLANE:
			return vertex.w() <= -EPSILON;
		case X_RIGHT:
			return vertex.x() >= vertex.w();
		case X_LEFT:
			return vertex.x() <= -vertex.w();
		case Y_TOP:
			return vertex.y() >= vertex.w();
		case Y_BOTTOM:
			return vertex.y() <= -vertex.w();
		case Z_NEAR:
			return vertex.z() >= vertex.w();
		case Z_FAR:
			return vertex.z() <= -vertex.w();
		default:
			return 0;
	}
}

交点的比例,适用于一个顶点在裁剪平面外,一个在内。得到交点比例,从而使得交点在平面上,不会因为模型的顶点分布影响最后的表面不是平面。
该函数的目的是为了在进行视景体裁剪时,计算顶点在裁剪平面上的交点比例,以便进行插值和裁剪操作,实现在裁剪平面上的平滑过渡和变换。

static float get_intersect_ratio(vec4 prev, vec4 curv,clip_plane c_plane){
	switch (c_plane){
		case W_PLANE:
			return (prev.w() + EPSILON) / (prev.w() - curv.w());//交点在w平面上的占比
		case X_RIGHT:
			return (prev.w() - prev.x()) / ((prev.w() - prev.x()) - (curv.w() - curv.x()));
		case X_LEFT:
			return (prev.w() + prev.x()) / ((prev.w() + prev.x()) - (curv.w() + curv.x()));
		case Y_TOP:
			return (prev.w() - prev.y()) / ((prev.w() - prev.y()) - (curv.w() - curv.y()));
		case Y_BOTTOM:
			return (prev.w() + prev.y()) / ((prev.w() + prev.y()) - (curv.w() + curv.y()));
		case Z_NEAR:
			return (prev.w() - prev.z()) / ((prev.w() - prev.z()) - (curv.w() - curv.z()));
		case Z_FAR:
			return (prev.w() + prev.z()) / ((prev.w() + prev.z()) - (curv.w() + curv.z()));
		default:
			return 0;
	}
}

clip_with_plane函数其实是sutherLand-Hodgman的算法的一个简化版,正版算法的讲述请看这篇文章。
surtherLand-Hodgman算法

//这个函数的作用是根据给定的裁剪平面对三维物体进行裁剪,将裁剪后的顶点存储在输出数据中,并返回裁剪后的顶点数量
//这个算法是sutherland-hodgman算法的一种变体。

static int clip_with_plane(clip_plane c_plane, int num_vert, payload_t& payload){
	int out_vert_num = 0;//用于记录裁剪后的顶点数量
	int previous_index, current_index;//用于记录当前顶点和前一个顶点的索引
	int is_odd = (c_plane + 1) % 2;//判断当前平面是否为奇数平面
	
	// set the right in and out datas
	// 设置输入和输出数据的指针,根据裁剪平面的奇偶性,设置对应的输入或者输出数据
	vec4* in_clipcoord	 = is_odd ? payload.in_clipcoord: payload.out_clipcoord;
	vec3* in_worldcoord  = is_odd ? payload.in_worldcoord: payload.out_worldcoord;
	vec3* in_normal		 = is_odd ? payload.in_normal: payload.out_normal;
	vec2* in_uv			 = is_odd ? payload.in_uv: payload.out_uv;
	vec4* out_clipcoord  = is_odd ? payload.out_clipcoord: payload.in_clipcoord;
	vec3* out_worldcoord = is_odd ? payload.out_worldcoord:payload.in_worldcoord;
	vec3* out_normal	 = is_odd ? payload.out_normal: payload.in_normal;
	vec2* out_uv		 = is_odd ? payload.out_uv: payload.in_uv;

	// tranverse all the edges from first vertex
	// 遍历所有的边,从第一个顶点开始
	for (int i = 0; i < num_vert; i++){
		current_index   = i;//当前坐标
		previous_index  = (i - 1 + num_vert) % num_vert;//前一个坐标
		vec4 cur_vertex = in_clipcoord[current_index];//当前顶点的裁剪坐标
		vec4 pre_vertex = in_clipcoord[previous_index];//前一个顶点的裁剪坐标

		bool is_cur_inside = is_inside_plane(c_plane, cur_vertex);//判断当前顶点是否在裁剪平面内
		bool is_pre_inside = is_inside_plane(c_plane, pre_vertex);//判断前一个顶点是否在裁剪平面内

		if (is_cur_inside != is_pre_inside){
			//如果当前顶点和前一个顶点位于平面两侧,则计算交点的比例
			float ratio = get_intersect_ratio(pre_vertex,cur_vertex,c_plane);

			//根据比例插值计算交点的裁剪坐标、世界坐标、法线和纹理坐标
			out_clipcoord[out_vert_num]  = vec4_lerp(pre_vertex,cur_vertex,ratio);
			out_worldcoord[out_vert_num] = vec3_lerp(in_worldcoord[previous_index],in_worldcoord[current_index],ratio);
			out_normal[out_vert_num]     = vec3_lerp(in_normal[previous_index],in_normal[current_index],ratio);
			out_uv[out_vert_num]         = vec2_lerp(in_uv[previous_index],in_uv[current_index],ratio);

			out_vert_num++;//增加裁剪后的顶点数量
		}
		if (is_cur_inside){
			//如果当前顶点在平面内,则将其添加到裁剪后的顶点列表中
			out_clipcoord[out_vert_num]  = cur_vertex;
			out_worldcoord[out_vert_num] = in_worldcoord[current_index];
			out_normal[out_vert_num]     = in_normal[current_index];
			out_uv[out_vert_num]         = in_uv[current_index];

			out_vert_num++;//增加裁剪后的顶点数量
		}
	}
	return out_vert_num;//返回裁剪后的顶点数量
}
//齐次裁剪(注意,先齐次裁剪之后再透视除法)
//依次使用clip_with_plane函数使得物体被裁剪平面的裁剪不受物体表面顶点分布密度的影响。
static int homo_clipping(payload_t& payload)
{
	int num_vertex = 3;//三角形的三个顶点
	num_vertex = clip_with_plane(W_PLANE, num_vertex, payload);
	num_vertex = clip_with_plane(X_RIGHT, num_vertex, payload);
	num_vertex = clip_with_plane(X_LEFT, num_vertex, payload);
	num_vertex = clip_with_plane(Y_TOP, num_vertex, payload);
	num_vertex = clip_with_plane(Y_BOTTOM, num_vertex, payload);
	num_vertex = clip_with_plane(Z_NEAR, num_vertex, payload);
	num_vertex = clip_with_plane(Z_FAR, num_vertex, payload);
	return num_vertex;
}

背面剔除
三角形在ndc空间中的坐标,返回值是根据三角形的顶点顺序,根据顶点顺序来判断三角形在正面还是在背面,其实就是求行列式的值|AB AC|,或者可以看做是夹角的sin正弦值。本质上和求法线向量其实是一样的,也就是根据法向量的方向来判断是否处于背面需要剔除。

static int is_back_facing(vec3 ndc_pos[3]){
	vec3 a = ndc_pos[0];
	vec3 b = ndc_pos[1];
	vec3 c = ndc_pos[2];
	//这应该是使用的那个根据三角形旋转方向判断正反的算法
	float signed_area = a.x() * b.y() - a.y() * b.x() +
		b.x() * c.y() - b.y() * c.x() +
		c.x() * a.y() - c.y() * a.x();   //|AB AC|
	return signed_area <= 0;
}

透视除法,将模型从裁剪 空间变换到NDC中

w表示了顶点距离相机的远近程度,用来表示透视效果。所以说对于正交投影来说,这个值是1,经过透视除法后,从裁剪空间变到NDC中。

for (int i = 0; i < 3; i++)
	{
		ndc_pos[i][0] = clipcoord_attri[i][0] / clipcoord_attri[i].w();
		ndc_pos[i][1] = clipcoord_attri[i][1] / clipcoord_attri[i].w();
		ndc_pos[i][2] = clipcoord_attri[i][2] / clipcoord_attri[i].w();
	}

深度测试
深度测试不得不提到画家算法。。。从远到近一层一层的画

先初始化设置zbuffer的大小和深度,无限远。

float* zbuffer   = (float*)malloc(sizeof(float) * width * height);
void clear_zbuffer(int width, int height, float* zbuffer)
{
	for (int i = 0; i < width * height; i++)
		zbuffer[i] = 100000;//设为100000,也就是无穷远了。
}

之后判断zbuffer[index]与当前点经过透视矫正后的z值进行判断,当z小于zbuffer内的,也就是说当前点更加近,所以说就把z的值更新到zbuffer内。之后再调用fragment shader进行像素着色。

透视插值(Perspective correct interpolation)

计算重心坐标公式
数学表示

代码实现:

static vec3 compute_barycentric2D(float x, float y, const vec3* v) 
{
	float c1 = (x*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*y + v[1].x()*v[2].y() - v[2].x()*v[1].y()) / (v[0].x()*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*v[0].y() + v[1].x()*v[2].y() - v[2].x()*v[1].y());
	float c2 = (x*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*y + v[2].x()*v[0].y() - v[0].x()*v[2].y()) / (v[1].x()*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*v[1].y() + v[2].x()*v[0].y() - v[0].x()*v[2].y());
	return vec3(c1, c2, 1 - c1 - c2);
}

判断点是否在三角形中,如果知道重心坐标的话,只需要确定是不是三个值都大于0。如果不知道重心坐标的话,就连接点到顶点,形成向量,注意起点要一致,之后看向量的叉积方向是否相同。相同则说明在三角形内,否则在三角形外部。

透视插值矫正

数学表示:

代码实现:

//需要对插值结果进行插值矫正,之前的插值结果不是线性的,中点会发生偏移。
//interpolation correct term,透视插值校正,之前的插值是不正确的,需要进行插值校正。
float normalizer = 1.0 / ((double)alpha / clipcoord_attri[0].w() + beta / clipcoord_attri[1].w() + gamma / clipcoord_attri[2].w());
//for larger z means away from camera, needs to interpolate z-value as a property			
float z = (alpha * screen_pos[0].z() / clipcoord_attri[0].w() + beta * screen_pos[1].z() / clipcoord_attri[1].w() +  gamma * screen_pos[2].z() / clipcoord_attri[2].w()) * normalizer;

值在shader当中传递

//里面包含了光照,变换矩阵,顶点属性,齐次裁剪,PBR,IBL等需要使用的属性变量。用于当vs和fs之间的一个桥梁,起到连接作用。
typedef struct payload
{
	//这就是传统意义上的mvp变换矩阵及其乘积
	mat4 model_matrix;//模型矩阵
	mat4 camera_view_matrix;//视图变换矩阵
	mat4 camera_perp_matrix;
	mat4 mvp_matrix;

	//光源视图变换矩阵和光源投影变换矩阵,可以用于生成shadow map
	mat4 light_view_matrix;
	mat4 light_perp_matrix;

	Camera* camera = nullptr;
	Model* model = nullptr;

	//顶点属性
	vec3 normal_attri[3];
	vec2 uv_attri[3];
	vec3 worldcoord_attri[3];
	vec4 clipcoord_attri[3];

	//为了齐次裁剪
	vec3 in_normal[MAX_VERTEX];
	vec2 in_uv[MAX_VERTEX];
	vec3 in_worldcoord[MAX_VERTEX];
	vec4 in_clipcoord[MAX_VERTEX];
	vec3 out_normal[MAX_VERTEX];
	vec2 out_uv[MAX_VERTEX];
	vec3 out_worldcoord[MAX_VERTEX];
	vec4 out_clipcoord[MAX_VERTEX];

	//用于IBL
	iblmap_t* iblmap = nullptr;

	//用于PBR片段着色器,归根结底因为PBR是一个全局光照,所以说要把直接光照和间接光照给分开。
	bool isPBR = false;

}payload_t;

天空盒(SkyBox)

//天空盒渲染的顶点着色器,参数是正在处理的三角形的索引和顶点在三角形中的索引,每个顶点执行一次,所有顶点之间都是相互独立的
void SkyboxShader::vertex_shader(int nfaces, int nvertex)
{
	//从模型数据中获取顶点坐标,调用vert方法获取指定面和顶点索引对应的顶点坐标
	vec3 temp = payload.model->vert(nfaces, nvertex);
	vec4 temp_vert = to_vec4(temp, 1.0f);

	//这一行代码获取模型数据中的法线向量,并将其转换为齐次坐标形式,并将结果存储在 temp_normal 变量中。
	vec4 temp_normal = to_vec4(payload.model->normal(nfaces, nvertex), 1.0f);

	payload.uv_attri[nvertex] = payload.model->uv(nfaces, nvertex);//存储纹理坐标
	payload.clipcoord_attri[nvertex] = payload.mvp_matrix * temp_vert;//将坐标顶点乘以mvp坐标得到裁剪空间的坐标
	
	//便于裁剪
	payload.in_uv[nvertex] = payload.uv_attri[nvertex];
	payload.in_clipcoord[nvertex] = payload.clipcoord_attri[nvertex];

	for (int i = 0; i < 3; i++)
	{
		//法线属性
		payload.normal_attri[nvertex][i]     = temp_normal[i];
		//世界坐标属性
		payload.worldcoord_attri[nvertex][i] = temp_vert[i];
		
		//为了裁剪
		payload.in_normal[nvertex][i]        = temp_normal[i];
		payload.in_worldcoord[nvertex][i]    = temp_vert[i];
	}
}
//fragment shader的参数是重心坐标值,需要使用重心坐标对颜色等属性进行插值
vec3 SkyboxShader::fragment_shader(float alpha, float beta, float gamma)
{
	vec4* clip_coords  = payload.clipcoord_attri;  //裁剪空间坐标数组,也就是三角形三个顶点的裁剪坐标。
	vec3* world_coords = payload.worldcoord_attri; //世界空间坐标数组,也就是三角形三个顶点的世界坐标。

	//使用裁剪空间坐标来构建属性插值系数
	float Z = 1.0 / (alpha / clip_coords[0].w() + beta / clip_coords[1].w() + gamma / clip_coords[2].w());
	//透视插值矫正,矫正worldPos,世界坐标用来当做入射向量,采样环境贴图
	vec3 worldpos = (alpha * world_coords[0] / clip_coords[0].w() + beta * world_coords[1] / clip_coords[1].w() + gamma * 		world_coords[2] / clip_coords[2].w()) * Z;
	
	vec3 result_color;
	//对cubemap进行采样,采样函数的参数是世界坐标(向量)和环境贴图
	result_color = cubemap_sampling(worldpos, payload.model->environment_map);
	return result_color * 255.f;
}

立方体采样:

/*
计算立方体贴图上给定方向的纹理坐标。
具体来说,采用一个vec3类型的方向向量作为输入,并计算出该方向向量在立方体贴图的哪一个面上,以及该面上的纹理坐标。
函数通过找到给定方向向量中长度最大的分量(即所谓的“主轴”)来确定方向向量所在的立方体面,然后将该面映射到UV空间上的规范化纹理坐标。
最后,该函数返回表示立方体贴图面的索引值。
*/

//立方体贴图的计算通常采用右手系,x轴指向右侧,y轴指向上方,z轴指向屏幕外方向。

static int cal_cubemap_uv(vec3& direction, vec2& uv){
	int face_index = -1;//初始时的面坐标设为-1.
//变量 ma 表示向量 direction 的三个分量中最大的那个(major axis),那么 sc 和 tc 就分别是 z 和 y 分量
	float ma = 0, sc = 0, tc = 0;//通过主轴来判断时哪个面,之后将该面的纹理坐标使用sc,tc的计算求得。
	float abs_x = fabs(direction[0]), abs_y = fabs(direction[1]), abs_z = fabs(direction[2]);
	if (abs_x > abs_y && abs_x > abs_z)			/* major axis -> x */{
		ma = abs_x;
		//正x轴方向的面
		if (direction.x() > 0)					/* positive x */{
			face_index = 0;
			sc = +direction.z();
			tc = +direction.y();
		}
		else									/* negative x */{
			face_index = 1;
			sc = -direction.z();
			tc = +direction.y();
		}
	}
	else if (abs_y > abs_z)						/* major axis->y */{
		ma = abs_y;
		if (direction.y() > 0)					/* positive y */{
			face_index = 2;
			sc = +direction.x();	//前两轮都是z变负值
			tc = +direction.z();
		}
		else										/* negative y */{
			face_index = 3;
			sc = +direction.x();
			tc = -direction.z();
		}
	}
	else										/* major axis -> z */{
		ma = abs_z;
		if (direction.z() > 0)					/* positive z */{
			face_index = 4;
			sc = -direction.x();		//最后一轮是x变负值
			tc = +direction.y();
		}
		else									/* negative z */{
			face_index = 5;
			sc = +direction.x();
			tc = +direction.y();
		}
	}
	uv[0] = (sc / ma + 1.0f) / 2.0f;//除以ma是为了保证得到的值的范围在[-1,1]内
	uv[1] = (tc / ma + 1.0f) / 2.0f;//除以ma是为了保证得到的值的范围在[-1,1]内
	return face_index;
}
vec3 texture_sample(vec2 uv, TGAImage* image){
	//立方体贴图采样里已经通过主轴将值限定到[0,1]里了
	//uv[0] = fmod(uv[0], 1);//fmod,求浮点数除法的余数
	//uv[1] = fmod(uv[1], 1);
	//printf("%f %f\n", uv[0], uv[1]);
	int uv0 = uv[0] * image->get_width();
	int uv1 = uv[1] * image->get_height();
	TGAColor c = image->get(uv0, uv1);
	vec3 ans;
	/*
	通常在计算机图形学中,TGA图像颜色值映射到三维向量时需要颠倒,因为TGA图像的颜色值通常是按照BGR(蓝绿红)的顺序排列的,而不是RGB(红绿蓝)的顺序。
	而在三维图形学中,通常使用的是RGB的颜色表示方式,因此需要将TGA图像中的颜色值颠倒过来。
	在给定的代码中,将TGA颜色值中的RGB通道映射到了vec3中的BGR通道,因此需要进行2-i的颠倒操作,即将B和R通道颠倒过来。
	*/
	
	for (int i = 0; i < 3; i++)
		ans[2 - i] = (float)c[i] / 255.f;
	return ans;
}
//立方体贴图采样
vec3 cubemap_sampling(vec3 direction, cubemap_t* cubemap){
	vec2 uv;
	vec3 color;
	int face_index = cal_cubemap_uv(direction, uv);//面的编号
	color = texture_sample(uv, cubemap->faces[face_index]);//在贴图上进行采样得到cubemap颜色
	return color;
}

切线空间法线贴图(Tangent space normal mapping)

理论:

法线贴图中的法线向量定义在切线空间中,在切线空间中,法线永远指向正Z方向。 法线相对于单个三角形的本地参考框架。
它就像法线贴图向量的本地空间;它们都被定义为指向正z方向,无论最终变换到什么方向。
使用一个特定的矩阵我们就能将本地/切线空间中的法线向量转成世界或视图空间下,使它们转向到最终的贴图表面的方向。
之后我们把切线空间的Z方向和表面的法线方向对齐即可。
这种矩阵叫做TBN矩阵这三个字母分别代表tangent、bitangent和normal向量。这是建构这个矩阵所需的向量。
要建构这样一个把切线空间转变为不同空间的变异矩阵,我们需要三个相互垂直的向量,它们沿一个表面的法线贴图对齐于:上、右、前;这和我们在摄像机教程中做的类似。

计算出切线和副切线并不像法线向量那么容易。从图中可以看到法线贴图的切线和副切线与纹理坐标的两个方向对齐。
我们就是用到这个特性计算每个表面的切线和副切线的。需要用到一些数学才能得到它们;

上面的话摘自learnOpenGL,一大段废话。。。。简单的说是就是,法线在法线贴图中是在切线空间中的,法线一直指向正Z方向,但是这个正Z方向和表面的法线方向不一定对齐。所以需要一个TBN矩阵来进行从切线空间到世界空间的一个转换。
TBN,切线,副切线,法线。tangent、bitangent和normal向量。这是建构这个矩阵所需的向量。

我们需要去做的,就是采样两个边的uv值,x1,y1和x2,y2。并且用这些插值计算了一个行列式的值(det)。
根据公式得到T.B三个向量后,结合之前的表面法线向量,进行施密特正交化,最后将在法线贴图中采样出来的法线向量对TBN三个轴做映射。

代码实现

//计算法线贴图纹理坐标的法线向量
//接受的参数分别是,原始法线向量,世界坐标数组,纹理坐标数组,当前片段的纹理坐标以及法线贴图的TGA图像
static vec3 cal_normal(vec3& normal, vec3* world_coords, const vec2* uvs, const vec2& uv, TGAImage* normal_map)
{
	//learnOpenGL里讲解过,这里是采样法线贴图里的纹理,并将其还原成法线值。
	//从法线贴图中采样得到一个法线样本
	vec3 sample = texture_sample(uv, normal_map);
	//对采样得到的法线样本进行范围映射,将其从因为在纹理里面的时候是0到1的,但是真正的法线向量是-1到+1的范围。
	sample = vec3(sample[0] * 2 - 1, sample[1] * 2 - 1, sample[2] * 2 - 1);

	//下面就要计算TBN矩阵
	
	//计算了纹理坐标的差,计算了两个边的插值,x1,y1和x2,y2.并用这些插值计算了一个行列式值(det)。
	float x1 = uvs[1][0] - uvs[0][0];
	float y1 = uvs[1][1] - uvs[0][1];
	float x2 = uvs[2][0] - uvs[0][0];
	float y2 = uvs[2][1] - uvs[0][1];

	float det = (x1 * y2 - x2 * y1);

	//计算了世界坐标之间的差,通过计算两个边之间的插值(e1和e2)
	vec3 e1 = world_coords[1] - world_coords[0];
	vec3 e2 = world_coords[2] - world_coords[0];

	//计算了切线轴tangent-axis和副切线轴bitangent-axis
	vec3 t = e1 * y2 + e2 * (-y1);
	vec3 b = e1 * (-x2) + e2 * x1;
	t /= det;//通过除以之前计算的行列式值来归一化
	b /= det;

	//施密特正交化,保证3个向量都是正交的,移除与目前轴相关的分量。
	normal = unit_vector(normal);
	t = unit_vector(t - dot(t, normal)*normal);
	b = unit_vector(b - dot(b, normal)*normal - dot(b, t)*t);

	//使用切线轴、副切线轴和原始切线向量按权重组合法线样本,得到最终的新法线向量。将新法线向量作为函数的返回值。
	vec3 normal_new = t * sample[0] + b * sample[1] + normal * sample[2];
	return normal_new;
}

ACES tone mapping算法和伽马矫正

这其实是在后处理阶段要做的事情

理论:

其实tone mapping也被成为曝光
为什么需要色域映射?
因为现在大部分大型的游戏或者一些影视作品都是使用HDR渲染,也就是高动态范围。但是显示器有的还是LDR的,如果不进行一个色域映射的话,就会使得渲染出来的结果要么过亮要么过暗。就比如说,台灯的亮度和太阳的亮度显示是一样的,都是一团糊,出现色偏现象。色偏现象是因为颜色亮度很高的时候,会使用截断操作。

在函数中,给定一个浮点数值 value,该函数通过将该值代入预定义的一些常量 (a、b、c、d、e),并进行一些简单的数学计算来计算映射后的值。
最终结果通过调用 float_clamp 函数来确保它在 0 到 1 之间。
这种色彩映射技术通常用于将一个色彩空间中的颜色值映射到另一个色彩空间中的颜色值,或者用于进行颜色校正和调整,以实现更好的图像质量或更好的视觉效果。

代码实现:

/*
这个函数也是用来进行色域映射的。。。把hdr转到ldr,这个函数其实就是Reinhard mapping的一部分。

这个函数实现了一种名为 "ACES" (Academy Color Encoding System) 的色彩管理系统中的色彩映射算法。
该算法通过将输入值 (通常是色彩值) 映射到新的输出值来改变颜色的外观。

在函数中,给定一个浮点数值 value,该函数通过将该值代入预定义的一些常量 (a、b、c、d、e),并进行一些简单的数学计算来计算映射后的值。
最终结果通过调用 float_clamp 函数来确保它在 0 到 1 之间。
这种色彩映射技术通常用于将一个色彩空间中的颜色值映射到另一个色彩空间中的颜色值,或者用于进行颜色校正和调整,以实现更好的图像质量或更好的视觉效果。
*/

static float float_aces(float value){
	float a = 2.51f;
	float b = 0.03f;
	float c = 2.43f;
	float d = 0.59f;
	float e = 0.14f;
	value = (value * (a * value + b)) / (value * (c * value + d) + e);
	return float_clamp(value, 0, 1);
}

注意在色域映射的第二行就进行了gamma矫正,gamma矫正是因为人眼睛成像和显像管显示的特性问题,使得显示出来的画面是在一个非线性空间,需要进行gamma矫正后,到达线性空间,更接近人眼看到的这个世界的情况。给颜色取一个幂运算,0.45或者是1/2.2次幂。

/*
Reinhard色调映射方法,输入的vec3是HDR,输出的color是LDR颜色。
目的是使HDR图像在LDR显示设备上更好的呈现出来。
在LDR中,色彩被限制在了0-1之间被表示,这样会导致失去很多的细节,比如灯泡和太阳,可能都会是一片白。太亮的地方和太暗的地方细节丢失。HDR就是通过色调映射,将LDR的转入到高动态范围中,
能保留更多的显示细节。
*/
static vec3 Reinhard_mapping(vec3& color)
{
	for (int i = 0; i < 3; i++)
	{
		color[i] = float_aces(color[i]);			
		color[i] = pow(color[i], 1.0 / 2.2);//Gamma矫正
	}
	return color;
}

Blinn – Phong着色模型(局部光照模型)

理论:

其实就是把局部光照分成三部分,环境光项,漫反射项和高光项。

环境光(Ambient Light):

环境光的计算通常是一个全局常量,表示整个场景中的整体光照强度。它可以看作是对所有光源发出的光线进行平均后的结果。环境光用于保持物体在暗处仍然可见,避免完全黑暗。
环境光的计算公式为:

Ambient = Ka * L
漫反射光(Diffuse Light):

漫反射光用于模拟由光源朝向物体表面的方向发射的光线,在物体表面进行漫反射散射的效果。漫反射光的强度取决于光源和物体表面法线之间的夹角。如果光源朝向表面法线的方向完全相同(即光线垂直入射),那么漫反射光最强;如果光源与表面法线的夹角为90度(即光线平行于表面),那么漫反射光为0。
漫反射光的计算公式为:

Diffuse = Kd * L * max(dot(N, L), 0)
镜面光(Specular Light):

镜面光用于模拟由光源以视线方向的镜面反射的形式照射到物体表面的光线。镜面光的强度与观察者与镜面反射光线的入射角成反比,并且通常在观察者看到的物体表面上产生高光反射。镜面光的强度取决于光源、视线方向和物体表面法线之间的夹角。入射角越小,镜面光越强。
镜面光的计算公式为:

Specular = Ks * L * pow(max(dot(R, V), 0), shininess)
FinalColor = Ambient + Diffuse + Specular

其中 FinalColor 表示最终的片段颜色,由环境光、漫反射光和镜面光三个成分相加得到。Ambient 是环境光成分,Diffuse 是漫反射光成分,Specular 是镜面光成分。这个公式用于计算每个片段的颜色,从而在渲染时获得逼真的光照效果。

Blinn-Phong着色器代码实现:

顶点着色器

输入是当前处理的三角形索引和顶点在三角形中的索引

void PhongShader::vertex_shader(int nfaces, int nvertex)
{
	vec4 temp_vert	 = to_vec4(payload.model->vert(nfaces, nvertex), 1.0f);//取当前顶点的位置信息
	vec4 temp_normal = to_vec4(payload.model->normal(nfaces, nvertex), 1.0f);//取出当前顶点的法线向量

	payload.uv_attri[nvertex]		 = payload.model->uv(nfaces, nvertex);//取出当前顶点的uv坐标
	payload.in_uv[nvertex]			 = payload.uv_attri[nvertex];//为了裁剪
	payload.clipcoord_attri[nvertex] = payload.mvp_matrix * temp_vert;//当前点坐标经过MVP矩阵后,从模型坐标转换到裁剪坐标系
	payload.in_clipcoord[nvertex]    = payload.clipcoord_attri[nvertex];//为了裁剪
	
	//将计算结果存储到 payload 结构体的属性数组中,以便在后续的光照计算和片段着色器阶段使用。
	for (int i = 0; i < 3; i++)
	{
		payload.worldcoord_attri[nvertex][i]		= temp_vert[i];
		payload.in_worldcoord[nvertex][i]			= temp_vert[i];
		payload.normal_attri[nvertex][i]	    	= temp_normal[i];
		payload.in_normal[nvertex][i]		  		= temp_normal[i];
	}
}
片段着色器
vec3 PhongShader::fragment_shader(float alpha, float beta, float gamma)
{
	vec4* clip_coords = payload.clipcoord_attri;	//裁剪坐标数组,把payload内的元素赋值过去
	vec3* world_coords = payload.worldcoord_attri;	//同上
	vec3* normals = payload.normal_attri;			//同上
	vec2* uvs = payload.uv_attri;					//同上

	//和其他着色器一样,透视插值矫正,对各个属性进行插值矫正,插值到重心
	float Z = 1.0 / ((double)alpha / clip_coords[0].w() + beta / clip_coords[1].w() + gamma / clip_coords[2].w());
	
	//对于blinn-phong来说,主要是法线向量,uv坐标,在世界坐标下计算,裁剪坐标是用来进行插值的
	vec3 normal = (alpha * normals[0] / clip_coords[0].w() + beta * normals[1] / clip_coords[1].w() + gamma * normals[2] / clip_coords[2].w()) * Z;
	vec2 uv = (alpha*uvs[0] / clip_coords[0].w() + beta * uvs[1] / clip_coords[1].w() + gamma * uvs[2] / clip_coords[2].w()) * Z;
	vec3 worldpos = (alpha*world_coords[0] / clip_coords[0].w() + beta * world_coords[1] / clip_coords[1].w() + gamma * world_coords[2] / clip_coords[2].w()) * Z;

	//如果存在法线贴图的话,直接在法线贴图中采样比较好
	if (payload.model->normalmap)
		normal = cal_normal(normal, world_coords, uvs, uv, payload.model->normalmap);

	// 获得环境光系数,镜面光系数和漫反射系数 ka,ks,kd
	vec3 ka(0.35, 0.35, 0.35);
	vec3 kd = payload.model->diffuse(uv);
	vec3 ks(0.8, 0.8, 0.8);

	//高光项的指数
	float p = 150.0;

	//单位光源向量,单位漫反射光强度,环境光强度,镜面光强度
	vec3 l = unit_vector(vec3(1, 1, 1));
	vec3 light_ambient_intensity = kd;
	vec3 light_diffuse_intensity = vec3(0.9, 0.9, 0.9);
	vec3 light_specular_intensity = vec3(0.15, 0.15, 0.15);

	// 在世界空间中计算着色的颜色
	// 光源为直接光
	// refer to: https://learnopengl.com/Lighting/Light-casters
	//得到单位法向量,单位视线向量,单位半程向量
	normal = unit_vector(normal);
	vec3 v = unit_vector(payload.camera->eye - worldpos);
	vec3 h = unit_vector(l + v);
	
	vec3 ambient, diffuse, specular;
	ambient = cwise_product(ka, light_ambient_intensity) ;//直接对应下标元素相乘
	diffuse = cwise_product(kd, light_diffuse_intensity) * float_max(0, dot(l, normal));
	specular = cwise_product(ks, light_specular_intensity) * float_max(0, pow(dot(normal, h), p));

	vec3 result_color(0, 0, 0);
	result_color = (ambient + diffuse + specular);
	return result_color * 255.f;
}

PBR

理论:

PBR理论引入了金属度概念,对于直接光照来说,PBR是物理的,遵循渲染方程。

在这里插入图片描述

将渲染方程拆开,将BRDF拆成漫反射项和镜面反射项,
在这里插入图片描述
对于漫反射项来说,使用lambert漫反射模型,
在这里插入图片描述
对于镜面反射来说,使用cook-torrance模型,引入了微表面理论,微表面理论认为物体表面其实是由无数细小的平面组成的,其中法线分布的越集中,其表面越光滑。法线分布的越杂乱,表面越粗糙。

在这里插入图片描述
分子上的DFG三项分别代表,法线分布函数,菲尼尔项和几何遮蔽项。
法线分布函数表示微表面的聚集程度。一般使用GGX模型,但是也有使用beckman模型的,两者的区别是GGX模型长尾,使得高光过渡更加柔和。
菲尼尔项来说,主要反映了是入射角度不同,反射光所占的比例。看过一篇介绍菲尼尔效应的文章,真正准确的菲涅尔项,需要考虑光线的S极化和P极化,之后将两个值做平均,但是一般情况我们使用施利克近似。
几何遮蔽项,主要反映了物体表面的自遮挡情况,分为shadow项和mask项。一个是入射方向的遮挡,一个是出射方向的遮挡。使用的史密斯方程两项相乘即可。

法线分布函数的公式:
在这里插入图片描述
菲涅尔项的公式:
在这里插入图片描述
几何遮蔽项公式:
在这里插入图片描述
在这里插入图片描述

法线分布函数代码实现:

/*D项:法线分布函数
微平面理论中的ggx分布函数。在该分布函数中,一个微平面的法向量与视角向量之间的夹角和法向量与光源向量之间的夹角共同决定了微平面反射的亮度。
实现金属或塑料等表面的微观粗糙度,以及提高实时渲染引擎中的反射表现效果。
*/
static float GGX_distribution(float n_dot_h, float roughness){
	float alpha = roughness * roughness;
	float alpha2 = alpha * alpha;

	float n_dot_h_2 = n_dot_h * n_dot_h;
	float factor = n_dot_h_2 * (alpha2 - 1) + 1;
	return alpha2 / (PI * factor * factor);
}

菲涅耳项代码实现:

//F:菲涅尔项是因观察角度与反射平面方向的夹角而引起的反射程度不同
//所以菲尼尔项计算的是微平面法向(真正有贡献的微平面)与观察方向的夹角
//F:菲涅尔项:主要是用来表示观察角度和反射之间的关系,也就是说不通观察角度下,反射光所占比例的多少。
//真正精确的菲尼尔项是要考虑光线的S极化,P极化,之后将两个值做平均,但是这里我们简单的使用施利克近似。
static vec3 fresenlschlick(float h_dot_v, vec3& f0){
	return f0 + (vec3(1.0, 1.0, 1.0) - f0) * pow(1 - h_dot_v, 5.0);
}
static vec3 fresenlschlick_roughness(float h_dot_v, vec3& f0, float roughness){
	float r1 = 1.0f - roughness;
	if (r1 < f0[0])
		r1 = f0[0];
	return f0 + (vec3(r1, r1, r1) - f0) * pow(1 - h_dot_v, 5.0f);
}

几何遮蔽项代码实现:

//G:几何遮蔽项,主要表示微表面的细小平面的自遮挡情况。一般用史密斯方程来表示,分别计算入射光线被遮挡的情况和视线被遮挡的情况,但是要注意这里的k根据是直接光照还是IBL间接光照,被α进行重定向。
static float SchlickGGX_geometry(float n_dot_v, float roughness){
	float r = (1 + roughness);
	float k = (double)r * r / 8.0;
	return n_dot_v / (n_dot_v*(1 - k) + k);
}
static float SchlickGGX_geometry_ibl(float n_dot_v, float roughness){
	float k = (double)roughness * roughness / 2.0;
	return n_dot_v / (n_dot_v*(1 - k) + k);
}
//G:史密斯方程,两个遮挡比相乘,几何遮挡计算的是物体宏观表面的影响,所以是宏观法向
static float geometry_Smith(float n_dot_v, float n_dot_l, float roughness){
	float g1 = SchlickGGX_geometry(n_dot_v, roughness);
	float g2 = SchlickGGX_geometry(n_dot_l, roughness);
	return g1 * g2;
}

PBR着色器代码实现:

PBR的顶点着色器

顶点着色器和blinn-phong的顶点着色器一模一样就可以,因为本来在blinn-phong顶点着色器中我们需要拿到的数据我们都拿全了。

void PBRShader::vertex_shader(int nfaces, int nvertex)
{
	vec4 temp_vert			 = to_vec4(payload.model->vert(nfaces, nvertex), 1.0f);//顶点坐标
	vec4 temp_normal		 = to_vec4(payload.model->normal(nfaces, nvertex), 1.0f);//顶点向量

	payload.uv_attri[nvertex]				 = payload.model->uv(nfaces, nvertex);//顶点的uv坐标
	payload.in_uv[nvertex]					 = payload.uv_attri[nvertex];//将顶点的uv坐标加到in_uv中,为了齐次裁剪
	payload.clipcoord_attri[nvertex]		 = payload.mvp_matrix * temp_vert;//mvp*顶点坐标,得到裁剪空间的值
	payload.in_clipcoord[nvertex]			 = payload.clipcoord_attri[nvertex];//将顶点的裁剪空间坐标加到in_clipcoord中去

	for (int i = 0; i < 3; i++)
	{
		payload.worldcoord_attri[nvertex][i] = temp_vert[i];
		payload.in_worldcoord[nvertex][i]    = temp_vert[i];
		payload.normal_attri[nvertex][i]     = temp_normal[i];
		payload.in_normal[nvertex][i]        = temp_normal[i];
	}
}
PBR片段着色器(直接光照)

直接光照部分是对渲染方程的一个拆分,反正这是个点光源,最后也不用积分。。。直接有限的几个值相加即可

vec3 PBRShader::direct_fragment_shader(float alpha, float beta, float gamma)
{
	vec3 CookTorrance_brdf;//用来存储cooktorrance brdf的结果

	vec3 light_pos = vec3(2, 1.5, 5);//光源的位置
	vec3 radiance  = vec3(3,3,3);//光源的辐照度,也就是光源的颜色

	//方便进行操作
	vec4* clip_coords  = payload.clipcoord_attri;//裁剪空间的坐标
	vec3* world_coords = payload.worldcoord_attri;//世界坐标
	vec3* normals      = payload.normal_attri;//法线向量
	vec2* uvs          = payload.uv_attri;//uv纹理坐标
	
	//对法线向量,uv坐标,世界坐标进行透视插值矫正
	float Z = 1.0 / (alpha / clip_coords[0].w() + beta / clip_coords[1].w() + gamma / clip_coords[2].w());

	vec3 normal = (alpha * normals[0] / clip_coords[0].w() + beta * normals[1] / clip_coords[1].w() + gamma * normals[2] / clip_coords[2].w()) * Z;
	vec2 uv = (alpha*uvs[0] / clip_coords[0].w() + beta * uvs[1] / clip_coords[1].w() +
gamma * uvs[2] / clip_coords[2].w()) * Z;
	vec3 worldpos = (alpha*world_coords[0] / clip_coords[0].w() + beta * world_coords[1] / clip_coords[1].w() + gamma * world_coords[2] / clip_coords[2].w()) * Z;

	vec3 l = unit_vector(light_pos - worldpos);//光源向量
	vec3 n = unit_vector(normal);//法向量
	vec3 v = unit_vector(payload.camera->eye - worldpos);//视线向量
	vec3 h = unit_vector(l + v);//半程向量

	//计算着色点法线向量与入射光的点乘结果,并与0取最大值。表示光线与法线之间夹角的余弦值,如果小于零则表示光线与法线方向相反,应该被视为无效光照。也就是说对半球积分,光源照不到的那半球不计入光照计算
	float n_dot_l = float_max(dot(n, l), 0);
	//初始化为黑色
	vec3 color(0,0,0);
	
	if (n_dot_l>0){//当有效光线的情况下才进行计算
		float n_dot_v = float_max(dot(n, v), 0);//计算法线向量与入射向量点乘结果
		float n_dot_h = float_max(dot(n, h), 0);//计算法线向量与半程向量点乘结果
		float h_dot_v = float_max(dot(h, v), 0);//计算半程向量与出射向量点乘结果

		float roughness = payload.model->roughness(uv);//从贴图中获取粗糙度
		float metalness = payload.model->metalness(uv);//从贴图中获取金属度
		
		float NDF = GGX_distribution(n_dot_h, roughness);//法线分布函数计算
		float G = geometry_Smith(n_dot_v, n_dot_l, roughness);//几何遮蔽函数计算

		//获得漫反射分量
		vec3 albedo = payload.model->diffuse(uv);//从纹理贴图中获取模型漫反射颜色
		vec3 temp = vec3(0.04, 0.04, 0.04);
		vec3 f0 = vec3_lerp(temp, albedo, metalness);//使用金属度和零向量进行插值得到F0,表示反射系数

		vec3 F = fresenlschlick(h_dot_v, f0);//它描述了光线从介质到表面的反射特性。
		vec3 kD = (vec3(1.0, 1.0, 1.0) - F)*(1 - (double)metalness);//不要忘了乘上1-metalness

		CookTorrance_brdf = (double)NDF * G * F / (4.0*n_dot_l*n_dot_v + 0.0001);

		vec3 Lo = (kD * albedo/PI + CookTorrance_brdf) * radiance * n_dot_l;//只有一个点光源。。。
	
		color = Lo;
	}

	Reinhard_mapping(color);//色域映射,调整亮度和对比度
	return color * 255.f;
}
PBR片段着色器(间接光照)(大名鼎鼎IBL)

这也就是大名鼎鼎的IBL。

//IBL fragment shader
vec3 PBRShader::fragment_shader(float alpha, float beta, float gamma)
{
	vec3 light_pos = vec3(2, 1.5, 5);//表示光源的位置

	//顶点着色器传过来的,声明了一些指向顶点属性的指针。
	vec4* clip_coords = payload.clipcoord_attri;//指向裁剪空间坐标
	vec3* world_coords = payload.worldcoord_attri;//指向世界坐标
	vec3* normals = payload.normal_attri;//指向法线坐标
	vec2* uvs = payload.uv_attri;//指向纹理坐标

	//透视插值矫正
	float Z = 1.0 / (alpha / (double)clip_coords[0].w() + beta / clip_coords[1].w() + gamma / clip_coords[2].w());
	//法线,uv,世界坐标。对这些量进行透视插值矫正
	vec3 normal = (alpha * normals[0] / clip_coords[0].w() + beta * normals[1] / clip_coords[1].w() + gamma * normals[2] / clip_coords[2].w()) * Z;
	vec2 uv = (alpha * uvs[0] / clip_coords[0].w() + beta * uvs[1] / clip_coords[1].w() +  gamma * uvs[2] / clip_coords[2].w())*Z;
	vec3 worldpos = (alpha * world_coords[0] / clip_coords[0].w() + beta * world_coords[1] / clip_coords[1].w() + gamma * world_coords[2] / clip_coords[2].w()) * Z;

	//从法线贴图中采样得到法线向量
	if (payload.model->normalmap)
	{
		normal = cal_normal(normal, world_coords, uvs, uv, payload.model->normalmap);
	}

	//这些法线,光源,视线向量等,都是在世界坐标进行计算的
	//法线向量
	vec3 n = unit_vector(normal);
	//光源向量
	vec3 l = unit_vector(light_pos - worldpos);
	//视线向量
	vec3 v = unit_vector(payload.camera->eye - worldpos);
	float n_dot_l = float_max(dot(n, l), 0);
	float n_dot_v = float_max(dot(n, v), 0);

	vec3 color(0.0f, 0.0f, 0.0f);
	
	//按光源积分域的,所以我觉得应该是法线和光线的点积
	if (n_dot_l>0)
	{
		float roughness = payload.model->roughness(uv);//进行纹理采样,得到纹理坐标处的粗糙度
		float metalness = payload.model->metalness(uv);//进行纹理采样,得到纹理坐标处的金属度
		float occlusion = payload.model->occlusion(uv);//进行纹理采样,得到纹理坐标处的遮挡度
		vec3 emission = payload.model->emission(uv);//进行纹理采样,得到纹理坐标处的自发光颜色

		//这里需要计算漫反射系数,从而计算lambertBRDF,diffuse color
		vec3 albedo = payload.model->diffuse(uv);//得到纹理坐标处的albode,也就是漫反射贴图的漫反射颜色值
		vec3 temp = vec3(0.04, 0.04, 0.04);//金属度为0,也就是非金属的反射系数,金属度越高,反射越高,这是金属度为0时的保底反射系数
		vec3 f0 = vec3_lerp(temp, albedo, metalness);

		vec3 F = fresenlschlick_roughness(n_dot_v, f0, roughness);//计算菲涅尔项
		vec3 kD = (vec3(1.0, 1.0, 1.0) - F)*(1 - (double)metalness);//计算漫反射系数,不要忘了(1-金属度)金属度为1,也就是纯金属没有漫反射

		//diffuse color,漫反射部分,由于是ibl的,所以这里应该是直接采样预滤波环境贴图了
		cubemap_t* irradiance_map = payload.iblmap->irradiance_map;
		
		//漫反射部分,由于是ibl的,所以这里应该是直接采样漫反射辐照度贴图了,得到漫反射光照
		vec3 irradiance = cubemap_sampling(n, irradiance_map);
		
		//对环境光照进行平方处理,即色彩映射。将环境光照的每个分量都进行平方操作
		for (int i = 0; i < 3; i++)
			irradiance[i] = pow(irradiance[i], 2.0f);
	
		//光照*lambert漫反射模型BRDF
		vec3 diffuse = irradiance * kD * albedo/PI;

		//镜面反射的irradiance的计算
		
		//1.镜面反射BRDF项的计算
		//specular color,镜面反射部分,由于是ibl,所以说这里应该是对lut进行查找,横坐标是视线向量和法向量间夹角的余弦值,纵坐标是roughness
		vec2 lut_uv = vec2(n_dot_v, roughness);//正如我所说的,lut表的横坐标是视线向量和法向量夹角的余弦值,也就是点乘,纵坐标是粗糙度
		vec3 lut_sample = texture_sample(lut_uv, payload.iblmap->brdf_lut);//对lut进行采样,得到BRDF信息
		float specular_scale = lut_sample.x();//菲涅尔响应的比例
		float specular_bias = lut_sample.y(); //菲涅尔响应的偏差
		
		//其实就是公式,L(x,w0) = F0*LUT.r+LUT.g,这里的f0依然是基础反射率,这里计算的还是specular的BRDF项
		vec3 specular = f0 * specular_scale + vec3(specular_bias, specular_bias, specular_bias);

		//2.镜面反射Lighting Term项计算
		//找到预滤波环境贴图的mipmap最多多少层
		float max_mip_level = (float)(payload.iblmap->mip_levels - 1);

		//根据roughness 定位到底要在哪层mipmap进行采样,这就是个公式,直接记住就可以了。roughness*maxmiplevel+0.5f。
		int specular_miplevel = (int)(roughness * max_mip_level + 0.5f);
		
		vec3 r = unit_vector(2.0 * dot(v, n) * n - v);//由view方向向量和法向量计算了反射向量,也就是入射光light的方向,把这个方向当做是预滤波环境贴图的采样向量
		//对该层级的光照信息进行采样,得到预滤波环境贴图的颜色值
		vec3 prefilter_color = cubemap_sampling(r, payload.iblmap->prefilter_maps[specular_miplevel]);

		for (int i = 0; i < 3; i++)
			prefilter_color[i] = pow(prefilter_color[i], 2.0f);
		
		//这个乘是对应直接相乘,对预滤波颜色值和原始镜面BRDF进行了直接对应相乘,公式里本来也是相乘的,因为本来也应该是split sum的方式
		specular = cwise_product(prefilter_color, specular);

		//最终颜色等于漫反射+镜面反射+自发光
		color = (diffuse + specular) + emission;
	}

	//进行色域映射,将HDR映射到LDR,因为最后是要在LDR屏幕上展示
	Reinhard_mapping(color);

	//为什么加上gamma校正后反而不对呢。。。。?因为他妈的在色域映射里面已经写了一遍gamma矫正了。。。无语
	//color = pow(color, vec3((float)1.0 / 2.2, (float)1.0 / 2.2, (float)1.0 / 2.2));

	return color * 255.f; 
}

IBL贴图的生成(改编自LearnOpenGL)

预计算漫反射贴图生成(漫反射部分)

/* IBL预计算漫反射贴图 */
/*
这个函数是一个经典的Van der Corput 序列生成函数,用于生成一个在 [0,1) 区间内的分布均匀的浮点随机数。
其输入参数 bits 是一个无符号整数,可以看做是随机种子,函数通过对其进行一系列的位操作和变换,将其映射到一个浮点数返回。
具体的操作是将输入值进行位反转(高低位颠倒)、交替位翻转等操作,以得到一个分布更加均匀的随机数序列。
这个函数在计算机图形学、蒙特卡洛渲染等领域都有广泛应用。
*/
//抄的opengl
float radicalInverse_VdC(unsigned int bits) {
	bits = (bits << 16u) | (bits >> 16u);
	bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
	bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
	bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
	bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
	return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}
/*
这个函数用于生成Hammersley采样点,它接收两个参数:i为采样点的索引,N为采样点的总数。
该函数使用Van der Corput序列生成[0,1]区间内的数值,其中一个参数使用i/N,另一个参数使用Van der Corput序列生成的值,最终生成一个二维向量(vec2),表示二维平面上的一个采样点的坐标。
通过循环调用该函数,可以生成一系列Hammersley采样点,用于渲染图像。
*/
//抄的opengl
//https://blog.csdn.net/lr_shadow/article/details/120446814  hammersley采样点
vec2 hammersley2d(unsigned int i, unsigned int N) //i是采样点的索引,N是总的采样点个数{
	return vec2(float(i) / float(N), radicalInverse_VdC(i));
}
/*
这个函数生成一个在半球体上均匀采样的向量。其中,参数u和v分别是[0,1]范围内的随机数,用于确定采样点在半球体表面的位置。
函数首先计算半球体表面上的一个点的极坐标角度phi和cosTheta,然后使用这些值计算出一个三维向量,即半球体上的一个采样点。
具体来说,phi表示该点在半球体表面的经度,cosTheta表示该点在半球体表面的纬度。函数返回的vec3向量即为半球体表面上的一个采样点的方向向量。
*/

//返回的向量都是在切线空间中的
vec3 hemisphereSample_uniform(float u, float v) 
{
	float phi = v * 2.0f * PI;
	float cosTheta = 1.0f - u;
	float sinTheta = sqrt(1.0f - cosTheta * cosTheta);
	return vec3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);
}
//这个函数是用来在半球上根据 cosine-weighted 分布进行采样的
//这个函数生成一个在半球体上均匀采样的向量。其中,参数u和v分别是[0,1]范围内的随机数,用于确定采样点在半球体表面的位置。
//函数首先计算半球体表面上的一个点的极坐标角度phi和cosTheta,然后使用这些值计算出一个三维向量,即半球体上的一个采样点。
//具体来说,phi表示该点在半球体表面的经度,cosTheta表示该点在半球体表面的纬度。函数返回的vec3向量即为半球体表面上的一个采样点的方向向量。
vec3 hemisphereSample_cos(float u, float v) {
	float phi = v * 2.0 * PI;
	float cosTheta = sqrt(1.0 - u);
	float sinTheta = sqrt(1.0 - (double)cosTheta * cosTheta);
	return vec3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);
}
基于GGX作为概率分布函数的重要性采样
//实现了一个基于GGX的重要性采样算法,用于在PBR中渲染中生成反射/折射光线的方向向量
//该算法的输入参数为一个2D随机数向量Xi、一个表面法向量N和一个粗糙度roughness,输出为一个在以N为法线的半球上采样的随机方向向量。
/*
GGX分布函数是指一种用于计算微平面法线与视线向量之间的几何遮蔽和阴影效果的函数,是PBR渲染中常用的分布函数之一。
而重要性采样是指在采样过程中使用概率密度函数(PDF)来引导采样,从而提高采样效率和采样质量的方法。
*/
//抄的opengl,而且,这些和他妈的PBR里面的重了
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness){
	//计算粗糙度对应的Alpha值:GGX分布函数的参数alpha等于roughness的平方。
	float a = roughness * roughness;
	//在半球空间内进行采样:用随机数向量的第一维u计算phi角度,用u对应的伪随机数值计算cosTheta,再计算sinTheta。
	float phi = 2.0 * PI * Xi.x();
	float cosTheta = sqrt((1.0 - Xi.y()) / (1.0 + ((double)a*a - 1.0) * Xi.y()));
	float sinTheta = sqrt(1.0 - (double)cosTheta * cosTheta);
	//从半球空间内的采样点H,构建出采样方向向量sampleVec:先从表面法向量N推导出tangent和bitangent向量,然后用H向量的分量乘以tangent,bitangent和N的线性组合得到采样方向向量sampleVec。
	
	//从球面坐标系到笛卡尔坐标系
	vec3 H;
	H[0] = cos(phi) * sinTheta;
	H[1] = sin(phi) * sinTheta;
	H[2] = cosTheta;
	
	//为了下面将从切线空间转换到世界空间做准备
	vec3 up = abs(N.z()) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
	vec3 tangent = unit_vector(cross(up, N));
	vec3 bitangent = cross(N, tangent);
	
	// 返回归一化的采样方向向量sampleVec。
	// 将采样向量从切线空间转化到世界空间来。
	vec3 sampleVec = tangent * H.x() + bitangent * H.y() + N * H.z();
	return unit_vector(sampleVec);
}
设置cubemap各个面上的法向量
/*
这段代码的作用是设置在立方体贴图上某个像素的法向量的坐标值。
立方体贴图是由6个面组成的立方体,每个面都对应了不同的方向(正/负 x/y/z),而每个像素可以看作是立方体贴图上的一个小立方体,
因此需要通过参数face_id来指定该像素所在的面,然后根据该像素在该面上的坐标(x,y),计算出该像素的法向量在世界空间中的坐标值(x_coord,y_coord,z_coord)。
具体的实现方式是根据面的方向和像素坐标计算出在该面上的坐标,再通过简单的数学运算转换为世界空间中的坐标。
*/
/*对cubemap的各个面设置不同的法向量*/
void set_normal_coord(int face_id, int x, int y, float& x_coord, float& y_coord, float& z_coord, float length = 255)
{
	switch (face_id){
	case 0:   //正x轴,右表面
		x_coord = 0.5f;
		y_coord = -0.5f + y / length;
		z_coord = -0.5f + x / length;
		break;
	case 1:   //负x轴,左表面		
		x_coord = -0.5f;
		y_coord = -0.5f + y / length;
		z_coord = 0.5f - x / length;
		break;
	case 2:   //正y轴 上表面
		x_coord = -0.5f + x / length;
		y_coord = 0.5f;
		z_coord = -0.5f + y / length;
		break;
	case 3:   //负y轴 下表面
		x_coord = -0.5f + x / length;
		y_coord = -0.5f;
		z_coord = 0.5f - y / length;
		break;
	case 4:   //正z轴 后表面
		x_coord = 0.5f - x / length;
		y_coord = -0.5f + y / length;
		z_coord = 0.5f;
		break;
	case 5:   //负z轴 前表面
		x_coord = -0.5f + x / length;
		y_coord = -0.5f + y / length;
		z_coord = -0.5f;
		break;
	default:
		break;
	}
}
/*漫反射部分*/
//生成辐照度图中的每个像素
void generate_irradiance_map(int thread_id, int face_id,TGAImage& image){
	const char* modelname5[] ={
		"obj/gun/Cerberus.obj",
		"obj/skybox/box.obj",
	};

	Model* model[1];
	model[0] = new Model(modelname5[1], 1);

	payload_t p;
	p.model = model[0];

	vec3 irradiance(0, 0, 0);
	
	for (int x = 0; x < 256; x++){
		for (int y = 0; y < 256; y++){
			float x_coord, y_coord, z_coord;
			//根据面的标识face_id,和当前像素坐标x和y,计算球面坐标系中的坐标x_coord,y_coord和z_coord
			set_normal_coord(face_id, x, y, x_coord, y_coord, z_coord);
			
			//创建法线向量normal并进行单位化处理
			vec3 normal = unit_vector(vec3(x_coord, y_coord, z_coord));		//z-axis
			vec3 up = fabs(normal[1]) < 0.999f ? vec3(0.0f, 1.0f, 0.0f) : vec3(0.0f, 0.0f, 1.0f);
			vec3 right = unit_vector(cross(up, normal));					//tagent x-axis
			up = cross(normal, right);					            		//tagent y-axis

			irradiance = vec3(0, 0, 0);//初始化辐照度为0
			float sampleDelta = 0.025f;//设置球面坐标系采样的角度增量
			int numSamples = 0;//初始化采样次数为0
			
			//在球面上采样生成预计算漫反射贴图
			for (float phi = 0.0f; phi < 2.0 * PI; phi += sampleDelta){
				for (float theta = 0.0f; theta < 0.5 * PI; theta += sampleDelta){
					
					// 球面坐标转换为直角坐标(在切线坐标系中)
					vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta));
					// 将切线空间中的采样向量转换为世界空间中的采样向量
					vec3 sampleVec = tangentSample.x() * right + tangentSample.y() * up + tangentSample.z() * normal;
					
					//对世界空间中的采样向量进行单位化处理
					sampleVec = unit_vector(sampleVec);
					
					//使用世界空间单位采样向量来对cubemap进行采样
					vec3 color = cubemap_sampling(sampleVec, p.model->environment_map);
					
					//根据采样的颜色值和角度值,累加到辐照度中
					irradiance += color * sin(theta) *cos(theta);
					
					numSamples++;//增加采样次数
				}
			}
			//计算平均辐照度,乘以PI并除以采样次数
			irradiance = PI * irradiance * (1.0f / numSamples);
			int red    = float_min(irradiance.x() * 255.0f, 255);
			int green  = float_min(irradiance.y() * 255.0f, 255);
			int blue   = float_min(irradiance.z() * 255.0f, 255);

			TGAColor temp(red, green, blue);
			image.set(x, y, temp);
		}
		cout<<x/256.f<<endl;
	}
}

生成预滤波环境贴图和lut_BRDF贴图(镜面反射部分)

/*
生成预滤波环境贴图的mipmap,定义了缩放因子和mipmap等级后,不断的初始宽高除以2的倍数。根据定义的roughness等级个数将0-1均分。
*/
/* 镜面反射部分 */
void generate_prefilter_map(int thread_id, int face_id, int mip_level,TGAImage& image)
{
	//计算预滤波环境贴图的尺寸,factor是用于缩放尺寸的因子,根据mipmap等级来决定
	int factor = 1;//缩放因子
	for (int i = 0; i < mip_level; i++)
		factor *= 2;//缩放因子不断地扩大
	int width = 512 / factor;
	int height = 512 / factor;
	
	if (width < 64)//最小到64*64
		width = 64;

	const char* modelname5[] =
	{
		"obj/gun/Cerberus.obj",
		"obj/skybox/box.obj",
	};
	
	//定义了不同粗糙度级别的值,按照个数,然后1.0均分存入roughness
	float roughness[10];
	for (int i = 0; i < 10; i++)
		roughness[i] = i * (1.0 / 9.0);
	roughness[0] = 0; roughness[9] = 1;

	Model* model[1];//创建了模型
	model[0] = new Model(modelname5[1], 1);

	payload_t p;//将模型放到payload中去
	p.model = model[0];
	
	vec3 prefilter_color(0, 0, 0);
	
	//生成预滤波环境贴图,对每个像素进行遍历和处理
	for (int x = 0; x < height; x++)
	{
		for (int y = 0; y < width; y++)
		{
			//调用set_normal_coord函数计算当前像素所对应的法线向量的球面坐标系,得到x_coord,y_coord,z_coord
			float x_coord, y_coord, z_coord;
			set_normal_coord(face_id, x, y, x_coord, y_coord, z_coord, float(width - 1));

			//根据球面坐标系中的法线向量,计算出与法线垂直的向上和向右的向量up和right
			vec3 normal = vec3(x_coord, y_coord, z_coord);
			normal = unit_vector(normal);					//z-axis
			vec3 up = fabs(normal[1]) < 0.999f ? vec3(0.0f, 1.0f, 0.0f) : vec3(0.0f, 0.0f, 1.0f);
			vec3 right = unit_vector(cross(up, normal));	//x-axis
			up = cross(normal, right);						//y-axis
			
			//初始化反射向量r和视线向量v,初始值都为法线向量
			vec3 r = normal;
			vec3 v = normal;

			prefilter_color = vec3(0, 0, 0);//初始为黑色
			float total_weight = 0.0f;//总权重
			int numSamples = 1024;//样本总数
			for (int i = 0; i < numSamples; i++)
			{
				vec2 Xi = hammersley2d(i, numSamples);//
				vec3 h = ImportanceSampleGGX(Xi, normal, roughness[mip_level]);//样本向量h
				vec3 l = unit_vector(2.0*dot(v, h) * h - v);//反射向量l
				
				//利用样本向量l在立方体贴图上进行采样,得到辐照度值
				vec3 radiance = cubemap_sampling(l, p.model->environment_map);
				
				//计算法线和样本向量之间的点积
				float n_dot_l = float_max(dot(normal, l), 0.0);
				
				//半球积分,另外半球是无效的
				if (n_dot_l > 0)
				{
					//点积大于0,则将点积乘辐照度加到prefilter_color上,并累加总权重total_weight
					prefilter_color += radiance * n_dot_l;
					total_weight += n_dot_l;
				}
			}

			prefilter_color = prefilter_color / total_weight;
			//cout << irradiance << endl;
			int red   = float_min(prefilter_color.x() * 255.0f, 255);
			int green = float_min(prefilter_color.y() * 255.0f, 255);
			int blue  = float_min(prefilter_color.z() * 255.0f, 255);

			//cout << irradiance << endl;
			TGAColor temp(red, green, blue);
			image.set(x, y, temp);
		}
		cout<<x / 512.0f<<endl;
	}
}
/* lut part */
vec3 IntegrateBRDF(float NdotV, float roughness)
{
	// 由于各向同性,随意取一个 V 即可
	vec3 V;//视线向量V,因为各向同性,所以随机选择一个方向
	V[0] = 0;//在切线空间中选择一个任意方向
	V[1] = sqrt(1.0 - (double)NdotV * NdotV);//
	V[2] = NdotV;//

	float A = 0.0f;
	float B = 0.0f;
	float C = 0.0f;

	vec3 N = vec3(0.0, 0.0, 1.0);//初始化法线向量N为(0,0,1),即z轴的正方向

	const int SAMPLE_COUNT = 1024;//定义采样次数为1024
	
	for (int i = 0; i < SAMPLE_COUNT; ++i)//开始循环进行积分,对BRDF进行采样。
	{
		//生成一个基于重要性采样的样本向量,即样本方向选择具有偏向性
		vec2 Xi = hammersley2d(i, SAMPLE_COUNT);
		{ 
			// A and B
			// 使用重要性采样生成半程向量H,并通过调用importanceSampleGGX实现
			vec3 H = ImportanceSampleGGX(Xi, N, roughness);
			//计算入射光线方向向量L,通过反射方向计算得出
			vec3 L = unit_vector(2.0 * dot(V, H) * H - V);
	
			//计算入射光线方向向量L与法线N的点积NdotL,确保为非负值
			float NdotL = float_max(L.z(), 0.0);
			//计算视线向量V与法线N的点积NdotV,确保为非负值
			float NdotV = float_max(V.z(), 0.0);
			//计算半程向量H与法线N的点积NdotH,确保为非负值
			float NdotH = float_max(H.z(), 0.0);
			//计算视线向量L与半程向量H的点积VdotH,确保为非负值
			float VdotH = float_max(dot(V, H), 0.0);

			//通过对一定数量的样本进行重要性采样来估计BRDF的两个重要分量:A和B。
			if (NdotL > 0.0)
			{
				//计算几何遮挡项G
				float G = geometry_Smith(NdotV, NdotL, roughness);
				//计算G的可见性修正因子G_Vis,该因子考虑了法线、入射光线和半程向量之间的关系
				float G_Vis = (G * VdotH) / (NdotH * NdotV);
				//计算菲涅尔反射项Fc,表示入射光线在表面上的反射强度,
				float Fc = pow(1.0 - VdotH, 5.0);
				
				A += (1.0 - Fc) * G_Vis;
				B += Fc * G_Vis;
			}
		}
	}
	//根据计算得到的A,B值以及一个常量C,将它们组成一个vec3向量并除以样本值得到平均值,vec3向量代表了BRDF积分的结果。
	return vec3(A, B, C) / float(SAMPLE_COUNT);
}
/* 对于查找表遍历所有的二维坐标*/
void calculate_BRDF_LUT(TGAImage& image)
{
	for (int i = 0; i < 256; i++)
	{
		for (int j = 0; j < 256; j++)
		{
			vec3 color;				//创建一个vec3类型的变量color来存储当前位置的颜色值
			
			if(int i == 0)			//条件判断i的值是否等于0
				//i等于0则传入较小的NdotV值(0.002f)和j除以256.0f的比例作为roughness参数,以计算该坐标点出的BRDF LUT颜色。
				color = IntegrateBRDF(0.002f, j / 256.0f);
			else
				//i不等于0的时候,将i除以256.0f和j除以256.0f作为NdotV和roughness参数传入IntegrateBRDF函数,以计算该坐标点的BRDF lut颜色。
				color = IntegrateBRDF(i /256.0f, j / 256.0f);
			
			//cout << irradiance << endl;
						
			int red   = float_min(color.x() * 255.0f, 255);
			int green = float_min(color.y() * 255.0f, 255);
			int blue  = float_min(color.z() * 255.0f, 255);
			
			//cout << irradiance << endl;
			
			TGAColor temp(red, green, blue);
			image.set(i, j, temp);
		}
	}
}

类UE的环绕式相机(其实和图形界面有很大关系)

曾经在腾讯游戏客户端公开课上跟着光子实验室的大佬们进行过ue4的学习,用ue4进行过简单的gameplay书写,所以比较习惯UE的相机。

类UE环绕式相机,意味着相机的位置需要根据一定的键盘鼠标事件处理而发生变化。

void updata_camera_pos(Camera& camera)
{
	vec3 from_target = camera.eye - camera.target;			// vector point from target to camera's position
	float radius = from_target.norm();

	float phi     = (float)atan2(from_target[0], from_target[2]); // azimuth angle(方位角), angle between from_target and z-axis,[-pi, pi]
	float theta   = (float)acos(from_target[1] / radius);		  // zenith angle(天顶角), angle between from_target and y-axis, [0, pi]
	float x_delta = window->mouse_info.orbit_delta[0] / window->width;
	float y_delta = window->mouse_info.orbit_delta[1] / window->height;

	// for mouse wheel
	radius *= (float)pow(0.95, window->mouse_info.wheel_delta);

	float factor = 1.5 * PI;
	// for mouse left button
	phi	  += x_delta * factor;
	theta += y_delta * factor;
	if (theta > PI) theta = PI - (double)EPSILON * 100;
	if (theta < 0)  theta = EPSILON * 100;

	camera.eye[0] = camera.target[0] + radius * sin(phi) * sin(theta);
	camera.eye[1] = camera.target[1] + radius * cos(theta);
	camera.eye[2] = camera.target[2] + radius * sin(theta) * cos(phi);

	// for mouse right button
	factor  = radius * (float)tan(60.0 / 360 * PI) * 2.2;
	x_delta = window->mouse_info.fv_delta[0] / window->width;
	y_delta = window->mouse_info.fv_delta[1] / window->height;
	vec3 left = (double)x_delta * factor * camera.x;
	vec3 up   = (double)y_delta * factor * camera.y;

	camera.eye += (left - up);
	camera.target += (left - up);
}

处理鼠标事件

void handle_mouse_events(Camera& camera)
{
	if (window->buttons[0])
	{
		vec2 cur_pos = get_mouse_pos();
		window->mouse_info.orbit_delta = window->mouse_info.orbit_pos - cur_pos;
		window->mouse_info.orbit_pos = cur_pos;
	}

	if (window->buttons[1])
	{
		vec2 cur_pos = get_mouse_pos();
		window->mouse_info.fv_delta = window->mouse_info.fv_pos - cur_pos;
		window->mouse_info.fv_pos = cur_pos;
	}

	updata_camera_pos(camera);
}

处理键盘事件

void handle_key_events(Camera& camera)
{
	float distance = (camera.target - camera.eye).norm();

	if (window->keys['W'])
	{
		camera.eye += -10.0 / window->width * camera.z*distance;
	}
	if (window->keys['S'])
	{
		camera.eye += 0.05f*camera.z;
	}
	if (window->keys[VK_UP] || window->keys['Q'])
	{
		camera.eye += 0.05f*camera.y;
		camera.target += 0.05f*camera.y;
	}
	if (window->keys[VK_DOWN] || window->keys['E'])
	{
		camera.eye += -0.05f*camera.y;
		camera.target += -0.05f*camera.y;
	}
	if (window->keys[VK_LEFT] || window->keys['A'])
	{
		camera.eye += -0.05f*camera.x;
		camera.target += -0.05f*camera.x;
	}
	if (window->keys[VK_RIGHT] || window->keys['D'])
	{
		camera.eye += 0.05f*camera.x;
		camera.target += 0.05f*camera.x;
	}
	if (window->keys[VK_ESCAPE])
	{
		window->is_close = 1;
	}
}

正在处理的问题:(MSAA黑线)

密集恐惧症慎入

黑线
在这里插入图片描述
在这里插入图片描述
十分纳闷,为啥会出现黑线???
这么黄金的准备面试时间我花了一下午搞这个。。。。我自己都快一脸黑线了。。。。

以下是错误代码:

//光栅化
for (int x = (int)xmin; x <= (int)xmax; x++)
{
	for (int y = (int)ymin; y <= (int)ymax; y++)
	{
		float count = 0.0f;
		vec3 color = { 0.0f,0.0f,0.0f };

		//获取该像素中心点在zbuffer内的坐标索引
		int index = get_index(x, y);

		//像素中心的重心坐标
		vec3 barycentric = compute_barycentric2D((float)(x + 0.5), (float)(y + 0.5), screen_pos);
		//alpha,beta,gamma就是用来万恶的插值的源泉
		float alpha = barycentric.x(); float beta = barycentric.y(); float gamma = barycentric.z();

		for (size_t i = 0; i < 4; i++)
		{
			//计算子采样点的重心坐标
			vec3 barysubcentric = compute_barycentric2D((float)(x + pos[i][0]), (float)(y + pos[i][1]), screen_pos);
			float subalpha = barysubcentric.x(); float subbeta = barysubcentric.y(); float subgamma = barysubcentric.z();
			if (is_inside_triangle(subalpha, subbeta, subgamma))
			{
				count++;
			}
		}
		//看子采样点是否在三角形内,使用子采样点的重心坐标
		if (is_inside_triangle(alpha, beta, gamma))
		{
			//这里是中心像素点的坐标插值。子采样点只进行一个覆盖率的计算,不进行着色操作。
			float normalizer = 1.0 / ((double)alpha / clipcoord_attri[0].w() + beta / clipcoord_attri[1].w() + gamma / clipcoord_attri[2].w());	
			float z = (alpha * screen_pos[0].z() / clipcoord_attri[0].w() + beta * screen_pos[1].z() / clipcoord_attri[1].w() + gamma * screen_pos[2].z() / clipcoord_attri[2].w()) * normalizer;

			if (zbuffer[index] > z)
			{	
				if (shader.payload.isPBR)
				{
					PBRShader* pshader = dynamic_cast<PBRShader*>(&shader);
					color = pshader->fragment_shader(alpha, beta, gamma) + pshader->direct_fragment_shader(alpha, beta, gamma);
				}
				else
				{
					color = shader.fragment_shader(alpha, beta, gamma);
				}
				zbuffer[index] = z;
				
				if (count > 0)
				{
					float temp = count / 4.0f;
					color *= temp;
				}
				for (int i = 0; i < 3; i++)
				{
					c[i] = (int)float_clamp(color[i], 0, 255);
				}
				//将颜色值写入到framebuffer中去
				set_color(framebuffer, x, y, c);
			}
		}	
	}
}

看到一个文章,可能能解决这个问题。
https://blog.csdn.net/weixin_51928794/article/details/117256226
现在在找工作阶段,先分清主次。。。。。

之前出现的问题:

1.渲染天空盒的时候,交界处有接缝。

某位大神的回答:问题可能出现在光栅化部分,也可能出现在裁剪部分。

光栅化使用GPU普遍使用的重心坐标插值,来计算重心坐标,是可以保证共用一条边的三角形完美贴合,既没有空隙,也没有重叠。
使用简化的算法,选择三角形的一个顶点作为起始点,然后起始点出发到另外两个顶点会有两条边,重心坐标是两条边的权重。
这种问题在小三角形上看不出问题,但是在大三角形上就容易出现问题。

问题的原因是对于共用一条边的两个三角形,两次选择的起始点可能会不一样,会产生很多小误差。所以可能造成那条边上的像素点,既不被这个三角形覆盖,也不被那个三角形覆盖(有空隙)。也可能会被两个三角形都覆盖(相互重叠,不过天空盒是不透明的,所以看不出来)。

大佬觉得将三角形分为上下两个平底三角形来光栅化的方法(也就是scanLine的方法)应该也有这个问题。

解决方法:加点bias,加点容错,稍微在三角形外面的像素也会被认为在三角形内部,这样就不可能会有空隙了,但会加重重复覆盖问题,不过鉴于这个问题只在天空盒上比较明显,而天空盒不是半透明的,所以说看不出来。

重心左边使用边界函数进行替代。

Top-left rule,为了避免同一个像素被相邻的三角形画两次,也就是重叠,用一个预定义好的填充规则去特殊的处理压在三角形边上的像素就可以了。
DX用的就是这个,这个就是说当一个像素刚好压在三角形边上的时候,只有这条边在三角形的左边,或者是上边的时候,才判定这个像素被三角形覆盖。

判定方法:一条边是水平的,并且边上的两个顶点都高于第三条边,就是上边。

使用edge function来算重心坐标,这样算出的重心坐标鲁棒性很强。

2.关于新的渲染模型问题。

这样的是怎么从pmx生成的,可渲染的。因为我去blender当中进行简单拆分生成obj之后,和贴图对不上,贴图会错乱,,对比一下发现。这个是怎么生成的呢?

开了编译器优化之后,渲染帧率直线上升。

加速纹理采样的方法:把贴图切分成M*n的小块,进行存储要比一行一行的存储能更有效的利用CPU缓存。GPU渲染器的话可以考虑使用GPU贴图压缩算法来处理贴图,比如ASTC。

光栅化思路:包围盒算法,扫描线算法

常规的空间变换,一个模型从加载到绘制到屏幕上需要经过多次的空间变换。
顺序是:模型空间(模型变换矩阵)->世界空间(视图变换)->相机空间(投影变换)->裁剪空间(进行透视除法,也就是xyz都除以w)->ndc空间->视口变换->屏幕空间。

3.透视除法的作用:除以w,也就是透视除法是为了实现近大远小的效果。

在裁剪空间中,w分量保存的是物体距离相机的距离。
假设有两个半径都为R的球,一个距离相机的距离为D,另一个距离相机的距离为2D。那它们各自的w分别是D和2D,经过透视除法之后,第一个球的半径将变成R/D,第二个球的半径变成0.5*R/D。第一个球的大小将是第二个球的两倍,这样就实现了近大远小的透视效果。

4.软光栅一般流程:

一般来说,流程应该是,在VS中输出MVP变换的结果,然后交给triangle函数进行透视除法,viewport变换,计算重心坐标,对varying进行插值,最后triangle拿插值后的varying作为输入调用Fragment,

draw_triange会调用VS,VS只需要输出MVP变换的结果。其他的操作,比如齐次裁剪、透视除法、背面剔除、viewport变换、计算重心坐标、对varying进行插值等都是由graphics_draw_triangle函数负责处理的。graphics_draw_triangle函数最后会调用FS,FS拿到的已经是插值后的varying了。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值