原文:【HLSL学习笔记】WPF Shader Effect Library算法解读之[BandedSwirl]
因工作原因,需要在Silverlight中使用Pixel Shader技术,这对于我来说可算是相当有难度了,首先我是个Java Web开发程序员,从来没正经地学过微软的开发语言和工具;其次,对于算法这种东西,向来有种天生的排斥,一看便头疼。不过项目逼到份上了,只能硬着头皮上,真不知道领导们是怎么想的。还是言归正传吧,记录记录自己的学习心得。
熟悉Silverlight的人都知道,SL中有一物名为Pixel Shader,江湖人称像素着色器。是Sl中可以单独操控像素的一门技术。SL3中集成了两个Shader,一为DropShadowEffect,二谓BlurEffect,可为用户产生投影与模糊的效果。此外,MS还为程序员开辟了自定义通道,程序员可以自行编写Shader文件,并通过C#封装,最终应用在SL中。而编写Shader文件则需要采用HLSL(高级着色器语言High-Level-Shader-Language),正如当年新手上路用文本文件编写Java的HelloWorld程序一样,Shader同样可以用文本文件编写,同样可以用命令行编译。本文是我工作中的一些琐碎的片段,主要针对的是开源的WPF Pixel Shader Effect Library项目中的Shader源代码进行研究和学习,从而不断的提高自己。
项目地址为:http://wpffx.codeplex.com/
其中提供了18个Shader:
图1 Shader列表
初学Shader,不知从何做起,前人有高质量的代码,何不直接取来,参详研习。这两天刚开始研究BandedSwirl.fx,做点记录:
有必要说的是逆向的解读别人的算法是一个非常痛苦的过程,只有通过特效展示结合算法文件自己去构建算法模型方可,至少这是我笨人所采用的笨办法:)
BandedSwirl直译的话应该叫做【带状螺旋】效果,初见时觉得挺震撼,觉得这种算法程序应该很长才是,没想到不过区区30行左右,可见Shader编程之精炼。先看看效果截图:
再来瞅瞅源代码:
图2 带状螺旋效果图
//++++++++++++++++++++++++++++++SRC+++++++++++++++++++++++++++++
//--------------------------------------------------------------------------------------
// 全局变量,供应用程序设置,在Silverlight中动态改变这些参数便成了动画
//--------------------------------------------------------------------------------------
//定义螺旋效果的中心点,默认是(0.5,0.5),存放在常量寄存器C0里
float2 center : register(C0);
//定义螺旋效果的强度
float spiralStrength : register(C1);
//距离的阈值
float distanceThreshold : register(C2);
//定义了2D采样器,如果应用在MediaElement上的话,MediaElement的实时画面便是采样器的内容,存放在临时寄存器S0中。
sampler2D implicitInputSampler : register(S0);
//--------------------------------------------------------------------------------------
// 像素着色器
//--------------------------------------------------------------------------------------
float4 main(float2 uv : TEXCOORD) : COLOR
{
//定义2d向由中心指向某一纹理坐标的向量
float2 dir = uv - center;
//求取向量的长度,l的值域为[0,√2/2]
float l = length(dir);
//向量除以自己的长度就是单位向量,只用于表示方向
dir = dir/l;
//求取向量和水平线的夹角,这里值域为[-PI/2,PI/2]
float angle = atan2(dir.y, dir.x);
//为了进行交替的螺旋对流,避免采用类似for循环的流程控制语句,导致额外的性能耗费,需要用一个值来控制螺旋对流的循环周期,这里采用了一个名为距离阈值的参数,remainder和l的关系图为一组周期为distanceThreshold的三角波,见图3
//值域为[0,1]
float remainder = frac(l / distanceThreshold);
//该参数是为了让螺旋周期内两路相反带装束过渡的更为平滑,在波形图上看就是为了让波形连续。
float preTransitionWidth = 0.25;
//定义一个参数,控制每个螺旋周期内的对流,每个漩涡周期内共有两路正向带状束和一路反向带装束,分别交替呈现。Fac和remainder的关系见图4
float fac;
//控制对流方向的交替以及波形的连续
if (remainder < .25)
{
fac = 1.0;
}
else if (remainder < 0.5)
{
fac = 1 - 8 * (remainder - preTransitionWidth);
}
else if (remainder < 0.75)
{
fac = -1.0;
}
else
{
fac = -(1 - 8 * (remainder - 0.75));
}
//计算基于参数【螺旋强度】的变换角度,意思就是在原来向量方向的基础上正向或者反向增大扭曲角度
float newAng = angle + fac * spiralStrength * l;
//按照螺旋强度计算出来的变换角度重新定义纹理坐标
float xAmt = cos(newAng) * l;
float yAmt = sin(newAng) * l;
float2 newCoord = center + float2(xAmt, yAmt);
//按照新的纹理坐标对原来的采样器进行纹理的渲染,得出最终的结果
return tex2D( implicitInputSampler, newCoord );
}
//++++++++++++++++++++++++++++++SRC+++++++++++++++++++++++++++++
图3 remainder和L的关系图
图4 fac和remainder的关系图
到这里也算大致搞明白这个算法的原理了,对于我来说,算是费了九牛二虎。从中也学到不少,以前自己写HLSL的时候,总喜欢用for循环来控制流程,比如要写一个百叶窗,首先想到的就是设定百叶窗的间隔,然后用1/间隔当做循环次数,每次用HLSLTester都勉强能编译过去,但是一旦到了要编译成ps_2_0的ps文件时总会报错,意思是每个Shader只允许64条汇编指令,超出这个数量就编译不过去。想想若是把for循环转变成汇编语言,会产生多少指令啊,效率能不低吗!!!
好像ps_3_0没有这个限制,但是SL貌似只支持ps_2_0,所以还是精炼下自己的代码吧。
总结一下心得:
1、 写Shader最好用向量,少用标量
2、 诸如for、do-while这一类的循环尽量少用