Relief是一种欺骗眼睛的技术,在fx composer里呆呆地看例程看了两天,总算想通了它的原理,这里写下我个人对RELIEF原理的理解,至于详细步骤,全在附件里。
 
  下面的图片分别是 1普通无反射光高洛得着色,2NormalBump效果 3Parralax效果 4 Rlief效果 5 ReliefSHADOW效果模型是一个简单8顶点的正方体
 





 
Relief 事实上是一种视差效果,只是一种基于经验的光照模型。
 
一、首先,必须理解TangentSpace切空间的概念
 
  由NormalMap引申出来的技术基本上就三种 NormalBump,parralax 和relief,这三种Shader技术全都需要用到Tangent Space的概念,Tangent Space是一种特殊的空间,ViewSpace视空间大家都知道,是以摄像机为原点,摄像机的xyz三方向构成的空间,WorldSpace是以xyz(0,0,0)为原点的空间,而TangentSpace(切空间)是个什么?我去年在做parralax的例程的时候在网上找过 ,有人解释说是 以顶点到视点的射线为Binormal,再以垂直于normal与binormal的射线为tangent构造出来的空间。我一直深信不移,可是在最近学Relief效果的时候发现,这绝对是错的。
 
对TangentSpace的理解,正确的是:
 
  -TangentSpace是以当前顶点为原点(所以每个顶点各有一个切空间)
  -以顶点的normal为z轴
  -以顶点所在纹理的U轴为Tangent方向(x方向)
  -以顶点所在纹理V轴的负方向 为Binormal方向(y方向)

  构造出来的空间(可能比较难理解,但TangentSpace是视差技术的基础)
 
二、顶点着色器
 
  在fxComposer的例程里,NormalBump parralax relief及扩展的reliefShadow技术中,所使用的VertexShader都是一样的:
 
V_OUT NormalBump_VS(V_IN IN)
{
V_OUT OUT=(V_OUT)0;
//output the vertexs position in mxWVP and mxWorldView
OUT.v4Pos = mul(IN.v4Pos,g_mxWVP);//use OUT.v4Pos=mul(float4(IN.v4Pos.xyz,1.0f),g_mxWVP);//????
OUT.v3PosVS = mul(IN.v4Pos,g_mxWorldView).xyz;
//制作一个3X3的世界-视转换矩阵
float3x3 mx3WorldView;
mx3WorldView[0]=g_mxWorldView[0].xyz;
mx3WorldView[1]=g_mxWorldView[1].xyz;
mx3WorldView[2]=g_mxWorldView[2].xyz;
//输出视空间下的光线方向
OUT.v3LightDirVS=mul(-g_v3LightDir,mx3WorldView);
//下面 输出视空间下的TANGENT BINORMAL NORMAL三个分量,
/*由于这三个分量正好是组建一个切空间的xyz ,也就是说我们可以在pixel shader里 使用这里传出的TBN三个向量,组建一个转换矩阵,将视空间里的向量转到切空间的对应值(或者将切空间的向量转至视空间)。这是很重要的。
*/
OUT.v3TangentVS = mul(IN.v3Tangent,mx3WorldView);
OUT.v3BinormalVS= mul(IN.v3Binormal,mx3WorldView);
OUT.v3NormalVS= mul(IN.v3Normal,mx3WorldView);

//输出纹理座标
OUT.v2Tex=IN.v2Tex;
return OUT;//完成
}
 
  以上就是normalBump parralax relief三种技术通用的Vertex Shader段,应该是比较容易理解的,要注意的是,进入着色器的顶点属性并不是基础的 possition texcood 和normal ,而是增加了tangent 与 binormal两个元素。顶点的TBN是在directX工作区段使用D3DXComputeTangentFrameEx()函数取得,具体可以参考direct3d9 SDK里parralax例程。
 
三、NormalBump的片段着色器
 
  NormalBump是一种很实用的技术,很多D3D9的游戏,比如我最忠爱的大航海OL的城内场景就用了NormalBump技术。其原理简单地来说,就是按当前顶点所在的纹理座标,从NormalMap(法线纹理)上取得rgb值,对应转换为当前顶点法线向量的xyz分量(即:用法线纹理里的法向量取代顶点真正的法向量进行光照计算)。
 
  原理很简单,怎么实现它?难点在于,NormalMap里取出的法向量怎么样变换到ViewSpace中去。这里就用到了从顶点着色器出来的三个值:v3TangentVS(在视空间中的tangent向量) v3BinomalVS(视空间中的binormal向量) v3NormalVS (视空间中的normal向量)
 
  用这三个向量组成的3X3矩阵可以将TangentSpace里的Normal向量转至视空间。有了这个从法线纹理中取出的并且转入视空间的Normal向量,再结合LightDirectionVS 足以做出效果不错的凹凸效果。
 
/////////////////////////////////////////////////////////////////////////// NormalBump的片段着色器
float4 NormalBump_PS(V_OUT IN) :COLOR0
{
//从法线纹理中取出当前顶点的纹理法向量(MapNormal)
//注意这个 v3MapNormal 所在的空间是TangentSpace
float3 v3MapNormal = tex2D(spNormalMap,IN.v2Tex).xyz-float3(0.5f,0.5f,0.5f);//in tangent space
//从切空间TangentSpace 转到视空间ViewSpace
//这里直接使用3分量直接组成向量,你也可以使用TBN做一个3x3矩阵来转换
v3MapNormal = normalize(v3MapNormal.x*IN.v3TangentVS
-v3MapNormal.y*IN.v3BinormalVS
+v3MapNormal.z*IN.v3NormalVS);
//取得当前点的纹理色彩 c3Tex
float3 c3Tex = tex2D(spBase,IN.v2Tex).xyz;//RGB
//下面是光照模型,不多解释了,
//无非以前用到法向量的地方,改成从法线纹理取出的法向量
//View and Lights directions in VS(the vector is form vertex!)
float3 v3ViewDirVS=normalize(IN.v3PosVS);
float3 v3LightDirVS=normalize(IN.v3LightDirVS);
//compute diffuse and specular terms
float att = saturate(dot(v3LightDirVS,IN.v3NormalVS));
float diff = saturate(dot(v3LightDirVS,v3MapNormal));
float spec = saturate(dot(normalize(v3LightDirVS-v3ViewDirVS),v3MapNormal));//dot (H,N)
spec = pow(spec,g_fSpecularExponent);
//compute the final color
float3 FinalColor = g_c4Ambient.xyz*c3Tex+
att*(c3Tex*diff+g_c4Specular.xyz*spec);
return float4(FinalColor.rgb,1.0f);
}//结束
 
四、Parralax的片段着色器解释
 
  Parralax技术是在NormalBump的基础上形成的一种简单的视差效果,NormalBump只是取法线纹理中的法线来替换原始法线,进入光照模型后实现凹凸效果。至于所在顶点的高度差,则完全不过问,而事实上,你可以使你的纹理有不同的高度,纹理的高度信息又在哪里?
 
  NormalBump技术用到了NormalMap的rgb分量,但如果是一张32位的纹理,它除了rgb之外还有一个alpha通道,alpha通道本意是用来作透明处理的,但并不是只能用来作透明的,在NormalMap中 alpha通道就是纹理的高度值
 



 
上面两张图分别是 石头法线纹理的 rgb分量(左图)及alpha分量(右图)
 
Alpha值越低(黑),所代表的高度也越低。
 
  所以,进入parralax 及relief的法线纹理必须是32位(or 64bit)argb的图,其中alpha通道指示了当前纹理所指向顶点的高度。
 
Parrlax相对于NormalBump而言,只多了一个步骤:按高度进行纹理座标的偏移
 
float2 v2Tex = -fHeight*mul(mx3TStoVS,v3ViewDirVS).xy+IN.v2Tex;
 
其中fHeight是从法线纹理alpha位取出并格式化的相对高度值,
 
mul(mx3TStoVS,v3ViewDirVS).xy 是视向量转至纹理空间UVSpace后的方向
 
也就是说,在纹理上,按高度及视线的方向,进行纹理座标的偏移 ,进一步体现凹凸的效果.
 
////////////////parrlax片段着色器代码
float4 Parallax_PS(V_OUT IN) : COLOR0
{
float4 v4MN = tex2D(spNormalMap,IN.v2Tex);
float3 v3MapNormal = v4MN.xyz-float3(0.5,0.5,0.5);
floatfHeight = v4MN.w*0.06-0.03;
float3x3 mx3TStoVS = float3x3(IN.v3TangentVS,IN.v3BinormalVS,IN.v3NormalVS);
float3 v3ViewDirVS=normalize(IN.v3PosVS);
float3 v3LightDirVS=normalize(IN.v3LightDirVS);
float2 v2Tex = -fHeight*mul(mx3TStoVS,v3ViewDirVS).xy+IN.v2Tex;//texcood offset

v3MapNormal = normalize(-v3MapNormal.x*IN.v3TangentVS
-v3MapNormal.y*IN.v3BinormalVS
+v3MapNormal.z*IN.v3NormalVS);
//load the Textures color (using float3 )
float3 c3Tex = tex2D(spBase,v2Tex).xyz;//RGB
float att = saturate(dot(v3LightDirVS,IN.v3NormalVS));
float diff = saturate(dot(v3LightDirVS,v3MapNormal));
float spec = saturate(dot(normalize(v3LightDirVS-v3ViewDirVS),v3MapNormal));//dot (H,N)
spec = pow(spec,g_fSpecularExponent);
//compute the final color
float3 FinalColor = g_c4Ambient.xyz*c3Tex+
att*(c3Tex*diff+g_c4Specular.xyz*spec);
return float4(FinalColor.rgb,1.0f);
}//parrlax PS 结束
 
四、Relief效果解释:
 
  Parrlax相对于NormalBump的效果有了很大的提高,但是缺点是很明显的,首先, texcood在边界上的情况下,容易出界 比如 tex = float2(0,0)的情况,如果向纹理的左上方向偏移-0.01,-0.02,实际取得的纹理会变成 texParrlaxed = float2(0.99,0.98);这当然是我们不愿意的。
 
  另一个缺点是纹理的偏移不能表现遮盖效果,在局部还会出现挤压或拉伸纹理造成失真的情况:如下图:
 

 
  Parrlax在实际使用时会出现纹理失真,所以某些时候的效果甚至还不如NormalBump,所以又出现了Relief的技术,大部分看过direct3d SDK 里parralaxSample(其实是Relief)效果的都会觉得很惊奇,能用法线纹理在一个平面上表现出如此生动的凹凸效果。但那个Sample里的内容又实在是让人困惑。我第一次看那个例程时用了一个星期,结果还是放弃了,后来接触到了FxComposer,才在里面看懂了Relief的含意,而这次只用了一天。还是那句话,d3d SDK真让人怀疑是不是有意误导别人的。Relief的意义在于纹理偏移的方式与parrlax不同 。
 
  首先取得 视方向在纹理空间的转换,这里 v3DirTS是视向量转换到TangentSpace的产物
 
float2 v2UVDir = -v3DirTS.xy;
float fOffset = GetParralaxOffset(spNormalMap,IN.v2Tex,v2UVDir);
 
  v2UVDir是纹理空间(2维空间)的一个向量,表现的是视点到顶点的方向,也叫视差向量
GetParrlaxOffset()是一个局部函数。作用是:按当前视差方向,在法线纹理上找到一个坡度的范围内,视差方向上纹理高度最高的一个点,如下图:
 

 
定义为
 
//////////////////////////////////// GetParralaxOffset()函数代码
float GetParralaxOffset(
in sampler2D spHeight,//the normalMaps sampler
in float2 v2UVOrg,//the original Texcood
in float2 v2UVDir)//the parralax direction in UVspace)
{

const int iStep1 = 15;//第一段搜索的步数
const int iStep2 = 5;//第二段搜索的步数
float fSize = 1.0/iStep1;//第一段搜索每步的步长
float fDepth = 0.0;//这里fDepth即是用来对比的高度值,也是一个迭代器
//按视差方向搜索当前坡最高点
for (int i =0; ilt; iStep1; i++)
{
// 取出这一步所在纹理上的高度值
float4 c4Temp = tex2D(spHeight,v2UVOrg+v2UVDir*fDepth);
// 如果这步取出的高度大于上一步的,迭代器累加
if(fDepth lt; c4Temp.w)
fDepth += fSize;
}// 第一段搜索完成
// 二段搜索,注意如果第一段已经搜索到坡顶,二段搜索要往回偏移
//而如果第一段搜索时尚未搜索到坡顶,则二段继续按视差方向修正
//这是一种经验的做法,比如你45度向下看一个坡,你不可能只看到坡顶而遮盖坡顶后面所有的东西,接近坡顶,而又低于坡顶的这一部分,你也是看的到的。找个实物来试试- -!。
for(int j=0; jlt;iStep2; j++)
{
fSize /=2;
float4 c4Temp = tex2D(spHeight,v2UVOrg+v2UVDir*fDepth);
if(fDepth lt; c4Temp.w)
fDepth += (2*fSize);
////向回偏移,每次偏移前一次的二分之一
fDepth -= fSize;
}
return fDepth;
}//结束 输出为在UV空间,视差方向上的偏移量
 
  好,到这里,偏移量(float型)有了,UV上的视差向量有了 ,就可以计算出当前点视差偏移后的纹理座标了。
 
float2 v2UVNew = IN.v2Tex + v2UVDir*fOffset;
 
  然后,按这个新的纹理座标取色彩纹理及法线纹理进光照模型计算。就出现你想要的RLIEF效果了,这里不再把relief的片段着色器代码帖出了,可以在我的例程CODE里找,或者你也可以用FxComposer看原版的relief效果。(绝对别去看d3dSDK里的parrlax例程)
 
再讲一下这三个技术的优缺点,仅表个人观点:
 
  relief的缺点,虽然没有计算过FPS,不过相信RELIEF应该比较吃显卡的性能。主要是在计算offset这段。最少的情况下两段搜索加起来也有20步,每个step取一次tex2D做高度比较,如果加上ShadowMap或ShadowValue 和SKinMesh这些个技术,我不知道那些个老显卡跑不跑的起来。但是RELIEF的效果 喔买高,这个效果实在是让人喜欢。
 
  PARRALAX,我已经表示无语了,我之前做的水果贪吃蛇就是用PARRALAX的,结果效果没表现出来,倒是经常出现纹理偏移出界。但一个技术关键还是在于用在什么地方和怎么用。只能说至少我是没用对地方。如果不算上纹理出界的毛病,45度视角以内的效果跟RELIEF差不了多少,但效率!效率!
 
  至于NormalBump 这是很基础的技术,但也是最实用的。大部分的时候,你做RELIEF的消耗掉的性能,还不如NormalBump加上复杂些的Mesh来的省心省力。
 
  至此,我的RELIEF旅行结束,不算上在D3DX SDK里浪费的时间,看例程花了2天,写自己的程序花了3天,写这个东东 花了1天,虽然花的时间长了点儿,但如果对你有帮助,我还是很有成就感的。
 
徐 潇
Dana9919@163.com
QQ:61092517
 
本文源码:
http://upload.gameres.com/20122/sf_912953_2046.7z (GameRes下载,1.7MB)
 
http://115.com/file/dp23f2f0#Relief效果.zip
http://115.com/file/be3yxl8n#Relief效果CODE.zip
 
  公开的RELIEF CODE 与Relief效果查看器有一定的区别,效果查看器使用C#做外框架(只因为懒的在D3D里做控件)。

  在程序中使用 A W S D R F 及 方向键可以调整视角,由于做了一些小小的修改,在视角lt;30左右的情况下,Relief的效果将不会有更大的变化(不是故意的,但是懒的去修改回来)
 锐亚教育

锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛