【问题标题】:OpenGL - object outlineOpenGL - 对象轮廓
【发布时间】:2019-05-22 16:52:28
【问题描述】:

我正在尝试实现选择轮廓功能。这就是我现在所做的。

如您所见,当鼠标悬停并在所选对象周围绘制轮廓时,对象被正确选择。

我现在想做的就是用这种方式勾勒出物体的可见边缘

左图是我现在拥有的,右图是我想要实现的。

这是我现在使用的程序。

void paintGL()
{
    /* ... */

    int w = geometry().width();
    int h = geometry().height();

    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);

    glEnable(GL_STENCIL_TEST);
    glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
    glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
    glStencilMask(0xFF);

    setClearColor(Qt::GlobalColor::darkGray);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    glStencilMask(0x00);
    DrawGeometry();

    if (HoveredSphere != RgbFromColorToString(Qt::GlobalColor::black))
    {
        glBindFramebuffer(GL_FRAMEBUFFER, addFBO(FBOIndex::OUTLINE));
        {
            glStencilFunc(GL_ALWAYS, 1, 0xFF);
            glStencilMask(0xFF);

            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

            DrawOutline(HoveredSphere, 1.0f - 0.025f);
        }
        glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject());

        glBindFramebuffer(GL_READ_FRAMEBUFFER, addFBO(FBOIndex::OUTLINE));
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, defaultFramebufferObject());
        {
            // copy stencil buffer
            GLbitfield mask = GL_STENCIL_BUFFER_BIT;
            glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, mask, GL_NEAREST);

            glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
            glStencilMask(0x00);

            glDepthFunc(GL_LEQUAL);
            DrawOutline(HoveredSphere, 1.0f);
        }
        glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject());
    }

    update();
}

其中DrawGeometry 绘制所有对象,DrawOutline 绘制选定对象,该对象按作为第二个参数传递的因子缩放。

感谢您的任何建议。

【问题讨论】:

  • 我能想到的两种方法:一种方法是预先计算形状重叠的“环”,然后使用 GL_LINES 或类似方法绘制该环。另一种方法是首先将图像渲染到帧缓冲区,其中突出显示的形状获得特殊值,然后使用 Sobel 运算符对其进行后处理。
  • @MichaelMahn 谢谢!我喜欢环的想法,我想尝试实现它,但我有一些疑问。第一次表演。最终场景有 1000 个球体,我应该计算每个可能的交叉点。这是我在加载模型时可以离线执行 1 次的事情。第二环近似。 GL_LINES 只能给出一个圆的近似值,而我将球体描述为冒名顶替者(使用光线追踪),获得“完美”球体。我应该找到一种方法来描绘一个“完美”的戒指。我不太喜欢 sobel-idea,因为我不知道用这种方法是否能得到正确的结果。
  • @Arctic Pi 是的,如果你能离线做一次,戒指的事情肯定会更好。如果您的球体移动并且您必须每帧执行一次,则必须考虑性能。如果我没有误解您的问题,Sobel 运算符也应该可以工作。您可以使用已有的模板缓冲区,如果片段由突出显示的球体渲染,并且至少有一个相邻片段不是,则该片段必须使用轮廓颜色。
  • @MichaelMahn 非常感谢您的回答。你给了我一个关于如何实现目标的想法,我做到了。今天下午,我将在问题的答案中向您展示结果。我仍然需要改进,为此我会在我的回答中要求您提供提示。再次感谢!

标签: c++ opengl graphics highlight outlining


【解决方案1】:

按照@MichaelMahn 的提示,我找到了解决方案。

首先,我在纹理中绘制所选对象的可见部分的轮廓。

然后我使用这个纹理通过检查相邻像素来计算轮廓,以确定我是否站在轮廓的边缘。

轮廓片段着色器

#version 450

uniform sampler2D silhouette;

in FragData
{
    smooth vec2 coords;
} frag;

out vec4 PixelColor;

void main()
{
    // if the pixel is black (we are on the silhouette)
    if (texture(silhouette, frag.coords).xyz == vec3(0.0f))
    {
        vec2 size = 1.0f / textureSize(silhouette, 0);

        for (int i = -1; i <= +1; i++)
        {
            for (int j = -1; j <= +1; j++)
            {
                if (i == 0 && j == 0)
                {
                    continue;
                }

                vec2 offset = vec2(i, j) * size;

                // and if one of the neighboring pixels is white (we are on the border)
                if (texture(silhouette, frag.coords + offset).xyz == vec3(1.0f))
                {
                    PixelColor = vec4(vec3(1.0f), 1.0f);
                    return;
                }
            }
        }
    }

    discard;
}

paintgl

void paintGL()
{
    int w = geometry().width();
    int h = geometry().height();

    setClearColor(Qt::GlobalColor::darkGray);

    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    DrawGeometry();

    // if we hover a sphere
    if (HoveredSphere != RgbFromColorToString(Qt::GlobalColor::black))
    {
        glBindFramebuffer(GL_READ_FRAMEBUFFER, defaultFramebufferObject());
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, addFBO(FBOIndex::SILHOUETTE));
        {
            // copy depth buffer
            GLbitfield mask = GL_DEPTH_BUFFER_BIT;
            glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, mask, GL_NEAREST);

            // set clear color
            setClearColor(Qt::GlobalColor::white);
            // enable depth test
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_LEQUAL);
            // clear color buffer
            glClear(GL_COLOR_BUFFER_BIT);

            // draw silhouette
            DrawSilhouette(HoveredSphere);
        }
        glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject());

        // clear depth buffer
        glClear(GL_DEPTH_BUFFER_BIT);

        // draw outline
        DrawOutline();
    }
}

问题 :: 现在我想参数化轮廓的宽度,其厚度目前固定为 1 像素。

非常感谢您的任何建议!

【讨论】:

  • 顺便说一下,GLSL 有一个函数 textureOffset(),它比你当前的代码对 texture(silhouette, frag.coords + vec2(i, j) * (1.0f / textureSize(silhouette, 0))).xyz 所做的更简单和更干净(我认为是textureOffset(silhouette, frag.coords, ivec2(i, j)).xyz?)。
  • 关于参数化宽度,我想你需要做的不仅仅是检查下一个像素,例如(x + 1, y) 以及它后面的 (x + 2, y) ,如果其中一个为真,则产生白色。你可能有类似的东西:ivec2 directions[4] = ivec2[4](ivec2(-1, 0), ivec2(1, 0), ivec2(0, -1), ivec2(0, 1)); for (uint i = 0; i &lt; 4; i++) { for (uint j = 1; j &lt;= width; j++) { if (textureOffset(silhouette, frag.coords + directions[i] * j) != vec3(1.0f)) { break; } else { PixelColor = vec4(vec3(1.0f), 1.0f); return; } } }——但我还没有测试过,请注意。
  • (附注:width 应该是着色器中的常量值,而不是统一值,因此着色器编译器可以展开该循环以获得更好的性能。)
  • @Andrea 非常感谢您的回答!我首先尝试采用您的方法,但我得到了错误的结果。所以我转向下面我的答案中描述的解决方案。我还有一个小问题,如果你想看看。 ;) 非常感谢!
  • @Andrea 我也试过textureOffset的方法,但不幸的是Qt5似乎不支持它:(感谢提示!;​​)
【解决方案2】:

感谢@Andrea 的建议,我找到了以下解决方案。

轮廓片段着色器

#version 450

uniform sampler2D silhouette;

in FragData
{
    smooth vec2 coords;
} frag;

out vec4 PixelColor;

void main()
{
    // outline thickness
    int w = 3;

    // if the pixel is black (we are on the silhouette)
    if (texture(silhouette, frag.coords).xyz == vec3(0.0f))
    {
        vec2 size = 1.0f / textureSize(silhouette, 0);

        for (int i = -w; i <= +w; i++)
        {
            for (int j = -w; j <= +w; j++)
            {
                if (i == 0 && j == 0)
                {
                    continue;
                }

                vec2 offset = vec2(i, j) * size;

                // and if one of the pixel-neighbor is white (we are on the border)
                if (texture(silhouette, frag.coords + offset).xyz == vec3(1.0f))
                {
                    PixelColor = vec4(vec3(1.0f), 1.0f);
                    return;
                }
            }
        }
    }

    discard;
}

现在,当所选对象位于窗口的边缘时,我仍然存在一个小问题。

如您所见,轮廓被锐减。

我尝试在参数GL_TEXTURE_WRAP_SGL_TEXTURE_WRAP_T 上“玩”glTexParameter

在上图中你可以看到我得到的效果

glTexParameters(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameters(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

在下图中你可以看到我得到的效果

glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, &(QVector4D (1.0f, 1.0f, 1.0f, 1.0f)[0]));
glTexParameters(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameters(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);

我希望在窗口边缘显示轮廓,但仅在必要时显示。

非常感谢!

【讨论】:

  • 嗯。您可以对窗口边框使用不同的检查:if(texture(silhouette, frag.coords) != vec3(1.0) &amp;&amp; (gl_FragCoord.x &lt; 2 || gl_FragCoord.y &lt; 2 || gl_FragCoord.x &gt;= viewportWidth - 2 || gl_FragCoord.y &gt;= viewportHeight - 2))。或者,您可以调整纹理的纹理重复设置,以处理读取边缘上的像素时发生的情况(我假设它当前是 CLAMP_TO_EDGE),这样它就会看到白色像素并绘制轮廓。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多