Direct3D提高篇之一 用HLSL实现PhotoShop滤镜效果.docx
《Direct3D提高篇之一 用HLSL实现PhotoShop滤镜效果.docx》由会员分享,可在线阅读,更多相关《Direct3D提高篇之一 用HLSL实现PhotoShop滤镜效果.docx(24页珍藏版)》请在冰豆网上搜索。
![Direct3D提高篇之一 用HLSL实现PhotoShop滤镜效果.docx](https://file1.bdocx.com/fileroot1/2022-11/25/6bff6571-9024-4d44-9ded-07a17c8615b7/6bff6571-9024-4d44-9ded-07a17c8615b71.gif)
Direct3D提高篇之一用HLSL实现PhotoShop滤镜效果
Direct3D提高篇之:
HLSL编程实现PhotoShop滤镜效果
潘李亮2007-3-16
xheartblue@
关于学习,中国有句古话叫“学以致用”,可见把学到的东西用于实际实践中是多么的重要,现在学习Direct3D/HLSL的人非常多,教程也非常多。
但是很多人不知道看完这些教程后该干什么,或者说可以怎么利用学到的知识,本文针对已经学习过Direct3D/HLSL的初学者,讲述如果将HLSL用于数字图像处理,带领大家一起体会HLSL的强大。
本文会对Direct3D/HLSL做一个简单的介绍,但是假设读者已经了解和掌握了Direct3D/HLSL的基本知识。
简介.
1)Direct3D和HLSL
众所周知,Direct3D是微软开发的用于编写Windows下高性能图形程序的3DAPI。
通过Direct3D,我们可以访问高速的图形加速卡。
它是DirectX众多成员的一部分。
HLSL全称HighLevelShadingLanguage.是MS推出Direct3D9时的一个重要更新。
所谓的ShadingLanguage还需要从Direct3D的图形管道说起,Direct3D在Direct3D8以前只能工作在固定管道(FixedFunctionPipe-line)的模式下,在固定管道模式下,图元从提交到被转化成可以显示的像素是按照实现定义好的流程和算法来完成,可以认为是固化在硬件中的死功能。
从Direct3D8开始,微软在Direct3D中引入了可编程管道(ProgramableFunctionPipeline)的概念,在可编程管道中,开发人员可以自己编写用于处理顶点和像素的程序,这些程序是运行在GPU上而不是CPU上的。
在Direct3D里面,用于处理顶点的程序叫VertexShader,用于处理像素的叫PixelShader。
(目前最新的Direct3D10中又引入了GeometryShader的概念)。
因为硬件的水平在进步,所以可编程管道的处理能力也在不断的提高,根据不同的硬件能力,Shader的版本也已经有对应的不同版本。
从Direct3D发布的最早的ShaderModel1.0到现在主流的ShaderModel3.0,可编程管道已经能提供一点范围的通用编程能力了,这就是所谓的GPGPU。
从名字上可知,HLSL是一种高级语言(HighLevel),那么必然有与之对应的LowLevelShadingLanguage,这个低级的语言就是ASM的Shader。
它是类似于汇编语言,难以编写和维护,而HLSL则跟我们熟悉的C/C++语言非常类似。
大大降低了开发人员学习的成本。
HLSL本身就是微软和nVidia联合开发的,nVidia的版本称为Cg,也就是CforGraphics。
可想而知,它和C是有同样的血统的。
本文不是Direct3D和HLSL的教程,如果读者觉得以上的概念还比较陌生,请先学习Direct3D的基础知识。
同时关于如何在Direct3D应用程序中使用HLSL编写的VertexShader和PixelShader,请参阅其它的教程和微软的DirectXSDK。
2)RenderMonkey简介
现在的开发人员可能都比较熟悉IDE的工作模式,尤其是使用VisualStudio一类开发工具的Windows程序开发人员。
在一个统一的开发环境中,可以编写和调试程序。
HLSL作为一种新的语言,GPU编程作为一种新事物,目前还没有很好的IDE能完整的支持编写,调试一体化的工作方式。
在本文我们将使用ATI的一个相对比较好用的开发HLSL的IDE:
RenderMonkey。
RenderMonkey是由前ATI开发的,用于编写Shader,并调试Shader的一个工具。
由于RenderMonkey支持插件,所以RenderMonkey既可以编写OpenGL的GLSL也可以编写Direct3D的HLSL。
它能支持创建RenderTarget,多Pass渲染,可以自由选择用哪个shadermodel来编译代码。
并能加亮显示shader代码。
经典的RenderMonkey界面如下图
左边为工作区,右边为预览区域。
下面为信息输出区。
在左边的工作区里可以看到。
我们可以对Shader的工程进行分组,其中每一个可以独立工作的工程称为一个Effect。
在同一时候预览区中只能预览当前激活的Effect。
每个Effect由不同的对象组成,其中比较重要的对象如下:
1)Pass.这个pass就是渲染中常提到的pass.代表一遍的渲染
2)几何体。
就是类红色茶壶表示的,它代表在渲染中使用的几何体。
3)纹理对象和RenderTarget对象(用一个铅笔表示)
4)Shader中用到的参数,这些参数可以是自定义的,也可以是预定义的(比如当前的观察矩阵,摄像机的位置等参数)。
5)每个pass中用到的Shader。
这些shader可以在RenderMonkey的代码编辑器中进行编辑,并调用命令来编译。
因为文章篇幅的关系,也不采用编写Direct3D程序加载HLSL的方式来做演示程序,而是直接使用RenderMonkey来作为演示的平台。
关于如何使用RenderMonkey,请参照RenderMonkey的帮助,或者打开RenderMonkey自带的例子,很容易就能掌握这个工具的使用方法。
GPGPU
本文将要介绍的是如何用HLSL来实现PhotoShop的滤镜效果,也就是说需要通过GPU来进行数字图像处理。
这是目前很流行的GPGPU的应用的一种。
我们知道,GPU和CPU的工作方式和用途都是不同的,CPU是通用的处理器,而GPU是专用于处理3D图形显示的,因此CPU的指令集更加丰富,而GPU的指令集更加有针对性,因此这就决定了GPU在牺牲了CPU的灵活性的前提上有更快的运行速度。
GPU特别适合处理那种可以大规模并行的算法,比如某些数字图像处理算法。
因为目前我们的程序只能通过Direct3D的API才能访问到GPU,一般我们采用PixelShader来进行GPGPU,所以我们要使用GPU来处理数据的时候,必须完成以下几件事:
1).将数据提交给GPU
2).调用对应命令让GPU开始处理数据
3).从GPU哪里取回处理完毕的数据。
我们可以通过两种方法将数据提交到GPU,纹理和shader的参数,纹理中一般保存我们需要进行处理的数据,而shader参数则一般是用于数据处理算法需要用到的一些参数。
当然这也不是绝对的。
当数据已经准备完毕后,我们调用Direct3D的drawPrimitive函数在屏幕上绘制一个纹理相同大小的矩形,把GPGPU的算法写到用于绘制这个矩形的PixelShader中。
当Direct3D开始绘制这个矩形以后,会为每一个象素调用一次整个PixelShader,然后把PixelShader的输出写入到RenderTarget中,因为我们绘制的矩形的大小和纹理的大小是一致的,所以输出象素和纹理的象素可以做到一一对应的关系,也就是说纹理中的每一个象素在经过PixelShader的运算后被输出掉RenderTarget里,等于对这个数据调用了一次我们需要的算法。
我们知道现代的GPU中往往有大量的PixelShader处理单元,而这些处理都是可以并行运行的,可想而知,这个处理是非常快速的。
经过前面的步骤,处理完的数据已经到了RenderTarget里了,我们可以事先自己创建一个RenderTarget(通常和输入纹理等大)来接受步骤2中的数据,然后Lock这个RenderTarget取回数据。
也可以在步骤二中直接把图象绘制到屏幕上,通过Capture屏幕来得到输出(对于图象处理也够了,就是速度慢,而且显得非常傻)。
GPGPU简单介绍到这里。
详细的GPGPU资料请参考www.gpgpu.org。
同时nvidia的网站和发布的SDK上也有很多关于GPGPU的例子。
接下来我们使用RenderMonkey来搭建一个用于数字图像处理的架子,以实现类似PhotoShop的滤镜效果
RenderMonkey图像处理的架子-图像黑白化
下面我们通过一个简单的例子,先来完成一个最简单的图像处理-把图像黑白化。
来说明RenderMonkey如何来处理数字图像。
RenderMonkey和VC类似,内置了一些工程代码。
在这里我们在RenderMonkey的工作区菜单里选择AddEffect->DirectX->Screen-AlignedQuad.在生成的工程中,我们看到RenderMonkey为我们显示了一个默认的图片,首先我们就是要修改这个图片,我们双击那个base图片对应的节点,选择一个我们要演示的图片。
如下图。
接下来,我们要开始进行我们关键的一步,编写处理图像的算法,我们双击刚才建立的项目中的singlepass->pixelshader.开始编辑PixelShader的代码。
我们知道,一个RGB颜色的亮度和各个分量之间的关系的公式为:
GrayValue=0.3*R+0.59*G+0.11*B
根据这个公式,我们的代码如下:
sampler2DTexture0;
float4main(float2texCoord:
TEXCOORD0):
COLOR
{
float4_inColor=tex2D(Texture0,texCoord);
floath=0.3*_inColor.x+0.59*_inColor.y+0.11*_inColor.z;
float4_outColor=float4(h,h,h,1.0);
return_outColor;
}
我来详细的解释一下这个PixelShader,首先我们定义的的sampler2DTexture0。
Texture0就是代表我们输入的图像。
这个图像在RenderMonkey的工作区中用两部分表示,首先需要在工作区中创建一个图像对象bas,然后需要在用到这个纹理图像的pass中创建一个纹理对象Texture0,然后让这个Texture0指向我们刚才创建的纹理图像bas,读者应该注意到了纹理对象的名字就是我们Shader里的sampler2D变量的名字,不错,RenderMonkey就是以这种方法把shader代码中的变量名字和工作区中的对象关联起来.不光纹理如此,其它的float4/float3/float2/float变量都如此.
接下来的main函数中,我们通过纹理采样的方式得到当前需要绘制的像素,float4_inColor=tex2D(Texture0,texCoord);也就是输入的颜色。
得到输入颜色后,我们可以通过上面给出的公式来计算出这个颜色的灰度值,并用这个值构造一个灰度颜色返回给Direct3D。
系统就会把这个颜色作为最终的色彩显示在窗口中,也就是得到一个黑白的图像。
最终结果如下图:
图:
图像去色效果
(注:
这个例子是最简单的HLSL用于图像处理的例子,如果读者觉得到目前为止还很有难度,建议重新温习一遍Direct3D和HLSL的知识)。
通过这个例子,我们已经基本了解了RenderMonkey处理图像的步骤和流程,下面我们通过分析一些更加复杂一点的例子来体会HLSL的强大能力
●入门效果之浮雕
"浮雕"图象效果是指图像的前景前向凸出背景。
常见于一些纪念碑的雕刻上,要实现浮雕其实非常简单。
我们把图象的一个象素和左上方的象素进行求差运算,并加上一个灰度。
这个灰度就是表示背景颜色。
这里我们设置这个插值为128(图象RGB的值是0-255)。
同时,我们还应该把这两个颜色的差值转换为亮度信息.否则浮雕图像会出现彩色
在使用HLSL处理浮雕效果的时候,两个问题我们需要注意一下。
其中一个图象边界,写过C++实现浮雕效果的朋友都知道,在处理边界象素的时候可能是取不到左上角象素的,这个时候就应该左做特殊处理,通常我们把边界位置的浮雕结果设置成背景颜色,但是使用HLSL的时候我们不需要在HLSL的shader中去对图象的边界做特殊处理,但是我们需要对纹理设置滤波器,这个滤波器我们设置为CLAMP模式就可以了.
第二个需要处理的问题是,我们知道PixelShader中,纹理的采样坐标是0-1.0,如果我们要取到左上我们需要知道纹理图象的大小,这样才能把一个象素的的偏移转换成0-1.0的值是多少,假设纹理的大小是[w,h],当前纹理坐标是[u,v],那么它左上角的象素的纹理坐标就是[u-1.0/w,v–1.0/h].RenderMonkey中无法知道这个纹理图像的大小,当然如果我们自己用VC++写一个程序的话,我们可以在加载图像或者从IDirect3DTexture9对象中得到纹理大小.然后当作一个constant常量设置给HLSL就可以了.当然也可以偷懒—假设纹理的大小就是1024x1024—得到的效果也是可以接受的.
好了,现在我来展示一下用来得到浮雕效果的HLSL的代码:
sampler2DTexture0;
float2TexSize;
float4main(float2texCoord:
TEXCOORD0):
COLOR
{
float2upLeftUV=float2(texCoord.x-1.0/TexSize.x,texCoord.y-1.0/TexSize.y);
float4bkColor=float4(0.5,0.5,0.5,1.0);
float4curColor=tex2D(Texture0,texCoord);
float4upLeftColor=tex2D(Texture0,upLeftUV);
//相减得到颜色的差
float4delColor=curColor-upLeftColor;
//需要把这个颜色的差设置
floath=0.3*delColor.x+0.59*delColor.y+0.11*delColor.z;
float4_outColor=float4(h,h,h,0.0)+bkColor;
return_outColor;
}
原图浮雕化后的图像
读者应该会发现,相对于C++版本的代码,HLSL的代码显得非常的干净和利索.没有分支,没有循环.最重要的是它的速度非常快,对一个2048x2048的图像完全可以做到>30fps的实时处理能力而不会耗费很多的CPU时间!
●入门效果之马赛克
接下来我们完成一个更加常见的效果—马赛克.图片的马赛克就是把图片的一个相当大小的区域用同一个点的颜色来表示.可以认为是大规模的降低图像的分辨率,而让图像的一些细节隐藏起来,比如电视中要秀一下某个罪犯的身材,却又不能展示他的脸,这个时候我们就可以给他的脸加一个马赛克.
用HLSL代码实现马赛克是非常简单的,但是同样的,我们需要一些额外的步骤,第一步就是先把纹理坐标转换成图像实际大小的整数坐标.接下来,我们要把图像这个坐标量化---比如马赛克块的大小是8x8象素。
那么我们可以用下列方法来得到马赛克后的图像采样值,假设[x.y]为图像的整数坐标:
[x,y]mosaic=[int(x/8)*8,int(y/8)*8].
得到这个坐标后,我们只要用相反的方法,把整数坐标转换回到0-1.0的纹理坐标。
具体的马赛克效果代码如下:
sampler2DTexture0;
float2TexSize;
float2mosaicSize=float2(8,8);
float4main(float2texCoord:
TEXCOORD0):
COLOR
{
//得到当前纹理坐标相对图像大小整数值。
float2intXY=float2(texCoord.x*TexSize.x,texCoord.y*TexSize.y);
//根据马赛克块大小进行取整。
float2XYMosaic=float2(int(intXY.x/mosaicSize.x)*mosaicSize.x,
int(intXY.y/mosaicSize.y)*mosaicSize.y);
//把整数坐标转换回纹理采样坐标
float2UVMosaic=float2(XYMosaic.x/TexSize.x,XYMosaic.y/TexSize.y);
returntex2D(Texture0,UVMosaic);
}
经过这个Shader处理后的图像结果如下:
图:
马赛克处理效果
读者可能会发现这个马赛克太普通了,确实它不够新颖,下面我们来改良一下,我们希望达到这样一个效果:
马赛克区域不是方的,而是圆的,圆形区域以外,我们用图像原来的颜色覆盖。
这样我们需要改变一下代码。
首先求出原来马赛克区域的正中心(原来是左上角):
然后计算图像采样点到这个中心的距离,如果在马赛克圆内,就用区域的中心颜色,否则就用原来的颜色。
改良后的代码如下,这里我们把马赛克区域大小调节成16x16。
这样效果更明显。
sampler2DTexture0;
float2TexSize;
float2mosaicSize=float2(16,16);
float4ps_main(float2texCoord:
TEXCOORD0):
COLOR
{
float2intXY=float2(texCoord.x*TexSize.x,texCoord.y*TexSize.y);
//马赛克中心不再是左上角,而是中心
float2XYMosaic=float2(int(intXY.x/mosaicSize.x)*mosaicSize.x,
int(intXY.y/mosaicSize.y)*mosaicSize.y)
+0.5*mosaicSize;
//求出采样点到马赛克中心的距离
float2delXY=XYMosaic-intXY;
floatdelL=length(delXY);
float2UVMosaic=float2(XYMosaic.x/TexSize.x,XYMosaic.y/TexSize.y);
float4_finalColor;
//判断是不是处于马赛克圆中。
if(delL<0.5*mosaicSize.x)
_finalColor=tex2D(Texture0,UVMosaic);
else
_finalColor=tex2D(Texture0,texCoord);
return_finalColor;
}
这个代码相对上面的代码复杂了一些,加了一个分支if/else。
注意,GPU是个高度并行的处理器,过多分支会降低Shader的运行速度。
这个改良的马赛克效果如下
图:
改良后的马赛克效果
●进阶效果之锐化模糊
以上两个效果相对比较简单,姑且称之为入门效果,它并没有用到太多数字图像处理或者信号处理方面的知识。
接下来我们要介绍稍微复杂一点的效果,第一个就是图像的模糊和锐化。
图像的模糊又成为图像的平滑(smoothing),我们知道人眼对高频成分是非常敏感的,如果在一个亮度连续变化的图像中,突然出现一个亮点,那么我们很容易察觉出来,类似的,如果图像有个突然的跳跃—明显的边缘,我们也是很容易察觉出来的。
这些突然变化的分量就是图像的高频成分。
人眼通常是通过低频成分来辨别轮廓,通过高频成分来感知细节的(这也是为什么照片分辨率低的时候,人们只能辨认出照片的大概轮廓,而看不到细节)。
但是这些高频成分通常也包含了噪声成分。
图像的平滑处理就是滤除图像的高频成分。
那么如何才能滤除图像的高频成分呢?
我们先来介绍一下图像数字滤波器的概念。
简单通俗的来说,图像的数字滤波器其实就是一个nxn的数组(数组中的元素成为滤波器的系数或者滤波器的权重,n称为滤波器的阶)。
对图像做滤波的时候,把某个像素为中心的nxn个像素的值和这个滤波器做卷积运算(也就是对应位置上的像素和对应位置上的权重的乘积累加起来),公式如下
其中x,y为当前正在处理的像素坐标。
通常情况下,我们滤波器的阶数为3已经足够了,用于模糊处理的3x3滤波器如下
。
经过这样的滤波器,其实就是等效于把一个像素和周围8个像素一起求平均值,这是非常合理的---等于把一个像素和周围几个像素搅拌在一起—自然就模糊了
用来对一个图像做滤波处理的函数如下:
//用来做滤波操作的函数
float4dip_filter(float3x3_filter,sampler2D_image,float2_xy,float2texSize)
{
//纹理坐标采样的偏移
float2_filter_pos_delta[3][3]=
{
{float2(-1.0,-1.0),float2(0,-1.0),float2(1.0,-1.0)},
{float2(0.0,-1.0),float2(0,0.0),float2(1.0,0.0)},
{float2(1.0,-1.0),float2(0,1.0),float2(1.0,1.0)},
};
//最终的输出颜色
float4final_color=float4(0.0,0.0,0.0,0.0);
//对图像做滤波操作
for(inti=0;i<3;i++)
{
for(intj=0;j<3;j++)
{
//计算采样点,得到当前像素附近的像素的坐标
float2_xy_new=float2(_xy.x+_filter_pos_delta[i][j].x,
_xy.y+_filter_pos_delta[i][j].y);
float2_uv_new=float2(_xy_new.x/texSize.x,_xy_new.y/texSize.y);
//采样并乘以滤波器权重,然后累加
final_color+=tex2D(_image,_uv_new)*_filter[i][j];
}
}
returnfinal_color;
}
剩下的工作,我们就是定义一个用来进行模糊的滤波器模板,并调用dip_filter这个函数就可以了。
代码如下:
sampler2DTexture0;
float2TexSize;
float4main(float2texCoord:
TEXCOORD0):
COLOR
{
float2intXY=float2(texCoord.x*TexSize.x,texCoord.y*TexSize.y);
//用于模糊操作的滤波器
float3x3_smooth_fil=float3x3(1/9.0,1/9.