You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何确保三角形边缘存在屏幕空间导数,解决抗锯齿失效问题?

解决SDF圆角正方形抗锯齿的导数缺失问题

问题现状

当前通过顶点着色器输出-1到1范围的UV,片段着色器用SDF绘制圆角正方形,遇到两个核心问题:

  • UV贴合四边形边缘时,SDF在边缘处没有相邻像素提供屏幕空间导数,直接导致锯齿
  • 缩小UV到95%预留边距的方案,在相机拉远或斜视角时,边距不足以覆盖屏幕空间的导数计算需求,锯齿问题复现

无需相机距离依赖的优化方案

方案1:手动推导SDF的屏幕空间导数

自动导数dFdx/dFdy依赖相邻像素的片段计算结果,当UV到达顶点边缘时没有相邻像素,导致导数失效。我们可以手动计算SDF的梯度,结合UV的屏幕空间变化率,替代自动导数的局限性。

修改后的代码

顶点着色器:新增输出裁剪空间坐标,用于后续计算屏幕空间UV变化

#version 330
out vec2 tex;
out vec4 clipPos;
uniform mat4 mvp;

const vec2 pos[4] = vec2[](
    vec2(-1.0, 1.0),
    vec2(1.0, 1.0),
    vec2(1.0, -1.0),
    vec2(-1.0, -1.0));

void main()
{
    tex = pos[gl_VertexID];
    clipPos = mvp * vec4(pos[gl_VertexID], 0.0, 1.0);
    gl_Position = clipPos;
}

片段着色器:手动计算SDF梯度与UV屏幕导数,彻底避免依赖自动导数

#version 330
in vec2 tex;
in vec4 clipPos;
out vec4 Out_Color;

uniform vec2 screenSize; // 传入屏幕分辨率,如vec2(1920, 1080)

float roundedBoxSDF(vec2 uv, float halfSize, float radius)
{
    vec2 q = abs(uv) - (halfSize - radius);
    return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius;
}

void main()
{
    // 1. 计算UV的屏幕空间变化率(每像素的UV变化量)
    vec2 ndc = clipPos.xy / clipPos.w;
    vec2 screenPixel = (ndc + 1.0) * 0.5 * screenSize;
    vec2 dTexPerPixelX = dFdx(tex) / dFdx(screenPixel.x);
    vec2 dTexPerPixelY = dFdy(tex) / dFdy(screenPixel.y);

    // 2. 计算SDF的梯度(距离场的变化方向)
    float halfSize = 1.0;
    float radius = 0.5;
    vec2 q = abs(tex) - (halfSize - radius);
    vec2 grad = vec2(0.0);
    
    if (q.x > 0.0 && q.y > 0.0) {
        grad = normalize(q); // 圆角区域梯度为指向圆角中心的单位向量
    } else if (q.x > 0.0) {
        grad = vec2(sign(tex.x), 0.0); // 水平边缘区域梯度为水平方向
    } else if (q.y > 0.0) {
        grad = vec2(0.0, sign(tex.y)); // 垂直边缘区域梯度为垂直方向
    }

    // 3. 计算SDF的屏幕空间变化率(距离场随屏幕像素的变化量)
    float dist = roundedBoxSDF(tex, halfSize, radius);
    float dDistDx = dot(grad, dTexPerPixelX);
    float dDistDy = dot(grad, dTexPerPixelY);
    float edgeSharpness = length(vec2(dDistDx, dDistDy));

    // 4. 抗锯齿计算
    float smoothedAlpha = 1.0 - smoothstep(-edgeSharpness, edgeSharpness, dist);
    Out_Color = vec4(vec3(1.0), smoothedAlpha);
}

方案2:扩大顶点四边形范围

直接把顶点的坐标扩大(比如从±1.0改成±1.2),保持UV输出仍为±1.0对应原正方形区域。扩大的部分会超出原正方形范围,即使相机拉远或斜视角,原正方形的边缘始终处于四边形内部,片段着色器可以正常获取相邻像素的导数。

修改后的顶点着色器:

#version 330
out vec2 tex;
uniform mat4 mvp;

// 顶点坐标扩大20%,确保原UV边缘始终在四边形内部
const vec2 pos[4] = vec2[](
    vec2(-1.2, 1.2),
    vec2(1.2, 1.2),
    vec2(1.2, -1.2),
    vec2(-1.2, -1.2));

void main()
{
    // UV仍输出原-1到1范围,对应原正方形
    tex = pos[gl_VertexID] / 1.2;
    gl_Position = mvp * vec4(pos[gl_VertexID], 0.0, 1.0);
}

片段着色器无需修改(直接使用roundedBoxSDF(tex, 1.0, 0.5)即可),因为原正方形的边缘现在完全处于扩大后的四边形内部,dFdx(dist)可以正常获取导数。

方案对比

  • 手动导数方案:无需修改顶点数据,适合不能调整顶点的场景,但需要额外计算梯度和屏幕空间变化率
  • 扩大顶点方案:实现简单,性能开销低,适合可以调整顶点的场景,完全规避导数缺失问题

内容的提问来源于stack exchange,提问作者FrostKiwi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 19:10:31