[OpenGL] 基于屏幕空间的轮廓线提取

 

概念引入

         《Real-time Rendering》一书中介绍了多种轮廓线提取的方式,根据对比以及综合考虑,最终使用了图像空间的方法来实现实时提取轮廓。该方法需要先将场景中的物体信息先渲染到纹理中,对使用延迟渲染的框架非常友好,并且弊端也是多种轮廓线提取方法中比较少的,具体内容可以参照原书的NPR章节。

方法概述

          (1) 准备2个颜色缓冲区,1个深度缓冲区。

          (2) 先用一般的方法渲染一遍场景。手动将颜色,法线(世界空间)+深度分别写入两个颜色缓冲区。深度缓冲区会自动写入。

          (3) 传入上一个pass得到的颜色和法线/深度纹理,对法线+深度图进行处理。对每个片段,使用sobel算子六次采样法线+深度图,横向和纵向各一次,以检测法线或深度不连续的地方。根据是否为轮廓,来决定当前像素从颜色纹理采样,还是直接绘制轮廓色。

具体实现

      (一)生成纹理图  

       我们首先要做的一步是将颜色,法线和深度渲染到纹理中,这意味着我们至少需要在一次drawcall中,渲染2张纹理。为了达到这一目的,我们需要使用MRT(多重渲染目标)技术。

         和之前一样,在初始化的时候生成一个帧缓冲区:

RenderCommon::RenderCommon()
{
    // ...
    // GLuint gBuffer;
    glGenFramebuffers(1, &gBuffer);
    // ...
}

         接下来,在窗口resize的时候生成绑定到这个帧缓冲区的几个纹理:

void MainWidget::resizeGL(int w, int h)
{
    float aspect = float(w) / float(h ? h : 1);
    const qreal zNear = 2.0, fov = 45.0;
    RenderCommon::Inst()->SetScreenXY(w, h);
    RenderCommon::Inst()->UpdateTexSize(); // 更新纹理大小
    RenderCommon::Inst()->GetProjectMatrix().setToIdentity();
    RenderCommon::Inst()->GetProjectMatrix().perspective(fov, aspect, zNear,
 RenderCommon::Inst()->GetZFarPlane());
}
void RenderCommon::UpdateTexSize()
{
    // 删除旧的纹理(如果存在的话)
    glDeleteTextures(2, gBufferTex);
    glDeleteTextures(1, &gBufferDepthTex);

    // 激活gBuffer缓冲区
    glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);

    // 分配两个颜色缓冲区的纹理
    glGenTextures(2, gBufferTex);
    for(int i = 0;i < 2; i++)
    {
        // 激活第i个颜色缓冲区
        glBindTexture(GL_TEXTURE_2D, gBufferTex[i]);
        // 生成纹理并初始化纹理信息
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F_ARB, screenX, screenY, 
0, GL_RGBA, GL_FLOAT, nullptr);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glBindTexture(GL_TEXTURE_2D, 0);
        // 将该纹理绑定到当前激活缓冲区,其中GL_COLOR_ATTACHMENT0,GL_COLOR_ATTACHMENT1
        // 代表绑定的第几个纹理
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D,
 gBufferTex[i], 0);
    }

    // 分配深度纹理
    glGenTextures(1, &gBufferDepthTex);
    // 激活深度纹理
    glBindTexture(GL_TEXTURE_2D,gBufferDepthTex);
    // 生成深度纹理并初始化纹理信息
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, screenX, screenY, 0, 
GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL);
    // 将该纹理绑定到当前激活缓冲区
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,
 gBufferDepthTex, 0);

    // 指定当前用了哪些颜色缓冲区
    GLuint attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
    glDrawBuffers(2, attachments);

    // 清空激活状态
    glBindTexture(GL_TEXTURE_2D, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

           关于这一步骤,有几个要注意的关键点:

          (1) 生成纹理和绑定这一步放在窗口的resize中,是为了随着屏幕大小变化,生成和屏幕大小一样大的纹理。

          (2) 通过设置GL_COLOR_ATTACHMENT0,GL_COLOR_ATTACHMENT1等,可以指定绑定到第几个颜色缓冲区,这几个枚举量是连续的,所以可以直接写成循环的形式。

          (3) 需要调用glDrawBuffers来指定我们使用了多少个颜色缓冲区,以及分别是什么。如果没有这一步,在多个缓冲区的情况下,默认只有第一个是生效的。

          (4) 深度缓冲区我们只需要生成并绑定到帧缓冲区即可,不需要读写操作。这一步是不可少的,不然就没有系统自动为我们做的深度测试相关操作,可能出现遮挡关系不对的问题。

           接下来,我们在片元着色器加几个重定向的操作,对不同的缓冲区分别写入数据:

layout(location = 0) out vec4 Color;
layout(location = 1) out vec4 Normal;

void main()
{
    // Color = ...
    // Normal =
}

         通过上述两个重定向标记,我们就不需要写入gl_FragColor,直接填充Color和Normal即可。

       (二) sobel算子检测

提取得到的轮廓

         我们认为法线或者深度不连续的地方为轮廓,而不连续的地方一般是一阶导数发生突变的地方。所以我们使用了图像的一 个一阶导数算子——sobel算子进行计算,它包含了x和y方向的两个算子,分别可以求出x和y两个方向的图像导数。具体的运算 + 比较方法如下:

    -1    -2   -1                 -1    0    1

[    0     0    0   ]         [    -2    0    2  ]

     1     2    1                  -1    0    1

       sobel_y                     sobel_x

#version 450 core

varying vec2 v_texcoord;
uniform float zFar;
uniform sampler2D Color;
uniform sampler2D Normal;

uniform int ScreenX;
uniform int ScreenY;

float sobel_y[] =
{
    -1,-2,-1,
    0 , 0, 0,
    1 , 2, 1,
};

float sobel_x[] =
{
    -1, 0, 1,
    -2, 0, 2,
    -1, 0, 1,
};


void main(void)
{
    vec4 curNormalX = vec4(0, 0, 0, 0);
    int k = 0;
    for(int i = -1; i <=1 ;i++)
    {
        for(int j = -1; j <= 1;j++)
        {
            vec4 ret =  texture2D(Normal, v_texcoord + vec2(1.0 * i / ScreenX, 1.0 * j / ScreenY));
            curNormalX += sobel_x[k++] * ret;
        }
    }
    k = 0;
    vec4 curNormalY = vec4(0, 0, 0, 0);
    for(int i = -1; i <=1 ;i++)
    {
        for(int j = -1; j <= 1;j++)
        {
            curNormalY += sobel_y[k++] * texture2D(Normal, v_texcoord + vec2(1.0 * i / ScreenX, 1.0 * j / ScreenY));
        }
    }


   vec4 size = sqrt(curNormalX * curNormalX + curNormalY * curNormalY);

   gl_FragColor = texture2D(Color, v_texcoord);
   float threshold = 100.0/zFar;
   if(size.x > threshold || size.y > threshold || size.z > threshold || size.w > 1.0/zFar)
   {
       gl_FragColor = vec4(0,0,0,1);
    }
}

         最终得到的是一个x方向的导数值gx,和y方向的导数值gy。

         我们可以利用这两个值得到方向值(tan(gy/gx)),以及模长(√(gx^2 + gy^2),此处先用了模长做判断,还没有试过方向,所在这两者哪个效果更准确上还没有研究。

         我们比较的阈值是一个和远裁剪面相关的值。

        

 

         发一处比较有意思的,被近裁剪面剪掉的部分,也有轮廓线。

        

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值