前段日子在公司做了一个图形学基础的小练习,主要就是从输入数据结构开始到最后显示到界面上,用C#编写软件模拟渲染流水线这一过程。
工程地址:https://github.com/BAJIAObujie/Renderer 目前功能基本完成,后续还有很多可以继续完善的地方。工程参考了知乎上几位大神的工程文件,想要了解的可以上知乎搜搜相关C++软渲染的话题。完成之后对理解Unity shader一些知识真的很有帮助。
这里先对之前碰到的一些问题,以及关键点做一些总结回顾。
一、输入的数据结构是一个点集,还有一个面集合,面主要是保存了点的索引,同时必须要注意点的索引的顺序,因为这一顺序与后面的法向量的计算有关。以下面这张图为例,输入的点集有八个点,设定的第一个面为1 5 6 3,第二个面为3 6 7 2。为什么这么安排呢?假设我们从200 200 200的位置往0 0 0 处看,应该只能显示出三个面,包括前面设定的两个面,因为后面要通过法向量来判断是否可以面消隐来提高渲染的效率。那么第一个面1563法向量怎么计算呢?我们取向量点1->点5,然后点5->点6,我们做叉乘计算,这里采用的是左手坐标系,所以是用左手定则,四指指向第一个向量,然后弯曲指向第二个向量,大拇指所指就是法向量。我们发现,从我们的位置看向原点的向量(-200 -200 -200) 与第一面的法向量点乘是负数,也就是两者之间的角度相差超过90度。这种情况下,我们说这个面是可以被显示的。同样的我们怎么来定义与第一面正好相对的这个面呢,它的法向量应该是-1 0 0。所以按照左手法则,我们可以定义这个面为2 7 4 0,这时大拇指是指向-x方向的。同理可得其他面。
二、一个点的法向量应该等于以这个点为顶点的所有三角形的法向量之和,所以只要计算出每个三角形的法向量,再把这个法向量加到此三角形三个顶点的法向量里即可。
以上这一句是摘自 点的法向量计算 这篇文章。以工程为例,我先计算出每一个面的法向量,然后把这个法向量加到每一个面的顶点上,当遍历完所有面的时候,顶点的法向量也就计算了出来。之前犯过一个错误,把四边形面拆分成两个三角面来计算,比如1 5 6和1 6 3。每个三角面同样带有一个法向量,然后遍历这些三角面,最后得到的法向量是错误的,比如来计算6这个顶点吧。我们划分为第一面为1 5 6和1 6 3。第二面为3 6 7和3 7 2,第三面为5 4 7 和5 7 6,当遍历每一个面的时候,我们发现顶点六,加了两次1 0 0,一次0 1 0,一次0 0 1。原因就是拆分为两个三角形的时候,重复的顶点相加了不止一次。
三、光照模型
常见的三个光照模型:Lambert,Phong,BlinnPhong
漫反射用到的是法向量与入射光线,是不必用到视线方向的。联想初中介绍的漫反射,无论从哪一个方向看亮度都是一样的。
而高光反射就要用到视线方向了。
三一、光栅化过程中的光照计算到底在哪个阶段
四、View矩阵的推导
摄像机的XYZ轴的由叉乘得来不提。重点是如何根据View空间的xyz轴在世界空间中的表示得出转换矩阵。
假设有这么一个点在三维坐标系中以 (a,b,c)表示,也就是说a乘上 (1,0,0),b乘上(0,1,0),c乘上(0,0,1)。这样就可以得到这个点在三维坐标系中的表示。
那么如何在世界空间表示一个View空间中的点呢?首先摄像机相对于世界空间坐标原点是camerapos,先做一段位移,然后abc再乘上view空间的基矩阵(这里的view空间的基矩阵是按世界空间来看的),因为不管从view还是世界空间看来,点的位置相对于view空间,都是abc乘上view的基矩阵,因为要用世界空间来表示,所以把view基矩阵XYZ用世界空间来表达,但是比例abc是不变的。
也就是某一点的worldpos = camerapos + aX + bY + cZ (这里的XYZ可能在世界坐标基矩阵100 010 001都有分量)
这里的abc是在view空间的坐标。因为我们要从世界空间转换到view空间,所以对上述等式做变换即可
把两个矩阵相乘也就得到
Matrix mat = new Matrix();
mat[0, 0] = Xaxis.x;
mat[1, 0] = Xaxis.y;
mat[2, 0] = Xaxis.z;
mat[3, 0] = -Vector.DotMultiply(Xaxis, eye);
mat[0, 1] = Yaxis.x;
mat[1, 1] = Yaxis.y;
mat[2, 1] = Yaxis.z;
mat[3, 1] = -Vector.DotMultiply(Yaxis, eye);
mat[0, 2] = Zaxis.x;
mat[1, 2] = Zaxis.y;
mat[2, 2] = Zaxis.z;
mat[3, 2] = -Vector.DotMultiply(Zaxis, eye);
mat[0, 3] = 0;
mat[1, 3] = 0;
mat[2, 3] = 0;
mat[3, 3] = 1;
也可参考这篇文章 View矩阵的工作原理
五、投影矩阵Project Matrix的推导
六、OpenGL Direct差别
N-1、Tips
1. 判断三维坐标系旋转正方向的简单方法
左手坐标系和右手坐标系的旋转正方向不同,以左手坐标系为例,确定绕着一个旋转轴旋转后,伸出左手,大拇指指向旋转轴正方向,四指弯曲的方向就是旋转正方向。右手坐标系同理。
另一个方法,不管是左手坐标系还是右手坐标系,旋转的方向统一是X->Y->Z->X。
N、目前不足的地方
1. 在MyStaticMethod类里有一个处理光照模型的算法,在里边手动定义了点光源,点光源不应该是进入fragment shader里才进入处理的。应该对顶点Vertex的顶点结构增加一个Lightview的向量,用来保存当前点指向PointLight的向量信息。然后在对顶点进行矩阵处理,在世界坐标系下的时候,就计算出当前顶点与点光源的Lightview,进入fragment shader后,LightView就可以和其它向量一样做插值运算了。(如果是多光源怎么办?虽然可以在顶点保存一个List
学习资料:
基本的空间变换
Unity 中的坐标系
OpenGL 法线贴图 切线空间 整理
法线贴图原理
学习shader之前必须知道的东西之计算机图形学(一)渲染管线N、OpenGL Direct差别