[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),此處先用了模長做判斷,還沒有試過方向,所在這兩者哪個效果更準確上還沒有研究。

         我們比較的閾值是一個和遠裁剪面相關的值。

        

 

         發一處比較有意思的,被近裁剪面剪掉的部分,也有輪廓線。

        

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章