XNA Shaders 编程系列4-法线映射
法线映射可以让由少量多边形构成的模型看起来像是由大量多边形构成的一样,无需添加更多的多边形。使用法线映射可以使表面(如墙壁)看起来更加富有细节和真实。展示法线映射的一个简单方法是模拟几何形状。要计算法线映射我们需要两个纹理:一个用于颜色贴图,如一张石头的纹理,另一个用于法线贴图,描述了法线的方向。我们通过储存在法线贴图中的法线信息计算光线,代替了前面使用顶点法线计算光线。
听起来挺容易?但是,在大多数法线映射技术(如我今天介绍的这个)中,法线信息是储存在一个称之为“纹理空间”坐标系统中,或“切线空间”坐标系统中。由于光线向量是在模型空间或世界空间中处理的,所以我们需要将光线矢量转换到法线贴图中的法线相同的空间(即切线空间)中去。
切线空间
看一下下面的图片展示了切线空间:
我们的shader将通过使用法线为纹理空间坐标系统创建一个W向量。然后,我们会在DirectX Util中一个叫做D3DXComputeTangent()的函数的帮助下计算U向量,接着通过叉乘W和U计算V向量(译者:?D3DXComputeTangent()在XNA中不支持,怀疑作者在粘帖别人的C++代码,因为这来自《Direct3D游戏编程入门教程》,原文地址http://www.gamasutra.com/features/20030418/engel_03.shtml)。
V = W×U
后面我们会深入讨论如何实现,但现在,让我们先讨论下一件事:纹理!您可能已经注意到,我们需要纹理去实现法线映射。而且需要指定两个纹理。那么如何加载纹理?在XNA中这是非常简单的,以后我会谈到这一点。这和在shader中实现纹理一样简单。
要处理纹理,我们需要建立一些被称为纹理采样器(texture sampler)的东西。纹理采样器,顾名思义,是用来设置纹理采样器状态的。这可以是纹理过滤(在我的例子中是Linear)的信息,以及纹理寻址方式,这可以是clamp(夹持),mirror(镜像),Wrap(包装)等。
要创建一个采样器,我们首先需要定义一个纹理采样器将使用的变量:
texture ColorMap;
现在,我们可以使用ColorMap创建一个纹理采样器:
sampler ColorMapSampler = sampler_state { Texture = <ColorMap>; // sets our sampler to use ColorMap MinFilter = Linear; // enabled trilinear filtering for this texture MagFilter = Linear; MipFilter = Linear; };
这样,我们得到了一个纹理和对应这个纹理的采样器。在我们可以开始使用纹理前,我们需要在technique设置采样器stage:
technique NormalMapping { pass P0 { VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_2_0 PS(); } }
Ok,现在我们已经做好使用纹理的准备了!
由于我们使用的是pixels shader将纹理映射到物体上,所以可以简单地创建一个叫做color的向量:
float4 Color;
并将这个值等于纹理坐标UV中的颜色值。这在HLSL中可以通过使用函数tex2D(s,t)很容易地做到,其中s是采样器,t是像素的纹理坐标。
Color = tex2D( ColorMapSampler, Tex ); // Tex 是pixel shader的输入,它来自于vertex shader的输出,就是纹理坐标。
纹理坐标?让我解释一下。纹理坐标存储在3维物体或模型中的二维坐标(U,V),用来将纹理映射到物体上,范围从0到1。 有了纹理坐标,模型就可以将纹理分配到不同的位置,比如说将一个虹膜纹理放到一个人的模型的眼球部位,或一张嘴的纹理放在人脸上。
至于照明算法,将使用镜面高光。希望现在你对法线贴图所需的东西有了一个全面的了解。
实现Shader
这个shader与镜面反射光照最大的不同是我们使用切线空间替代了模型空间,并使用一个法线贴图获得法线方向计算光线。
在shader文件中声明一些全局变量:
float4x4 matWorldViewProj ; float4x4 matWorld ; float3 vecLightDir ; float3 vecEye ;
这里没有新的东西,接着创建颜色贴图和法线贴图的实例和采样器。
texture ColorMap; sampler ColorMapSampler = sampler_state { Texture = <ColorMap>; MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = Clamp; AddressV = Clamp; }; texture NormalMap; sampler NormalMapSampler = sampler_state { Texture = <NormalMap>; MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = Clamp; AddressV = Clamp; };
我们创建了颜色贴图的纹理实例和采样器。这些纹理在程序中通过参数设置,两个纹理都使用了三线过滤。 Vertex Shader返回的output结构和镜面反射shader一样:
struct OUT { float4 Pos : POSITION; float2 Tex : TEXCOORD0; float3 Light :TEXCOORD1; float3 View : TEXCOORD2; };
让我们继续处理Vertex Shader,这里有很多新东西,主要是因为我们要计算切线空间。看一下代码:
OUT VS(float4 Pos : POSITION, float2 Tex : TEXCOORD, float3 N : NORMAL, float3 T : TANGENT ) { OUT Out = (OUT)0; Out.Pos = mul(Pos, matWorldViewProj); float3x3 worldToTangentSpace; worldToTangentSpace[0] = mul(T, matWorld); worldToTangentSpace[1] = mul(cross(T, N), matWorld); worldToTangentSpace[2] = mul(N, matWorld); Out.Tex = Tex; float4 PosWorld = mul(Pos, matWorld); Out.Light = mul(worldToTangentSpace, vecLightDir); // L Out.View = mul(worldToTangentSpace, vecEye - PosWorld.xyz); // V return Out; }
我们还是首先转换位置。然后,创建一个3x3矩阵worldToTangentSpace用来将世界空间转换到切线空间。从vertex shader中我们获得了基于切线控件矩阵转换过的位置、光线和观察向量。如前所述,这是因为法线贴图是储存在切线空间中的,因此,要基于法线贴图计算正确的光线方向,应该在同一空间中计算所有的向量。
现在我们已经使向量在正确的空间中,可以准备实现Pixel Shader了。
pixelshader需要从颜色贴图获得像素的颜色,从法线贴图获得法线。做完这一切,我们就可以基于法线计算环境,漫反射和镜面反射的光照了。看一下pixel shader的代码:
float4 PS(float2 Tex: TEXCOORD0, float3 L : TEXCOORD1, float3 V :TEXCOORD2) : COLOR { float4 Color = tex2D(ColorMapSampler, Tex); float3 N =(2 * (tex2D(NormalMapSampler, Tex)))- 1.0; float3 LightDir = normalize(L); // L float3 ViewDir = normalize(V); // V float D = saturate(dot(N, LightDir)); float3 R = normalize(2 * D * N - LightDir); // R float S = min(pow(saturate(dot(R, ViewDir)), 3), Color.w); return 0.2 * Color + Color * D + S; }
除了N变量和镜面反光的计算,没什么新东西。法线贴图使用与颜色贴图同样的函数:tex2D(s,t);我们必须确保法线范围可以从-1到1,所以我们将法线乘2减1(译者注:从法线贴图中的颜色数据获得法线向量要用到公式(each r, g and b value-0.5)/0.5,施以这样的偏移是必须的,因为法线贴图是以无符号纹理格式存储的,其中每个值都是在[0,1]区间内,而存在这样的限制主要是为了兼容老式硬件。因此,必须将这些法线还原到有符号区间中去。可参见我翻译的这篇文章http://shiba.hpe.cn/jiaoyanzu/WULI/Article208)。
float3 N =(2 * (tex2D(NormalMapSampler, Tex)))- 1.0;
同时,我们可以使用颜色贴图的alpha通道指定纹理不同部位的反光程度。最后,我们创建technique并初始化采样器。
technique NormalMapping { pass P0 { VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_2_0 PS(); } }
使用shader
处理纹理没什么新的东西。要在XNA中初始化并使用纹理需要使用Texture2D类。
Texture2D normalMap ;
现在,使用Content.Load函数载入texutre,假定您已经创建了一个法线贴图和颜色贴图:
object.colorMap = Content.Load<Texture2D>("stone"); normalMap = Content.Load<Texture2D>("normal");
注意:当添加sphere.x文件后,别忘了在内容处理器属性中选择"Generate Tangent Frames"。
将它们传递到shader,和传递其他参数的做法是一样的。
effect.Parameters["ColorMap"].SetValue(colorMap); effect.Parameters["NormalMap"].SetValue(normalMap);
练习
1.改变不同的colormaps看看结果。
2.尝试不同的模型,比如一个立方体用于创建一块砖墙或石墙。
3.实现自由控制所有光线的值(环境,漫反射,镜面高光),并能启用或禁用不同的算法(提示:使用Boolean将不用的值设置为0)。这能让shader更酷和更灵活。
文件下载(已下载 1806 次)发布时间:2009/4/3 下午12:10:49 阅读次数:16384