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