光栅化插值方法-透视除法

光栅化插值方法

发表于2015/7/4 14:40:27  1547人阅读

分类: OpenGL杂谈 图形学杂谈

光栅化是在计算机上生成图像的重要步骤,然而不管是opengl还是directx还是其它的图形接口都封装了光栅化方法.我自己做了个光栅器,接下来就说一下如何实现光栅化的.


为什么要光栅化?

图形管线的输入是图元顶点,输出的则是像素(pixel),这个步骤当中还有个中间产物叫做片段(fragment),一个片段对应一个像素,但片段比像素多了用于计算的属性,例如:深度值和法向量. 通过片段可以计算出最终将要生成像素的颜色值,我们把输入顶点计算片段的过程叫作光栅化.为什么要光栅化?因为要生成用以计算最终颜色的片段.


光栅化的输入和输出分别是啥?

和普通函数一样,光栅化函数也需要输入和输出,从之前的定义来看函数的输入就是组成图元的顶点结构,输出的就是片段结构,为什么说是结构?因为这些可以用c语言中的struct描述.


光栅化发生在哪一步?

通常在图形接口中会暴露顶点处理程序和片段处理程序(感觉着色器听起来也是云里雾里就换成处理程序),但是这当中gpu会进行光栅化插值计算,这也就是为什么片段处理程序的input是顶点处理程序的output经过了插值以后得到的值.既然光栅化是在顶点处理程序以后发生的步骤,那么输入的顶点结构是经过顶点处理以后的,也就是进行过mvp变换,乘以透视矩阵之后的顶点,注意:这步还没有做透视除法,光栅化插值发生在裁剪空间,绝不是标准化空间,所以顶点位置是四维齐次坐标不是三维坐标!


怎么实现光栅化方法?

首先我们可以确定的是光栅化的输入和输出分别是啥.并且应该知道手上可以是用的数据都是啥.

先对输入的顶点进行处理变换到屏幕坐标,对把裁剪空间的顶点坐标转换成标准化空间,就像这样:

	ndcA.x=clipA.x/clipA.w;
	ndcA.y=clipA.y/clipA.w;
	ndcB.x=clipB.x/clipB.w;
	ndcB.y=clipB.y/clipB.w;
	ndcC.x=clipC.x/clipC.w;
	ndcC.y=clipC.y/clipC.w;

接着对顶点的标准坐标进行视口变换:

	viewPortTransform(face->ndcA.x,face->ndcA.y,fb->width,fb->height,scrAX,scrAY);
	viewPortTransform(face->ndcB.x,face->ndcB.y,fb->width,fb->height,scrBX,scrBY);
	viewPortTransform(face->ndcC.x,face->ndcC.y,fb->width,fb->height,scrCX,scrCY);

然后得到三个二维坐标代表三个顶点最终在屏幕上的位置,它们可以组成一个二维三角形,求取三角形的包围盒:

	int minX=max(0,min(scrAX,min(scrBX,scrCX)));
	int maxX=min(fb->width-1,max(scrAX,max(scrBX,scrCX)));
	int minY=max(0,min(scrAY,min(scrBY,scrCY)));
	int maxY=min(fb->height-1,max(scrAY,max(scrBY,scrCY)));
要注意不要超过屏幕范围,屏幕范围以外的点都裁剪掉.


遍历这个包围盒,取得潜在可能片段的屏幕位置:

	for(int scrX=minX;scrX<=maxX;scrX++) {
		for(int scrY=minY;scrY<=maxY;scrY++) {
             ....
		}
	}

分别求取片段对应的标准化空间坐标:

			invViewPortTransform(scrX,scrY,fb->width,fb->height,ndcX,ndcY);
这里用了逆视口变换,视口变换和逆视口变换很方便,只要对坐标进行缩放和平移就行了.


那么我们得到了可能片段的标准化空间的x和y坐标,为什么是可能片段呢?因为现在还没法确定这些片段在将要被光栅化三角形的外部还是内部,我们只计算三角形内部的片段.

然而知道了这些有什么用呢?

这边有一个公式可以算出三个顶点对片段产生影响的比例,也叫权值:


这个公式的a b c分别代表三角形的三个顶点, ax ay aw 分别是顶点a在裁剪空间的齐次坐标(是四维的)的x y w值,这边没用到z值,因为z也要通过这个权值进行计算.

这个怎么推导这个公式?

已知待光栅化三角形abc的三个顶点在裁剪空间的齐次坐标,把权值alpha beta gamma设为pa pb pc,可得每个片段的裁剪空间齐次坐标为:

x=pa*ax+pb*bx+pc*cx

y=pa*ay+pb*by+pc*cy

z=pa*az+pb*bz+pc*cz

w=pa*aw+pb*bw+pc*cw


然后计算片段在标准化坐标系的坐标值为:

nx=x/w

ny=y/w

nz=z/w

nw=1


可以推得:

x=w*nx

y=w*ny

w=w


因为:

x=pa*ax+pb*bx+pc*cx

y=pa*ay+pb*by+pc*cy

w=pa*aw+pb*bw+pc*cw

转换为3x3矩阵就是

ax    bx    cx              pa            w*nx

ay    by    cy      *      pb     =     w*ny

aw   bw   cw             pc            w


其中nx和ny就是之前取得的片段在标准化坐标系的x y值;并且由于pa pb pc是比值,所以w可以去除;这样只要求取3x3矩阵的逆就可以取得pa pb pc的值.

但是要注意pa+pb+pc=1,所以计算出值以后要进行如下处理:

			float sum=pa+pb+pc;
			pa/=sum; pb/=sum; pc/=sum;


然后把有比值小于0的片段抛弃:

			if(pa<0||pb<0||pc<0)
				continue;


接下来就可以用这三个权值对顶点属性进行插值运算了.


具体的光栅化函数是这样:

void rasterize(FrameBuffer* fb,DepthBuffer* db,FragmentShader fs,Face* face) {
	float ndcX=0,ndcY=0,clipW=0;
	int scrAX,scrAY,scrBX,scrBY,scrCX,scrCY;
	viewPortTransform(face->ndcA.x,face->ndcA.y,fb->width,fb->height,scrAX,scrAY);
	viewPortTransform(face->ndcB.x,face->ndcB.y,fb->width,fb->height,scrBX,scrBY);
	viewPortTransform(face->ndcC.x,face->ndcC.y,fb->width,fb->height,scrCX,scrCY);
	int minX=max(0,min(scrAX,min(scrBX,scrCX)));
	int maxX=min(fb->width-1,max(scrAX,max(scrBX,scrCX)));
	int minY=max(0,min(scrAY,min(scrBY,scrCY)));
	int maxY=min(fb->height-1,max(scrAY,max(scrBY,scrCY)));
	for(int scrX=minX;scrX<=maxX;scrX++) {
		for(int scrY=minY;scrY<=maxY;scrY++) {
			invViewPortTransform(scrX,scrY,fb->width,fb->height,ndcX,ndcY);
			VECTOR4D ndcPixel(ndcX,ndcY,1,0);
			VECTOR4D proportion4D=face->clipMatrixInv*ndcPixel;
			VECTOR3D proportionFragment(proportion4D.x,proportion4D.y,proportion4D.z);
			float pa=proportionFragment.x;
			float pb=proportionFragment.y;
			float pc=proportionFragment.z;
			float sum=pa+pb+pc;
			pa/=sum; pb/=sum; pc/=sum;
			if(pa<0||pb<0||pc<0)
				continue;

			Fragment frag;
			interpolate3f(pa,pb,pc,face->clipA.w,face->clipB.w,face->clipC.w,clipW);
			interpolate3f(pa,pb,pc,face->clipA.z,face->clipB.z,face->clipC.z,frag.ndcZ);
			frag.ndcZ/=clipW;
			if(frag.ndcZ<-1||frag.ndcZ>1)
				continue;
			if(db!=NULL) {
				float storeZ=readDepth(db,scrX,scrY);
				if(storeZ<frag.ndcZ)
					continue;
				writeDepth(db,scrX,scrY,frag.ndcZ);
			}

			interpolate3f(pa,pb,pc,face->clipA.x,face->clipB.x,face->clipC.x,frag.ndcX);
			frag.ndcX/=clipW;
			interpolate3f(pa,pb,pc,face->clipA.y,face->clipB.y,face->clipC.y,frag.ndcY);
			frag.ndcY/=clipW;

			interpolate3f(pa,pb,pc,face->clipA.nx,face->clipB.nx,face->clipC.nx,frag.nx);
			interpolate3f(pa,pb,pc,face->clipA.ny,face->clipB.ny,face->clipC.ny,frag.ny);
			interpolate3f(pa,pb,pc,face->clipA.nz,face->clipB.nz,face->clipC.nz,frag.nz);

			interpolate3f(pa,pb,pc,face->clipA.s,face->clipB.s,face->clipC.s,frag.s);
			interpolate3f(pa,pb,pc,face->clipA.t,face->clipB.t,face->clipC.t,frag.t);

			FragmentOut outFrag;
			fs(frag,outFrag);
			drawPixel(fb,scrX,scrY,outFrag.r,outFrag.g,outFrag.b);
		}
	}
}

光栅化完成了,这下就能自己实现opengl和directx了!
<think>我们正在讨论透视投影的数学原理和模型。根据用户的问题,我们需要解析透视投影的数学模型和原理。结合之前的回答和引用内容,我们可以从以下几个方面展开: 1. **透视投影的基本原理**:从三维空间到二维平面的投影过程,模拟人眼或相机的成像。 2. **数学模型**:包括基本透视公式、齐次坐标表示、透视投影矩阵等。 3. **灭点与平行线**:解释灭点的形成原理和计算方法。 4. **逆透视变换的条件**:引用[1]中提到,在特定条件(如地面点Z=0)下,逆透视变换是可行的。 5. **透视投影中的深度信息**:引用[2]中提到了深度信息在投影后的畸变以及其在渲染中的作用。 6. **透视变换的求解**:引用[3]给出了一个求解透视变换矩阵的线性方程组形式。 我们将按照以下结构组织回答: ### 一、透视投影基本原理 - 介绍透视投影的几何原理,即从三维空间点通过投影中心(如相机光心)投影到二维平面(成像平面)的过程。 - 引用[1]和[2]中的相关内容,说明投影变换不是一一映射(缺少深度信息),但在特定条件下可逆。 ### 二、基本透视公式 - 给出基本透视公式:$x' = f \cdot x / z$, $y' = f \cdot y / z$,其中$(x, y, z)$是三维点坐标(相机坐标系),$f$是焦距,$(x', y')$是投影点坐标。 - 解释公式的几何意义:物体离相机越远(z越大),投影点越靠近图像中心。 ### 三、齐次坐标与透视投影矩阵 - 解释齐次坐标的作用(引用[2]提到,投影可视为齐次坐标系的坐标变换):通过引入齐次坐标,可以将透视投影表示为线性变换(矩阵乘法)。 - 给出透视投影矩阵的标准形式(如OpenGL中的投影矩阵): $$ \begin{bmatrix} \frac{f}{\text{aspect}} & 0 & 0 & 0 \\ 0 & f & 0 & 0 \\ 0 & 0 & \frac{\text{far}+\text{near}}{\text{near}-\text{far}} & \frac{2 \cdot \text{far} \cdot \text{near}}{\text{near}-\text{far}} \\ 0 & 0 & -1 & 0 \end{bmatrix} $$ 其中,$f = \cot(\text{fov}/2)$,fov为垂直视野角度,aspect为宽高比,near和far分别为近远裁剪平面。 - 说明矩阵乘法的过程:将三维齐次坐标点$(x, y, z, 1)$乘以该矩阵得到裁剪空间坐标,然后进行透视除法(除以w分量)得到标准化设备坐标。 ### 四、灭点计算 - 解释灭点:空间中的一组平行线(方向向量为$\vec{d}=(d_x, d_y, d_z)$)在投影后相交于灭点。 - 计算灭点:取该方向上的无穷远点$(d_x, d_y, d_z, 0)$(齐次坐标),用透视投影矩阵变换后,再除以w分量(若w不为0)得到灭点坐标。具体步骤: 1. 将方向向量表示为齐次坐标:$\mathbf{P} = (d_x, d_y, d_z, 0)^T$。 2. 乘以透视投影矩阵:$\mathbf{P}' = \mathbf{M} \cdot \mathbf{P}$。 3. 透视除法:灭点坐标为$\left( \frac{P'_x}{P'_w}, \frac{P'_y}{P'_w} \right)$。注意,如果$P'_w=0$,则灭点在无穷远处。 ### 五、透视校正纹理映射 - 引用[2]中提到的深度插值问题:在光栅化过程中,需要对纹理坐标进行插值,但简单的线性插值会导致纹理扭曲。 - 透视校正插值原理:在屏幕空间进行插值时,需要根据深度值进行修正。具体方法- 在三角形三个顶点处,已知纹理坐标$(u,v)$和深度值$z$(或$1/w$,其中$w$是投影变换后的w分量)。 - 在三角形内部进行插值时,先对$1/w$、$u/w$、$v/w$进行线性插值,然后通过$u = (u/w)/(1/w)$恢复纹理坐标。 ### 六、逆透视变换 - 引用[1]中提到:在智能车应用场景中,所有点都在地面(Z=0),因此深度信息没有丢失,逆透视变换可行。 - 数学方法:通过已知的投影变换矩阵(或单应性矩阵)求逆,将二维点投影回三维平面(Z=0平面)。 ### 代码示例:透视投影矩阵 使用Python实现一个透视投影矩阵,并演示如何投影一个三维点。 ```python import numpy as np def perspective_projection_matrix(fov, aspect, near, far): f = 1.0 / np.tan(np.radians(fov) / 2) return np.array([ [f / aspect, 0, 0, 0], [0, f, 0, 0], [0, 0, (far + near) / (near - far), (2 * far * near) / (near - far)], [0, 0, -1, 0] ]) # 示例:投影一个三维点 def project_point(point, projection_matrix): # 将点转换为齐次坐标 homogeneous_point = np.array([point[0], point[1], point[2], 1.0]) # 应用投影矩阵 clipped_point = projection_matrix @ homogeneous_point # 透视除法 ndc_point = clipped_point / clipped_point[3] # 标准化设备坐标(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值