欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

[计算机绘图]绘制填充模型:重心坐标、Z 缓冲区

最编程 2024-04-29 21:10:56
...
目录
  • 一、点乘和叉乘
    • 0. 向量表示
    • 1. 点乘
    • 2.叉乘
      • 2.1 坐标运算方式
      • 2.2 向量运算方式
      • 2.3 叉乘的用途
  • 二、Bounding Box
  • 三、重心坐标
  • 四、z-buffer
  • 五、总结

一、点乘和叉乘

点乘和叉乘是向量运算中常用的两种运算符(如果没有记错的话,高中数学就涉及到这方面内容,并且本篇文章要介绍的和高中的哪些毫无出入,这里权当复习)。

至于矩阵和向量的点乘和叉乘,这里不涉及,毕竟矩阵就是一堆向量的集合而已。同时对于向量运算时的一些注意事项,比如什么样的两个向量才能运算,本篇文章不关心这些问题。

0. 向量表示

在坐标系中,一个点可以表示成:

\[P_1 = \begin{pmatrix} x_1 \\ y_1 \\ z_1 \\ \end{pmatrix} 或者 P_1 = \begin{pmatrix} x_1,y_1 ,z_1 \end{pmatrix} \]

这种写法没有任何问题,但是坐标系中的一个向量同样可以表示成:

\[V_1 = \begin{pmatrix} x_1 \\ y_1 \\ z_1 \\ \end{pmatrix} 或者 V_1 = \begin{pmatrix} x_1,y_1 ,z_1 \end{pmatrix} \]

因为向量本身就是一个仅表示方向和大小的量,对于其起点并没有什么要求,即一条向量可以在空间中任意地摆放,只要其方向和长度相等的都是同一个向量。

从向量的坐标表示中直接可以看出其方向(至于你能不能看出来就不管了,给出一条向量,暂且认为是一个三维的,把3个坐标往坐标系中一放,自然而然知道向量指向哪);至于向量的长度,即模长\(|V_1| = \sqrt{x_1^2+y_1^2+z_1^2}\),但是一般我们是不关心的模长的。是单位向量不香么?咋地带着长度,向量就长的好看了?对自己的计算水平没点数么,从小我们的数学老师就教导我们:上来先化简,能写分数的绝不写小数,保不齐最后结果就是个0

扯得有点远,总结一下:

  1. 一般我们喜欢的是单位向量,准确的说是比较关心方向特性
  2. 向量的形式和点的表示形式是一样的

那么问题来了,给出一个坐标值表示\(\begin{pmatrix} x_1,y_1 ,z_1 \end{pmatrix}\),这个表示的是点?还是向量?

有人说,可以通过在变量上边加“\(\rightarrow\)”或者粗体方式来区分,可你见过直接对坐标来这一套么?

因此,一些数学家选择增加一维来标识这个问题。
以下为方便,只会出现列向量(个人喜好)

\[P_1 = \begin{pmatrix} x_1 \\ y_1 \\ z_1 \\ 1 \\ \end{pmatrix}, V_1 = \begin{pmatrix} x_1 \\ y_1 \\ z_1 \\ 0 \\ \end{pmatrix} \]

很容易发现,对于点来说,增加的一维的值为1。而对于向量则为0。
那么把点的设为0不行么?

可以。但是不方便。

我们都知道,\(V = P_1 - P_2\)
如果把刚定义的点和向量表示形式带进来,则:

\[P_1 - P_2= \begin{pmatrix} x_1 \\ y_1 \\ z_1 \\ 1 \\ \end{pmatrix} - \begin{pmatrix} x_2 \\ y_2 \\ z_2 \\ 1 \\ \end{pmatrix} = \begin{pmatrix} x_1-x_2 \\ y_1-y_2 \\ z_1-z_2 \\ 0 \\ \end{pmatrix} = V \]

同样的我们也知道:

\(P' = P + V\)
\(V' = V_1 + V_2\)
\(V' = V_1 - V_2\)

使用上述的点和向量的定义方式,可以很好的解释这些。

当然了,\(P_1+P_2=?\),这种表示本身就没有意义。两个点直接相加是什么意思?难道是线性组合(\(P'=tP_1+(1-t)P_2\))?但线性组合仍旧不会有悖于前面的理解(有兴趣可以自行推导)。可能在数学里会对这个有明确的定义吧,但是目前对于我们来说意义不大,姑且就不考虑,以后如果有了答案,再来修改

所以,之后我们就使用这里得到的点和向量的定义

\[P = \begin{pmatrix} x \\ y \\ z \\ 1 \\ \end{pmatrix}, V = \begin{pmatrix} x \\ y \\ z \\ 0 \\ \end{pmatrix} \]

1. 点乘

设两个向量\(V_1\)\(V_2\)

\[V_1 = \begin{pmatrix} x_1 \\ y_1 \\ z_1 \\ 0 \\ \end{pmatrix}, V_2 = \begin{pmatrix} x_2 \\ y_2 \\ z_2 \\ 0 \\ \end{pmatrix} \]

则:

\[V_1 \cdot V_2 => V_1^T \cdot V_2 = \begin{pmatrix} x_1,y_1,z_1,0 \\ \end{pmatrix} \cdot \begin{pmatrix} x_2 \\ y_2 \\ z_2 \\ 0 \\ \end{pmatrix} = x_1 \cdot x_2 + y_1 \cdot y_2 + z_1 \cdot z_2+0 \cdot 0 = x_1 \cdot x_2 + y_1 \cdot y_2 + z_1 \cdot z_2 \]

为什么这里是\(V_1^T \cdot V_2\)而不是\(V_1 \cdot V_2^T\)或者其他形式?别问,问就是因为已经知道了点乘结果是一个数,而矩阵乘法的套路就是左行右列,同时因为都是列向量,即都是\(3 \times 1\)的,所以只能把前面的转置了。拿着答案推过程而已。当然你过你喜欢写成\(V_1 \cdot V_2\)也可以,大不了\(V_1\)天生行向量、\(V_2\)天生列向量呗。

以上只是一个点乘的运算接法,记得高中那会特别喜欢坐标形式。因为知道了两个向量的坐标,什么点乘、叉乘、加减啊,只要当时脑子清醒,不就是十以内乘除、五十以内的加减运算么,算一遍、检查一遍都嫌丢人。

实际上有用的是点乘的向量表示:\(V_1 \cdot V_2 = |V_1| \cdot |V_2| \cdot cos\theta\)

可以看到,对于点乘结果,实际起作用的是两个向量之间的夹角\(\theta\),假设两个向量长度是固定的。于是如果这两个向量都是单位向量,那 \(V_1 \cdot V_2 = cos\theta\),这岂不美的一匹:用坐标表示求点乘值,只要保证这两个向量是单位向量,夹角值自然就知道了。

知道夹角值,自然而然知道两个向量的关系:是重合?是垂直?还是有一定的夹角?然后自然而然也能求所谓的一个向量在另一个向量上的投影长度。

以Blinn-Phong模型为例,或者说最基本的Phong模型,这里先给出Blinn-Phong模型的公式:

\[I = I_aK_a + I_pK_d(l \cdot n) + I_pK_s(h \cdot n)^p \]

先不去关心这个式子到底有什么意义,具体的会在之后讲到。单看其中的点乘\(l \cdot n\)\(h \cdot n\),然后\(l、n、h\)分别为光源光线、物体上一点的法线、入射光线和视线的合成方向这三个向量的单位向量,是不是立马就“我懂了”:这里得到的实际就是两个\(cos\theta\),就是想得到光线在物体发现上的投影值,值越大,说明光线照射的越多,就“越亮”(单纯说亮不太准确,因为这里有两个\(cos\theta\),它们的作用不同,姑且先暂时这么认为吧)。并且在Blinn和Phong大师的论文中本来就是这么解释的,只是网上流传的比较广泛的Blinn-Phong模型算法的大多都是这个公式,这个公式多简洁,可以说Blinn-Phong模型就是这么个公式。直接拿来套个C++跑起来简直不要太爽。但话又说回来,我会了和我懂了是一样的么?

所以,说了那么多,总结起来点乘运算作用就是一个公式\(单位向量 + 点乘运算 \rightarrow 向量间夹角cos\theta\)

起码在计算机图形中是这样的!

2.叉乘

向量间另一个重要的运算是叉乘。叉乘就比较麻烦了,叉乘结果还是一个向量,又是右手法则的,又是行列式正负正的,很容易倒腾不清。

2.1 坐标运算方式

\[V_1 \times V_2 = \begin{pmatrix} x_1 \\ y_1 \\ z_1 \\ \end{pmatrix} \times \begin{pmatrix} x_2 \\ y_2 \\ z_2 \\ \end{pmatrix} = \begin{pmatrix} \textbf{i} && \textbf{j} && \textbf{k} \\ x_1 && y_1 && z_1 \\ x_2 && y_2 && z_2 \\ \end{pmatrix} = \begin{pmatrix} y_1z_2 - z_1y_2 \\ z_1x_2 - x_1z_2 \\ x_1y_2 - y_1x_2\\ \end{pmatrix} \]

那么4维的怎么计算呢?别问,问就是不会。
计算机图形中最高也就显示到3维,高维度的,别说显示了,理解都费劲。
所以先把基础搞懂,先入门计算机图形学,然后再深究原因(但愿一年之后还记得这句话)。

2.2 向量运算方式

\[V_1 \times V_2 = |V_1| \cdot |V_2| \cdot sin\theta \]

还没完,用右手法则确定方向,所以准确说上式应该是:\(|V_1 \times V_2| = |V_1| \cdot |V_2| \cdot sin\theta\)

至于右手法则怎么用,请参考这篇文章:

https://blog.****.net/dcrmg/article/details/52416832

2.3 叉乘的用途

前面说的只是叉乘的计算方式。对于叉乘,一个重要的特性就是,叉乘结果仍为一个向量。那么重点来了,向量是有方向的,即让叉乘结果仍是向量,那么就可以用叉乘的这个方向结果(也就是这个向量)来搞点事情。

对于三角形(为什么是三角形?因为对一个物体进行建模,大多用的网格来进行的,而常用的网格,无非就是三角形和四边形,三角形最简单也最常用)。

上图中\(\Delta ABC\),两个点\(P_1\)\(P_2\),其中点\(P_2\)在三角形内部,点\(P_1\)在三角形外部。
对于左图,点\(P_2\)在三角形内部,分别计算\(AP_2 \times AB\)\(BP_2 \times BC\)\(CP_2 \times CA\),判断三个叉乘的结果正负(正表示垂直直面向外,负表示垂直纸面向内)。
对于右图,点\(P_1\)在三角形外部,分别计算\(AP_1 \times AB\)\(BP_1 \times BC\)\(CP_1 \times CA\),判断三个叉乘的结果正负。

很容易发现,当只沿着一个方向(顺时针或者逆时针)进行运算时,三次结果的方向是一致的。所谓的一个方向,就是先选定一个方向(顺/逆),依次经过的三角形顶点构成的顶点序列,而不是单纯的沿着ABC。

所以,叉乘可以判断点与三角形的位置关系(内/外)。

总结来看,通过点乘判向量间的位置关系,通过叉乘判点与图形的位置关系。
对于计算机图形学来说,点乘、叉乘也就这么多内容。

二、Bounding Box

Bounding Box好像被翻译成包围和。
很容易理解,一张图就可以说明。

对于左图,需要将所有的网格与三角形进行位置判断,这个计算量是很大的,同时很多判断是无用的。
对于右图,引入一个Bounding Box将三角形包围起来,想法很简单:如果点不在Bounding Box内,自然也就不在三角形内部。因此,极大的减少了运算量。
这里可能有人要说了,我知道哪些点要计算啊,只计算那些点不行么?当然可以,我也会:先把图片进行边缘处理,提取出三角形来,然后计算像素坐标,保证一多余的点也不计算。
对此我想起来大学的高数考试:题目让证明拉格朗日中值定理,有同学上来就由拉格朗日中值定理得,巴拉巴拉,定理得证。

同时,我写了一个很low的C语言版本的判断点的位置关系的程序。

//-----------------------------------------------
//功能描述:判断点是否在三角形内
//
//参    数:x,y[in],       屏幕上要判断的点的坐标(计算像素点的中点,像素点差值为1)
//         *ppointp[in],  三角形顶点坐标,[x1, y1, z1], [x2, y2, z2], [x3, y3, z3]
//
//返 回 值:1, 在内部
//         0, 在外部
//
//备注内容:无
//-----------------------------------------------
static int _IsInsideTriangle( float x, float y, float *ptpoint )
{
	float AB[2];
	float BC[2];
	float CA[2];
	
	float AP[2];
	float BP[2];
	float CP[2];
	
	int is_inside[2];
	
	AB[0] = ptpoint[3*1+0] - ptpoint[3*0+0];
	AB[1] = ptpoint[3*1+1] - ptpoint[3*0+1];
	
	BC[0] = ptpoint[3*2+0] - ptpoint[3*1+0];
	BC[1] = ptpoint[3*2+1] - ptpoint[3*1+1];

	CA[0] = ptpoint[3*0+0] - ptpoint[3*2+0];
	CA[1] = ptpoint[3*0+1] - ptpoint[3*2+1];

	AP[0] = x - ptpoint[3*0+0];
	AP[1] = y - ptpoint[3*0+1];
	
	BP[0] = x - ptpoint[3*1+0];
	BP[1] = y - ptpoint[3*1+1];

	CP[0] = x - ptpoint[3*2+0];
	CP[1] = y - ptpoint[3*2+1];
	
	is_inside[0] = ((AB[0]*AP[1] - AB[1]*AP[0]) > 0) 
			    && ((BC[0]*BP[1] - BC[1]*BP[0]) > 0)
			    && ((CA[0]*CP[1] - CA[1]*CP[0]) > 0);
	is_inside[1] = ((AB[0]*AP[1] - AB[1]*AP[0]) < 0) 
			    && ((BC[0]*BP[1] - BC[1]*BP[0]) < 0)
			    && ((CA[0]*CP[1] - CA[1]*CP[0]) < 0);

	return (is_inside[0] || is_inside[1]);
}

三、重心坐标

重心坐标不单纯是一个坐标。实际上是一个线性组合的表示形式。

本文前面曾出现一个公式:\(P'=tP_1+(1-t)P_2\),这个实际是两点的线性组合,如果还没有接触到这个名词也没关系,大家一定见过\(P'=\frac{P_1+P_2}{2}\)吧,这个就是\(t=\frac{1}{2}\)时的特例。

那么同样的,三个点怎么线性组合?

\(P'=\alpha P_1+ \beta P_2 + \gamma P_3\)

貌似有点点问题?

\(P'=\alpha P_1+ \beta P_2 + \gamma P_3, 其中 \alpha + \beta + \gamma = 1 且 \alpha, \beta ,\gamma \in [0,1]\)

这样就没问题了。

上述的式子就是重心坐标的表达式,通过这个式子可以求出三角形内部任意一个点的值。

通过给定合法的\(\alpha, \beta ,\gamma\),即可通过三个顶点对三角形内部任何一个点进行插值(所以重心坐标可以看作是一个插值算法,和后续常用的双线性插值、双三次插值等具有一样的功能)。

那么问题来了,如何确定\(\alpha, \beta ,\gamma\)的值?
这个问题等价于:给定三个顶点坐标(包括三个顶点的信息,如着色值、纹理坐标、法线向量等),如何求出这个三角形内部任意点的信息(着色值、纹理坐标、法线向量等)?
比较懒,直接用之前的一张图片,就不重新作图了。
从图中可以看出,所谓的任意点,实际也不是很“任意”。对于这个图,就是三角形内部的栅格的中心

  • 哪些栅格?
    这个通过前面的叉乘运算就可以判断出来。
  • 什么是栅格的中心?
    就是栅格的像素坐标+0.5呗,因为每个栅格就是一个像素点。
  • 每个栅格都有哪些信息?
    栅格本身只有\(x、y\)值,但从上一章博文中可以知道通过视图变换矩阵得到顶点在屏幕上的坐标的\(x、y\)值。而屏幕就是一个二维平面,知道\(x、y\)足够了。

现在问题的突破口就很明显了:如何通过这些栅格的中心的屏幕坐标值得到其他信息?
然后我们还知道,通过重心坐标,可以求解这些信息。

那么:如何通过这些栅格的中心和三角形顶点的坐标值求重心坐标的参数\(\alpha, \beta ,\gamma\)

到了比较麻烦的推导,我只好认怂了。大家参考这篇文章。

https://blog.****.net/weixin_39874589/article/details/110813161

同样,写了一个很low的C语言版本的重心坐标的参数计算的程序。

//-----------------------------------------------
//功能描述:计算三角形重心坐标
//
//参    数:x,y[in],       屏幕上要判断的点的坐标(计算像素点的中点,像素点差值为1)
//         *ppointp[in],  三角形顶点坐标,[x1, y1, z1], [x2, y2, z2], [x3, y3, z3]
//         *pOut[out],	  重心坐标,[α,β,γ]
//
//返 回 值:void
//
//备注内容:无
//-----------------------------------------------
static void _ComputeBarycentric2D( float x, float y, float *ptpoint,
								   float *pOut )
{
    pOut[0] = (x*(ptpoint[3*1+1] - ptpoint[3*2+1]) + y*(ptpoint[3*2+0] - ptpoint[3*1+0]) + ptpoint[3*1+0]*ptpoint[3*2+1] - ptpoint[3*2+0]*ptpoint[3*1+1]) 
			/ (ptpoint[3*0+0]*(ptpoint[3*1+1] - ptpoint[3*2+1]) + (ptpoint[3*2+0] - ptpoint[3*1+0])*ptpoint[3*0+1] + ptpoint[3*1+0]*ptpoint[3*2+1] - ptpoint[3*2+0]*ptpoint[3*1+1]);
   
	pOut[1] = (x*(ptpoint[3*2+1] - ptpoint[3*0+1]) + y*(ptpoint[3*0+0] - ptpoint[3*2+0]) + ptpoint[3*2+0]*ptpoint[3*0+1] - ptpoint[3*0+0]*ptpoint[3*2+1]) 
			/ (ptpoint[3*1+0]*(ptpoint[3*2+1] - ptpoint[3*0+1]) + (ptpoint[3*0+0] - ptpoint[3*2+0])*ptpoint[3*1+1] + ptpoint[3*2+0]*ptpoint[3*0+1] - ptpoint[3*0+0]*ptpoint[3*2+1]);
    
	pOut[2] = (x*(ptpoint[3*0+1] - ptpoint[3*1+1]) + y*(ptpoint[3*1+0] - ptpoint[3*0+0]) + ptpoint[3*0+0]*ptpoint[3*1+1] - ptpoint[3*1+0]*ptpoint[3*0+1]) 
			/ (ptpoint[3*2+0]*(ptpoint[3*0+1] - ptpoint[3*1+1]) + (ptpoint[3*1+0] - ptpoint[3*0+0])*ptpoint[3*2+1] + ptpoint[3*0+0]*ptpoint[3*1+1] - ptpoint[3*1+0]*ptpoint[3*0+1]);
}

将得到的重心坐标的参数和三个顶点的信息一起作用,就可以得到任意点着色值、纹理坐标、法线向量等信息。

四、z-buffer

对于单个三角形(或者图形)的绘制,按照之前的步骤执行下来就可绘制一个单个图形(可以着色,也可以贴图)。

但如果想要绘制多个图形,目前还不可以。

为了解决这个问题,引入了z-buffer。z-buffer实质就是一个当前z值的数组(还记得在视图变换中保留的那个z轴信息吧,这里就用上了),每次需要绘制新的图形时,就将z-buffer中的值和当前带插入的值进行比较,如果距离观察点近(这里无所谓值大、值小,因为如果坐标系的建立方式不同,远离观察点的点未必z值就大),那么更新z-buffer相应的位置。

如何得到三角形内部每一个点的z值?嘿嘿,重心坐标哇。
上图是我计算机图形学课程汇报的PPT的截图,就不码字了。

五、总结

由于我们已经把三维物体划分成许许多多的三角形网格(这个姑且认为就是三角形网格,暂时没必要去纠结到底是四边形网格好还是三角形网格好)。对于这些三角形网格,我们能利用的就是顶点,所以一些三维模型文件,就是一些点的集合,在这个文件中(比如.obj文件),给出的是顶点的信息(如法线值、纹理坐标),以及点与点之间的连接关系。

所以如果你可以对一个三角形进行响应的操作,那么对于复杂的模型,无非是重复。但是量变最终一定会导致质变,所以随着三角形数量的增多,才会出现各种各样的优化问题,这也是为什么现在计算机图形学研究方向(实时渲染?本来在一天完成的工作量,现在需要在一秒甚至更短时间内完成,这个不仅仅是靠硬件的更新换代就能快速解决的)。

或者是因为我才入门,才会这么说。毕竟我到现在还在迷恋Blinn-Phong的那个公式。

原文地址:https://www.cnblogs.com/dengchow/p/14150898.html