65.9K
CodeProject 正在变化。 阅读更多。
Home

模板缓冲区发光 - 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (9投票s)

2011年2月11日

CPOL

6分钟阅读

viewsIcon

32865

downloadIcon

990

通过基本后期处理创建更强的辉光效果。

引言

在上一篇文章中,我们通过多彩的轮廓线突出了最终用户看到的物体。通过在模板缓冲区中对像素进行遮罩,我们能够精确地生成感兴趣的模型周围的辉光。这个过程对某些物体效果很好。然而,对其他物体来说,效果并非完全正确。以这个圆环为例。它的内环缺少轮廓线。

stencilbufferglowspart21.png

在本文中,我们将构建一个能够为任意形状模型生成轮廓辉光效果。

背景

简要回顾一下上一篇文章的逻辑。首先,我们正常渲染模型,同时创建模型的逐像素遮罩。然后,我们生成辉光效果。最后,我们在尊重遮罩的前提下,将辉光与原始图像合成。在本文中,我们在遮罩和合成方面采用了类似的逻辑。然而,生成实际辉光的程序要鲁棒得多。

步骤 1

将模型渲染到两个帧缓冲区、深度缓冲区和模板缓冲区(即使用多重渲染目标)。

stencilbufferglowspart22.png

第二步

模糊第二个渲染目标中的图像。

stencilbufferglowspart23.png

步骤 3

将第二个渲染目标渲染到帧缓冲区,同时对当前模板缓冲区中的值进行像素遮罩。

stencilbufferglowspart24.png

后期处理

在我们深入实际实现之前,让我们对图像处理进行一个非常基础和通用的概述,特别是适用于模糊的概念。如果你从未进行过后期处理,这应该会减轻学习曲线,并有望阐明即将出现的着色器和代码。

我选择使用卷积来生成模糊。卷积将给定像素周围像素的加权和作为该像素的新值。考虑以下卷积核

stencilbufferglowspart25.png

对于图像中的每个像素,该像素的新值将是其左上角邻居值的一倍,加上其上方邻居值的两倍,加上其右上角邻居值的两倍,加上其左侧邻居值的两倍,加上其自身值的四倍,等等。在此特定滤波器中,你可能需要将最终结果(或所有核值)除以十六。这将保留图像的原始亮度,因为此核当前将原始颜色值的十六倍添加到正在进行卷积的像素中。

接下来,我们需要讨论可分离性。如果一个核可以表示为一个列向量和一个行向量的乘积,那么它就是可分离的。在上面的例子中,考虑 `[1 2 1]T * [1 2 1]`,其中“T”表示转置。将这两个向量相乘会产生原始核。因此,我们现在可以通过 `[1 2 1]T` 然后 `[1 2 1]` 来卷积我们的图像,以产生与原始矩阵相同的结果。为什么这很重要?嗯,在这种情况下,我们将纹理查找次数从九次减少到六次。这并不算太显著。然而,在一个十五乘十五的核中,查找次数将从二百二十五次降至三十次。

最后,在此项目中,核权重是通过高斯分布程序化生成的。别慌。我们刚才讨论的所有内容也适用于高斯核。然而,权重不是硬编码的,而是原始像素距离的函数。

示例/演示要求

由于此项目基于 DirectX 10,您需要 Windows Vista 或更高版本。此外,我使用的是 DirectX 2010 年 2 月 SDK。您需要安装 2010 年 2 月或更高版本的可再发行组件。最后,虽然不是必需的,但我建议拥有兼容 DirectX 10 的 GPU。

源代码要求

首先,所有示例要求也适用于源代码。此外,您还需要安装完整的 DirectX SDK,2010 年 2 月或更高版本。安装 SDK 后,在 IDE 中包含 DirectX 和 DXUT 头文件并链接相应的库。**注意**:Microsoft 不分发 DXUT 库。SDK 文件夹中会有一个包含 DXUT 头文件的 Visual Studio 项目。构建此项目以创建库。

使用代码

从技术角度来看,此项目中有三个主要概念在交互:模板遮罩、多重渲染目标和后期处理。

首先,所有模板操作与上一篇文章中讨论的操作相同或相似。如果您需要复习或对这些概念不熟悉,我建议您回顾本系列的第 1 部分。

接下来,虽然使用多重渲染目标的概念可能看似微不足道,但实现起来可能很棘手。在概念解释中,我们描述使用主帧缓冲区和辅助渲染目标。在这里,我们实际上使用了三个渲染目标。一个仍然是主渲染目标。另外两个充当辅助渲染目标,中间结果在它们之间来回“弹跳”。

以下是设置渲染目标以及在着色器中读取和写入它们的视图的方法。

// Set up the second and third render targets.
D3D10_TEXTURE2D_DESC desc;
ZeroMemory( &desc, sizeof(desc) );
desc.Width = pBufferSurfaceDesc->Width;
desc.Height = pBufferSurfaceDesc->Height;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = pBufferSurfaceDesc->Format;
desc.SampleDesc.Count = 1;
desc.Usage = D3D10_USAGE_DEFAULT;
desc.BindFlags = D3D10_BIND_RENDER_TARGET | D3D10_BIND_SHADER_RESOURCE;

V_RETURN( pd3dDevice->CreateTexture2D( &desc, NULL, 
          &g_pSecondRenderTarget ) );
V_RETURN( pd3dDevice->CreateTexture2D( &desc, NULL, 
          &g_pThirdRenderTarget ) );

// Set up the resource view for the second and third render targets.
D3D10_RENDER_TARGET_VIEW_DESC rtDesc;
rtDesc.Format = desc.Format;
rtDesc.ViewDimension = D3D10_RTV_DIMENSION_TEXTURE2D;
rtDesc.Texture2D.MipSlice = 0;

V_RETURN( pd3dDevice->CreateRenderTargetView( g_pSecondRenderTarget, 
          &rtDesc, &g_pSecondRenderTargetRTView ) );
V_RETURN( pd3dDevice->CreateRenderTargetView( g_pThirdRenderTarget, 
          &rtDesc, &g_pThirdRenderTargetRTView ) );

// Set up the shader resource view for the second and third render targets.
D3D10_SHADER_RESOURCE_VIEW_DESC srDesc;
srDesc.Format = desc.Format;
srDesc.ViewDimension = D3D10_SRV_DIMENSION_TEXTURE2D;
srDesc.Texture2D.MostDetailedMip = 0;
srDesc.Texture2D.MipLevels = 1;

V_RETURN( pd3dDevice->CreateShaderResourceView( g_pSecondRenderTarget, 
          &srDesc, &g_pSecondRenderTargetSRView ) );
V_RETURN( pd3dDevice->CreateShaderResourceView( g_pThirdRenderTarget, 
          &srDesc, &g_pThirdRenderTargetSRView ) );

为了同时向两个目标写入数据,我们需要设置输出合并器。这可以在一个 API 调用中完成。(顺便说一句,请注意,所有渲染目标只能绑定一个深度模板缓冲区。) 

// Set primary and secondary render targets.
ID3D10RenderTargetView* pRTViews[2];
pRTViews[0] = pOrigRTView; 
pRTViews[1] = g_pSecondRenderTargetRTView;

pd3dDevice->OMSetRenderTargets( 2, pRTViews, g_pDSView );

此外,着色器需要反映此设置。像素着色器需要与渲染目标相同数量的输出。在此项目中,有两个目标具有相同的输出;然而,这不是必需的。

现在,我们需要讨论后期处理步骤。在像素着色器实现中,一个固有的问题是如何将渲染目标数据实际传输到像素着色器。你不能直接“读取”数据。你必须渲染它。绘制一个全屏四边形可以解决这个问题。以下是设置方法

// Full Screen Quad Vertices
{
        ScreenVertex svQuad[4];
        svQuad[0].pos = D3DXVECTOR4( -1.0f, 1.0f, 0.5f, 1.0f );
        svQuad[0].tex = D3DXVECTOR2( 0.0f, 0.0f );
	svQuad[1].pos = D3DXVECTOR4( 1.0f, 1.0f, 0.5f, 1.0f );
	svQuad[1].tex = D3DXVECTOR2( 1.0f, 0.0f );
	svQuad[2].pos = D3DXVECTOR4( -1.0f, -1.0f, 0.5f, 1.0f );
	svQuad[2].tex = D3DXVECTOR2( 0.0f, 1.0f );
	svQuad[3].pos = D3DXVECTOR4( 1.0f, -1.0f, 0.5f, 1.0f );
	svQuad[3].tex = D3DXVECTOR2( 1.0f, 1.0f );

	D3D10_BUFFER_DESC vbdesc =
	{
		4 * sizeof( ScreenVertex ),
		D3D10_USAGE_DEFAULT,
		D3D10_BIND_VERTEX_BUFFER,
		0,
		0
	};
	D3D10_SUBRESOURCE_DATA InitData;
	InitData.pSysMem = svQuad;
	InitData.SysMemPitch = 0;
	InitData.SysMemSlicePitch = 0;
	V_RETURN( pd3dDevice->CreateBuffer( &vbdesc, &InitData, 
                  &g_pScreenQuadVB ) );
}

此外,请确保在顶点着色器中省略变换管线。顶点已经跨越屏幕空间,无需进行任何变换。

对于像素着色器,模糊核首先由 CPU 设置并加载到着色器中。然后,着色器通过在预先计算的核位置采样原始图像来卷积每个片段。最后,它将它们乘以相应的核权重。

要了解更多关于位置和权重是如何计算的,我建议直接查看代码或观看“HDRToneMappingCS11”DirectX 示例。这种模糊方法大致借鉴了他们像素着色器辉光实现。

最后,我们有一个合成步骤,将模糊后的图像与原始图像合并。在渲染全屏四边形时,我们使用 alpha 测试丢弃不贡献的像素,同时为剩余像素着色辉光的颜色。以下是像素着色器的一个片段。

PS_OUTPUT PSOutline( PS_INPUT input) : SV_Target{	
    PS_OUTPUT output;
	output.result1 = txDiffuse.Sample( samLinear, input.Tex );
		// alpha test
	if( output.result1.w == 0.0f )	{
		discard;
	}
	// Assign Glow Color
	output.result1.x = 1.0f;
	output.result1.y = 0.0f;
	output.result1.z = 0.0f;
	return output;
}

技术回顾

使用多重渲染目标,将模型渲染到目标一和目标二。

stencilbufferglowspart26.png

用水平模糊向量卷积目标二中的结果。将结果存储在渲染目标三中。

stencilbufferglowspart27.png

用垂直模糊向量卷积目标三中的结果。将结果存储在渲染目标二中。

stencilbufferglowspart28.png

将目标二中的结果与原始渲染目标(目标一)合成。

stencilbufferglowspart29.png

结论

通过利用后期处理,我们将轮廓辉光效果扩展到了任意形状的模型。然而,我们也增加了我们实现的耗时和空间消耗。如果您正在寻找改进此实现的方法,将下采样合并到后期处理步骤中可能会有所价值。

此外,如果您想要一个更复杂的模板项目,可以研究模板阴影实现。

祝好!

历史

  • 当前修订:版本 1.0
© . All rights reserved.