快速近似抗锯齿技术

快速近似抗锯齿技术 FXAA(Fast Approximate Anti-aliasing)

一、背景

​ ​​​  FXAA抗锯齿技术也是基于形态学屏幕后处理抗锯齿方案的一种,它由NVIDIA的Timothy Lottes提出。相较于MLAA,FXAA只需要一个Pass就可以完成抗锯齿,牺牲了质量提高了性能,最终效果会有些模糊。FXAA的实现流程如下:

​ ​​​  a. 计算图像亮度

​ ​​​  b. 找到高对比度的像素

​ ​​​  c. 标记对比边缘

​ ​​​  d. 计算混合系数

​ ​​​  e. 搜索边缘的终点

二、方案

2.1 计算图像亮度

​ ​ ​​​  FXAA通过选择性降低图像对比度,平滑视觉上明显的锯齿和孤立像素,因此首先需要计算输入纹理的亮度。亮度可以通过在上一个pass计算保存到纹理的A通道,这样可以减少后续采样的计算量。也可以直接使用G通道作为亮度。最后仍然可以手动计算亮度,代码如下。

float sampleLuminace(vec2 uv)
{
    vec3 color=texture(colorMap, uv).rgb;
    return dot(color,vec3(0.2126,0.71515,0.0721));
}

2.2 混合高对比度像素

​ ​ ​​​  首先计算局部对比度。局部对比度通过比较当前像素和相邻像素的亮度来找到,声明存储像素对比度的数据结构,然后采样十字范围的相邻像素。

struct LuminaceData{
	float M,N,S,W,E;
};
float sampleLuminace(vec2 uv,vec2 offset,vec2 texSize)
{
    uv+=offset/texSize;
    return sampleLuminace(uv);
}
LuminaceData sampleLuminaceNeighbor(vec2 uv,vec2 texSize)
{
    LuminaceData data;
    data.M=sampleLuminace(uv);
    data.N=sampleLuminace(uv,vec2(0,1),texSize);
    data.E=sampleLuminace(uv,vec2(1,0),texSize);
    data.S=sampleLuminace(uv,vec2(0,-1),texSize);
    data.W=sampleLuminace(uv,vec2(-1,0),texSize);
    return data;
}

​ ​ ​​​  在sampleLuminaceNeighbor函数中顺带计算邻近像素最大亮度、最小亮度、对比度。

data.max=max(data.M,max(data.N,max(data.E,max(data.S,data.W))));
data.min=min(data.M,min(data.N,min(data.E,min(data.S,data.W))));
data.contrast=data.max-data.min;

​ ​ ​​​  可以看到茶壶模型粗糙的轮廓。我们可以设置对比度绝对阈值和相对阈值,用于排除低对比度像素。

if(l.contrast<max(0.0833,l.max*0.15))
    return texture(colorMap, vTexCoords);//0.0833 upper、0.0625 faster、0.0312 lower

​ ​ ​​​  下图将小于绝对阈值的像素显示为红色,小于相对阈值的像素显示为绿色,两个都小于的显示为黄色。

​ ​ ​​​  然后计算高对比度像素的混合因子。这里需要补充计算相邻的对角线上像素的亮度,得到所有相邻像素的平均亮度。再计算平均亮度和当前像素亮度的差值,并除以对比度来归一化。最后使用smoothstep函数平滑,输出平方后的结果。

float determinePixelBlendFactor(LuminaceData l)
{
    float f=2*(l.N+l.E+l.S+l.W);
    f+=l.NE+l.NW+l.SE+l.SW;
    f/=12.0;
    f=clamp(abs(f-l.M)/l.contrast,0.0,1.0);
    float blendFactor=smoothstep(0.0,1.0,f);
    return blendFactor*blendFactor;
}

​ ​ ​​​  再确定混合方向,找到中间像素和相邻的哪个像素做混合。选择十字线相邻像素的哪一个,由对比度梯度的方向决定。对于水平边缘,我们将N像素和S像素相加减去2倍M像素亮度,这样可以计算出亮度梯度的变化率,再加上NE像素和SE像素相加减去2倍E像素亮度,加上NW像素和SW像素相加减去2倍W像素亮度,最终得到水平边缘的亮度梯度变化率。类似地计算到垂直边缘的亮度梯度变化率,然后比较可知混合的方向。

struct EdgeData{
	bool bHorizontal;
};
EdgeData determineEdge(LuminaceData l,vec2 texSize)
{
    EdgeData data;
    float hori=abs(l.N+l.S-2.0*l.M)*2.0+abs(l.NE+l.SE-2.0*l.E)+abs(l.NW+l.SW-2.0*l.W);
    float vert=abs(l.E+l.W-2.0*l.M)*2.0+abs(l.NE+l.NW-2.0*l.N)+abs(l.SE+l.SW-2.0*l.S);
    data.bHorizontal=hori>=vert;
    
    return data;
}

​ ​ ​​​  知道边缘方向后,如果是水平方向就沿着边缘垂直混合,然后判断向上还是向下混合。通过计算N像素和M像素差值与S像素和M像素亮度差值,差值大的做混合。下图将混合方向为E和N方向的像素着色为红色,为W和S方向的像素着色为白色。

float pLum=hori>=vert?l.N:l.E;
float nLum=hori>=vert?l.S:l.W;
float pGrad=abs(pLum-l.M);
float nGrad=abs(nLum-l.M);
data.pixelStep=hori>=vert?1.0/texSize.y:1.0/texSize.x;
if(pGrad<nGrad){
	data.pixelStep*=-1.0;
}

​ ​ ​​​  最后将混合方向乘以混合系数得到uv偏移量,采样时自动根据纹理偏移量做相邻像素和当前像素颜色的线性插值。下图左侧是无抗锯齿,右侧是抗锯齿效果。

2.3 混合边缘像素

​ ​ ​​​  上一步只在3乘以3的范围内做混合抗锯齿,但很多情况下边缘很长,它的局部是水平或者垂直的,整体上却是倾斜的。如果我们可以搜索到整个边缘,就可以更好的计算混合因子,从而平滑整个长度的边缘。

​ ​ ​​​  首先我们需要沿着上一步确定的边缘,往它的两侧搜索直到找到它的端点。我们可以通过沿着边缘采样像素对并比较他们的对比度梯度和原始边缘的对比度梯度来实现。边缘的位置可以在两个像素的边上,这样可以直接插值得到像素亮度平均值。我们将上一步混合的临近像素的亮度和当前像素亮度的平均值和搜索得到的边缘平均值相减,得到亮度梯度差,如果这个亮度梯度差大于上一步计算的亮度梯度差的0.25倍,则说明搜索到端点。

float determineEdgeBlendFactor(LuminaceData l,EdgeData e,vec2 uv,vec2 texSize)
{
        vec2 texcoord=uv;
        vec2 edgeStep;
        if(e.bHorizontal){
            texcoord.y+=e.pixelStep*0.5;
            edgeStep=vec2(1.0/texSize.x,0.0);
        }
        else {
            texcoord.x+=e.pixelStep*0.5;
            edgeStep=vec2(0.0,1.0/texSize.y);
        }
        float edgeLum=(l.M+e.oppositeLum)*0.5;
        float gradThreshold=e.grad*0.25;

        vec2 puv=texcoord+edgeStep;
        float pLumDelta=sampleLuminace(puv)-edgeLum;
        bool pAtEnd=abs(pLumDelta)>=gradThreshold;
        for(int i=1;i<FXAA_MAX_EAGE_SEARCH_SAMPLE_COUNT&&!pAtEnd;i++)
        {
            puv+=edgeStep;
            pLumDelta=sampleLuminace(puv)-edgeLum;
            pAtEnd=abs(pLumDelta)>=gradThreshold;
        }
		return pLumDelta;
}

​ ​ ​​​  类似的搜索到另外一个方向的端点。比较得到较小的端点距离。我们通过越接近端点,混合因子越大来平滑倾斜的边缘。同时需要比较沿边缘的亮度增量符号和跨边缘的亮度增量符号,来确定是否应该混合。

float determineEdgeBlendFactor(LuminaceData l,EdgeData e,vec2 uv,vec2 texSize)
{
        vec2 texcoord=uv;
        vec2 edgeStep;
        if(e.bHorizontal){
            texcoord.y+=e.pixelStep*0.5;
            edgeStep=vec2(1.0/texSize.x,0.0);
        }
        else {
            texcoord.x+=e.pixelStep*0.5;
            edgeStep=vec2(0.0,1.0/texSize.y);
        }
        float edgeLum=(l.M+e.oppositeLum)*0.5;
        float gradThreshold=e.grad*0.25;

        vec2 puv=texcoord+edgeStep*edgeSearchSteps[0];
        float pLumDelta=sampleLuminace(puv)-edgeLum;
        bool pAtEnd=abs(pLumDelta)>=gradThreshold;
        for(int i=1;i<FXAA_MAX_EAGE_SEARCH_SAMPLE_COUNT&&!pAtEnd;i++)
        {
            puv+=edgeStep*edgeSearchSteps[i];
            pLumDelta=sampleLuminace(puv)-edgeLum;
            pAtEnd=abs(pLumDelta)>=gradThreshold;
        }
        if(!pAtEnd)puv+=edgeStep;

        vec2 nuv=texcoord-edgeStep*edgeSearchSteps[0];
        float nLumDelta=sampleLuminace(nuv)-edgeLum;
        bool nAtEnd=abs(nLumDelta)>=gradThreshold;
        for(int i=1;i<FXAA_MAX_EAGE_SEARCH_SAMPLE_COUNT&&!nAtEnd;i++)
        {
            nuv-=edgeStep*edgeSearchSteps[i];
            nLumDelta=sampleLuminace(nuv)-edgeLum;
            nAtEnd=abs(nLumDelta)>=gradThreshold;
        }
        if(!nAtEnd)puv-=edgeStep;

        float pDistance,nDistance;
        if(e.bHorizontal){
            pDistance=puv.x-uv.x;
            nDistance=uv.x-nuv.x;
        }
        else {						
            pDistance=puv.y-uv.y;
            nDistance=uv.y-nuv.y;
        }
        float shortDistan;
        bool deltaSign;
        if(pDistance<nDistance){
            shortDistan=pDistance;
            deltaSign=pLumDelta>=0;
        }
        else {						
            shortDistan=nDistance;
            deltaSign=nLumDelta>=0;
        }
        if(deltaSign==(l.M-edgeLum)>=0.0)return 0.0;
        return 0.5-shortDistan/(pDistance+nDistance);
}

​ ​ ​​​  最终得到的混合因子与上一步得到的混合因子,我们取最大值。FXAA抗锯齿效果和其他抗锯齿对比如下图所示。

posted @ 2025-06-13 10:56  王小于的啦  阅读(133)  评论(0)    收藏  举报