首页 > 代码库 > 光栅化插值方法

光栅化插值方法

光栅化是在计算机上生成图像的重要步骤,然而无论是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了!


光栅化插值方法