在刚开始学习图形学的时候,很多网上,某乎,百度的一致说图形学设涉及到的数学很多,让很多对图形学感兴趣,但是数学不好的同学望而却步。其实如果不是为了做科研工作,作为学习图形学,涉及到的数学部分并不是很多,而且也不是很难。用的最多的就是线性代数中很简单的部分。线性代数中难的部分作为学习图形学基本用不着。
在学习图形学的时候也完全没必要害怕。要用到的数学知识有:
在高级图形学中还会用到微积分和概率的部分知识,但是其复杂程度没有达到高数下册后部分。
除了数学外与图形学相关的重要的课程还有:
物理:光学,力学
其他涉及到的课程还有:信号处理,数值分析
这一块高中学过了。在图形学中也使用的最多。
向量的加法,点乘和叉乘都有几何意义的计算方式和数学坐标系下的计算方式。这两个都需要很熟悉,更重的是要知道在图形学的用法。
向量的加法的结果还是一个向量。
从几何的角度看向量的加法—高中知识
在坐标系中的向量加法—在以后的图形学中的计算都是用这个来计算向量的加法。这个计算向量的长度是非常方便的。
向量的点乘是一个数.。
向量点乘在图形学中的用法如下:
下面这张图中着色点的颜色都是利用v,n,l三个向量之间的点乘来计算的,具体可以结合代码看。
for (auto& light : lights)
{
// TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular*
// components are. Then, accumulate that result on the *result_color* object.
//vector of light and eye.
Eigen::Vector3f lightDir=light.position-point;
lightDir=lightDir.normalized();
Eigen::Vector3f eyeDir=eye_pos-point;
// eyeDir=eyeDir.normalized();
//distance between light with point
Eigen::Vector3f lightDir_len=light.position-point;
// float r= lightDir_len.norm();
float r= sqrt(pow(lightDir_len[0],2)+pow(lightDir_len[1],2)+pow(lightDir_len[2],2));
Eigen::Vector3f halfVector=(lightDir+eyeDir).normalized();
//diffuse
Eigen::Vector3f diffuse=kd.cwiseProduct((light.intensity/(r*r))*MAX(0,normal.dot(lightDir)));
//ambient
Eigen::Vector3f ambient=ka.cwiseProduct(amb_light_intensity);
//specular
Eigen::Vector3f specular=ks.cwiseProduct((light.intensity/(r*r))*pow(MAX(0,normal.dot(halfVector)),p));
if (flag)
{
ambient= Eigen::Vector3f(0,0,0);
}
result_color += ambient+diffuse+specular;
flag=true;
点乘的数学计算如下:
向量叉乘的结果还是一个向量,其方向用右手螺旋定则来确定。(或者左手定则)
openGL中使用的是左手坐标系。
如下图:判断b在a的左边还是右边
用a叉乘b,得到的结果是正的,说明b在a的左侧
用b叉乘a,得到的结果是负的,说明b在a的右侧
bool insideTriangle(int x, int y, const Triangle& t)
{
Eigen::Vector2f p(x,y);
Eigen::Vector2f A(t.v[0].x(),t.v[0].y());
Eigen::Vector2f B(t.v[1].x(),t.v[1].y());
Eigen::Vector2f C(t.v[2].x(),t.v[2].y());
Eigen::Vector2f Ap(p.x()-A.x(),p.y()-A.y());
Eigen::Vector2f AB(B.x()-A.x(),B.y()-A.y());
Eigen::Vector2f Bp(p.x()-B.x(),p.y()-B.y());
Eigen::Vector2f BC(C.x()-B.x(),C.y()-B.y());
Eigen::Vector2f Cp(p.x()-C.x(),p.y()-C.y());
Eigen::Vector2f CA(A.x()-C.x(),A.y()-C.y());
if(cross(AB,Ap)*cross(BC,Bp)*cross(CA,Cp)>=0)//根据叉乘的结果来判断是否(x,y)是否在三角形内部
return true;
else
return false;
}
下面为右手坐标系
向量规范化,即让向量的长度为1。
向量的规范化在后面的光照模型和光线追踪部分使用也很多,很多计算出来的向量都要进行规范化。
//向量的规格化:就是让向量的长度等于1;
//向量长度 length = sqrt(x² + y² + z²);
//要让长度=1,那么向量 V(normalize) = V(src)/length=V(src)/sqrt(x² + y² + z²)=V(x/length,y/length,z/length);
Vector3 normalize(Vector3 const & v)
{
//float sqr = v.x*v.x + v.y*v.y + v.z*v.z;
//return v*inversesqrt(sqr);
float length = sqrt(v.x*v.x + v.y*v.y + v.z*v.z); //向量长度;
return Vector3(v.x / length, v.y / length, v.z / length); //获取到规格化的向量;
}
矩阵主要是在图形学的模型变换,view 变换,投影变换中使用。这些变换的计算都是基于矩阵的相乘进行的。
26是两个矩阵乘积的二行四列:
计算方法:找第一个矩阵的第二行(5,2),第二个矩阵的第四列(4,3),把两个向量求个点积就可以了。26=54+26;
61是两个矩阵乘积的二行三列:
计算方法:找第一个矩阵的第二行(5,2)和第二个矩阵的第三列(9,8)求点积就是61;59+28=61=61;
总结归纳这个记忆方法如下:
需要算两个矩阵乘积的第几行第几列的数值,直接在第一个矩阵中找第几行,在第二个矩阵中找第几列,然后将两个找到的向量按照顺序求向量点积。点积的数值就是要求的数值。
在这里要始终认为矩阵在左边,向量在右边(Matrix X vector)。
向量永远是一个列向量(mX1)。
这个就是图形学变换的基础。一个点想要变成另外一个点,乘以一个矩阵做变换,这个矩阵就是变换矩阵。
在Ray traycing中使用这个数学知识。
平面的一条法线以及平面上的任意一点就可以定义一个平面
平面的一个特性:平面的法线与平面的任意两个点的连线垂直(初中知识)
上面的算法只是求出了平面与直线的交点,在图形学中需要判断这个交点是否在三角形内部,采用向量叉乘那部分的方法就可以了。
但是这个方法比较麻烦,下面的方法可以直接判断交点是否在三角形内部。
利用重心坐标来求出三角形内的任何一个点,然后和光线上的点做比较,只要相等就是一个点。需要满足的条件是t>0,三角形三个顶点P0,P1,P2的系数要大于0.
需要解出t,b1,b2。三个方程和三个未知数,肯定可以求出来。
可以直接使用下面这个就可以解出来了:
在渲染方程中用到了微积分。
也就是BRDF中主要使用的就是微积分的思想。
在下面计算dA出收到的irradiance,先算一个方向收到的Radiance,然后再求积分,将各个方向的radiance积分起来,这个地方用的就是微积分思想。
主要是在蒙特卡洛积分中使用概率论的知识。
蒙特卡洛积分----给你任意一个函数求它的定积分。但是这个函数比较复杂,求不出它的解析式,蒙特卡洛就可以解决这个。
蒙特卡洛是通过随机采样的方法做的。在积分域内不断去采样。最后求和去平均。
p(X)就是概率密度函数pdf.
图形学中的很多情况要用到基本的三角知识,三角知识可以帮我们记忆基本定义。
以一点出发的两条射线形成一个角。角度定义为这两条射线 在单位圆周上截出的圆弧长度。
一般规定用较短的弧长来表示角度,符号由两条射线的顺序来确定,这时所有弧度值都位于[-π,π].每个角度都是两条射线在单位圆周上切出的圆弧的长度。
已知一个直角三角形,根据毕达哥拉斯定理有以下等式:
a²+b²=c²
定义角α的正弦、余弦以及其他三角函数如下
sinα = b/c
cscα = c/b
cosα = a/c
secα = c/a
tanα = b/a
cotα = a/b
注意:在计算机图形学中经常约定逆时针旋转为正方向。
三角函数是周期函数,多个自变量值对应同一个函数值,这 就意味着这些函数在实数域R内是不可逆的。
为了使三角函数可逆,必须对取值范围进行限制。定义域和值域规定为:
asin:[-1,1]→[-π/2,π/2]
acos:[-1,1]→[0,π]
atan:R→[-π/2,π/2]
atan2:R→[-π,π]
最后一个函数atan2(s,c)是非常有用的。其中s与sinA成正比,c与cosA成正比,且比例因子相同,atan2(s,c)的函数值为A。
假使该比例因子为整数,其中的一种理解方式为,函数返回极坐标下二维笛卡尔点(s,c)的角度。
在此列出各种有用的三角函数公式。
诱导公式:
sin(-A) = -sinA
cos(-A) = cosA
tan(-A) = -tanA
sin(π/2-A) = cosA
cos(π/2-A) = sinA
tan(π/2-A) = cotA
毕达哥拉斯公式:
sin²A + cos²A = 1
sec²A - tan²A = 1
csc²A - cot²A = 1
和差公式:
sin(A+B) = sinAcosB + cosAsinB
sin(A-B) = sinAcosB - cosAsinB
sin(2A) = 2sinAcosA
cos(A+B) = cosAcosB - sinAsinB
cos(A-B) = cosAcosB + sinAsinB
cos(2A) = cos²A - sin²A
tan(A+B) = (tanA+tanB)/(1-tanAtanB)
tan(A-B) = (tanA-tanB)/(1+tanAtanB)
tan(2A) = (2tanA)/(1-tan²A)
半角公式:
sin²(A/2) = (1-cosA)/2
cos²(A/2) = (1+cosA)/2
积化和差公式:
sinAsinB = -(cos(A+B)-cos(A-B))/2
sinAcosB = (sin(A+B)+sin(A-B))/2
cosAcosB = (cos(A+B)+cos(A-B))/2
对于变长为a,b,c 对应角分别为A,B,C的任意三角形,下列公式都成立:
sinA/a = sinB/b = sinC/c(正弦定理)
c² = a²+b²-2abcosC(余弦定理)
(a+b)/(a-b) = tan((A+B)/2)/tan((A-b)/2)(正切定理)
三角形的面积同样可以通过三条边长表示出来:
s = sqrt((a+b+c)(-a+b+c)(a-b+c)(a+b-c))/2
重心坐标:数学中,重心坐标是由单形(如三角形或四面体等)顶点定义的坐标。重心坐标是齐次坐标的一种。
作用:
三角形内的任意点都可以用三角形的三个点的坐标线性表示。如下图所示:
所以只要求出a,β,γ。(a,β,γ)就是重心坐标。
a,β,γ要满足全都大于0的条件。
a,β,γ计算方式如下:
代码如下:
static std::tuple<float, float, float> computeBarycentric2D(float x, float y, const Vector4f* 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());
float c3 = (x*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*y + v[0].x()*v[1].y() - v[1].x()*v[0].y()) / (v[2].x()*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*v[2].y() + v[0].x()*v[1].y() - v[1].x()*v[0].y());
return {c1,c2,c3};//c1,c2,c3=a,β,γ
}
然后就可以利用重心坐标对ABC三个顶点的属性进行插值。下面代码使用的alpha,beta,gamma来源于上面三角形内的点求出来的alpha,beta,gamma
tatic Eigen::Vector3f interpolate(float alpha, float beta, float gamma, const Eigen::Vector3f& vert1, const Eigen::Vector3f& vert2, const Eigen::Vector3f& vert3, float weight)
{
return (alpha * vert1 + beta * vert2 + gamma * vert3) / weight;
}
static Eigen::Vector2f interpolate(float alpha, float beta, float gamma, const Eigen::Vector2f& vert1, const Eigen::Vector2f& vert2, const Eigen::Vector2f& vert3, float weight)
{
auto u = (alpha * vert1[0] + beta * vert2[0] + gamma * vert3[0]);
auto v = (alpha * vert1[1] + beta * vert2[1] + gamma * vert3[1]);
u /= weight;
v /= weight;
return Eigen::Vector2f(u, v);
}
注意:重心坐标在投影之后坐标会变化,所以插值三维空间的属性,就应该插值三维空间中的重心坐标,然后再对应到二维空间中。
重心坐标在图形学中使用的整个逻辑是:
遍历整个三角形内部的像素坐标,对每个像素坐标求一个重心坐标,然后利用这个重心坐标对三角形三个顶点所带的属性进行插值。
其实就是用了两次重心坐标:第一次已知条件是三角形内部坐标,求出这个已知坐标的重心坐标;第二次是利用这个点的重心坐标对三角形顶点的其他属性,比如所带的颜色,法向量等进行插值计算。这样就求出了了每个像素的法线,颜色以及各种各样的属性。
几何部分主要是利用数学来表示几何体的方式分为两种:
隐式表示和显式表示
用隐式方程表示,给你一个关系,满足这个关系的点,就是这个几何体上的点。
判断一个点是否在这个面上是非常简单的事情。
直接把所有的点或者通过参数映射的表示。
测试一个点在不在几何体内比较难。
用一系列的控制点来绘制一条曲线,曲线并不一定通过控制点,只要通过起止点即可。
通过不同的t来生成。
cv::Point2i recursive_bezier(const std::vector<cv::Point2i> &control_points, float t)
{
// TODO: Implement de Casteljau's algorithm
auto &p_0 = control_points[0];
auto &p_1 = control_points[1];
auto &p_2 = control_points[2];
auto &p_3 = control_points[3];
auto point = std::pow(1 - t, 3) * p_0 + 3 * t * std::pow(1 - t, 2) * p_1 +
3 * std::pow(t, 2) * (1 - t) * p_2 + std::pow(t, 3) * p_3;
return point;
}
void bezier(const std::vector<cv::Point2i> &control_points, cv::Mat &window)
{
for (double t = 0; t <= 1.0; t += 0.001)
{
auto point= recursive_bezier(control_points,t);
window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
}
}
几个控制点的基函数加权平均,是对贝塞尔曲线的拓展。
B样条具有局部性,更容易控制,影响范围小
及其复杂。基函数很复杂
在水平方向上做一次贝塞尔曲线,然后再另外一个方向再做一次贝塞尔曲线。需要两个t。
https://www.cc.gatech.edu/~turk/math_gr.html
http://staff.ustc.edu.cn/~lgliu/Resources/CG/Math_for_CG_Turk_CN.htm