概念引入
《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),此處先用了模長做判斷,還沒有試過方向,所在這兩者哪個效果更準確上還沒有研究。
我們比較的閾值是一個和遠裁剪面相關的值。
發一處比較有意思的,被近裁剪面剪掉的部分,也有輪廓線。