第二部分屏幕效果
屏幕效果:将你的视口渲染为一张临时图片并通过着色器对这个图片进行操作从而达到你想要的特殊效果
这种效果,因为只需要将屏幕的像素渲染一次,所以无论你的着色器多么复杂这付出的成本都是很低的,但是却可以以这种很低的代价达到很好的效果。
Chapter5 Looking Through a Filter
制作屏幕特效的流程,首先场景被渲染为一张临时贴图,然后用滤镜对其进行处理最后显示给用户。
一些硬件可能会对渲染目标做出限制,如果没有正确显示可以检查输出窗口的错误.
要将视口渲染到一张纹理上首先需要创建一张可渲染纹理图片,右键效果→add texture→add a renderable texture,这样我们就创建了一张可渲染纹理。,双击我们创建的可渲染纹理,得到以下窗口:
在这个新窗口的第一部分是设置纹理的大小,由于我们这张纹理用于屏幕滤镜的处理,素以应该和视口大小匹配,所以我们勾选使用视口尺寸保证纹理的大小与视口大小相匹配。
在第一部分的下方有个选项叫做自动生成mip-map但这个功能在处理非正方形贴图和长宽非2的乘方的贴图时,在不同硬件上可能会存在纹理,如果不确定你的硬件支持应该关闭它。
第二部分是一个下拉框,这用于设置图片格式,在当前的工作总我们保持默认就好。
创建完可渲染纹理后我们需要在通道中创建渲染目标来把通道渲染到纹理上,在通道上右键创建渲染目标,右键渲染目标,将其指向我们所创建的纹理上,双击渲染目标得到以下窗口:
在这个窗口中包含是否清楚颜色和深度缓冲的选项,对于第一次使用这个纹理的渲染目标应该将颜色清除为黑色深度缓冲清除为1.0,以便清除纹理所带有的原来的垃圾信息,对于后面也使用该纹理的渲染目标应该取消清除。
为了将你的渲染的纹理投射到视口上,你需要创建一个平面覆盖摄像机所看到的画面,并把这个屏幕上赋予你渲染的纹理如下图所示:
处理UV坐标
因为当渲染屏幕信息的时候每一个点的信息位于像素点的左上方,担当硬件读取纹理时,只会读取每个像素的中心,这会导致屏幕上的纹理产生一些偏移,为了纠正这种情况你需要将纹理偏移半个像素,相当于1/Width和1/Height。
首先应该把用于显示纹理的平面导入到效果中,右键效果添加模型,找到ScreenAlignedQuad.3ds模型文件,然后添加渲染通道并将模型引入,然后引入stream mapping,提供必要的信息,最后新建texture object引入渲染目标纹理
然后处理代码部分,顶点着色器首先需要将位置信息传递给像素着色器,因为所使用的的几何体已经设置好,所以顶点位置已经位于屏幕空间,所以不需要设置位置信息,只需要将其传递给像素着色器,因为没有提供合适的纹理坐标,所以需要利用位置信息构建纹理坐标,因为屏幕空间中顶点的坐标范围在(–1,-1)到(1,1)的范围我们需要把他映射到(0,0)到(1,1)的空间内,可以利用一下代码实现:
Out.texCoord.x = 0.5 * (1 + Pos.x);
Out.texCoord.y = 0.5 * (1 - Pos.y);
因为之前所说的渲染到纹理,与采样器读取纹理的方式不匹配需要对图像进行偏移,首先在效果中新建常量fInverseViewportWidth与fInverseViewportHeigh作为x与y偏移的值然后通过一下代码实现:
Out.texCoord.x = 0.5 * (1 + Pos.x - viewport_inv_width);
Out.texCoord.y = 0.5 * (1 - Pos.y - viewport_inv_height);
最终顶点着色器的代码应该如下所示:
float4x4 view_proj_matrix; f
loat viewport_inv_width;
float viewport_inv_height;
struct VS_OUTPUT {
float4 Pos : POSITION;
float2 texCoord : TEXCOORD0;
};
VS_OUTPUT vs_main(float4 Pos: POSITION) {
VS_OUTPUT Out;
Out.texCoord.x = 0.5 * (1 + Pos.x - viewport_inv_width);
Out.texCoord.y = 0.5 * (1 - Pos.y - viewport_inv_height);
return Out;
}
渲染目标纹理
像之前渲染茶壶的纹理一样你只需要采样可渲染纹理,然年用tex2D将其用坐标映射到模型上即可代码如下:
sampler Texture0;
float4 ps_main(float2 texCoord: TEXCOORD0) : COLOR
{
return tex2D(Texture0, texCoord);
}
现在你的屏幕上应该已经正确显示了你所渲染的物体,有时由于所使用的模型是背对摄像机的,所以需要关闭背面消隐才能正确显示,右键在通道中新建Render State节点代开找到D3DRS_CULLMODE
在右边的VALUE下选择 D3DCULL_NONE就可以正确显示了。
渲染黑白效果
黑白效果可以简单的理解为rgb三个通道加在一起的平均亮度强度,可以简单的将其相加然后除3,但这并不严谨因为人眼对各种颜色的感知能力并不同,实际上人眼对三个通道的各自的感知强度可以表示为
强度=0.299*红色 + 0.587 * 绿色 + 0.184 * 蓝色,使用这个公式为我们之前所制作的shader添加黑白效果代码如下:
sampler Texture0;
float4 ps_main(float2 texCoord: TEXCOORD0) : COLOR {
float4 col = tex2D(Texture0, texCoord);
float Intensity;
Intensity = 0.299*col.r + 0.587*col.g + 0.184*col.r;
Intensity = dot(col,float4(0.299,0.587,0.184,0));
return float4(Intensity.xxx,col.a);
}
在以上代码中除了以权重等式的方式计算强度还使用了点积的方式计算,因为点积在HLSL中效率更高,且可以将其拓展为矩阵的形式点积可以拓展为一下等式:
黑白滤镜执行后的效果
接下来我们思考如何将多种不同的颜色效果,以相同的方式表达,对于我们之前的黑白滤镜我们在计算时使用了点积作为运算方式,这种方式实质上可以看作为矩阵与向量的相乘。
如上图所示,对于我们之前的黑白滤镜我们可以构建一个矩阵。
让我们在rendermonkey中完成这一操作,在效果中新建一个matrix类型的变量将其填充为以上矩阵中的数据并将其命名为COLOR_Filter最后只需将该矩阵与你的颜色相乘即可得到结果。需要注意的是,在pixelshader 中我们需要设置matrix packing来确定矩阵和向量的顺序如果要遵循向量在矩阵右边的模式需要打开shader的porpertise选项找到matrix packing 设置为Row major
这样我们就在视口中得到了和之前一样的黑白效果
制作模糊滤镜
盒式滤镜模糊
盒式滤镜模糊就是分别采样某一个像素点的上下左右的四个像素点然后相加取平均数,作为当前像素的值具体原理如下图所示
因为这种模糊和卷积滤镜需要你对图像进行多次处理,所以你可以将每次偏移的数量和权重存放到一个数组中如下:
const float4 blur_filter[4] = {
-1.0,0.0 , 0,0.25,
1.0 ,0.0 , 0,0.25,
0.0 ,-1.0, 0,0.25,
0.0 ,1.0 , 0,0.25
};
有了这个数组你可以利用for循环来计算着色器,每次循环都累加起来并乘以权重,完整的像素着色器代码如下:
float fInverseViewportWidth;
float fInverseViewportHeigh;
sampler Texture0;
const float4 blur_filter[4] = {
-1.0,0.0 , 0,0.25,
1.0 ,0.0 , 0,0.25,
0.0 ,-1.0, 0,0.25,
0.0 ,1.0 , 0,0.25
};
float4 ps_main(float2 Texc:TEXCOORD0) : COLOR0
{
float4 Render = float4 (0.0,0.0,0.0,0.0);
for (int i=0;i<4;i++)
{
Render += blur_filter[i].w * (tex2D(Texture0,Texc + float2(blur_filter[i].x * fInverseViewportWidth,blur_filter[i].y*fInverseViewportHeigh)));
}
return Render;
}
编译完成后模糊的结果如下所示你可以看到模糊的程度没有那么严重,因为盒式滤镜只对像素临近的像素进行采样,如果要加强模糊效果你可以在这次模糊的结果上进行第二次模糊,或者使用更加复杂的滤镜内核但这会产生更高的成本,会在以后的内容中涉及。
接下来我们拓展到其他的两个使用卷积滤波的滤镜边缘检测和锐化,下图展示了两个滤镜的滤波器内核
我们只需要找到滤波器中的非零项,然后通过和之前的模糊滤镜一样构建数组,前两项表示坐标位置最后一项代表权重即可,通过上图我们可以分别构建边缘检测和锐化所需的卷积滤波数组
边缘检测
const float4 EdgeDetection[6] = {
1.0, 1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 2.0,
-1.0, 1.0, 0.0, 1.0,
1.0, -1.0, 0.0, -1.0,
0.0, -1.0, 0.0, -2.0,
-1.0, -1.0, 0.0, -1.0
};
锐化
const float4 Sharpening[5] = {
0.0, 0.0, 0.0, 11.0/3.0,
1.0, 0.0, 0.0, -2.0/3.0,
0.0, 1.0, 0.0, -2.0/3.0,
-1.0, 0.0, 0.0, -2.0/3.0,
0.0, -1.0, 0.0, -2.0/3.0
};
只改变我们之前构建的模糊数组为这两个数组编译之后可以在窗口分别看到这两个卷积核所产生的效果
动态模糊
产生原因:因为人眼或者相机并不是以无限的速度来获取信息,而是以每秒对物体进行多次拍照,如果物体运动的速度超过了拍摄图像的速度那么物体就会在图像上形成模糊。
要想模拟这种效果一种方法是计算物体从某一帧到下一帧的移动速度然后依靠速度信息来计算模糊的结果,但这成本较高,我们可以通过将图像前一帧与当前帧的像素混合,来大致模拟这种效果,这也是一种在游戏中广泛使用的方式,同时运动模糊还可以在硬件不支持抗锯齿时,起到抗锯齿的作用。
整个模糊的过程如下图所示前一帧的渲染结果与当前帧的像素混合,然后递归循环,同时作为结果输出
构建动态模糊shader
打开之前编写好的屏幕渲染工程,目前只缺少将前一帧和当前帧混合的通道,所以 新建一个通道并名,命名为Blur_Pass,可以看到我们新建的通道默认放在整个效果的最后,但因为rendermonkey是根据这些通道在工作区的顺序来渲染的所以我们需要将这个新的通道向上移动,保证他在最终呈现效果通道之前,只需要点住通道向上拖动即可。
删除通道中默认的内容将之前用于显示渲染目标的通道的内容复制到blur通道中,接下来我们在效果中新建一个渲染目标用来存放前一帧的渲染结果,以便之后用来与当前帧混合,选中模糊通道中的render target节点将其指向新建的渲染目标
因为你需要混合前一帧和当前帧的像素所以你需要一个阈值来控制混合的程度,所以在效果中新建一个Color变量并将其亮度调整为0.5,
最后只需要将两个渲染纹理引用到blur通道并进行混合即可,完整的像素着色器如下
float4 blur_factor;
sampler Texture0;
sampler Texture1;
float4 ps_main(float2 texCoord: TEXCOORD0) : COLOR
{
float4 Render = tex2D(Texture0,texCoord);
float4 prviewrender = tex2D(Texture1,texCoord);
return lerp(Render,prviewrender,blur_factor);
}
Exercise 1: OLD TIME MOVIE
只需要在之前黑白滤镜矩阵的a通道的值改为如下图所示
完整的像素着色器代码
sampler Texture0;
float4x4 color_filter;
float4 ps_main(float2 Texc:TEXCOORD0) : COLOR0
{
float4 Render = tex2D(Texture0,Texc);
return mul(color_filter,Render);
}
Exercise 2: GAUSS FILTER
在本章中,你已经学会了如何实现一个简单的盒式滤镜。还有许多其他形式的滤镜可以用来模糊图像。最流行的是高斯滤镜。这种滤镜需要沿着7×7网格读取更多的像素,这就需要对纹理进行多次采样。这种滤镜是可以分离的。这意味着整个滤镜可以被分解成水平和垂直通道。
和之前的边缘检测和锐化一样我们首先构建y方向的滤镜卷积数组如下所示
const float4 Gauss_Blur[7] =
{
0.0, 3.0, 0, 1.0/64.0,
0.0, 2.0, 0, 6.0/64.0,
0.0, 1.0, 0, 15.0/64.0,
0.0, 0.0, 0, 20.0/64.0,
0.0, -1.0, 0, 15.0/64.0,
0.0, -2.0, 0, 6.0/64.0,
0.0, -3.0, 0, 1.0/64.0
};
然后复制当前通道,新建一个渲染目标并指向新的纹理,同时将texture0指向前一个通道的结果,最后构建x方向的数组
const float4 Gauss_Blur[7] =
{
3.0, 0.0, 0, 1.0/64.0,
2.0, 0.0, 0, 6.0/64.0,
1.0, 0.0, 0, 15.0/64.0,
0.0, 0.0, 0, 20.0/64.0,
-1.0, 0.0, 0, 15.0/64.0,
-2.0, 0.0, 0, 6.0/64.0,
-3.0, 0.0, 0, 1.0/64.0
};
最后我们就完成了高斯模糊的滤镜效果完整的像素着色器代码如下
#blur1 y方向
sampler Texture0;
float RT_X;
float RT_Y;
float Blur_factor;
const float4 Gauss_Blur[7] =
{
0.0, 3.0, 0, 1.0/64.0,
0.0, 2.0, 0, 6.0/64.0,
0.0, 1.0, 0, 15.0/64.0,
0.0, 0.0, 0, 20.0/64.0,
0.0, -1.0, 0, 15.0/64.0,
0.0, -2.0, 0, 6.0/64.0,
0.0, -3.0, 0, 1.0/64.0
};
float4 ps_main(float2 Texc:TEXCOORD0) : COLOR0
{
float4 Col = float4(0.0,0.0,0.0,0.0);
for (int i = 0;i < 7;i++)
{
Col += Gauss_Blur[i].w * tex2D(Texture0,Texc + float2(Gauss_Blur[i].x*RT_X*Blur_factor,Gauss_Blur[i].y*RT_Y*Blur_factor));
}
return Col;
}
#blur2 x方向
sampler Texture0;
float RT_X;
float RT_Y;
float Blur_factor;
const float4 Gauss_Blur[7] =
{
0.0, 3.0, 0, 1.0/64.0,
0.0, 2.0, 0, 6.0/64.0,
0.0, 1.0, 0, 15.0/64.0,
0.0, 0.0, 0, 20.0/64.0,
0.0, -1.0, 0, 15.0/64.0,
0.0, -2.0, 0, 6.0/64.0,
0.0, -3.0, 0, 1.0/64.0
};
float4 ps_main(float2 Texc:TEXCOORD0) : COLOR0
{
float4 Col = float4(0.0,0.0,0.0,0.0);
for (int i = 0;i < 7;i++)
{
Col += Gauss_Blur[i].w * tex2D(Texture0,Texc + float2(Gauss_Blur[i].x*RT_X*Blur_factor,Gauss_Blur[i].y*RT_Y*Blur_factor));
}
return Col;
}
最后的效果为
这里我添加了更多的通道多次采样以便达到更好的效果。