如何优化shader 优化随心所欲用SF的方法

您的位置: &
蒙牛随变冰淇淋:随心所欲,相变就变
优质期刊推荐您的位置: &
随心所欲用XP
优质期刊推荐Shader_伤城文章网
XNA Shader 编程教程 1-环境光照 -Shader 简史在 DirectX8 之前,GPU 使用固定方式变换像素和顶点,即所谓的“固定管道”。这使得 开发者不可能改变像素和顶点转化和处理的进程,使大多数游戏的图像表现看起来非常相 似。 DirectX8 提出了顶点和像素着色器,这让开发者可以在管道中决定如何处理顶点和像 素,使他们获得了很强的灵活性。 一开始 shader 编程使用汇编语言程序使用的着色器, 这对 shader 开发者来说相当困难, Shader Model 1.0 是唯一支持的版本。但 DirectX9 发布后这一切改变了,开发者能够使用高 级着色语言(HLSL)取代了汇编语言,HLSL 语法类似 C 语言,这使 shader 更容易编写, 阅读和学习。 DirectX 10.0 提出了一个新的 shader――Geometry Shader 作为 Shader Model 4.0 的组成 部分。但这需要一个最先进的显卡和 Windows Vista 才能支持。 XNA 支持 Shader Model 1.0 至 3.0,可以在 XP,Vista 和 XBox360!上运行。Shader? ?嗯,历史已经说得够多了……那么什么是 shader? 正如我所说的,shader 可以用来定制管道的步骤,使开发者能够决定如何处理像素/顶 点。 如下图所示,应用程序在渲染时启动并使用 shader,顶点缓冲区通过向 pixel shader 发 送所需的顶点数据与 pixel shader 协同工作,并在帧缓冲中创建了一个图像。但请注意许多 GPU 不支持所有的 shader 模式,在开发 shader 时应引起足够重视。一 个 shader 最好要有一个类似/简单的效果,使程序在较旧的计算机上也能工作正常。Vertex shaderVertex shaders 用来逐顶点地处理顶点数据。例如可以通过将模型中的每个顶点沿着法 线方向移动到一个新位置使一个模型变“胖”(这称之为 deform shaders)。 Vertex shaders 从应用程序代码中定义的一个顶点结构获取数据,并从顶点缓冲区加载 这个结构传递到 shader。这个结构描述了每个顶点的属性:位置,颜色,法线,切线等。1 接着 Vertex shader 将输出传递到 pixel shader。可以通过在 shader 中定义一个结构包含 你想要存储的数据, 并让 Vertex shader 返回这个实例来决定传递什么数据, 或通过在 shader 中定义参数,使用 out 关键字来实现。输出可以是位置,雾化,颜色,纹理坐标,切线, 光线位置等。struct VS_OUTPUT { float4 Pos: POSITION; }; VS_OUTPUT VS( float4 Pos: POSITION ) { VS_OUTPUT Out = (VS_OUTPUT) 0; ... return O } // or float3 VS(out float2 tex : TEXCOORD0) : POSITION { tex = float2(1.0, 1.0); return float3(0.0, 1.0, 0.0); }Pixel ShaderPixel Shader 对给定的模型/对象/一组顶点处理所有像素(逐像素)。这可能是一个金 属盒,我们要自定义照明的算法,色彩等等。Pixel Shader 从 vertex shaders 的输出值获取数 据,包括位置,法线和纹理坐标:float4 PS(float vPos : VPOS, float2 tex : TEXCOORD0) : COLOR { ... return float4(1.0f, 0.3f, 0.7f, 1.0f); } pixel shader 可以有两个输出值:颜色和深度。HLSLHLSL 是用来开发 shader 的。在 HLSL 中,您可以声明变量,函数,数据类型,测试 (if/else/for/do/while+) 以及更多功能以建立一个顶点和像素的处理逻辑。 下面是一些 HLSL 的关键字。这不是全部,但是最重要的。 数据类型: bool true or false int 32-bit integer2 half 16bit integer float 32bit float double 64bit double 向量: float3 vectorTest float vectorTest[3] vector vectorTest float2 vectorTest bool3 vectorTest 矩阵: float3x3: 3x3 矩阵,float 类型 float2x2: 2x2 矩阵, float 类型 还有很多辅助函数处理复杂的数学表达式: cos( x ) 返回 x 的余弦值 sin( x) 返回 x 的正弦值 cross( a, b ) 返回向量 a 和向量 b 的叉乘 dot( a,b ) 返回向量 a 和向量 b 的点乘 normalize( v ) 返回一个归一化的向量 v(v / |v|) 完整列表请看:/en-us/library/bb509611.aspx (译者:推荐看 clayman 的博客中的 The Complete Effect and HLSL Guide 连载) HSLS 提供了大量的函数让你使用!它们能帮助你解决不同的问题。 Effect 文件 Effect 文件(.fx)让开发 shader 变得更容易,你可以在.fx 文件中存储几乎所有关于着 色的东西, 包括全局变量, 函数, 结构, vertex shader, pixel shader, 不同的 techniques/passes, 纹理等等。 我们前面已经讨论了在 shader 中声明变量和结构,但什么是 technique/passes?这很简 单。一个 Shader 可以有一个或一个以上的 technique。每个 technique 都有一个唯一的名称, 我们可以通过设置 Effect 类中的 CurrentTechnique 属性选择使用哪个 technique。effect.CurrentTechnique = effect.Techniques[&AmbientLight&];在这里,我们设置“effect”使用 technique“AmbientLight”。一个 technique 可以有一 个或多个 passes,但请确保处理所有 passes 以获得我们希望的结果。 这个例子包含一个 technique 和一个 pass:technique Shader {3 pass P0 { VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_1_1 PS(); } }这个例子包含一个 technique 和两个 pass:technique Shader { pass P0 { VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_1_1 PS(); } pass P1 { VertexShader = compile vs_1_1 VS_Other(); PixelShader = compile ps_1_1 PS_Other(); } }这个例子包含二个 technique 和一个 pass:technique Shader_11 { pass P0 { VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_1_1 PS(); } } technique Shader_2a { pass P0 { VertexShader = compile vs_1_1 VS2(); PixelShader = compile ps_2_a PS2(); } }我们可以看到, 一个 technique 有两个函数, 一个是 pixel shader, 另一个是 vertex shader。VertexShader = compile vs_1_1 VS2();4 PixelShader = compile ps_1_1 PS2();这告诉我们,这个 technique 将使用 VS2()作为 vertex shader,PS2()作为 pixel shader, 并且支持 Shader Model 1.1 或更高版本。这就让 GPU 支持更高版本的 shader 变得可能。在 XNA 中实现 Shader 很简单。事实上,只需几行代码就可以加载和使用 shader。下面是步 骤: 1. 编写 shader 2. 把 shader 文件(.fx)导入到“Contents” 3. 创建一个 Effect 类的实例 4. 初始化 Effect 类的实例。 5. 选择使用的 technique 6. 开始 shader 7. 传递不同的参数至 shader 8. 绘制场景 9. 结束 shader 更详细的步骤: 1.记事本和 Visual Studio 等都可以用来编写 shader。也有一些 shader 的 IDE 可用,我 个人喜欢使用 nVidias 的 FX Composer: /object/fx_composer_home.html 。(译者:还推荐一个 shader 的 IDE:AMD 公司的 RenderMonkey,可在 /developer/rendermonkey/downloads.html 下载最新版本 1.81 (93.9MB, 2008 年 4 月 8 日)个人用下来的感觉好像 nvidia 实力更强一些, , 文档也很详实, RenderMonkey 而 上手更容易。) 2.当 shader 建立后,将其拖放到“Content”目录,自动生成素材名称。 3.XNA 框架有一个 Effect 类用于加载和编译 shader。要创建这个类的实例可用以下 代码:EEffext 属于 Microsoft.Xna.Framework.Graphics 类库,因此,记得添加 using 语句块:using Microsoft.Xna.Framework.Graphics4.要初始化 shader,我们可以使用 Content 从项目或文件中加载:5 effect = Content.Load(&Shader&);“Shader”是你添加到 Content 目录的 shader 名称。 5.选择使用何种 technique:effect.CurrentTechnique = effect.Techniques [&AmbientLight& ];6.要使用 Effect,请调用 Begin()函数:effect.Begin();此外,您必须启动所有 passes。foreach (EffectPass pass in effect.CurrentTechnique.Passes) { // Begin current pass pass.Begin();7.有很多方法可以设置 shader 的参数,但对这个教程来说下列方法够用了。注:这不 是最快的方法,在以后的教程中我将回到这里:effect.Parameters[&matWorldViewProj&].SetValue(worldMatrix * viewMatrix*projMatrix);其中“matWorldViewProj”是在 shader 中定义的:float4x4 matWorldViewP将 matWorldViewProj 设置为 worldMatrix * viewMatrix * projMatrix。 SetValue 设置参数并将它传递到 shader,GetValue 从 shader 获取值,Type 是获取的数 据类型。例如,GetValueInt32()得到一个整数。 8.渲染你想要这个 shader 处理/转换的场景/对象。 9.要停止 pass,调用 pass.End(),要停止 shader,调用 Effect 的 End()方法:pass.End(); effect.End();为了更好地理解步骤,可参见源代码。环境光照( 环境光照(Ambient light) )6 OK, 我们终于到了最后一步, 实现 shader! 不坏吧?首先, 什么是“Ambient light” ? 环境光是场景中的基本光源。如果你进入一个漆黑的屋子,环境光通常是零,但走到外面 时,总是有光能让你看到。环境光没有方向(译者:所以也将其称为“全局光照模型”), 在这里应确保对象不会自己发光,它有一个基本的颜色。环境光的公式是: I = Aintensity* Acolor 其中 I 是光的实际颜色, Aintensity 是光的强度(通常在 0.0 和 1.0 之间),Acolor 环 境光的颜色,这个颜色可以是固定值,参数或纹理。好吧,现在开始实现 Shader。首先, 我们需要一个矩阵表示世界矩阵:float4x4 matWorldViewP在 shader 顶端声明这个矩阵 (作为全局变量) 然后, 我们需要知道 vertex shader 向 pixel shader 传递了哪些值。这可以通过建立一个结构(可以命名为任何值)实现:struct OUT { float4 Pos: POSITION; };我们创建了一个名为 OUT 的结构, 其中包含一个 float4 类型的名叫 Pos 的变量。 ” “: 后面的 POSITION 告诉 GPU 在哪个寄存器(register)放置这个值?嗯,什么是寄存器?寄 存器是 GPU 中保存数据的一个容器。GPU 使用不同的寄存器保存位置,法线,纹理坐标 等数据,当定义一个 shader 传递到 pixel shader 的变量时,我们必须决定在 GPU 的何处保 存这个值。 看一下 vertex shader:OUT VertexShader( float4 Pos: POSITION ) { OUT Out = (OUT) 0; Out.Pos = mul(Pos, matWorldViewProj); return O }我们创建了一个 OUT 类型的函数,它的参数是 float4 类型的 Pos:POSITION。这是模 型文件/应用程序/游戏中定义的顶点位置。 然后, 我们建立一个名叫 OUT 的 OUT 结构实例。 这个结构必须被填充并从函数返回,以便后继过程处理。输入参数中的 Pos 不参与后继过 程的处理, 但需要乘以 worldviewprojection 矩阵使之以正确放置在屏幕上。 由于 Pos 是 OUT 中的唯一变量,我们已经返回它并继续前进。现在开始处理 pixel shaders,我们声明为一个 float4 类型的函数, 返回存储在 GPU 中的 COLOR 寄存器上的 float4 值。 我们在 pixel shader 中进行环境光的算法:7 float4 PixelShader() : COLOR { float Ai = 0.8f; float4 Ac = float4(0.075, 0.075, 0.2, 1.0); return Ai * Ac; }这里我们使用上面的公式计算目前像素的颜色。Ai 是环境光强度,Ac 是环境光颜色。 最后,我们必须定义 technique 并将 pixel shader 和 vertex shader 函数绑定到 technique 上:technique AmbientLight { pass P0 { VertexShader = compile vs_1_1 VertexShader(); PixelShader = compile ps_1_1 PixelShader(); } }好了, 完成了! 现在, 我建议你看看源代码, 并调整各个参数更好地理解如何使用 XNA 实现 shader。 环境光满足下列公式: I = Aintensity * Acolor 漫反射光的公式以此为基础,在方程中添加了一个定向光: I = Aintensity*Acolor + Dintensity*Dcolor *N.L 从这个公式可以看到我们仍然使用环境光,但需要额外两个变量描述漫反射的的颜色 和强度,两个向量 N 描述表面的法线,L 描述光线的方向。我们可以将漫反射光线作为表 示表面反射光线的多少。 光线反射的强度随着 N 和 L 夹角的变小而变得更强。如果 L 与 N 平行则反射最强烈, 如果 L 平行于表面,则反射最弱。8 要计算 L 和 N 的夹角,我们可以使用点乘或标量乘积。这条规则可用来计算给定两个 向量间的夹角,可以定义如下: N.L = |N| * |L| * cos(a) (译者注:这个公式实际上是 Lambert 定理的简化形式,若归一化 N 和 L,则这个公 式可简化为 N.L=cos(a)) 这里|N|是向量 N 的长度,|L|是向量 L 的长度,cos(a)是两个向量之间夹角的余弦。实现 shader我们需要三个全局变量: float4x4 matWorldViewP float4x4 matInverseW float4 vLightD 我们仍然要教程 1 中的 worldviewprojection 矩阵, 但除此之外, 我们还需要 InverseWorld 矩阵计算出与世界矩阵相关的正确法线,而 vLightDirection 表示光线的方向。我们还需要 在 vertex shader 中定义 OUT 结构,这样才能在 pixel shader 中获得正确的光线方向:struct OUT { float4 Pos: POSITION; float3 L: TEXCOORD0; float3 N: TEXCOORD1; };这里定义了位置 Pos, 光线方向 L 和法线方向 N 存储在不同的寄存器中。 TEXCOORDn 可用于任何值,现在我们还没有使用任何纹理坐标,所以我们可以用这些寄存器储存这两 个向量。9 OK,现在处理 vertex shader:OUT VertexShader( float4 Pos: POSITION, float3 N: NORMAL ) { OUT Out = (OUT) 0; Out.Pos = mul(Pos, matWorldViewProj); Out.L = normalize(vLightDirection); Out.N = normalize(mul(matInverseWorld, N)); return O }我们从模型文件获取位置和法线并通过它们传递到 shader。根据这些值和全局变量我 们可以转换位置,法线和光线方向,并转换和归一化表面的法线。 然后,在 Pixel Shader 中获取 TEXCOORD0 中的的值并把它放在 L 中,TEXCOORD1 中的值放入 N。这些寄存器的数据是由 vertex shader 添加的。然后,我们在 pixel shader 执 行上面的漫反射方程:float4 PixelShader(float3 L: TEXCOORD0, float3 N: TEXCOORD1) : COLOR { float Ai = 0.8f; float4 Ac = float4(0.075, 0.075, 0.2, 1.0); float Di = 1.0f; float4 Dc = float4(1.0, 1.0, 1.0, 1.0); return Ai * Ac + Di * Dc * saturate(dot(L, N)); }译者注:因为 dot(L, N)的范围在(-1,1)之间,所以需要 saturate 将它截取到(0,1) 之间。 使用的 technique 如下:technique DiffuseLight { pass P0 { VertexShader = compile vs_1_1 VertexShader(); PixelShader = compile ps_1_1 PixelShader(); } }好了,这就是漫反射光照!可以下载源码更好地理解原理,希望你能感受到 shader 的 威力并知道如何在程序中实现。 译者注:还看过一个例子中不使用 InverseWorld 矩阵,而是使用 Out.N = normalize(mul(N,matWorld));而本例使用的是 Out.N = normalize(mul(matInverseWorld, N));10 另外本例中的 L 向量对应的 vLightDirection 在程序中设置为: Vector4 vLightDirection = new Vector4(0.0f, 0.0f, 1.0f, 1.0f); 这表示指向 z 轴正方向,即垂直屏幕向外,这里实际是指“顶点指向光源的方向”,也 就是说光线的方向是垂直屏幕向里的,教程里使用“光线方向”容易引起误解,至少我是一 开始就搞错了。习惯上光线的方向是指“从光源指向顶点的方向”,这时应该是用 saturate( dot(-L,N));而不是本例中的 saturate(dot(L, N));因为根据 Lambert 定理,光线的方 向是指从顶点指向光源的方向,而导入的 L 是指光源指向顶点的方向,所以要-L。XNA Shader 编程教程 3-镜面反射光照 -这次我们将实现一个叫做镜面反射的光线算法。该算法是建立在前面环境光照和漫反 射光照的基础上的,所以,如果你前面没有弄懂,现在是时候了。:)在这个教程中,您需要一些 shader 编程的基本知识,矢量和矩阵的数学知识。镜面反射光照迄今为止,我们已经实现了一个很好的照明模式。但是,如果我们想绘制一个抛光或 闪耀的物体该怎么办?比如说金属表面,塑料,玻璃,瓶子等。为了模拟这种情况,我们 需要在照明算法中加入一个新的向量:eye 向量。您可能会想什么是“eye”向量?这很容易 回答:“eye”向量是从相机位置指向观察目标的向量。在程序代码中已经有了这个向量:viewMatrix= Matrix.CreateLookAt( new Vector3(x, y, zHeight), Vector3.Zero, Vector3.Up );&The eye& 的位置在这:Vector3(x, y, zHeight)让我们把这个向量存储在一个变量中: Vector4 vecEye = new Vector4(x, y, zHeight,0); 让我们深入讨论如何在创建这个向量后使用 shader。镜面高光的公式是 I=AiAc+Di*Dc*N.L+Si*Sc*(R.V)^n11 其中 R=2*(N.L)*N-L -如我们所见,我们需要新的 Eye 向量和一个反射向量 R。为了计算镜面光,我们需要 将 R 点乘 V 并进行 n 次幂的运算,而指数 n 表示光泽属性,n 越大说明物体表面越光滑, 反光越强。实现 Shader如前面的截图可见,现在这个对象看起来很有光泽,只需通过 shader 就能实现!很酷 吧! 首先声明一些变量:float4x4 matWorldViewP float4x4 matW float4 vecLightD float4 vecE12 float4 vDiffuseC float4 vSpecularC float4 vA然后是 OUT 结构。Shader 将返回经过变换的位置 Pos,光线向量 L,法线向量 N 和观 察向量 V(Eye 向量)。struct OUT { float4 Pos : POSITION; float3 L : TEXCOORD0; float3 N : TEXCOORD1; float3 V : TEXCOORD2; };除了 V 向量,在 vertex shader 没有新的东西。V 可以通过将 Eye 向量减去经变换后的 位置向量得到。由于 V 是 OUT 结构的一部分,而且我们已经定义了 OUT out,所以可以通 过下列代码计算 V:float4 PosWorld = mul(Pos,matWorld); Out.V= vecEye - PosWorld这里的 vecEye 向量是通过参数(相机位置)传递到 shader 中的。OUT VS(float4 Pos : POSITION, float3 N :NORMAL) { OUT Out = (OUT)0; Out.Pos = mul(Pos, matWorldViewProj); Out.N = mul(N, matWorld); float4 PosWorld = mul(Pos, matWorld); Out.L = vecLightD Out.V = vecEye - PosW return O }然后处理 pixelshader。首先归一化 Normal,LightDir 和 ViewDir 简化计算。 Pixelshader 会根据前面镜面反射公式返回 float4 代表当前像素的最终颜色和光强。然 后使用教程 2 同样的方法计算漫反射光线的方向。 Pixel Shader 中新的东西是通过 L 和 N 计算反射向量 R, 并使用这个向量计算镜面反射 光。因此,首先计算反射向量 R: R = 2 * (N.L) * N C L13 在前面我们已经在计算漫反射光时计算了 N 和 L 的点乘。所以可以使用如下代码:float3 Reflect = normalize(2 * Diff * Normal - LightDir);译者注:也可以使用 HLSL 内置的函数 reflect 计算 Reflect 向量,注意在 LightDir 前有 个负号,想想为什么:float3 Reflect = normalize(reflect(-LightDir, Normal));现在只剩下计算镜面反射光了。我们知道,这需要计算反射向量 R 和观察向量 V 的 n 次幂:(R.V)^n n 表示物体的光泽程度,n 越大,反光区域越小。您可能注意到,我们使用了一个新的 HLSL 函数 pow(a,b),它返回 a 的 b 次方。float Specular = pow(saturate(dot(Reflect, ViewDir)), 15);我们终于准备将一切整合在一起并计算出最终的像素颜色:return vAmbient + vDiffuseColor * Diff + vSpecularColor * S这个公式你应该很熟悉了,对不对? 我们首先计算环境光和漫反射光并把它们相加。 然后将镜面反光的颜色乘以刚才算出的反光强度 Specular,并和环境光颜色和漫反射颜色 相加。本教程的 pixel shader 如下所示:float4 PS(float3 L: TEXCOORD0, float3 N : TEXCOORD1, float3 V : TEXCOORD2) : COLOR { float3 Normal = normalize(N); float3 LightDir = normalize(L); float3 ViewDir = normalize(V); float Diff = saturate(dot(Normal, LightDir)); // R = 2 * (N.L) * N C L float3 Reflect = normalize(2 * Diff * Normal - LightDir); float Specular = pow(saturate(dot(Reflect, ViewDir)), 15); // R.V^n // I = A + Dcolor * Dintensity * N.L + Scolor * Sintensity * (R.V)n return vAmbient + vDiffuseColor * Diff + vSpecularColor * S }当然,我们还要指定一个 technique 并编译 Vertex 和 Pixel shader:technique SpecularLight { pass P0 {14 // compile shaders VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_2_0 PS(); } }shader( .fx )文件的完整代码如下:float4x4 matWorldViewP float4x4 matW float4 vecLightD float4 vecE float4 vDiffuseC float4 vSpecularC float4 vA struct OUT { float4 Pos : POSITION; float3 L : TEXCOORD0; float3 N : TEXCOORD1; float3 V : TEXCOORD2; }; OUT VS(float4 Pos : POSITION, float3 N : NORMAL) { OUT Out = (OUT)0; Out.Pos = mul(Pos, matWorldViewProj); Out.N = mul(N, matWorld); float4 PosWorld = mul(Pos, matWorld); Out.L = vecLightD Out.V = vecEye - PosW return O } float4 PS(float3 L: TEXCOORD0, float3 N : TEXCOORD1, float3 V : TEXCOORD2) : COLOR { float3 Normal = normalize(N); float3 LightDir = normalize(L); float3 ViewDir = normalize(V); float Diff = saturate(dot(Normal, LightDir)); // R = 2 * (N.L) * N - L float3 Reflect = normalize(2 * Diff * Normal - LightDir); float Specular = pow(saturate(dot(Reflect, ViewDir)), 15); // R.V^n // I = A + Dcolor * Dintensity * N.L + Scolor * Sintensity * (R.V)n return vAmbient + vDiffuseColor * Diff + vSpecularColor * S }15 technique SpecularLight { pass P0 { // compile shaders VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_2_0 PS(); } }使用 shader相对上一个教程几乎没有新的东西,除了设置 vecEye 参数。我们只是将相机的位置传 递给 shader。如果您使用的是相机类,那么在这个类中可能有一个方法可以获取相机的位 置,这取决于您如何处理。在我的例子,我使用了相同的变量设置相机的位置,并创建了 一个 vector。Vector4 vecEye = new Vector4(x, y, zHeight,0);并将它传递到 shader 中effect.Parameters[&vecEye&].SetValue(vecEye);从程序中设置参数以及如何实现 Shader 应该不是一个新话题了,所以我不打算进一步 详细说明这一点。请参阅教程 2 和教程 1。别忘了将 technique 设置为“SpecularLight”。 纹理就是 3D 模型外表面的图案,本质是把平面图形贴到 3D 物体表面上,XNA 支持 bmp、jpg、tga、png、dds 格式的图片。纹理的 x、y 坐标一般称为 Tu、Tv,坐标原点在纹 理图片的左上角,向右为 Tu 轴正方向,向下为 Tv 轴的正方向,无论纹理图片的大小,纹 理坐标范围都为 0.0 到 1.0,即所有纹理图形的右下角的 Tu、Tv 坐标为(1.0,1.0)。例如 3D 模型的一个正方形表面为一矩形,其左上角、右上角、左下角、右下角 4 个顶点的纹理 坐标分别是:(0,0)、(1,0)、(0,1)、(1,1),则表示要将图片的所有部分都 贴到这个矩形表面,如纹理坐标分别为(0,0)、(0.5,0)、(0,0.5)、(0.5,0.5), 则表示将图片的左上角四分之一部分贴到这个矩形表面。 要在 XNA 中实现纹理,可在 shader 文件中加入如下代码:texture modelT //纹理变量 sampler ModelTextureSampler = sampler_state //纹理采样器 { Texture = &modelTexture&; //纹理采样器使用的纹理对象 MinFilter = L //缩小图形使用线性滤波16 MagFilter = L //放大图形使用线性滤波 MipFilter = L //Mipmap 使用线性滤波 AddressU = W //U、V 方向上的纹理寻址模式都采用 Wrap 方式 AddressV = W };纹理过滤过滤是指通过给定的 uv 坐标从纹理贴图中获取图素的一种方法, Direct3D 提供 3 种纹 理过滤方法和所谓的“mipmap” MIP 是拉丁语“multum in λparvo”的缩写, ( 可以解释为“many things in a small place”小中见大、小型而内容丰富),3 种纹理过滤方法是: 最近点采样 线性纹理采样 各向异性(anisotropic) 纹理过滤应考虑两种不同的情况:放大(magnification)和缩小(minification)。举例 来说,将一张 64×64 的纹理映射到一个 400×400 像素的多边形时,便会发生放大,这会导 致“锯齿”。而将一张 64×64 的纹理映射到一个 10×10 像素的多变形时,缩小会导致“像素抖 动”,看上去使人晕眩。 Mipmap 由一系列纹理组成, 其中每张纹理是一张分辨率逐渐降低的图像, 每一级的高 度和宽度都是前一级的高度和宽度的一半,使用 mipmap 可以确保无论是靠近还是远离纹 理,纹理都可以保持它原有的真实性和质量,从而减少了放大和缩小带来的负面效果。 大多数硬件都支持 4 种不同的过滤:点采样、双线性过滤(Direct3D 中称之为线性过 滤)、三线性过滤(Direct3D 中称之为“线性过滤+mipmap”)、各向异性过滤。 从上面的代码可知我们使用的是三线过滤。纹理寻址模式代码中的“AddressU = W AddressV = W”称为纹理寻址模式(texture-addressing mode)。当顶点的纹理坐标设置在 0 到 1 之间,以此来选择一张纹理贴图不难理解。但如 果给出的纹理坐标超出了这个范围会如何?这取决于所选择的纹理寻址模式。有几种不同 的寻址模式(以下例子中的纹理坐标都是(0,0)、(3,0)、(0,3)、(3,3): 1.包装(wrap):在纹理坐标的每个整数相接处重复纹理,这也是默认模式,如图:17 2.镜像(mirror):在每个整数边界对纹理做一次镜像。3.夹持(clamp):只将纹理应用到多边形上一次,然后对超出部分涂上纹理边缘像素 的颜色。还有边框颜色(border color)和一次镜像(MirrorOnce)不是很常用。在纹理上应用漫反射和镜面反射然后在 pixelSahder 加入以下代码:18 float4 textureColor = tex2D(ModelTextureSampler, TexCoord); return vAmbient +textureC程序截图如下:如要在纹理上加上漫反射光照,代码应变为 :return vAmbient +textureColor*vDiffuseColor * D程序截图如下:如要再加上镜面高光,代码应变为:return vAmbient + textureColor*(vDiffuseColor * Diff + vSpecularColor * Specular);程序截图如下:所谓逐顶点光照,简单地说就是在 vetext shader 中计算光照颜色,该过程将为每个顶 点计算一次光照颜色,然后在通过顶点在多边形所覆盖的区域对像素颜色进行线形插值。19 现实中,光照值取决于光线角度,表面法线,和观察点(对于镜面高光来说)。具体实现 时的 shader 代码如下://相关全局变量 shared float4x4 matWorldViewP shared float4x4 matW shared float3 lightP shared float4 ambientLightC shared float4 diffuseLightC shared float4 specularLightC shared float3 cameraP //VertexShader 输出结构 struct VertexShaderOutput { float4 Position : POSITION; float4 Color : COLOR0; }; //PixelShader 输入结构,只接受从 VertexShader 传来的颜色 struct PixelShaderInput { float4 Color: COLOR0; }; VertexShaderOutput VertexDiffuseAndPhong(float3 position : POSITION,float3 normal : NORMAL ) { VertexShaderO //transform the input position to the output output.Position = mul(float4(position, 1.0), matWorldViewProj); float3 worldNormal = mul(normal, matWorld); float4 worldPosition = mul(float4(position, 1.0), matWorld); worldPosition = worldPosition / worldPosition.w; float3 directionToLight = normalize(lightPosition - worldPosition.xyz); float diffuseIntensity = saturate( dot(directionToLight, worldNormal)); float4 diffuse= diffuseLightColor * diffuseI float3 reflectionVector = normalize(reflect(-directionToLight, worldNormal)); float3 directionToCamera = normalize(cameraPosition - worldPosition.xyz); float4 specular = specularLightColor * pow(saturate(dot(reflectionVector, directionToCamera)), 20); output.Color = specular + diffuse + ambientLightC output.Color.a = 1.0; //return the output structure20
} float4 { return input.C } technique PerVertexDiffuseAndPhong { pass P0 { //set the VertexShader state to the vertex shader function VertexShader = compile vs_2_0 VertexDiffuseAndPhong(); //set the PixelShader state to the pixel shader function PixelShader = compile ps_2_0 SimplePixelShader(); } } SimplePixelShader(PixelShaderInput input) : COLOR由以上代码可见,各像素的颜色计算都是在 VertexShader 中实现的。程序截图如下:当考虑光照时,大部分人都认为逐顶点光照已经足够好了。对于镶嵌度较高的模型来 说是这样,但对某些多边形较少的模型来说却不一定。比如这个示例,球的多边形较少, 可以明显看出棱角分明,高光效果也不理想。直接对顶点颜色进行插值所得的结果通常不 够精确,特别是对面积较大的多边形来说。当处理高精度多边形模型时,由于每个多边形 所覆盖的区域很小,因此插值之后每个像素的误差也很小,所以逐顶点光照可以工作的很 好。而当处理低模时,这种误差就变的很大了。逐像素光照逐像素光照是对所有光照元素进行单独插值, 简单地说就是在 pixelshader 中计算颜色。 具体实现时的 shader 代码如下://全局变量 shared float4x4 matWorldViewP21 shared float4x4 matW shared float3 cameraP shared float3 lightP shared float4 ambientLightC shared float4 diffuseLightC shared float4 specularLightC struct VertexShaderOutputPerPixelDiffuse { float4 Position : POSITION; float3 WorldNormal : TEXCOORD0; float3 WorldPosition : TEXCOORD1; }; struct PixelShaderInputPerPixelDiffuse { float3 WorldNormal : TEXCOORD0; float3 WorldPosition : TEXCOORD1; }; VertexShaderOutputPerPixelDiffuse PerPixelDiffuseVS( float3 position : POSITION, float3 normal : NORMAL ) { VertexShaderOutputPerPixelD //transform the input position to the output output.Position = mul(float4(position, 1.0), matWorldViewProj); output.WorldNormal = mul(normal, matWorld); float4 worldPosition = mul(float4(position, 1.0), matWorld); output.WorldPosition = worldPosition / worldPosition.w; //return the output str } float4 DiffuseAndPhongPS(PixelShaderInputPerPixelDiffuse input) : COLOR { //calculate per-pixel diffuse float3 directionToLight = normalize(lightPosition - input.WorldPosition); float diffuseIntensity = saturate( dot(directionToLight, input.WorldNormal)); float4 diffuse = diffuseLightColor * diffuseI //calculate Phong components per-pixel float3 reflectionVector = normalize(reflect(-directionToLight, input.WorldNormal)); float3 directionToCamera = normalize(cameraPosition - input.WorldPosition); //calculate specular component float4 specular = specularLightColor *22 pow(saturate(dot(reflectionVector, directionToCamera)), 20); //all color components are summed in the pixel shader float4 color = specular + diffuse + ambientLightC color.a = 1.0; } technique PerPixelDiffuseAndPhong { pass P0 { VertexShader = compile vs_2_0 PerPixelDiffuseVS(); PixelShader = compile ps_2_0 DiffuseAndPhongPS(); } }由上面两段代码对比可知,算法实际上是一样的,只不过颜色的计算过程一个放在 VertexShader 中,而另一个放在 PixelShader 中。程序截图如下,源代码中可以通过按空格 键切换两种效果,逐像素光照效果好:使用逐像素光照的另一个好处是可以在渲染时添加并不存在的表面细节。通过 bump map 或 normal map,可以在像素级别让原本平坦的表面表现出近似的凹凸效果。 当然,由于逐像素的计算量要比逐顶点要大,所以请根据具体情况灵活选择,如果你 使用 BasicEffect,那么默认是使用逐顶点光照,你必须添加 basicEffect.PreferPerPixelLighting=true 才能开启逐像素光照。XNA Shader 编程教程 3.3-多纹理 -3D 图形的一个表面可以贴上多个图片,一般称为多层纹理或多重纹理。例如一个正方 形有两个纹理,纹理 1 是墙壁图案:23 纹理 2 是光影图案:两个纹理混合后的效果如图所示:主要的 shader 代码如下:float4x4 WorldViewProj : WORLDVIEWPROJECTION; Texture Texture1; Texture Texture2; sampler TextureSampler1 = sampler_state { texture = &Texture1&; MipFilter = LINEAR;24 MinFilter = LINEAR; MagFilter = LINEAR; }; sampler TextureSampler2 = sampler_state { texture = &Texture2&; MipFilter = LINEAR; MinFilter = LINEAR; MagFilter = LINEAR; }; void TransformV1_1(in float4 inputPosition:POSITION,in float2 inputTexCoord:TEXCOORD0, out float4 outputPosition:POSITION,out float2 outputTexCoord:TEXCOORD0, out float2 outputSecondTexCoord:TEXCOORD1) { outputPosition = mul(inputPosition,WorldViewProj); outputTexCoord = inputTexC outputSecondTexCoord = inputTexC } void TextureColorV1_1(in float4 P:POSITION,in float2 textureCoords : TEXCOORD0, in float2 textureCoords2 : TEXCOORD1,out float4 diffuseColor : COLOR0) { float4 diffuseColor1 = tex2D(TextureSampler1, textureCoords); float4 diffuseColor2 = tex2D(TextureSampler2, textureCoords2); diffuseColor=lerp(diffuseColor1,diffuseColor2,0.6f); } technique RenderScene { pass P0 { VertexShader = compile vs_1_1 TransformV1_1(); PixelShader = compile ps_1_1 TextureColorV1_1(); } }基本思路就是先用对应的采样器获取两纹理的颜色,接着使用 lerp 函数在两个颜色之 间(前两个参数)进行线性插值,而第 3 个参数决定前 2 个参数在插值时的比例,你可以 在源代码中试着调整这个参数看看效果。XNA Shaders 编程系列 4-法线映射 -25 法线映射可以让由少量多边形构成的模型看起来像是由大量多边形构成的一样,无需 添加更多的多边形。使用法线映射可以使表面(如墙壁)看起来更加富有细节和真实。展 示法线映射的一个简单方法是模拟几何形状。要计算法线映射我们需要两个纹理:一个用 于颜色贴图,如一张石头的纹理,另一个用于法线贴图,描述了法线的方向。我们通过储 存在法线贴图中的法线信息计算光线,代替了前面使用顶点法线计算光线。26 听起来挺容易?但是,在大多数法线映射技术(如我今天介绍的这个)中,法线信息 是储存在一个称之为“纹理空间”坐标系统中,或“切线空间”坐标系统中。由于光线向量是 在模型空间或世界空间中处理的,所以我们需要将光线矢量转换到法线贴图中的法线相同 的空间(即切线空间)中去。切线空间看一下下面的图片展示了切线空间:我们的 shader 将通过使用法线为纹理空间坐标系统创建一个 W 向量。然后,我们会在 DirectX Util 中一个叫做 D3DXComputeTangent()的函数的帮助下计算 U 向量,接着通过叉 乘 W 和 U 计算 V 向量(译者:?D3DXComputeTangent()在 XNA 中不支持,怀疑作者在 粘帖别人的 C++代码,因为这来自《Direct3D 游戏编程入门教程》,原文地址 /features//engel_03.shtml)。 V = W×U 后面我们会深入讨论如何实现,但现在,让我们先讨论下一件事:纹理!您可能已经 注意到,我们需要纹理去实现法线映射。而且需要指定两个纹理。那么如何加载纹理?在 XNA 中这是非常简单的,以后我会谈到这一点。这和在 shader 中实现纹理一样简单。 要处理纹理,我们需要建立一些被称为纹理采样器(texture sampler)的东西。纹理采 样器,顾名思义,是用来设置纹理采样器状态的。这可以是纹理过滤(在我的例子中是 Linear)的信息,以及纹理寻址方式,这可以是 clamp(夹持),mirror(镜像),Wrap(包 装)等。 要创建一个采样器,我们首先需要定义一个纹理采样器将使用的变量:texture ColorM现在,我们可以使用 ColorMap 创建一个纹理采样器:27 sampler ColorMapSampler = sampler_state { Texture = &ColorMap&; // sets our sampler to use ColorMap MinFilter = L // enabled trilinear filtering for this texture MagFilter = L MipFilter = L AddressU = C // sets our texture to clamp AddressV = C };这样,我们得到了一个纹理和对应这个纹理的采样器。在我们可以开始使用纹理前, 我们需要在 technique 设置采样器 stage:technique NormalMapping { pass P0 { Sampler[0] = (ColorMapSampler); VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_2_0 PS(); } }Ok,现在我们已经做好使用纹理的准备了! 由于我们使用的是 pixels shader 将纹理映射到物体上,所以可以简单地创建一个叫做 color 的向量:float4 C并将这个值等于纹理坐标 UV 中的颜色值。这在 HLSL 中可以通过使用函数 tex2D(s,t) 很容易地做到,其中 s 是采样器,t 是像素的纹理坐标。Color = tex2D( ColorMapSampler, Tex ); // Tex 是 pixel shader 的输入,它来自于 vertex shader 的输出,就是纹理坐标。纹理坐标?让我解释一下。纹理坐标存储在 3 维物体或模型中的二维坐标(U,V), 用来将纹理映射到物体上,范围从 0 到 1。 有了纹理坐标,模型就可以将纹理分配到不同 的位置,比如说将一个虹膜纹理放到一个人的模型的眼球部位,或一张嘴的纹理放在人脸 上。28 至于照明算法,将使用镜面高光。希望现在你对法线贴图所需的东西有了一个全面的 了解。实现 Shader这个 shader 与镜面反射光照最大的不同是我们使用切线空间替代了模型空间,并使用 一个法线贴图获得法线方向计算光线。首先声明一些全局变量:float4x4 matWorldViewP float4x4 matW float4 vecLightD float4 vecE这里没有新的东西,接着创建颜色贴图和法线贴图的实例和采样器。texture ColorM sampler ColorMapSampler = sampler_state { Texture = &ColorMap&; MinFilter = L MagFilter = L MipFilter = L AddressU = C AddressV = C }; texture NormalM29 sampler NormalMapSampler = sampler_state { Texture = &NormalMap&; MinFilter = L MagFilter = L MipFilter = L AddressU = C AddressV = C };我们创建了颜色贴图的纹理实例和采样器。这些纹理在程序中通过参数设置,两个纹 理都使用了三线过滤。 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 worldToTangentS worldToTangentSpace[0] = mul(T, matWorld); worldToTangentSpace[1] = mul(cross(T, N), matWorld); worldToTangentSpace[2] = mul(N, matWorld); Out.Tex = T float4 PosWorld = mul(Pos, matWorld); Out.Light = mul(worldToTangentSpace, vecLightDir); // L Out.View = mul(worldToTangentSpace, vecEye - PosWorld); // V return O }我们还是首先转换位置。然后,创建一个 3x3 矩阵 worldToTangentSpace 用来将世界空 间转换到切线空间。从 vertex shader 中我们获得了基于切线控件矩阵转换过的位置、光线 和观察向量。如前所述,这是因为法线贴图是储存在切线空间中的,因此,要基于法线贴 图计算正确的光线方向,应该在同一空间中计算所有的向量。 现在我们已经使向量在正确的空间中,可以准备实现 Pixel Shader 了。30 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]区间内, 而存在这样的限制主要是为了兼容老式硬件。因此,必须将这些法线还原到有符号区间中 去。可参见我翻译的这篇文章 /jiaoyanzu/WULI/showArticle.aspx?articleId=208&classId=4)。float3 N =(2 * (tex2D(NormalMapSampler, Tex)))- 1.0;同时,我们可以使用颜色贴图的 alpha 通道指定纹理不同部位的反光程度。最后,我们 创建 technique 并初始化采样器。technique NormalMapping { pass P0 { Sampler[0] = (ColorMapSampler); Sampler[1] = (NormalMapSampler); VertexShader = compile vs_1_1 VS(); PixelShader = compile ps_2_0 PS(); } }使用 shader处理纹理没什么新的东西。要在 XNA 中初始化并使用纹理需要使用 Texture2D 类。31 Texture2D normalM现在,使用 Content.Load 函数载入 texutre,假定您已经创建了一个法线贴图和颜色贴 图:object.colorMap = Content.Load&Texture2D&(&stone&); normalMap = Content.Load&Texture2D&(&normal&);将它们传递到 shader,和传递其他参数的做法是一样的。effect.Parameters[&ColorMap&].SetValue(colorMap); effect.Parameters[&NormalMap&].SetValue(normalMap);练习1.改变不同的 colormaps 看看结果。 2.尝试不同的模型,比如一个立方体用于创建一块砖墙或石墙。 3.实现自由控制所有光线的值(环境,漫反射,镜面高光),并能启用或禁用不同的 算法(提示:使用 Boolean 将不用的值设置为 0)。这能让 shader 更酷和更灵活。 译者: 我用的 teapot 模型, 程序截图如上图所示, 但在 RenderMonkey 中的截图 (下图) 比这个效果好得多,这个示例有缺陷吗?还是我设置不当,我猜可能的原因是.x 文件并不 包含切线数据,而 RenderMonkey 对于那些简单的球、方块、茶壶会自动生成切线数据, 而在你的程序中需要自己实现,就像《Professional XNA Game Programming: For Xbox 360 and Windows》中的那样编写自定义素材导入器产生切线,但我这个茶壶在 DirectX Viewer 查看的确包含了切线数据和副法线数据,还没有解决。XNA Shader 编程教程 5-变形 -在前面的几个教程中我们介绍了几个光照算法,今天的教程相对比较短,是一个纯粹 的 vertex shader 效果使物体变形。32 使物体变形由于 vertex shader 可以逐顶点地转换顶点,所以可以用来使物体/网格发生变形。举例 来说,如果一个游戏能够让你创建自己的角色,包括改变皮肤颜色,眼睛颜色,头发,衣 服等,我们就可以创建一个 vertex shader 设置角色的体形属性,将这个属性设为 0 说明它 “瘦”而 1 说明“胖”。胖/瘦 瘦要实现这个功能,我们只需一个 vertex shader 让顶点沿着法线移动。如果我们将所有 的顶点沿着法线移动,则物体将会变得更大或更小。33 海浪你也可以不用制作一个很大的骨骼动画网格去创建一个逼真的海面,而可以用一个 vertex shader 代替。要做到这一点,你需要一个很大的平面网格代表平静的海面。你可以在 3ds 中制作,或编程实现。这会需要许多顶点,而 shader 会根据正弦/余弦函数向上或向下 移动这些顶点。如图所示,我们定义了一个有很多顶点的平面。并使用 Vertex Shader 将所有顶点沿 Y 轴方向遵循正弦函数移动,可表示为: f(y)=sin(y) 就是说顶点 X 是随 pos.Y = sin(X.pos+time)这个规律运动的。这将产生海面的波浪。当 然,现在还很简单,不是很漂亮。海面的实现有许多种不同的算法,所以如果你想深入研 究,可在 goolge 上搜索一下。要使海浪更漂亮,你可以设置一个法线贴图在海面的大波浪 上创建一些小的凹凸,也可以组合正弦波和余弦波制作更真实的海浪。模拟谐振球这就是这个教程要实现的东西,相当于瘦/胖算法和海浪算法的组合。该示例将使用一 个球体对象,并设置一种正弦/余弦函数使顶点沿法线方向移动使其基于时间而变形。34 实现 Shader这个 shader 只是一个 Vertex Shader。pixel shader 只处理基本光照使其看起来更真实。 您可以自己添加法线映射获得更酷的效果。 在这个 shader 中需要一个时间变量,这样我们可以根据时间制作一个动画效果,然后 我们只需设置大量的正弦和余弦函数使其看起来很酷。下面是 Vertex Shader 代码:float4 g_fT OUT VS(float4 Pos : POSITION, float3 N : NORMAL) { OUT Out = (OUT)0; float angle=(g_fTime%360)*2; float freqx = 1.0f+sin(g_fTime)*4.0f; float freqy = 1.0f+sin(g_fTime*1.3f)*4.0f; float freqz = 1.0f+sin(g_fTime*1.1f)*4.0f; float amp = 1.0f+sin(g_fTime*1.4)*30.0f; float f = sin(N.x*freqx + g_fTime) * sin(N.y*freqy + g_fTime) * sin(N.z*freqz + g_fTime); Pos.z += N.z * amp * Pos.x += N.x * amp * Pos.y += N.y * amp * Out.Pos = mul(Pos, matWorldViewProj); Out.N = mul(N, matWorld); float4 PosWorld = mul(Pos, matWorld); Out.L = vecLightD Out.V = vecEye - PosW return O }Shader 计算了振度和频率以找到一个平滑值,而顶点可以移动到这个点上。XNA Shader 编程教程 6-Shader 演示 -35 本教程不学新的东西, 只是将不同 shader 技术整合到一个场景中, 展示 shader 的威力。请不要着重于源代码,而应把注意力放在 shader 上。也可以让性能更好,但这个示例 把关键点都涉及到了。 使用手柄摇杆可以四处走动,按 A/X 切换 shader 的开启和关闭。要运行这个场景,你 应该把 X360 手柄连接到 USB 接口上,或者取消代码中键盘控制代码的注释,这样你就可 以使用键盘和鼠标控制相机的移动。天空球( 天空球(skysphere) )这个场景使用一个简单的球作为天空球,并使球缓慢地转动让天空更加生动。要使效 果更好,可以添加更多天空纹理在球上并以不同的速度旋转天空球。小岛小岛是一个三维模型。小岛使用一个法线贴图和教程 4 中相同的 shader 技术,能使模 型看起来有更多的细节。海面海洋是一个由许多顶点构成的平面,我们使用教程 5 中的变形 shader 创建波浪,并使 用教程 4 中的法线贴图产生海面上的涟漪。我使用了两个沿不同方向移动的法线贴图产生 波浪上的小细节:36 Normal = (Normal 1+ Normal2)/2;这里使用两个法线并将它们叠加在一起, 在计算漫反射和镜面反射时取它们的平均值。 还移动了颜色贴图的纹理坐标产生流水效果。37 XNA Shader 编程教程 7-卡通渲染 -今天,我将讨论一个简单的算法,可以通过使用 Cel shading/Toon shading 渲染一个非 真实感的场景。要实现这个效果,你需要两个 shaders: (a)Toon shader 会根据纹理添加光线,使用的是教程 2 讨论国的过的漫反射算法。 (b)一个 post process edge detection(离屏边缘检测)算法。 首先,我们使用 Toon shader 将场景渲染到渲染目标(render target),然后将这个纹理 在 shader(b)中检测边缘,最后将场景和边缘组合起来:Shader (a) + (b) 产生最终的输出颜色。Cel/Toon shader要创建 cel/toon shader 我们需要计算漫反射光 ( N 点乘 L )并将它作为纹理的 x 坐标:Tex.y = 0.0f; Tex.x = saturate(dot(L, N)); float4 CelColor = tex2D(CelMapSampler, Tex);38 而 2D 纹理(分辨率 32x1)的 CelMapSampler 采用器如下图所示:如果 L 和 N 垂直(点乘=0),那么将使用坐标为(0.0,0.0)的纹理像素。如果 N 和 L 平行(点乘=1),那么将使用坐标为(1.0,0.0)的纹理像素,其他像素在 0.0~1.0 的范围 内。如你所见,纹理只有 3 种不同的颜色。要从 pixel shader 返回 CelColor,output 将使用 指定纹理作为漫反射 shader:但是我们仍需要纹理,这可以通过在教程 2 中同样的方法实现,只不过不是用纹理的 颜色乘以漫反射颜色,而是用 toon-shaded 漫反射映射的 CelColor 乘以纹理颜色:return (Ai*Ac*Color)+(Color*Di*CelColor);最终结果如下图所示:39 不是很难吧?:)看起来不错,但在某些情况下,我们可能还希望在图像边缘有黑边。 要实现这一点: 一个办法是第一次绘制对象的全黑的图像,然后绘制对象的 cel-shaded 图像,但小一 点点; 另一个办法是将场景渲染到一个纹理,然后对这个纹理应用边缘检测 shader,这也是 本教程的做法。后屏边缘检测( 后屏边缘检测(Post process Edge Detection) )边缘检测 shader 使用下列矩阵作为核心过滤器:核心过滤器的工作原理是将该矩阵作用到图像中的每个像素。核心包含乘法因子应用 于像素和它的临近像素。当所有的值相乘后,像素就会被替换成乘积的总和。选择不同的 核心,可应用不同的过滤。 使用这个 shader 可以创建一个黑白纹理,其边缘是黑色的,其余是白色的。这使得创 建一个具有边缘的普通场景很容易:40 Color*result.Color 是场景纹理,而 result.xxxx 是边缘检测纹理。当 Color 乘以 result,不是边缘部 分的像素是白色的即 1.0,而边缘是黑色的即 0.0。将 Color 乘以 1.0 颜色不变,而乘以黑色 边缘( 0.0 )则变成 0.0(黑色)。 如你所见,toon-shaders 不难,通过几行代码就能实现。XNA Shader 编程教程 8-光泽贴图(Gloss Map) -光泽贴图( )这个教程通过实现光泽贴图来更好地控制镜面反射。什么是光泽贴图? 什么是光泽贴图?光泽贴图是一张黑白纹理,我们使用这张纹理控制特定顶点的反射程度,黑白纹理让 我们可以很容易地做到这点 Shader 中颜色的格式如下:r,g,b,每个分量的变化范围是从 0.0 到 1.0。而一个黑白 纹理意味着每个分量具有相同的值: White=(1,1,1)41 Light gray=(0.8,0.8,0.8) Dark gray=(0.2,0.2,0.2) Black=(0,0,0) 下图是 Gloss 贴图的例子:我们想让白色的部分反光最强,灰色其次,而黑色部分没有反光。实现 Shader我们需要加载镜面反射纹理并将它传递到 shader 中。在 shader 中需要从纹理获取 glossmap 的颜色并将它储存在一个变量中。float4 GlossMapColor = tex2D(GlossMapSampler, Tex);现在,我们需要使用 GlossMapColor 以决定图素的镜面反射的颜色:// R = 2 * (N.L) * N C L float3 Reflect = normalize(2 * Diff * Normal - LightDir); float Specular = pow(saturate(dot(Reflect, ViewDir)), 20); // R.V^n请注意,现在我们在“Specular”变量中存储的是最大反光强度,为了减少反光,我们需 要以某种方式修改“Specular”,我们如何做到这一点?你猜的对,将镜面反光的颜色乘以 GlossMapColor。光泽贴图中只包含黑白两色,即每个 RGB 分量都相同。所以我们可以只 使用一个分量乘以“Specular”:// R = 2 * (N.L) * N C L float3 Reflect = normalize(2 * Diff * Normal - LightDir); float Specular = pow(saturate(dot(Reflect, ViewDir)), 20); // R.V^n Specular = Specular*GlossMapColor.x;这里我使用 GlossMapColor 的 x 分量乘以 “Specular”。GlossMapColor.x 范围从 0.0 到 1.0,相当于镜面反射的比例。如果 glossmap 是黑色的,则 Specular 将乘以 0.0,使它的值42 为 0.0。如果是白色的,Specular 将保持不变,而镜面反射程度取决于 GlossMapColor 的灰 度。 知道了这一点,我们就可以实现光照方程了:return vAmbient + vDiffuseColor * ColorMapColor * Diff + vSpecularColor * S在这个例子中我还增加了一个颜色纹理让效果更明显:)何时使用光泽映射? 何时使用光泽映射?只要你想控制反射,就可以使用光泽映射!比如一个生锈的铁杯在某些部位仍会反光, 而生锈的部位反光较少。旧汽车,光滑/粗糙的冰面等情况也适用。.1 颜色,发光,凹凸和反射纹理贴图 颜色,发光,颜色贴图我们想在一个模型上贴上一张纹理,这个例子中是将一张地球纹理贴在一个球上。你 要做的就是将颜色贴图作为模型的漫反射颜色,所以我们获取漫反射值并将它乘以纹理颜 色获取最终的颜色。我们需要 World * View * Projection 矩阵和 World 矩阵,这里我仍然包 含了环境光,但没有用它,只使用了默认值,因为我们要用到漫反射光,所以需要知道光 线方向。下面有两个新东西:texture 和 sampler。其中 texture 是传递给 shader 的纹理的参 数,这个例子中是 ColorMap。Sampler 用在 pixel shader 中获取纹理的像素。float4x4 wvp : WorldViewP43 float4x4 world : W float AmbientIntensity = 1; float4 AmbientColor : AMBIENT = float4(0,0,0,1); float3 LightDirection : Direction = float3(0,1,1); texture ColorMap : D sampler ColorMapSampler = sampler_state { texture = &ColorMap&; };VS_IN 中添加了一个新成员:TexCoord,你的模型应该包含这个信息,否则无法正确 加上纹理。TexCoord 的数据类型是 float2 (Vector2),每个分量的变化范围是 0 到 1,表示 纹理坐标。如果分量的值是 0.5,0.5 表示处于纹理的中央。这两个量也叫做 U 和 V,类似于 X 和 Y, 但在 UV 映射中称为 U 和 V。 输出的 VS_OUT 结构包含 Position, Light 和 Normal, 还增加了一个 TexCoord。struct VS_IN { float4 Position : POSITION; float2 TexCoord : TEXCOORD0; float3 Normal : NORMAL; }; struct VS_OUT { float4 Position : POSITION; float2 TexCoord : TEXCOORD0; float3 Light : TEXCOORD1; float3 Normal : TEXCOORD2; }; struct PS_OUT { float4 Color : COLOR; };在 vertex shader 中将 TexCoord 传递到 pixel shader。VS_OUT VS_ColorMap(VS_IN input) { VS_OUT output = (VS_OUT)0; output.Position = mul(input.Position,wvp); output.Light = LightD output.TexCoord = input.TexC44 output.Normal = mul(input.Normal,world); }在 pixel shader 中我们计算了光线方向、漫反射和环境光颜色,但我们从当前像素的采 样器中获取当前像素的纹理坐标,并乘以刚才计算的漫反射值调整像素的颜色,接着加上 环境光颜色获得像素的最终颜色。PS_OUT PS_ColorMap(VS_OUT input) { PS_OUT output = (PS_OUT)0; float3 LightDir = normalize(input.Light); float Diffuse = saturate(dot(LightDir,normalize(input.Normal))); float4 texCol = tex2D(ColorMapSampler,input.TexCoord); float4 Ambient = AmbientIntensity * AmbientC texCol *= D output.Color = Ambient + texC }发光贴图( 发光贴图(Glow Map) )45 我们可以在 shader 中添加另一个纹理和采样器来使用发光贴图。texture GlowMap : D sampler GlowMapSampler = sampler_state { texture = &GlowMap&; };Shader 结构并没有变化, 我们只是在 pixel shader 中加入了发光贴图的计算。 发光贴图 将贴图中的像素颜色乘以(1-Diffuse),pixel shader 代码如下:PS_OUT PS_ColorGlowMap(VS_OUT input) { PS_OUT output = (PS_OUT)0; float3 LightDir = normalize(input.Light); float Diffuse = saturate(dot(LightDir,normalize(input.Normal))); float4 texCol = tex2D(ColorMapSampler,input.TexCoord); float4 glowCol = tex2D(GlowMapSampler,input.TexCoord); float4 Ambient = AmbientIntensity * AmbientC float4 glow = glowCol * saturate(1-Diffuse); texCol *= D output.Color = Ambient + texCol + }程序截图如下:46 凹凸贴图( 凹凸贴图(Bump Map) )要让凹凸贴图工作正常你必须先对 shader 和模型做件事。 首先要求模型有“切线”数据, 切线用来产生正确的“切线空间”, Wolfgang 比我解释得清楚得多, 建议你去买一本他的书。 要让素材管道知道我们想让模型使用切线,只需选择模型,在属性面板中展开 Content Processor 并将 Generate Tangent Frame 这个选项设置为 True,默认是 false。47 现在当模型被编译后(如果 Vertex Channel 不正确你会得到一个编译错误,仍得不到 切线数据)就会获取切线数据。我们为凹凸贴图添加一个纹理:texture BumpM sampler BumpMapSampler = sampler_state { Texture = &BumpMap&; };结构也要改变:struct VS_IN { float4 Position : POSITION; float2 TexCoord : TEXCOORD0; float3 Normal : NORMAL; float3 Tangent : TANGENT; }; struct VS_OUT { float4 Position : POSITION; float2 TexCoord : TEXCOORD0; float3 Light : TEXCOORD1; };48 我们需要将切线数据添加到 VS_IN 结构中,这样我们可以使用新生成的切线数据,在 VS_OUT 中移除 Normal,因为我们将在 vertex shader 中使用世界切线空间转换光线,使用 凹凸(法线)贴图获取法线。 vertex shader 如下:VS_OUT VS_ColorGlowBump(VS_IN input) { VS_OUT output = (VS_OUT)0; output.Position = mul(input.Position,wvp); float3x3 worldToTangentS worldToTangentSpace[0] = mul(input.Tangent,world); worldToTangentSpace[1] = mul(cross(input.Tangent,input.Normal),world); worldToTangentSpace[2] = mul(input.Normal,world); output.Light = mul(worldToTangentSpace,LightDirection); output.TexCoord = input.TexC }如你所见,我们用前面相同的方式转换了位置,接着计算切线空间矩阵,并用这个矩 阵转换光线方向。PS_OUT PS_ColorGlowBump(VS_OUT input) { PS_OUT output = (PS_OUT)0; float3 Normal = (2 * (tex2D(BumpMapSampler,input.TexCoord))) - 1.0; float3 LightDir = normalize(input.Light); float Diffuse = saturate(dot(LightDir,Normal)); float4 texCol = tex2D(ColorMapSampler,input.TexCoord); float4 glowCol = tex2D(GlowMapSampler,input.TexCoord); float4 Ambient = AmbientIntensity * AmbientC float4 glow = glowCol * saturate(1-Diffuse); texCol *= D output.Color = Ambient + texCol + }在 pixel shader 中我们从凹凸贴图中获得法线,归一化光线方向,然后用前面相同的方 式计算漫反射,只不过这次是使用从切线空间产生的数据。49 反射贴图( 反射贴图(Reflective Map) )这个贴图与颜色和发光贴图一样简单,我们通过一个反射贴图实现一个镜面高光,再 次添加一个纹理和采样器管理反射贴图:texture ReflectionMap : D sampler ReflectionMapSampler = sampler_state { texture = &ReflectionMap&; };VS_IN 结构保持不变而 VS_OUT 需要为镜面反射做出改变:struct VS_OUT { float4 Position : POSITION; float2 TexCoord : TEXCOORD0; float3 Light : TEXCOORD1; float3 CamView : TEXCOORD2; float4 posS : TEXCOORD3; float3 Normal : TEXCOORD4; };50 接着在 vertex shader 中添加镜面反光代码PS_OUT PS_Ambient(VS_OUT input) { PS_OUT output = (PS_OUT)0; float3 Normal = (2 * (tex2D(BumpMapSampler,input.TexCoord))) - 1.0; float3 LightDir = normalize(input.Light); float Diffuse = saturate(dot(LightDir,Normal)); float4 texCol = tex2D(ColorMapSampler,input.TexCoord); float4 glowCol = tex2D(GlowMapSampler,input.TexCoord); float4 Ambient = AmbientIntensity * AmbientC float4 glow = glowCol * saturate(1-Diffuse); texCol *= D float3 Half = normalize(normalize(LightDirection) + normalize(input.CamView)); float specular = pow(saturate(dot(normalize(input.Normal),Half)),25); float4 specCol = 2 * tex2D(ReflectionMapSampler,input.TexCoord) * (specular * Diffuse); output.Color = Ambient + texCol + glow + specC }截图如下:另外,如果你看一下图片的话会发现我在属性面板的 Content Processor 中将 Resize To Power of Two 设置成了 True,这样做对图像处理更友好。XNA Shader 编程教程系列 9- Post process 波动效果 -51 Post processing? ?Post processing 将一个 effect,或 effect 的组合作用到一个图像/帧/视频上,让他们看起 来效果更酷,Post processing 可以看作对场景施加一个滤镜。 这个例子我们将场景绘制到一张纹理,然后施加波动效果。这让场景看起来象是在水 下一样。 要实现这一点,我们需要使用某种圆周运动改变纹理坐标向量,然后使用这个改变后 的纹理坐标载入在 ColorMapSampler 中的颜色!截图如下:图中的绿点之是表示某个纹理坐标,我们基于时间让它旋转,对所有纹理坐标都施加 这个动作就会实现我们想要的波动效果!52 本例中的场景包含一张使用 SpriteBatch 的背景,使用漫反射光照的三维模型。这个场 景被渲染到一个纹理, 然后使用这个纹理绘制场景。 当绘制纹理时, 我们添加了 post process shader。因此,我们使用两个 shader。一个用于场景中的物体,一个用于 post process shader。实现 shaderpost process shader 只需要用到 pixel shader,要实现动态 shader,需要定义一个计时器。float fT这个计时器由应用程序设置, 将用于每个纹理坐标的 sin/cos 运动, 使它们可以旋转! ) : 我们还需要 ColorMap 纹理包含场景,并在每一帧进行更新。sampler ColorMapSampler : register(s0);做好准备后,就可以看看 Pixel Shader 函数了:float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR { Tex.x += sin(fTimer+Tex.x*10)*0.01f; Tex.y += cos(fTimer+Tex.y*10)*0.01f; float4 Color = tex2D(ColorMapSampler, Tex); return C }这个 shader 只是简单地让当前纹理坐标的 X 和 Y 分量旋转。在 sin 中的 fTimer+Tex.x 使 Tex.x 在每帧沿 X 方向变化, 同理也发生在 Y 方向。 如果我们使用 sin(fTimer)/cos(fTimer) 代替 Tex.x/Tex.y,所有的纹理坐标将会向着同一方向旋转。你可以尝试一下更好地理解这 些参数。 最后,我们需要名为 PostProcess 的 technique:technique PostProcess { pass P0 { PixelShader = compile ps_2_0 PixelShader(); } }使用 shader将 shader 加入到我们想要的任何场景中很简单,只需将场景渲染到一个纹理中:RenderTarget2D renderT renderTarget = new RenderTarget2D(graphics.GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight,53 1, graphics.GraphicsDevice.DisplayMode.Format); graphics.GraphicsDevice.SetRenderTarget(0, renderTarget); // Render our scene graphics.GraphicsDevice.SetRenderTarget(0, null); SceneTexture = renderTarget.GetTexture();这里的 SceneTexture 是一个 Texture2D 对象。 现在我们需要显示 SceneTexture 并将 post process effect 作用到 SceneTexture 上:spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.SaveState); { // Apply the post process shader effectPostOutline.Begin(); { effectPostOutline.CurrentTechnique.Passes[0].Begin(); { effectPostOutline.Parameters[&fTimer&].SetValue(m_Timer); spriteBatch.Draw(SceneTexture, new Rectangle(0, 0, 800, 600), Color.White); effectPostOutline.CurrentTechnique.Passes[0].End(); } } effectPostOutline.End(); } spriteBatch.End();好了! 现在我们得到了一个非常简单但又很酷的 post process effect。 可以试着改变每个 纹理坐标运动的方式,你可以得到一个很酷的失真效果。XNA Shader 编程系列教程 10-Post process 图片反相 -54 这个 shader 很简单,但我还有很多问题,所以我决定写一篇很短的教程。要从一个纹 理采用器获取颜色,你通常这样做:float4 Color = tex2D(ColorMapSampler, Tex);要获取反相颜色(即补色),你只需用 1 减去颜色的每个通道的值就可以了:float4 ColorInverse = 1.0f - tex2D(ColorMapSampler, Tex);实现 shader它很短,所以我只写 shader 代码了:sampler ColorMapSampler : register(s0); // Negative image float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR { float4 Color = 1.0f - tex2D(ColorMapSampler, Tex); // Keep our alphachannel at 1. Color.a = 1.0f; return C } technique PostProcess { pass P0 { // A post process shader only needs a pixel shader.55 PixelShader = compile ps_2_0 PixelShader(); } }首先,我们从采样器中用 1 减获取相反的值,但这样做把 alpha 通道也相反了,如果这 正是你想要的,那无关紧要,但如果你想保持原始图片的 alpha 值,你必须将它恢复回来, 或手动地设置某个值。在这个例子中,我们将 alpha 设置为 1,即图片是不透明的。XNA Shader 编程教程 11-Post process 灰度图 -此教程是建立在教程 9.1 基础上的。如果你还没理解教程 9.1,请先弄懂它。 要制作一张黑白图片或场景,我们需要将场景纹理转换成它的反相颜色,这可以在一 个 post process pixel shader 中实现。一张黑白图片只由一些灰度的像素组成,所以我们的 shader 必须将一个颜色变成灰度。有几个方法可以使用,我将介绍这些方法中的两个。1.平均颜色 .一种方法是将颜色的三个通道相加并除以 3,这可以获得颜色平均值,并将各个颜色 通道设置为这个平均值:Color.rgb = (Color.r + Color.g + Color.b)/3;这将把一个像素的所有颜色通道都设置为同一个值,结果不错,但如果你仔细观察, 它是不正确的!让我们看一下另一种方法。56 2.灰度强度 .人的眼睛对绿色比红色和蓝色更加敏感,将图片转换为灰度的一种常用方法是使用一 个设置好的值,这个值表示三个颜色通道的灰度强度/权重:g = 0.3 R + 0.59 G + 0.11 B译者注:按照《GPU 精粹 1》中文版第 253 页的说法,推荐数值是(0.222,0.707,0.071), 因为这个数值遵循一个被称为 ITU Rec 709 的国际工业标准, 而不是这个教程中的(0.3, 0.59, 0.11)。 这样可以正确地转换图片,在 shader 中可以这样实现:Color.rgb = dot(Color.rgb, float3(0.3, 0.59, 0.11));下图是这两种不同方法的区别,在更鲜艳的场景或包含更多绿色的图片中区别更大。实现 shader我在 shader 中两种方法都使用了。1.平均颜色 .sampler ColorMapSampler : register(s0); float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR {57 float4 Color = tex2D(ColorMapSampler, Tex); Color.rgb = (Color.r + Color.g + Color.b)/3; // Keep our alphachannel at 1. Color.a = 1.0f; return C } technique PostProcess { pass P0 { PixelShader = compile ps_2_0 PixelShader(); } }2. 灰度强度sampler ColorMapSampler : register(s0); float4 PixelShader(float2 Tex:TEXCOORD0) : COLOR { float4 Color = tex2D(ColorMapSampler, Tex); Color.rgb = dot(Color.rgb, float3(0.3, 0.59, 0.11)); // Keep our alphachannel at 1. Color.a = 1.0f; return C } technique PostProcess { pass P0 { PixelShader = compile ps_2_0 PixelShader(); } }XNA Shader 编程教程 12- Post process 噪点 扭曲 噪点/扭曲 -58 此教程是建立在教程 9.1 基础上的。如果你还没理解教程 9.1,请先弄懂它。 要在场景中添加噪点/扭曲,你必须在纹理坐标中添加一个扭曲值,然后使用新的纹理 坐标查询纹理采样器。 我们还想实现噪点动画,所以还需要一个 timer 作用在扭曲值上。 我们还需要一个值表示扭曲程度,一个 seed 用于扭曲算法。实现 shader首先要在 shader 中定义一些全局变量:// This will use the texture bound to the object( like from the sprite batch ). sampler ColorMapSampler : register(s0); // A timer to animate our shader float fT // the amount of distortion float fNoiseA // just a random starting number int iSColorMapSampler 是渲染的场景,fTimer 是定时器,fNoiseAmount 是扭曲程度,它的 值在 0.001 至 0.5 是最好的,iSeed 是用于计算噪点的种子。 接着,在 shader 中添加代码。首先要计算噪点因子:float NoiseX = iSeed * fTimer * sin(Tex.x * Tex.y+fTimer); NoiseX=fmod(NoiseX,8) * fmod(NoiseX,4);59 上面的代码只是一个使用 seed 的随机函数,使用 timer 和纹理坐标让每个像素的值有 所不同,可以通过改变它们获得不同的效果。这里我们使用了一个新函数:fmod(x,y)。这 个函数返回一个 x 被 y 除并取模的浮点数。接着计算扭曲程度用来影像 x 和 y 分量。我们 稍微改变扭曲程度使看了来有点随机:float DistortX = fmod(NoiseX,fNoiseAmount); float DistortY = fmod(NoiseX,fNoiseAmount+0.002);现在计算新的纹理坐标:float2 DistortTex = float2(DistortX,DistortY);最后,将这个新纹理坐标和旧纹理坐标混合形成一个稍有扭曲的纹理坐标:float4 Color=tex2D(ColorMapSampler, Tex+DistortTex);下面是 shader 代码:// Global variables // This will use the texture bound to the object( like from the sprite batch ). sampler ColorMapSampler : register(s0); // A timer to animate our shader float fT // the amount of distortion float fNoiseA // just a random starting number int iS // Noise float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR { // Distortion factor float NoiseX = iSeed * fTimer * sin(Tex.x * Tex.y+fTimer); NoiseX=fmod(NoiseX,8) * fmod(NoiseX,4); // Use our distortion factor to compute how much it will affect each// texture coordinate float DistortX = fmod(NoiseX,fNoiseAmount); float DistortY = fmod(NoiseX,fNoiseAmount+0.002); // Create our new texture coordinate based on our distortion factor60 float2 DistortTex = float2(DistortX,DistortY); // Use our new texture coordinate to look-up a pixel in ColorMapSampler. float4 Color=tex2D(ColorMapSampler, Tex+DistortTex); // Keep our alphachannel at 1. Color.a = 1.0f; return C } technique PostProcess { pass P0 { // A post process shader only needs a pixel shader. PixelShader = compile ps_2_0 PixelShader(); } }XNA Shader 编程教程 13-Alpha 映射 -这个教程我们将实现一个简单但重要的 shader:Alpha 贴图!当你想绘制一个部分透明 的 3D 物体时使用 Alpha 贴图是很有用的。 比如一个有窗框的窗户, 可以让窗框不透明而使 其他部分透明!你可以在很多场合使用 Alpha 贴图,比如冰面、皮肤、花、昆虫翅膀等。Alpha 映射这个 shader 的基本思路是使用一张纹理:alpha 贴图。这个纹理是一张灰度图,其中黑 色表示完全透明,灰色表示介于透明和不透明之间,而白色表示完全不透明。你可以将这 张纹理想象成一张包含物体透明信息的图片,其中的灰度颜色代表透明百分比。颜色 0.061 (黑色)表示透明,颜色 0.5(灰色)表示半透明,而 1.0(白色)表示不透明。 所以 shader 需要两张纹理: ColorMap 代表物体颜色,AlphaMap 代表透明度。实现 Shader本教程只使用 Diffuse Shader,并支持 Alpha 映射,当然你也可以在 shader 中实现任何 你想要的效果。首先定义两个纹理:Color Map,Alpha Map。并添加到 shader 中:texture ColorM sampler ColorMapSampler = sampler_state { Texture = &ColorMap&; MinFilter = L MagFilter = L MipFilter = L AddressU = M AddressV = M }; texture AlphaM sampler AlphaMapSampler = sampler_state { Texture = &AlphaMap&; MinFilter = L MagFilter = L MipFilter =L AddressU = M AddressV = M };然后设置 color 的 alpha 通道并返回存储在 Alpha 贴图中的值,这一步在 Pixel Shader 中进行:Color = (Ai*Ac*Color)+(Color*Di*Dd); Color.a = tex2D(AlphaMapSampler, Tex).r;62 return C这里我们和以前一样计算了漫反射颜色。接着处理 Colors.a 分量,这个分量是 Colors 的 alpha 通道,并将这个分量设置为 alpha 贴图中的值。Alpha 贴图中的所有分量都是相同 的(因为它是一张灰度图),所以你用 r、g、b 通道皆可,我使用了 r 通道: Technique 代码如下:technique DiffuseShader { pass P0 { AlphaBlendEnable = T SrcBlend = SrcA DestBlend = InvSrcA Sampler[0] = (ColorMapSampler); Sampler[1] = (AlphaMapSampler); VertexShader = compile vs_2_0 VertexShader(); PixelShader = compile ps_2_0 PixelShader(); } }如你所见,我们将 AlphaBlendEnable 设置为 true,并使用 SrcAlpha/InvSrcAlpha 作为混 合函数,这意味着我们使用 alpha 通道使物体透明。使用 shader使用 shader 没什么新的东西,别忘了将 color 和 alpha 纹理传递到 shader 中。 我还添加了一个叫做 m_Overlay 的 overlay 纹理。这个纹理用来覆盖在整个屏幕之上, 使用了这个.PNG 文件的 alpha 值, 这些 alpha 值可以在 Photoshop 或其他图像编辑器中设置。spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState); { spriteBatch.Draw(m_Overlay, new Rectangle(0, 0, 800,600), Color.White); } spriteBatch.End();XNA Shader 编程教程 14-透射 -63 上个教程我们使用了 alpha 贴图和 alpha 通道让物体变得透明,这次我们将通过实现透 射(transmittance)更深入地学习透明。透射( 透射(Transmittance) )像玻璃、水、水晶、空气等物体会在光线穿过它们时吸收一定的光线,在教程 13 中, 我们使用 alpha 贴图使物体透明并使用颜色为 RGB(0.5,0.5,0.5)的 alpha 贴图创建了一个透明 球,这个方法能用在很多场合,但这样做会使透明效果太平淡。 真实世界中的物体,比如说玻璃球,当光线穿过它时还会吸收/散射一定的光线,光线 进入玻璃球越深,在光线穿出前吸收和散射的越多,这叫做透射 ( wikipedia )。要计算透射 ( T ),我们可以使用 Beer-Lamberts 定律( wikipedia )处理透射的光线,让我们看一下 Beer-Lamberts 定律更好地理解原理!(译者注:在计算漫反射时我们使用的是 Lambert 定 律,这个定律又叫做 Lambert 余弦定律,简单地说就是反射强度是法线和入射光方向的点 乘,Lambert 全名 Johann Heinrich Lambert,生于 1728 年 8 月 26 日,死于 1728 年九月 25 日,是瑞士数学家、物理学家和天文学家,而 Beer-Lamberts 定律是光线透射的规律,这个 规律是由 Pierre Bouguer 在 1729 年前发现的,但常常被误认为是 Lambert 发现的,事实上 Lambert 只是于 1760 年引用了 Bouguer 的文章,1852 年 August Beer 改进了这个规律) T=e-a′cd (公式 1) 这个公式中的 T 表示透射,a'是吸收因子,c 是物体的浓度(译者:原文是 consistensy, 没这个单词,怀疑是 consistency),d 是物体的厚度。所以要使用这个公式,我们需要知道 a'、c 和 d。64 先来看 c。c 控制光线的吸收程度,这个值可以设置为大于 0 的任何值。然后是 a',我 们可以通过公式 1 的变形计算 a':(公式 2) 公式 2 中的 T 是透射中最暗的颜色,对应最远的距离。 最后获取 d。这个值设置物体的厚度。 本教程中,我们将计算任何简单物体(不包含孔或突起物,如球,简单玻璃形状等) 的 c。这个 shader 很复杂,因为我们将在后面的教程中也要正确运行。 现在我们获取计算给定点的 T 的所有变量,可以使用 T 表示光线的吸收程度,这可以 通过将 T 乘以光线(透射后的像素)的混合颜色做到。 那么我们如何计算每根光线在透射体中的前进距离?可以使用深度缓冲 ( wikipedia )! 深度缓冲(即 Z 缓冲)可以看成一张包含场景的灰度图,灰度值表示物体距离相机的 远近。所以,本文最开始的一张图片我们看到的是一个复杂的玻璃物体,而场景深度缓冲 看起来应像下图所示:深度缓冲需要正确的介于近裁平面和远裁平面的值,最完美的是近裁平面是透射体的 最近顶点,远裁平面是最远顶点。 知道了这些,我们就可以通过使用两个深度缓冲纹理找到任何角度对应的透射体的厚 度。通过使用剔除,我们可以在一个深度缓冲纹理中绘制透射体的前表面,使用另一个深 度缓冲纹理中绘制透射体的后表面。下面两张图显示了这两个不同的纹理:65 在一个深度缓冲纹理中的后表面在另一个深度缓冲纹理中的前表面最后的纹理 灰度值显示了光线能在透射体中前进多远,白色代表长而黑色代表短或不前进。实现 Shader66 本教程使用三个 technique。一个只是处理镜面反射,另一个将场景绘制到一张深度纹 理,第三个是 post process shader 将透射效果应用到物体上。我们并不想让场景中的所有物 体都有透射效果。所以使用 post process shader 时,我们首先将没有透射体的场景绘制到一 张纹理(背景纹理),然后在第二个 pass 中单独绘制透射体,最后将在 post process shader 中把两者组合起来。首先处理镜面反射:float4x4 matWorldViewP float4x4 matInverseW float4 vLightD float4 vecLightD float4 vecE float4 vDiffuseC float4 vSpecularC float4 vA texture ColorM sampler ColorMapSampler = sampler_state { Texture = &ColorMap&; MinFilter = L MagFilter = L MipFilter = L AddressU = C AddressV = C }; struct OUT { float4 Pos : POSITION; float2 Tex : TEXCOORD0; float3 L : TEXCOORD1; float3 N : TEXCOORD2; float3 V : TEXCOORD3; }; OUT VertexShader( float4 Pos: POSITION, float2 Tex :TEXCOORD, float3 N: NORMAL ) { OUT Out = (OUT) 0; Out.Pos = mul(Pos, matWorldViewProj); Out.Tex = T Out.L = normalize(vLightDirection); Out.N = normalize(mul(matInverseWorld, N)); Out.V = vecEye - P67 return O } float4 PixelShader(float2 Tex: TEXCOORD0,float3 L: TEXCOORD1, float3 N: TEXCOORD2, float3 V: TEXCOORD3) : COLOR { float3 ViewDir = normalize(V); // Calculate normal diffuse light. float4 Color = tex2D(ColorMapSampler, Tex); float Diff = saturate(dot(L, N)); float3 Reflect = normalize(2 * Diff * N - L); float Specular = pow(saturate(dot(Reflect, ViewDir)), 128); // R.V^n //I = A + Dcolor * Dintensity * N.L + Scolor * Sintensity * (R.V)n return Color*vAmbient + Color*vDiffuseColor * Diff + vSpecularColor * S } technique EnvironmentShader { pass P0 { VertexShader = compile vs_2_0 VertexShader(); PixelShader = compile ps_2_0 PixelShader(); } }接着是 Depth Texture shader。这个 shader 只将场景绘制成灰度,每个顶点/像素的深度 用一个介于 0.0 至 1.0 之间的值表示, 表示最靠近相机而 0.0 表示在远裁平面 1.0 (Pos.w) 。 所以要获取顶点深度值, 我们只需获取顶点的 Z 值, Z 值除以 W 值使深度值在投影矩阵 将 的近裁平面和远裁平面之间。 vertex-shader 计算两个值:Position 和 Distance。struct OUT_DEPTH { float4 : POSITION; float Distance : TEXCOORD0; };下面就可以实现 Depth texture vertex shader:OUT_DEPTH RenderDepthMapVS(float4 vPos: POSITION) { OUT_DEPTH O68 // Translate the vertex using matWorldViewProj. Out.Position = mul(vPos, matWorldViewProj); // Get the distance of the vertex between near and far clipping plane in matWorldViewProj. Out.Distance.x = 1-(Out.Position.z/Out.Position.w); return O }首先我们将顶点乘以 world*view*projection 矩阵进行转换。然后将距离值设置为正确 的深度值,这可以通过 Position.z / Position.w 得到,让我们获得了介于近裁平面和远裁平面 之间 的深度值。下面是 pixel shader!我们将 OUT_DEPTH 中的 Distance 值转换到纹理, 以便接下来使用:float4 RenderDepthMapPS( OUT_DEPTH In ) : COLOR { return float4(In.Distance.x,0,0,1); }下面是 technique:technique DepthMapShader { pass P0 { ZEnable = TRUE; ZWriteEnable = TRUE; AlphaBlendEnable = FALSE; VertexShader = compile vs_2_0 RenderDepthMapVS(); PixelShader = compile ps_2_0 RenderDepthMapPS(); } }Technique 中没有新的东西,只是打开 Z 缓冲,并使之可写。最后是透射的 post process shader!首先我们需要作为背景的场景纹理、透射体场景(包含所以具有透射效果的物体的 纹理)和两个深度纹理!texture D1M; sampler D1MSampler = sampler_state { Texture = &D1M&; MinFilter = L MagFilter = L MipFilter = L69 AddressU = C AddressV = C }; texture D2M; sampler D2MSampler = sampler_state { Texture = &D2M&; MinFilter = L MagFilter = L MipFilter = L AddressU = C AddressV = C }; texture BGS sampler BGSceneSampler = sampler_state { Texture = &BGScene&; MinFilter = L MagFilter = L MipFilter = L AddressU = C AddressV = C }; texture S sampler SceneSampler = sampler_state { Texture = &Scene&; MinFilter = L MagFilter = L MipFilter = L AddressU = C AddressV = C };DM1 是第一张深度贴图纹理,包含透射体的后表面。DM2 是第二张深度贴图纹理, 包含透射体的前表面,BGScene 包含背景,Scene 包含透射体场景/颜色。 然后添加两个变 量,}

我要回帖

更多关于 shader 性能优化 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信