实现简易gpu光栅器

Number of views 139

CPU实现一套软光栅的核心实现往往是(有些实现会涉及到逐片元操作,下图忽略):

而我们主要讨论的是如何判断点在三角形内。

要判断一个点在三角形内一般有如下三种方法:角度累加,Crossproduct Side Algorithm,重心坐标。

角度累加

有一点P,P点与三角形三个顶点的连线,构成的角度为360度,我们可以认定该点在三角形内,否则为三角形外,如图:

上图可发现需要求取六个角的角度,角度计算可通过向量点乘的方式,如:

$$
∠PCA=arccos((CP⋅CA)/(|CP|∗|CA|))
$$

再通过上述的方式把6个角的角度相加,结果为π时,则点在三角形内。

为了简化三角形中角度的运算,可以通过 ∠APB 与 ∠APC 以及 ∠BPC 三个角度的累加运算获得。

但是该方法由于涉及到浮点数的运算,很难使得最后的运算结果等于2π,并且它的运算效率较慢,在运算效率要求较高的光栅化中,该方法并不适合。

Crossproduct Side算法

点与边存在三种关系:点在边上,点在边的左边以及点在边的右边。而计算这三种关系的过程就是通过“Crossproduct side算法",这个算法名字不是正式的叫法,但是为了更好的描述,取一个直观的名字是有必要的。

该算法的计算过程通过向量叉乘的方式,但是二维向量对叉乘算法并没有太多相关的定义,但是通常可以通过下面的方式定义:

$$
Z = vx1 * vy2 - vy1 * vx2
$$

这是三维向量叉乘计算时求取Z分量的方法(也是行列式的求法)。

这里的Z是标量,叉乘不是向量么?为什么可以通过判断标量的正负来判定指向的方向呢?我们可以取两个二维向量,分别为A(1, 2)与B(3, 1)。

此时A向量可以看成:A(1, 2, 0);

B向量可以看成: B(3, 1, 0);

那么cross(A, B)可得C(0, 0, -5);

由于A与B向量的z值都为0,所以叉乘后向量的x,y值也为0,那么我们只需要计算Z值即可判定两个二维向量叉乘后所指向的方向。

右手坐标系举例,当AB向量叉乘于AP1向量,Z指向正在看屏幕的自己;当AB向量叉乘于AP2向量,Z与看向屏幕的方向相同。到这其实已经可以得出结论:当AB向量与其上方的任意点形成的AX1向量叉乘,Z的指向必然指向看屏幕的自己;反之当AB向量与其下方的任意点形成的AX2向量叉乘,Z必然与看向屏幕的方向相同;当点在AB上,则Z的值为0。

那么剩下的唯一的问题是:如何判定Z指向的哪个方向是我们想要的指向?这时候就可以利用三角形的另一个顶点C, 当AB向量叉乘于AC向量,与AB向量叉乘于AP1向量,具有相同的指向,那么就可以认为P1点与C点同侧;同样的,CA向量叉乘于CP1向量,与CA向量叉乘于CB向量,具有相同指向,那么就可以认为P1点与B点同侧;最后,BC向量叉乘于BP1向量,与BC向量叉乘于BA向量,具有相同指向,那么就可以认为P1点与A点同侧。总结上面的结论,当上面的所有条件都满足,则可以认为P1点在三角形内。

上述条件中,可以发现:AB向量叉乘AP1向量,BC向量叉乘于BP1向量,以及CA向量叉乘CP1向量,它们都同时通过右手法则计算Z的指向,并且得出的Z指向相同时(同时>0或同时<0),则可以认为P1点在三角形内,这样计算过程就可以减少一个比较操作(与三角形另一个顶点是否同向的比较)。

重心坐标

Crossproduct Side方法的优点在于它易于理解,便于记忆,且只需要记住目标点与三条边的关系即可。接下来介绍另一种验证点在三角形的方法, 它通过重心坐标计算,由于它执行效率更快,很多光栅化,甚至是模型线框的实现都可以见到它的身影。

重心坐标的实现也有很多方式,主要分析其中的一种实现。

由上图可知:

$$
AP = uAB + vAC
$$

又因为:

$$
P - A = u(B - A) + v(C - A)
$$

所以:

$$
P = (1 - u -v)A + uB + vC
$$

这是得出的第一个公式,先记下,稍后要用。

因为:

$$
AP = uAB + vAC
$$

所以:

$$
uAB + vAC + PA = 0
$$

上式的0代表0向量。

上式可分解为向量点乘的形式:

大家都知道点乘是两个向量的点乘,而向量内部的每个分量都是标量,但是仔细观察上式,会发现右边的向量,内部的每个分量都是一个向量,这个在数学当中真的很难找到定义,不过没关系,既然右边向量的每个分量都是向量,那就把它拆解看看?

(注意:此时上面的0依旧是0向量)拆解后发现右边的向量居然可以被拆解为3x2的矩阵,剩下的就是矩阵乘法

(注意:此时上面的0是标量),上图中1与2的式子类似,以1为例分析,由于[u, v, 1]点乘于[Bx-Ax, Cx-Ax, Px-Ax]等于0,又根据向量点乘的公式:

$$
A · B = |A||B|cos\theta
$$

又因为[u, v, 1]与[Bx-Ax, Cx-Ax, Px-Ax]的模长不可能为0(你们都懂),所以只有A向量与B向量的夹角为90度时,才为0;所以[u, v, 1]与[Bx-Ax, Cx-Ax, Px-Ax]相互垂直,同理[u, v, 1]与[By-Ay, Cy-Ay, Py-Ay]也相互垂直。

根据两向量叉乘可得到一个与该两向量垂直的向量,可得:

由于叉乘的结果是浮点数,所以向量[u, v, 1]中的z分量不一定会准确为1,由于z不会准确为1,假设结果为[x, y, z],那么u与v的结果如下:

image1742214533999.png

(为什么?想象二维向量如果有第三个分量如何求取点?)结合第一个公式,可得最终P的结果:

$$
P = (1 - x/z -y/z)A + x/zB + y/zC
$$

要让P点在△ABC内,需要满足:

image1742214572922.png

实例

// 顶点结构
struct VerInfo{
    vec3 v1;
    vec3 v2;
    vec3 v3;
    vec2 p;
    vec2 uv1;
    vec2 uv2;
    vec2 uv3;
};

vec3 frag_color = vec3(0.,0.,0.);
// vertex
const vec3 vertices[6] = vec3[6](vec3(-0.4, 0.,0.),
    				 vec3(0.4,0.,0.),
                              	 vec3(-0.3, 0.45, 0.),
                                 vec3(0.42,0.,0.),
                              	 vec3(-0.3, 0.46, 0.),
                              	 vec3(0.5, 0.5, 0.)
                             );
// uvs
const vec2 uvs[6] = vec2[6](vec2(0.,0.),
                            vec2(1.,0.),
                            vec2(0.,1.),
                            vec2(0.,1.),
                            vec2(1.,0.),
                            vec2(1.,1.)
							);
// indices
const int indices[6] = int[6](0,1,2,
                              4,3,5
                                 );

// 获取重心坐标
vec2 getBary(vec3 v1, vec3 v2, vec3 v3, vec3 p) {
    vec3 v12 = v2 - v1;
    vec3 v13 = v3 - v1;
    vec3 pv1 = v1 - p;
  
    vec3 c1 = vec3(v12.x, v13.x, pv1.x);
    vec3 c2 = vec3(v12.y, v13.y, pv1.y);
  
    vec3 u = cross(c1, c2);
    return vec2(u.x/u.z, u.y/u.z);
}
// 通过重心坐标判断点是否在三角形内
void triangle(VerInfo verInfo) {
    vec3 v1 = verInfo.v1;
    vec3 v2 = verInfo.v2;
    vec3 v3 = verInfo.v3;
    vec3 p = vec3(verInfo.p.x, verInfo.p.y, 0.);
    vec2 bary = getBary(v1,v2,v3,p);
    if(bary.x >= 0. && bary.y >= 0. && (bary.x + bary.y)<=1.) {
        vec2 uv = (1.-(bary.x+bary.y))*verInfo.uv1 + bary.x*verInfo.uv2 + bary.y*verInfo.uv3;
    	frag_color += texture(iChannel1, uv).xyz;
    }
}
// 绘制模型
void drawModel(vec2 p) {
    for(int i = 0; i < 6; i+=3) {
    	vec3 v1 = vertices[indices[i]];
        vec3 v2 = vertices[indices[i+1]];
        vec3 v3 = vertices[indices[i+2]];
  
        vec2 uv1 = uvs[indices[i]];
        vec2 uv2 = uvs[indices[i+1]];
        vec2 uv3 = uvs[indices[i+2]];
        VerInfo verInfo = VerInfo(v1,v2,v3,p,uv1,uv2,uv3);
        triangle(verInfo);
    }
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/iResolution.xy;
	uv -= 0.5;
    uv *=2.;
    uv.x *= iResolution.x/iResolution.y;
    drawModel(uv);
  
    // Output to screen
    fragColor = vec4(frag_color, 1.);
}

上述代码是在Shadertoy上简单编写的代码,这是链接地址。(要在PC端浏览器打开)

还有如下效果也部分用到重心坐标实现(看日期是去年6月份敲的,记得当时纯手打了两天^_^)这是地址,感兴趣的可以去看看实现:

为了完整性,贴出Crossproduct side算法实现:

boolean crossproductSide(aPoint: Vec2, bPoint: Vec2, cPoint: Vec2, pPoint: Vec2) {
        let ab = this.cross(aPoint, bPoint, pPoint);
        let bc = this.cross(bPoint, cPoint, pPoint);
        let ca = this.cross(cPoint, aPoint, pPoint);
        if((ab >= 0 && bc >= 0 && ca >= 0) || (ab <= 0 && bc <= 0 && ca <= 0)) {
            return true;
        }
        return false;
}
0 Answers