OpenGL 简单抗锯齿多边形网格着色器

OpenGL simple antialiased polygon grid shader

如何在片段着色器中制作带有抗锯齿线的测试网格图案?

我记得我觉得这很有挑战性,所以我会post在这里为未来的自己和任何想要同样效果的人提供答案。

此着色器旨在在单独的渲染调用中渲染到已经带纹理的平面“上方”。我这样做的原因 - 是因为在我的程序中,我通过多次渲染调用生成表面纹理,慢慢地逐层构建它。然后我想在它上面制作一个简单的黑色网格,所以我进行了最后一次渲染调用来执行此操作。

所以这里的底色是(0,0,0,0),基本上什么都没有。然后我可以使用 GL 混合模式将此着色器的结果覆盖在我的纹理上。

请注意,您无需单独执行此操作。您可以轻松地修改此代码以显示特定颜色(如平滑灰色)或什至您选择的纹理。只需将纹理传递给着色器并相应地修改最后一行。

另请注意,我使用了在着色器编译期间设置的常量。基本上,我只是加载着色器字符串,但在将其传递给着色器编译器之前 - 我搜索 __CONSTANT_SOMETHING 并将其替换为我想要的实际值。别忘了那都是文字,所以需要用文字替换,例如:

//java code
shaderCode = shaderCode.replaceFirst("__CONSTANT_SQUARE_SIZE", String.valueOf(GlobalSettings.PLANE_SQUARE_SIZE));

这是我的着色器:

顶点:

#version 300 es

precision highp float;
precision highp int;

layout (location=0) in vec3 position;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform vec2 coordShift;
uniform mat4 modelMatrix;

out highp vec3 vertexPosition;

const float PLANE_SCALE = __CONSTANT_PLANE_SCALE;   //assigned during shader compillation

void main()
{
    // generate position data for the fragment shader
    // does not take view matrix or projection matrix into account
    // TODO: +3.0 part is contingent on the actual mesh. It is supposed to be it's lowest possible coordinate.
    // TODO: the mesh here is 6x6 with -3..3 coords. I normalize it to 0..6 for correct fragment shader calculations
    vertexPosition = vec3((position.x+3.0)*PLANE_SCALE+coordShift.x, position.y, (position.z+3.0)*PLANE_SCALE+coordShift.y);

    // position data for the OpenGL vertex drawing
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

注意,我在这里计算了VertexPosition,并将其传递给片段着色器。这是为了让我的网格在对象移动时“移动”。问题是,在我的应用程序中,我的地面基本上固定在主要实体上。实体(称之为角色或其他)不会在平面上移动或改变其相对于平面的位置。但是为了创造运动的错觉 - 我计算坐标偏移(相对于正方形大小)并使用它来计算顶点位置。

它有点复杂,但我想我会把它包括在内。基本上,如果正方形大小设置为 5.0(即我们有一个 5x5 米的正方形网格),那么 (0,0) 的 coordShift 将意味着角色站在正方形的左下角; (2.5,2.5) 的 coordShift 将在中间,而 (5,5) 将在右上角。超过 5 后,移位循环回到 0。低于 0 - 它循环到 5。

所以基本上网格总是在一个正方形内“移动”,但因为它是均匀的 - 错觉是你在无限网格表面上行走。

另请注意,您可以使用 multi-layered 网格进行同样的操作,例如每 10 条线更粗。您真正需要做的就是确保您的 coordShift 代表您的网格图案移动的最大距离。

以防万一有人想知道我为什么让它循环 - 这是为了精确起见。当然,您可以将原始角色的坐标传递给着色器,它会在 (0,0) 附近正常工作,但是当您离开 10000 个单位时 - 您会注意到一些严重的精度故障,例如您的线条变形甚至“模糊”,就像它们是用刷子做的。

这是片段着色器:

#version 300 es

precision highp float;

in highp vec3 vertexPosition;

out mediump vec4 fragColor;

const float squareSize = __CONSTANT_SQUARE_SIZE;
const vec3 color_l1 = __CONSTANT_COLOR_L1;

void main()
{
    // calculate deriviatives
    // (must be done at the start before conditionals)
    float dXy = abs(dFdx(vertexPosition.z)) / 2.0;
    float dYy = abs(dFdy(vertexPosition.z)) / 2.0;
    float dXx = abs(dFdx(vertexPosition.x)) / 2.0;
    float dYx = abs(dFdy(vertexPosition.x)) / 2.0;

    // find and fill horizontal lines
    int roundPos = int(vertexPosition.z / squareSize);
    float remainder = vertexPosition.z - float(roundPos)*squareSize;
    float width = max(dYy, dXy) * 2.0;

    if (remainder <= width)
    {
        float diff = (width - remainder) / width;
        fragColor = vec4(color_l1, diff);
        return;
    }

    if (remainder >= (squareSize - width))
    {
        float diff = (remainder - squareSize + width) / width;
        fragColor = vec4(color_l1, diff);
        return;
    }

    // find and fill vertical lines
    roundPos = int(vertexPosition.x / squareSize);
    remainder = vertexPosition.x - float(roundPos)*squareSize;
    width = max(dYx, dXx) * 2.0;

    if (remainder <= width)
    {
        float diff = (width - remainder) / width;
        fragColor = vec4(color_l1, diff);
        return;
    }

    if (remainder >= (squareSize - width))
    {
        float diff = (remainder - squareSize + width) / width;
        fragColor = vec4(color_l1, diff);
        return;
    }

    // fill base color
    fragColor = vec4(0,0,0, 0);
    return;
}

目前仅针对 1 像素粗线构建,但您可以通过控制“宽度”来控制粗细

在这里,第一个重要的部分是 dfdx / dfdy 函数。这些是 GLSL 函数,我只想说它们可以让您根据您所在平面上那个点的 Z-distance 来确定您的片段在屏幕上占据的世界坐标 space 的多少。 好吧,那是一口。不过,如果您为他们阅读文档,我相信您可以弄明白。

然后我将这些输出的最大值作为宽度。基本上,根据您的相机看起来的方式,您想稍微“拉伸”线条的宽度。

remainder - 基本上是这个片段离我们想要在世界坐标中绘制的线有多远。如果太远 - 我们不需要填充它。

如果您只在此处取最大值,您将得到 non-antialiased 行 1 pizel 宽。它基本上看起来像是来自 MS 画图的完美 1 像素线条形状。 但是增加宽度,会使那些直线段进一步拉伸并重叠。

大家可以看到我这里比较余数和线宽。宽度越大 - 剩余部分“击中”它的余数就越大。我必须从两边进行比较,因为否则你只会看到靠近负坐标一侧的线的像素,而忽略正坐标,它仍然可以击中它。

现在,为了实现简单的抗锯齿效果,我们需要让那些重叠的片段在接近尾端时“淡出”。为此,我计算分数以查看余数在直线内的深度。当分数等于 1 时,这意味着我们要绘制的线基本上直接穿过我们当前正在绘制的片段的中间。随着分数接近0,意味着片段离线越来越远,因此应该越来越透明。

最后,我们分别从水平线和垂直线的两侧进行此操作。我们必须将它们分开,因为 dFdX / dFdY 需要对于垂直线和水平线不同,所以我们不能在一个公式中完成它们。

最后,如果我们没有击中任何一条线足够近 - 我们用透明颜色填充片段。

我不确定这是否是该任务的最佳代码 - 但它确实有效。如果您有任何建议,请告诉我!

p.s。着色器是为 Opengl-ES 编写的,但它们也应该适用于 OpenGL。

如果我可以与您分享我用于 anti-aliased 网格的代码,它可能有助于提高复杂性。我所做的就是使用纹理坐标在平面上绘制网格。我使用 GLSL 的 genType fract(genType x) 来重复纹理 space。然后我使用绝对值函数来计算每个像素到网格线的距离。剩下的操作就是将其解释为颜色。

您可以直接在 Shadertoy.com 上使用此代码,方法是将其粘贴到新的着色器中。

如果您想在代码中使用它,您唯一需要的行是从 gridSize 变量开始到 grid 变量结束的部分。

iResolution.y是屏幕高度,uv是你平面的纹理坐标。

gridSizewidth 应该提供统一变量。

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    // aspect correct pixel coordinates (for shadertoy only)
    vec2 uv = fragCoord / iResolution.xy * vec2(iResolution.x / iResolution.y, 1.0);
    // get some diagonal lines going (for shadertoy only)
    uv.yx += uv.xy * 0.1;

    // for every unit of texture space, I want 10 grid lines
    float gridSize = 10.0;
    // width of a line on the screen plus a little bit for AA
    float width = (gridSize * 1.2) / iResolution.y;

    // chop up into grid
    uv = fract(uv * gridSize);
    // abs version
    float grid = max(
        1.0 - abs((uv.y - 0.5) / width),
        1.0 - abs((uv.x - 0.5) / width)
    );

    // Output to screen (for shadertoy only)
    fragColor = vec4(grid, grid, grid, 1.0);
}

快乐着色!