XNA Shader编程教程1-环境光照
Shader简史
在DirectX8之前,GPU使用固定方式变换像素和顶点,即所谓的“固定管道”。这使得开发者不可能改变像素和顶点转化和处理的进程,使大多数游戏的图像表现看起来非常相似。
DirectX8提出了顶点和像素着色器,这让开发者可以在管道中决定如何处理顶点和像素,使他们获得了很强的灵活性。
一开始shader编程使用汇编语言程序使用的着色器,这对shader开发者来说相当困难,Shader Model 1.0是唯一支持的版本。但DirectX9发布后这一切改变了,开发者能够使用高级着色语言(HLSL)取代了汇编语言,HLSL语法类似C语言,这使shader更容易编写,阅读和学习。
DirectX 10.0提出了一个新的shader——GeometryShader作为ShaderModel 4.0的组成部分。但这需要一个最先进的显卡和WindowsVista才能支持。
XNA支持Shader Model 1.0至3.0,可以在XP,Vista和XBox360!上运行。
Shader?
嗯,历史已经说得够多了……那么什么是shader?
正如我所说的,shader可以用来定制管道的步骤,使开发者能够决定如何处理像素/顶点。
如下图所示,应用程序在渲染时启动并使用shader,顶点缓冲区通过向pixel shader发送所需的顶点数据与pixel shader协同工作,并在帧缓冲中创建了一个图像。
但请注意许多GPU不支持所有的shader模式,在开发shader时应引起足够重视。一个shader最好要有一个类似/简单的效果,使程序在较旧的计算机上也能工作正常。
Vertex shader
Vertex shaders用来逐顶点地处理顶点数据。例如可以通过将模型中的每个顶点沿着法线方向移动到一个新位置使一个模型变“胖”(这称之为deform shaders)。
Vertex shaders从应用程序代码中定义的一个顶点结构获取数据,并从顶点缓冲区加载这个结构传递到shader。这个结构描述了每个顶点的属性:位置,颜色,法线,切线等。
接着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 Out;
}
// or
float3 VS(out float2 tex : TEXCOORD0) : POSITION
{
tex = float2(1.0, 1.0);
returnfloat3(0.0, 1.0, 0.0);
}
Pixel Shader
Pixel 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可以有两个输出值:颜色和深度。
HLSL
HLSL是用来开发shader的。在HLSL中,您可以声明变量,函数,数据类型,测试(if/else/for/do/while+)以及更多功能以建立一个顶点和像素的处理逻辑。下面是一些HLSL的关键字。这不是全部,但是最重要的。
数据类型:
bool true or false
int 32-bit integer
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|)
完整列表请看:http://msdn2.microsoft.com/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
{
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();
PixelShader = compile ps_1_1 PS2();
这告诉我们,这个technique将使用VS2()作为vertex shader,PS2()作为pixelshader,并且支持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:http://developer.nvidia.com/object/fx_composer_home.html。(译者:还推荐一个shader的IDE:AMD公司的RenderMonkey,可在http://ati.amd.com/developer/rendermonkey/downloads.html下载最新版本1.81(93.9MB,2008年4月8日),个人用下来的感觉好像nvidia实力更强一些,文档也很详实,而RenderMonkey上手更容易。)
2.当shader建立后,将其拖放到“Content”目录,自动生成素材名称。
3.XNA框架有一个Effect类用于加载和编译shader。要创建这个类的实例可用以下代码:
Effect effect;
Effext属于Microsoft.Xna.Framework.Graphics类库,因此,记得添加using语句块:
using Microsoft.Xna.Framework.Graphics
4.要初始化shader,我们可以使用Content从项目或文件中加载:
effect = Content.Load("Shader");
“Shader”是你添加到Content目录的shader名称。
5.选择使用何种technique:
effect.CurrentTechnique = effect.Techniques["AmbientLight" ];
6.要使用Effect,请调用Begin()函数:
effect.Begin();
此外,您必须启动所有passes。
foreach (EffectPass pass ineffect.CurrentTechnique.Passes)
{
// Begincurrent pass
pass.Begin();
7.有很多方法可以设置shader的参数,但对这个教程来说下列方法够用了。注:这不是最快的方法,在以后的教程中我将回到这里:
effect.Parameters["matWorldViewProj"].SetValue(worldMatrix* viewMatrix*projMatrix);
其中“matWorldViewProj”是在shader中定义的:
float4x4 matWorldViewProj;
将matWorldViewProj设置为worldMatrix * viewMatrix * projMatrix。
SetValue设置参数并将它传递到shader,GetValue 从shader获取值,Type是获取的数据类型。例如,GetValueInt32()得到一个整数。
8.渲染你想要这个shader处理/转换的场景/对象。
9.要停止pass,调用pass.End(),要停止shader,调用Effect的End()方法:
pass.End();
effect.End();
为了更好地理解步骤,可参见源代码。
环境光照(Ambientlight)
OK,我们终于到了最后一步,实现shader!不坏吧?首先,什么是“Ambient light”?环境光是场景中的基本光源。如果你进入一个漆黑的屋子,环境光通常是零,但走到外面时,总是有光能让你看到。环境光没有方向(译者:所以也将其称为“全局光照模型”),在这里应确保对象不会自己发光,它有一个基本的颜色。环境光的公式是:
I = Aintensity* Acolor
其中I是光的实际颜色, Aintensity是光的强度(通常在0.0和1.0之间),Acolor环境光的颜色,这个颜色可以是固定值,参数或纹理。好吧,现在开始实现Shader。首先,我们需要一个矩阵表示世界矩阵:
float4x4 matWorldViewProj ;
在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 Out;
}
我们创建了一个OUT类型的函数,它的参数是float4类型的Pos:POSITION。这是模型文件/应用程序/游戏中定义的顶点位置。然后,我们建立一个名叫OUT的OUT结构实例。这个结构必须被填充并从函数返回,以便后继过程处理。输入参数中的Pos不参与后继过程的处理,但需要乘以worldviewprojection矩阵使之以正确放置在屏幕上。由于Pos是OUT中的唯一变量,我们已经返回它并继续前进。现在开始处理pixel shaders,我们声明为一个float4类型的函数,返回存储在GPU中的COLOR寄存器上的float4值。我们在pixel shader中进行环境光的算法:
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平行于表面,则反射最弱。
要计算L和N的夹角,我们可以使用点乘或标量乘积。这条规则可用来计算给定两个向量间的夹角,可以定义如下:
N.L = |N| * |L| * cos(a)
(译者注:这个公式实际上是Lambert定理的简化形式,若归一化N和L,则这个公式可简化为N.L=cos(a))
这里|N|是向量N的长度,|L|是向量L的长度,cos(a)是两个向量之间夹角的余弦。
实现shader
我们需要三个全局变量:
float4x4 matWorldViewProj ;
float4x4 matInverseWorld ;
float4 vLightDirection ;
我们仍然要教程1中的worldviewprojection矩阵,但除此之外,我们还需要InverseWorld矩阵计算出与世界矩阵相关的正确法线,而vLightDirection表示光线的方向。我们还需要在vertexshader中定义OUT结构,这样才能在pixel shader中获得正确的光线方向:
struct OUT
{
float4 Pos:POSITION;
float3 L: TEXCOORD0;
float3 N:TEXCOORD1;
};
这里定义了位置Pos,光线方向L和法线方向N存储在不同的寄存器中。TEXCOORDn可用于任何值,现在我们还没有使用任何纹理坐标,所以我们可以用这些寄存器储存这两个向量。
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 Out;
}
我们从模型文件获取位置和法线并通过它们传递到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));
另外本例中的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)
让我们把这个向量存储在一个变量中: Vector4vecEye = new Vector4(x, y, zHeight,0);
让我们深入讨论如何在创建这个向量后使用shader。镜面高光的公式是
I=AiAc+Di*Dc*N.L+Si*Sc*(R.V)^n
其中R=2*(N.L)*N-L
如我们所见,我们需要新的Eye向量和一个反射向量R。为了计算镜面光,我们需要将R点乘V并进行n次幂的运算,而指数n表示光泽属性,n越大说明物体表面越光滑,反光越强。
实现Shader
如前面的截图可见,现在这个对象看起来很有光泽,只需通过shader就能实现!很酷吧!
首先声明一些变量:
float4x4 matWorldViewProj;
float4x4 matWorld;
float4 vecLightDir;
float4 vecEye;
float4 vDiffuseColor;
float4 vSpecularColor;
float4 vAmbient;
然后是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 =vecLightDir;
Out.V = vecEye- PosWorld;
return Out;
}
然后处理pixelshader。首先归一化Normal,LightDir和ViewDir简化计算。
Pixelshader会根据前面镜面反射公式返回float4代表当前像素的最终颜色和光强。然后使用教程2同样的方法计算漫反射光线的方向。
Pixel Shader中新的东西是通过L和N计算反射向量R,并使用这个向量计算镜面反射光。因此,首先计算反射向量R:
R = 2 * (N.L) * N – L
在前面我们已经在计算漫反射光时计算了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 *Specular;
这个公式你应该很熟悉了,对不对?我们首先计算环境光和漫反射光并把它们相加。然后将镜面反光的颜色乘以刚才算出的反光强度Specular,并和环境光颜色和漫反射颜色相加。本教程的pixel shader如下所示:
float4 PS(float3 L:TEXCOORD0, float3 N : TEXCOORD1, float3 V : TEXCOORD2) : COLOR
{
float3 Normal = normalize(N);
float3LightDir = 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 * Specular;
}
当然,我们还要指定一个technique并编译Vertex 和Pixel shader:
technique SpecularLight
{
pass P0
{
// compileshaders VertexShader = compile vs_1_1 VS();
PixelShader= compile ps_2_0 PS();
}
}
shader( .fx )文件的完整代码如下:
float4x4 matWorldViewProj;
float4x4 matWorld;
float4 vecLightDir;
float4 vecEye;
float4 vDiffuseColor;
float4 vSpecularColor;
float4 vAmbient;
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 =vecLightDir;
Out.V = vecEye- PosWorld;
return Out;
}
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 * Specular;
}
technique SpecularLight
{
pass P0
{
// compileshaders
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 modelTexture; //纹理变量
sampler ModelTextureSampler = sampler_state //纹理采样器
{
Texture =
MinFilter =Linear; //缩小图形使用线性滤波
MagFilter =Linear; //放大图形使用线性滤波
MipFilter =Linear; //Mipmap使用线性滤波
AddressU =Wrap; //U、V方向上的纹理寻址模式都采用Wrap方式
AddressV =Wrap;
};
纹理过滤
过滤是指通过给定的uv坐标从纹理贴图中获取图素的一种方法,Direct3D提供3种纹理过滤方法和所谓的“mipmap”(MIP是拉丁语“multum in lparvo”的缩写,可以解释为“many things in asmall place”小中见大、小型而内容丰富),3种纹理过滤方法是:
最近点采样
线性纹理采样
各向异性(anisotropic)
纹理过滤应考虑两种不同的情况:放大(magnification)和缩小(minification)。举例来说,将一张64×64的纹理映射到一个400×400像素的多边形时,便会发生放大,这会导致“锯齿”。而将一张64×64的纹理映射到一个10×10像素的多变形时,缩小会导致“像素抖动”,看上去使人晕眩。
Mipmap由一系列纹理组成,其中每张纹理是一张分辨率逐渐降低的图像,每一级的高度和宽度都是前一级的高度和宽度的一半,使用mipmap可以确保无论是靠近还是远离纹理,纹理都可以保持它原有的真实性和质量,从而减少了放大和缩小带来的负面效果。
大多数硬件都支持4种不同的过滤:点采样、双线性过滤(Direct3D中称之为线性过滤)、三线性过滤(Direct3D中称之为“线性过滤+mipmap”)、各向异性过滤。
从上面的代码可知我们使用的是三线过滤。
纹理寻址模式
代码中的“AddressU= Wrap; AddressV = Wrap;”称为纹理寻址模式(texture-addressingmode)。当顶点的纹理坐标设置在0到1之间,以此来选择一张纹理贴图不难理解。但如果给出的纹理坐标超出了这个范围会如何?这取决于所选择的纹理寻址模式。有几种不同的寻址模式(以下例子中的纹理坐标都是(0,0)、(3,0)、(0,3)、(3,3):
1.包装(wrap):在纹理坐标的每个整数相接处重复纹理,这也是默认模式,如图:
2.镜像(mirror):在每个整数边界对纹理做一次镜像。
3.夹持(clamp):只将纹理应用到多边形上一次,然后对超出部分涂上纹理边缘像素的颜色。
还有边框颜色(bordercolor)和一次镜像(MirrorOnce)不是很常用。
在纹理上应用漫反射和镜面反射
然后在pixelSahder加入以下代码:
float4 textureColor = tex2D(ModelTextureSampler,TexCoord);
return vAmbient +textureColor;
程序截图如下:
如要在纹理上加上漫反射光照,代码应变为:
return vAmbient +textureColor*vDiffuseColor * Diff;
程序截图如下:
如要再加上镜面高光,代码应变为:
return vAmbient + textureColor*(vDiffuseColor * Diff +vSpecularColor * Specular);
程序截图如下:
所谓逐顶点光照,简单地说就是在vetextshader中计算光照颜色,该过程将为每个顶点计算一次光照颜色,然后在通过顶点在多边形所覆盖的区域对像素颜色进行线形插值。现实中,光照值取决于光线角度,表面法线,和观察点(对于镜面高光来说)。具体实现时的shader代码如下:
//相关全局变量
shared float4x4 matWorldViewProj;
shared float4x4 matWorld;
shared float3 lightPosition;
shared float4 ambientLightColor;
shared float4 diffuseLightColor;
shared float4 specularLightColor;
shared float3 cameraPosition;
//VertexShader输出结构
struct VertexShaderOutput
{
float4 Position: POSITION;
float4 Color :COLOR0;
};
//PixelShader输入结构,只接受从VertexShader传来的颜色
struct PixelShaderInput
{
float4 Color:COLOR0;
};
VertexShaderOutput VertexDiffuseAndPhong(float3 position: POSITION,float3 normal : NORMAL )
{
VertexShaderOutputoutput;
//transform theinput position to the output
output.Position= mul(float4(position, 1.0), matWorldViewProj);
float3worldNormal = mul(normal, matWorld);
float4worldPosition = mul(float4(position, 1.0), matWorld);
worldPosition =worldPosition / worldPosition.w;
float3directionToLight = normalize(lightPosition - worldPosition.xyz);
floatdiffuseIntensity = saturate( dot(directionToLight, worldNormal));
float4 diffuse=diffuseLightColor * diffuseIntensity;
float3reflectionVector = normalize(reflect(-directionToLight, worldNormal));
float3directionToCamera = normalize(cameraPosition - worldPosition.xyz);
float4 specular= specularLightColor * pow(saturate(dot(reflectionVector,
directionToCamera)), 20);
output.Color =specular + diffuse + ambientLightColor;
output.Color.a= 1.0;
//return theoutput structure
return output;
}
float4 SimplePixelShader(PixelShaderInputinput) : COLOR
{
return input.Color;
}
technique PerVertexDiffuseAndPhong
{
pass P0
{
//set theVertexShader state to the vertex shader function
VertexShader = compile vs_2_0 VertexDiffuseAndPhong();
//set thePixelShader state to the pixel shader function
PixelShader= compile ps_2_0 SimplePixelShader();
}
}
由以上代码可见,各像素的颜色计算都是在VertexShader中实现的。程序截图如下:
当考虑光照时,大部分人都认为逐顶点光照已经足够好了。对于镶嵌度较高的模型来说是这样,但对某些多边形较少的模型来说却不一定。比如这个示例,球的多边形较少,可以明显看出棱角分明,高光效果也不理想。直接对顶点颜色进行插值所得的结果通常不够精确,特别是对面积较大的多边形来说。当处理高精度多边形模型时,由于每个多边形所覆盖的区域很小,因此插值之后每个像素的误差也很小,所以逐顶点光照可以工作的很好。而当处理低模时,这种误差就变的很大了。
逐像素光照
逐像素光照是对所有光照元素进行单独插值,简单地说就是在pixelshader中计算颜色。具体实现时的shader代码如下:
//全局变量
shared float4x4 matWorldViewProj;
shared float4x4 matWorld;
shared float3 cameraPosition;
shared float3 lightPosition;
shared float4 ambientLightColor;
shared float4 diffuseLightColor;
shared float4 specularLightColor;
struct VertexShaderOutputPerPixelDiffuse
{
float4 Position: POSITION;
float3WorldNormal : TEXCOORD0;
float3WorldPosition : TEXCOORD1;
};
struct PixelShaderInputPerPixelDiffuse
{
float3WorldNormal : TEXCOORD0;
float3WorldPosition : TEXCOORD1;
};
VertexShaderOutputPerPixelDiffuse PerPixelDiffuseVS(float3 position : POSITION, float3 normal : NORMAL )
{
VertexShaderOutputPerPixelDiffuse output;
//transform theinput 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 theoutput structure
return output;
}
float4 DiffuseAndPhongPS(PixelShaderInputPerPixelDiffuseinput) : COLOR
{
//calculateper-pixel diffuse
float3directionToLight = normalize(lightPosition - input.WorldPosition);
floatdiffuseIntensity = saturate( dot(directionToLight, input.WorldNormal));
float4 diffuse= diffuseLightColor * diffuseIntensity;
//calculatePhong components per-pixel
float3reflectionVector = normalize(reflect(-directionToLight, input.WorldNormal));
float3directionToCamera = normalize(cameraPosition - input.WorldPosition);
//calculatespecular component float4 specular = specularLightColor *
pow(saturate(dot(reflectionVector, directionToCamera)), 20);
//all colorcomponents are summed in the pixel shader
float4 color =specular + diffuse + ambientLightColor;
color.a = 1.0;
return color;
}
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是墙壁图案:
纹理2是光影图案:
两个纹理混合后的效果如图所示:
主要的shader代码如下:
float4x4 WorldViewProj : WORLDVIEWPROJECTION;
Texture Texture1;
Texture Texture2;
sampler TextureSampler1 = sampler_state
{
texture =
MipFilter = LINEAR;
MinFilter =LINEAR;
MagFilter =LINEAR;
};
sampler TextureSampler2 = sampler_state
{
texture =
MipFilter =LINEAR;
MinFilter =LINEAR;
MagFilter =LINEAR;
};
void TransformV1_1(in float4 inputPosition:POSITION,infloat2 inputTexCoord:TEXCOORD0, out float4
outputPosition:POSITION,outfloat2 outputTexCoord:TEXCOORD0, out float2
outputSecondTexCoord:TEXCOORD1)
{
outputPosition= mul(inputPosition,WorldViewProj);
outputTexCoord= inputTexCoord;
outputSecondTexCoord = inputTexCoord;
}
void TextureColorV1_1(in float4 P:POSITION,in float2textureCoords : TEXCOORD0, in float2 textureCoords2 :
TEXCOORD1,outfloat4 diffuseColor : COLOR0)
{
float4diffuseColor1 = tex2D(TextureSampler1, textureCoords);
float4diffuseColor2 = tex2D(TextureSampler2, textureCoords2);
diffuseColor=lerp(diffuseColor1,diffuseColor2,0.6f);
}
technique RenderScene
{
pass P0
{
VertexShader = compile vs_1_1 TransformV1_1();
PixelShader = compile ps_1_1TextureColorV1_1();
}
}
基本思路就是先用对应的采样器获取两纹理的颜色,接着使用lerp函数在两个颜色之间(前两个参数)进行线性插值,而第3个参数决定前2个参数在插值时的比例,你可以在源代码中试着调整这个参数看看效果。
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中实现纹理一样简单。
要处理纹理,我们需要建立一些被称为纹理采样器(texturesampler)的东西。纹理采样器,顾名思义,是用来设置纹理采样器状态的。这可以是纹理过滤(在我的例子中是Linear)的信息,以及纹理寻址方式,这可以是clamp(夹持),mirror(镜像),Wrap(包装)等。
要创建一个采样器,我们首先需要定义一个纹理采样器将使用的变量:
texture ColorMap;
现在,我们可以使用ColorMap创建一个纹理采样器:
sampler ColorMapSampler =sampler_state
{
Texture =
MinFilter = Linear; // enabled trilinear filtering for this texture
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp; // sets our texture to clamp
AddressV = Clamp;
};
这样,我们得到了一个纹理和对应这个纹理的采样器。在我们可以开始使用纹理前,我们需要在technique设置采样器stage:
technique NormalMapping
{
pass P0
{
Sampler[0] = (ColorMapSampler);
VertexShader = compile vs_1_1 VS();
PixelShader = compile ps_2_0 PS();
}
}
Ok,现在我们已经做好使用纹理的准备了!
由于我们使用的是pixelsshader将纹理映射到物体上,所以可以简单地创建一个叫做color的向量:
float4 Color;
并将这个值等于纹理坐标UV中的颜色值。这在HLSL中可以通过使用函数tex2D(s,t)很容易地做到,其中s是采样器,t是像素的纹理坐标。
Color = tex2D( ColorMapSampler, Tex);
// Tex 是pixel shader的输入,它来自于vertexshader的输出,就是纹理坐标。
纹理坐标?让我解释一下。纹理坐标存储在3维物体或模型中的二维坐标(U,V),用来将纹理映射到物体上,范围从0到1。有了纹理坐标,模型就可以将纹理分配到不同的位置,比如说将一个虹膜纹理放到一个人的模型的眼球部位,或一张嘴的纹理放在人脸上。
至于照明算法,将使用镜面高光。希望现在你对法线贴图所需的东西有了一个全面的了解。
实现Shader
这个shader与镜面反射光照最大的不同是我们使用切线空间替代了模型空间,并使用一个法线贴图获得法线方向计算光线。首先声明一些全局变量:
float4x4 matWorldViewProj ;
float4x4 matWorld ;
float4 vecLightDir ;
float4 vecEye ;
这里没有新的东西,接着创建颜色贴图和法线贴图的实例和采样器。
texture ColorMap;
sampler ColorMapSampler =sampler_state
{
Texture =
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
texture NormalMap;
sampler NormalMapSampler =sampler_state
{
Texture =
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
我们创建了颜色贴图的纹理实例和采样器。这些纹理在程序中通过参数设置,两个纹理都使用了三线过滤。 VertexShader返回的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); // V return Out;
}
我们还是首先转换位置。然后,创建一个3x3矩阵worldToTangentSpace用来将世界空间转换到切线空间。从vertexshader中我们获得了基于切线控件矩阵转换过的位置、光线和观察向量。如前所述,这是因为法线贴图是储存在切线空间中的,因此,要基于法线贴图计算正确的光线方向,应该在同一空间中计算所有的向量。
现在我们已经使向量在正确的空间中,可以准备实现Pixel Shader了。
pixelshader需要从颜色贴图获得像素的颜色,从法线贴图获得法线。做完这一切,我们就可以基于法线计算环境,漫反射和镜面反射的光照了。看一下pixelshader的代码:
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/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类。
Texture2D normalMap ;
现在,使用Content.Load函数载入texutre,假定您已经创建了一个法线贴图和颜色贴图:
object.colorMap =Content.Load
normalMap =Content.Load
将它们传递到shader,和传递其他参数的做法是一样的。
effect.Parameters["ColorMap"].SetValue(colorMap);
effect.Parameters["NormalMap"].SetValue(normalMap);
练习
1.改变不同的colormaps看看结果。
2.尝试不同的模型,比如一个立方体用于创建一块砖墙或石墙。
3.实现自由控制所有光线的值(环境,漫反射,镜面高光),并能启用或禁用不同的算法(提示:使用Boolean将不用的值设置为0)。这能让shader更酷和更灵活。
译者:我用的teapot模型,程序截图如上图所示,但在RenderMonkey中的截图(下图)比这个效果好得多,这个示例有缺陷吗?还是我设置不当,我猜可能的原因是.x文件并不包含切线数据,而RenderMonkey对于那些简单的球、方块、茶壶会自动生成切线数据,而在你的程序中需要自己实现,就像《ProfessionalXNA Game Programming: For Xbox 360 and Windows》中的那样编写自定义素材导入器产生切线,但我这个茶壶在DirectXViewer查看的确包含了切线数据和副法线数据,还没有解决。
XNA Shader编程教程5-变形
在前面的几个教程中我们介绍了几个光照算法,今天的教程相对比较短,是一个纯粹的vertexshader效果使物体变形。
使物体变形
由于vertex shader可以逐顶点地转换顶点,所以可以用来使物体/网格发生变形。举例来说,如果一个游戏能够让你创建自己的角色,包括改变皮肤颜色,眼睛颜色,头发,衣服等,我们就可以创建一个vertexshader设置角色的体形属性,将这个属性设为0说明它“瘦”而1说明“胖”。
胖/瘦
要实现这个功能,我们只需一个vertex shader让顶点沿着法线移动。如果我们将所有的顶点沿着法线移动,则物体将会变得更大或更小。
海浪
你也可以不用制作一个很大的骨骼动画网格去创建一个逼真的海面,而可以用一个vertexshader代替。要做到这一点,你需要一个很大的平面网格代表平静的海面。你可以在3ds中制作,或编程实现。这会需要许多顶点,而shader会根据正弦/余弦函数向上或向下移动这些顶点。
如图所示,我们定义了一个有很多顶点的平面。并使用Vertex Shader将所有顶点沿Y轴方向遵循正弦函数移动,可表示为:
f(y)=sin(y)
就是说顶点X是随pos.Y =sin(X.pos+time)这个规律运动的。这将产生海面的波浪。当然,现在还很简单,不是很漂亮。海面的实现有许多种不同的算法,所以如果你想深入研究,可在goolge上搜索一下。要使海浪更漂亮,你可以设置一个法线贴图在海面的大波浪上创建一些小的凹凸,也可以组合正弦波和余弦波制作更真实的海浪。
模拟谐振球
这就是这个教程要实现的东西,相当于瘦/胖算法和海浪算法的组合。该示例将使用一个球体对象,并设置一种正弦/余弦函数使顶点沿法线方向移动使其基于时间而变形。
实现Shader
这个shader只是一个VertexShader。pixel shader只处理基本光照使其看起来更真实。您可以自己添加法线映射获得更酷的效果。
在这个shader中需要一个时间变量,这样我们可以根据时间制作一个动画效果,然后我们只需设置大量的正弦和余弦函数使其看起来很酷。下面是VertexShader代码:
float4 g_fTime; 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 * f;
Pos.x += N.x * amp * f;
Pos.y += N.y * amp * f;
Out.Pos =mul(Pos, matWorldViewProj);
Out.N = mul(N, matWorld);
float4 PosWorld = mul(Pos, matWorld);
Out.L = vecLightDir;
Out.V = vecEye - PosWorld;
return Out;
}
Shader计算了振度和频率以找到一个平滑值,而顶点可以移动到这个点上。
XNA Shader编程教程6-Shader演示
本教程不学新的东西,只是将不同shader技术整合到一个场景中,展示shader的威力。
请不要着重于源代码,而应把注意力放在shader上。也可以让性能更好,但这个示例把关键点都涉及到了。
使用手柄摇杆可以四处走动,按A/X切换shader的开启和关闭。要运行这个场景,你应该把X360手柄连接到USB接口上,或者取消代码中键盘控制代码的注释,这样你就可以使用键盘和鼠标控制相机的移动。
天空球(skysphere)
这个场景使用一个简单的球作为天空球,并使球缓慢地转动让天空更加生动。要使效果更好,可以添加更多天空纹理在球上并以不同的速度旋转天空球。
小岛
小岛是一个三维模型。小岛使用一个法线贴图和教程4中相同的shader技术,能使模型看起来有更多的细节。
海面
海洋是一个由许多顶点构成的平面,我们使用教程5中的变形shader创建波浪,并使用教程4中的法线贴图产生海面上的涟漪。我使用了两个沿不同方向移动的法线贴图产生波浪上的小细节:
Normal = (Normal 1+ Normal2)/2;
这里使用两个法线并将它们叠加在一起,在计算漫反射和镜面反射时取它们的平均值。还移动了颜色贴图的纹理坐标产生流水效果。
XNA Shader编程教程7-卡通渲染
今天,我将讨论一个简单的算法,可以通过使用Cel shading/Toonshading渲染一个非真实感的场景。
要实现这个效果,你需要两个shaders:
(a)Toon shader会根据纹理添加光线,使用的是教程2讨论国的过的漫反射算法。
(b)一个postprocess edge detection(离屏边缘检测)算法。
首先,我们使用Toon shader将场景渲染到渲染目标(rendertarget),然后将这个纹理在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);
而2D纹理(分辨率32x1)的CelMapSampler采用器如下图所示:
如果L和N垂直(点乘=0),那么将使用坐标为(0.0,0.0)的纹理像素。如果N和L平行(点乘=1),那么将使用坐标为(1.0,0.0)的纹理像素,其他像素在0.0~1.0的范围内。如你所见,纹理只有3种不同的颜色。要从pixelshader返回CelColor,output将使用指定纹理作为漫反射shader:
但是我们仍需要纹理,这可以通过在教程2中同样的方法实现,只不过不是用纹理的颜色乘以漫反射颜色,而是用toon-shaded漫反射映射的CelColor乘以纹理颜色:
return(Ai*Ac*Color)+(Color*Di*CelColor);
最终结果如下图所示:
不是很难吧?:)看起来不错,但在某些情况下,我们可能还希望在图像边缘有黑边。要实现这一点:
一个办法是第一次绘制对象的全黑的图像,然后绘制对象的cel-shaded图像,但小一点点;
另一个办法是将场景渲染到一个纹理,然后对这个纹理应用边缘检测shader,这也是本教程的做法。
后屏边缘检测(Postprocess Edge Detection)
边缘检测shader使用下列矩阵作为核心过滤器:
核心过滤器的工作原理是将该矩阵作用到图像中的每个像素。核心包含乘法因子应用于像素和它的临近像素。当所有的值相乘后,像素就会被替换成乘积的总和。选择不同的核心,可应用不同的过滤。
使用这个shader可以创建一个黑白纹理,其边缘是黑色的,其余是白色的。这使得创建一个具有边缘的普通场景很容易:
Color*result.xxxx;
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)
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– 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– 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,使它的值为0.0。如果是白色的,Specular将保持不变,而镜面反射程度取决于GlossMapColor的灰度。
知道了这一点,我们就可以实现光照方程了:
return vAmbient +vDiffuseColor * ColorMapColor * Diff + vSpecularColor * Specular;
在这个例子中我还增加了一个颜色纹理让效果更明显:)
何时使用光泽映射?
只要你想控制反射,就可以使用光泽映射!比如一个生锈的铁杯在某些部位仍会反光,而生锈的部位反光较少。旧汽车,光滑/粗糙的冰面等情况也适用。
.1 颜色,发光,凹凸和反射纹理贴图
颜色贴图
我们想在一个模型上贴上一张纹理,这个例子中是将一张地球纹理贴在一个球上。你要做的就是将颜色贴图作为模型的漫反射颜色,所以我们获取漫反射值并将它乘以纹理颜色获取最终的颜色。我们需要World *View * Projection矩阵和World矩阵,这里我仍然包含了环境光,但没有用它,只使用了默认值,因为我们要用到漫反射光,所以需要知道光线方向。下面有两个新东西:texture和sampler。其中texture是传递给shader的纹理的参数,这个例子中是ColorMap。Sampler用在pixelshader中获取纹理的像素。
float4x4 wvp :WorldViewProjection;
float4x4 world :World;
floatAmbientIntensity = 1;
float4 AmbientColor: AMBIENT = float4(0,0,0,1);
float3LightDirection : Direction = float3(0,1,1);
texture ColorMap :Diffuse;
samplerColorMapSampler = sampler_state
{
texture =
};
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传递到pixelshader。
VS_OUTVS_ColorMap(VS_IN input)
{
VS_OUT output = (VS_OUT)0;
output.Position = mul(input.Position,wvp);
output.Light = LightDirection;
output.TexCoord = input.TexCoord;
output.Normal = mul(input.Normal,world);
return output;
}
在pixel shader中我们计算了光线方向、漫反射和环境光颜色,但我们从当前像素的采样器中获取当前像素的纹理坐标,并乘以刚才计算的漫反射值调整像素的颜色,接着加上环境光颜色获得像素的最终颜色。
PS_OUTPS_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 *AmbientColor;
texCol *= Diffuse;
output.Color = Ambient + texCol;
return output;
}
发光贴图(Glow Map)
我们可以在shader中添加另一个纹理和采样器来使用发光贴图。
texture GlowMap :Diffuse;
samplerGlowMapSampler = sampler_state
{
texture =
};
Shader结构并没有变化,我们只是在pixel shader 中加入了发光贴图的计算。发光贴图将贴图中的像素颜色乘以(1-Diffuse),pixelshader代码如下:
PS_OUTPS_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 *AmbientColor;
float4 glow = glowCol *saturate(1-Diffuse);
texCol *= Diffuse;
output.Color = Ambient + texCol + glow;
return output;
}
程序截图如下:
凹凸贴图(Bump Map)
要让凹凸贴图工作正常你必须先对shader和模型做件事。首先要求模型有“切线”数据,切线用来产生正确的“切线空间”,Wolfgang比我解释得清楚得多,建议你去买一本他的书。要让素材管道知道我们想让模型使用切线,只需选择模型,在属性面板中展开ContentProcessor并将Generate Tangent Frame 这个选项设置为True,默认是false。
现在当模型被编译后(如果Vertex Channel不正确你会得到一个编译错误,仍得不到切线数据)就会获取切线数据。我们为凹凸贴图添加一个纹理:
texture BumpMap ;
samplerBumpMapSampler = sampler_state
{
Texture =
};
结构也要改变:
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;
};
我们需要将切线数据添加到VS_IN结构中,这样我们可以使用新生成的切线数据,在VS_OUT中移除Normal,因为我们将在vertexshader中使用世界切线空间转换光线,使用凹凸(法线)贴图获取法线。
vertex shader如下:
VS_OUTVS_ColorGlowBump(VS_IN input)
{
VS_OUT output = (VS_OUT)0;
output.Position = mul(input.Position,wvp);
float3x3 worldToTangentSpace;
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.TexCoord;
return output;
}
如你所见,我们用前面相同的方式转换了位置,接着计算切线空间矩阵,并用这个矩阵转换光线方向。
PS_OUTPS_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 *AmbientColor;
float4 glow = glowCol * saturate(1-Diffuse);
texCol *= Diffuse;
output.Color = Ambient + texCol + glow;
return output;
}
在pixel shader中我们从凹凸贴图中获得法线,归一化光线方向,然后用前面相同的方式计算漫反射,只不过这次是使用从切线空间产生的数据。
反射贴图(ReflectiveMap)
这个贴图与颜色和发光贴图一样简单,我们通过一个反射贴图实现一个镜面高光,再次添加一个纹理和采样器管理反射贴图:
textureReflectionMap : Diffuse;
samplerReflectionMapSampler = sampler_state
{
texture =
};
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;
};
接着在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 * AmbientColor;
float4 glow = glowCol * saturate(1-Diffuse);
texCol *= Diffuse;
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 + specCol;
return output;
}
截图如下:
另外,如果你看一下图片的话会发现我在属性面板的Content Processor中将ResizeTo Power of Two设置成了True,这样做对图像处理更友好。
XNA Shader编程教程系列9- Postprocess波动效果
Post processing?
Post processing将一个effect,或effect的组合作用到一个图像/帧/视频上,让他们看起来效果更酷,Postprocessing可以看作对场景施加一个滤镜。
这个例子我们将场景绘制到一张纹理,然后施加波动效果。这让场景看起来象是在水下一样。
要实现这一点,我们需要使用某种圆周运动改变纹理坐标向量,然后使用这个改变后的纹理坐标载入在ColorMapSampler中的颜色!截图如下:
图中的绿点之是表示某个纹理坐标,我们基于时间让它旋转,对所有纹理坐标都施加这个动作就会实现我们想要的波动效果!
本例中的场景包含一张使用SpriteBatch的背景,使用漫反射光照的三维模型。这个场景被渲染到一个纹理,然后使用这个纹理绘制场景。当绘制纹理时,我们添加了postprocess shader。因此,我们使用两个shader。一个用于场景中的物体,一个用于postprocess shader。
实现shader
post process shader只需要用到pixelshader,要实现动态shader,需要定义一个计时器。
float fTimer;
这个计时器由应用程序设置,将用于每个纹理坐标的sin/cos运动,使它们可以旋转!:)我们还需要ColorMap纹理包含场景,并在每一帧进行更新。
samplerColorMapSampler : register(s0);
做好准备后,就可以看看Pixel Shader函数了:
float4PixelShader(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 Color;
}
这个shader只是简单地让当前纹理坐标的X和Y分量旋转。在sin中的fTimer+Tex.x使Tex.x在每帧沿X方向变化,同理也发生在Y方向。如果我们使用sin(fTimer)/cos(fTimer)代替Tex.x/Tex.y,所有的纹理坐标将会向着同一方向旋转。你可以尝试一下更好地理解这些参数。
最后,我们需要名为PostProcess的technique:
techniquePostProcess
{
pass P0
{
PixelShader = compile ps_2_0PixelShader();
}
}
使用shader
将shader加入到我们想要的任何场景中很简单,只需将场景渲染到一个纹理中:
RenderTarget2DrenderTarget;
renderTarget = newRenderTarget2D(graphics.GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight,
1,graphics.GraphicsDevice.DisplayMode.Format);
graphics.GraphicsDevice.SetRenderTarget(0,renderTarget);
// Render our scene
graphics.GraphicsDevice.SetRenderTarget(0,null);
SceneTexture =renderTarget.GetTexture();
这里的SceneTexture是一个Texture2D对象。现在我们需要显示SceneTexture并将postprocess 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, newRectangle(0, 0, 800, 600), Color.White);
effectPostOutline.CurrentTechnique.Passes[0].End();
}
}
effectPostOutline.End();
}
spriteBatch.End();
好了!现在我们得到了一个非常简单但又很酷的post process effect。可以试着改变每个纹理坐标运动的方式,你可以得到一个很酷的失真效果。
XNA Shader编程系列教程10-Postprocess图片反相
这个shader很简单,但我还有很多问题,所以我决定写一篇很短的教程。要从一个纹理采用器获取颜色,你通常这样做:
float4 Color =tex2D(ColorMapSampler, Tex);
要获取反相颜色(即补色),你只需用1减去颜色的每个通道的值就可以了:
float4 ColorInverse= 1.0f - tex2D(ColorMapSampler, Tex);
实现shader
它很短,所以我只写shader代码了:
samplerColorMapSampler : register(s0);
// Negative image
float4PixelShader(float2 Tex:TEXCOORD0) : COLOR
{
float4 Color = 1.0f - tex2D(ColorMapSampler, Tex);
// Keep our alphachannel at 1.
Color.a = 1.0f;
return Color;
}
techniquePostProcess
{
pass P0
{
// A post process shader only needs apixel shader.
PixelShader = compile ps_2_0PixelShader();
}
}
首先,我们从采样器中用1减获取相反的值,但这样做把alpha通道也相反了,如果这正是你想要的,那无关紧要,但如果你想保持原始图片的alpha值,你必须将它恢复回来,或手动地设置某个值。在这个例子中,我们将alpha设置为1,即图片是不透明的。
XNA Shader编程教程11-Postprocess灰度图
此教程是建立在教程9.1基础上的。如果你还没理解教程9.1,请先弄懂它。
要制作一张黑白图片或场景,我们需要将场景纹理转换成它的反相颜色,这可以在一个postprocess pixel shader中实现。一张黑白图片只由一些灰度的像素组成,所以我们的shader必须将一个颜色变成灰度。有几个方法可以使用,我将介绍这些方法中的两个。
一种方法是将颜色的三个通道相加并除以3,这可以获得颜色平均值,并将各个颜色通道设置为这个平均值:
Color.rgb = (Color.r + Color.g + Color.b)/3;
这将把一个像素的所有颜色通道都设置为同一个值,结果不错,但如果你仔细观察,它是不正确的!让我们看一下另一种方法。
人的眼睛对绿色比红色和蓝色更加敏感,将图片转换为灰度的一种常用方法是使用一个设置好的值,这个值表示三个颜色通道的灰度强度/权重:
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中两种方法都使用了。
sampler ColorMapSampler : register(s0);
float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR
{
float4 Color = tex2D(ColorMapSampler, Tex);
Color.rgb = (Color.r + Color.g + Color.b)/3;
// Keep our alphachannel at 1.
Color.a = 1.0f;
return Color;
}
technique PostProcess
{
pass P0
{
PixelShader = compile ps_2_0 PixelShader();
}
}
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 Color;
}
technique PostProcess
{
pass P0
{
PixelShader = compile ps_2_0 PixelShader();
}
}
XNA Shader编程教程12- Postprocess噪点/扭曲
此教程是建立在教程9.1基础上的。如果你还没理解教程9.1,请先弄懂它。
要在场景中添加噪点/扭曲,你必须在纹理坐标中添加一个扭曲值,然后使用新的纹理坐标查询纹理采样器。我们还想实现噪点动画,所以还需要一个timer作用在扭曲值上。我们还需要一个值表示扭曲程度,一个seed用于扭曲算法。
实现shader
首先要在shader中定义一些全局变量:
// This will use thetexture bound to the object( like from the sprite batch ).
samplerColorMapSampler : register(s0);
// A timer toanimate our shader
float fTimer;
// the amount ofdistortion
float fNoiseAmount;
// just a randomstarting number
int iSeed;
ColorMapSampler是渲染的场景,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);
上面的代码只是一个使用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 tothe object( like from the sprite batch ).
sampler ColorMapSampler :register(s0);
// A timer to animate our shader
float fTimer;
// the amount of distortion
float fNoiseAmount;
// just a random starting number
int iSeed;
// 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 distortionfactor 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 factor
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 Color;
}
technique PostProcess
{
pass P0
{
// A post process shaderonly needs a pixel shader.
PixelShader = compile ps_2_0PixelShader();
}
}
XNA Shader编程教程13-Alpha映射
这个教程我们将实现一个简单但重要的shader:Alpha贴图!当你想绘制一个部分透明的3D物体时使用Alpha贴图是很有用的。比如一个有窗框的窗户,可以让窗框不透明而使其他部分透明!你可以在很多场合使用Alpha贴图,比如冰面、皮肤、花、昆虫翅膀等。
Alpha映射
这个shader的基本思路是使用一张纹理:alpha贴图。这个纹理是一张灰度图,其中黑色表示完全透明,灰色表示介于透明和不透明之间,而白色表示完全不透明。你可以将这张纹理想象成一张包含物体透明信息的图片,其中的灰度颜色代表透明百分比。颜色0.0(黑色)表示透明,颜色0.5(灰色)表示半透明,而1.0(白色)表示不透明。
所以shader需要两张纹理:
ColorMap代表物体颜色,AlphaMap代表透明度。
实现Shader
本教程只使用Diffuse Shader,并支持Alpha映射,当然你也可以在shader中实现任何你想要的效果。首先定义两个纹理:ColorMap,Alpha Map。并添加到shader中:
texture ColorMap;
samplerColorMapSampler = sampler_state
{
Texture =
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Mirror;
AddressV = Mirror;
};
texture AlphaMap;
samplerAlphaMapSampler = sampler_state
{
Texture =
MinFilter = Linear;
MagFilter = Linear;
MipFilter =Linear;
AddressU = Mirror;
AddressV = Mirror;
};
然后设置color的alpha通道并返回存储在Alpha贴图中的值,这一步在PixelShader中进行:
Color =(Ai*Ac*Color)+(Color*Di*Dd);
Color.a = tex2D(AlphaMapSampler, Tex).r;
return Color;
这里我们和以前一样计算了漫反射颜色。接着处理Colors.a分量,这个分量是Colors的alpha通道,并将这个分量设置为alpha贴图中的值。Alpha贴图中的所有分量都是相同的(因为它是一张灰度图),所以你用r、g、b通道皆可,我使用了r通道:Technique代码如下:
techniqueDiffuseShader
{
pass P0
{
AlphaBlendEnable = True;
SrcBlend = SrcAlpha;
DestBlend = InvSrcAlpha;
Sampler[0] = (ColorMapSampler);
Sampler[1] = (AlphaMapSampler);
VertexShader = compile vs_2_0VertexShader();
PixelShader = compile ps_2_0PixelShader();
}
}
如你所见,我们将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, newRectangle(0, 0, 800,600), Color.White);
}
spriteBatch.End();
XNA Shader编程教程14-透射
上个教程我们使用了alpha贴图和alpha通道让物体变得透明,这次我们将通过实现透射(transmittance)更深入地学习透明。
透射(Transmittance)
像玻璃、水、水晶、空气等物体会在光线穿过它们时吸收一定的光线,在教程13中,我们使用alpha贴图使物体透明并使用颜色为RGB(0.5,0.5,0.5)的alpha贴图创建了一个透明球,这个方法能用在很多场合,但这样做会使透明效果太平淡。
真实世界中的物体,比如说玻璃球,当光线穿过它时还会吸收/散射一定的光线,光线进入玻璃球越深,在光线穿出前吸收和散射的越多,这叫做透射 ( wikipedia)。要计算透射( T ),我们可以使用Beer-Lamberts定律( wikipedia)处理透射的光线,让我们看一下Beer-Lamberts定律更好地理解原理!(译者注:在计算漫反射时我们使用的是Lambert定律,这个定律又叫做Lambert余弦定律,简单地说就是反射强度是法线和入射光方向的点乘,Lambert全名JohannHeinrich Lambert,生于1728年8月26日,死于1728年九月25日,是瑞士数学家、物理学家和天文学家,而Beer-Lamberts定律是光线透射的规律,这个规律是由PierreBouguer在1729年前发现的,但常常被误认为是Lambert 发现的,事实上Lambert只是于1760年引用了Bouguer的文章,1852年AugustBeer改进了这个规律)
T=e-a′cd(公式1)
这个公式中的T表示透射,a'是吸收因子,c是物体的浓度(译者:原文是consistensy,没这个单词,怀疑是consistency),d是物体的厚度。所以要使用这个公式,我们需要知道a'、c和d。
先来看c。c控制光线的吸收程度,这个值可以设置为大于0的任何值。然后是a',我们可以通过公式1的变形计算a':
(公式2)
公式2中的T是透射中最暗的颜色,对应最远的距离。
最后获取d。这个值设置物体的厚度。
本教程中,我们将计算任何简单物体(不包含孔或突起物,如球,简单玻璃形状等)的c。这个shader很复杂,因为我们将在后面的教程中也要正确运行。
现在我们获取计算给定点的T的所有变量,可以使用T表示光线的吸收程度,这可以通过将T乘以光线(透射后的像素)的混合颜色做到。
那么我们如何计算每根光线在透射体中的前进距离?可以使用深度缓冲 ( wikipedia )!
深度缓冲(即Z缓冲)可以看成一张包含场景的灰度图,灰度值表示物体距离相机的远近。所以,本文最开始的一张图片我们看到的是一个复杂的玻璃物体,而场景深度缓冲看起来应像下图所示:
深度缓冲需要正确的介于近裁平面和远裁平面的值,最完美的是近裁平面是透射体的最近顶点,远裁平面是最远顶点。
知道了这些,我们就可以通过使用两个深度缓冲纹理找到任何角度对应的透射体的厚度。通过使用剔除,我们可以在一个深度缓冲纹理中绘制透射体的前表面,使用另一个深度缓冲纹理中绘制透射体的后表面。下面两张图显示了这两个不同的纹理:
在一个深度缓冲纹理中的后表面
在另一个深度缓冲纹理中的前表面
最后的纹理
灰度值显示了光线能在透射体中前进多远,白色代表长而黑色代表短或不前进。
实现Shader
本教程使用三个technique。一个只是处理镜面反射,另一个将场景绘制到一张深度纹理,第三个是postprocess shader将透射效果应用到物体上。我们并不想让场景中的所有物体都有透射效果。所以使用postprocess shader时,我们首先将没有透射体的场景绘制到一张纹理(背景纹理),然后在第二个pass中单独绘制透射体,最后将在postprocess shader中把两者组合起来。首先处理镜面反射:
float4x4matWorldViewProj;
float4x4matInverseWorld;
float4vLightDirection;
float4 vecLightDir;
float4 vecEye;
float4vDiffuseColor;
float4vSpecularColor;
float4 vAmbient;
texture ColorMap;
samplerColorMapSampler = sampler_state
{
Texture =
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
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 = Tex;
Out.L = normalize(vLightDirection);
Out.N = normalize(mul(matInverseWorld, N));
Out.V = vecEye - Pos;
return Out;
}
float4PixelShader(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 * Specular;
}
techniqueEnvironmentShader
{
pass P0
{
VertexShader = compile vs_2_0VertexShader();
PixelShader = compile ps_2_0PixelShader();
}
}
接着是Depth Textureshader。这个shader只将场景绘制成灰度,每个顶点/像素的深度用一个介于0.0至1.0之间的值表示,1.0表示最靠近相机而0.0 表示在远裁平面(Pos.w)。所以要获取顶点深度值,我们只需获取顶点的Z值,将Z值除以W值使深度值在投影矩阵的近裁平面和远裁平面之间。vertex-shader计算两个值:Position和Distance。
struct OUT_DEPTH
{
float4 : POSITION;
float Distance : TEXCOORD0;
};
下面就可以实现Depth texturevertex shader:
OUT_DEPTHRenderDepthMapVS(float4 vPos: POSITION)
{
OUT_DEPTH Out;
// Translate the vertex usingmatWorldViewProj.
Out.Position = mul(vPos, matWorldViewProj);
// Get the distance of the vertex betweennear and far clipping plane in matWorldViewProj.
Out.Distance.x =1-(Out.Position.z/Out.Position.w);
return Out;
}
首先我们将顶点乘以world*view*projection矩阵进行转换。然后将距离值设置为正确的深度值,这可以通过Position.z/ Position.w得到,让我们获得了介于近裁平面和远裁平面之间的深度值。下面是pixelshader!我们将OUT_DEPTH中的Distance值转换到纹理,以便接下来使用:
float4RenderDepthMapPS( OUT_DEPTH In ) : COLOR
{
return float4(In.Distance.x,0,0,1);
}
下面是technique:
techniqueDepthMapShader
{
pass P0
{
ZEnable= TRUE;
ZWriteEnable= TRUE;
AlphaBlendEnable= FALSE;
VertexShader= compile vs_2_0 RenderDepthMapVS();
PixelShader= compile ps_2_0 RenderDepthMapPS();
}
}
Technique中没有新的东西,只是打开Z缓冲,并使之可写。最后是透射的postprocess shader!首先我们需要作为背景的场景纹理、透射体场景(包含所以具有透射效果的物体的纹理)和两个深度纹理!
texture D1M;
sampler D1MSampler =sampler_state
{
Texture =
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
texture D2M;
sampler D2MSampler =sampler_state
{
Texture =
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
texture BGScene;
samplerBGSceneSampler = sampler_state
{
Texture =&kt;BGScene>;
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
texture Scene;
sampler SceneSampler= sampler_state
{
Texture =
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
DM1是第一张深度贴图纹理,包含透射体的后表面。DM2是第二张深度贴图纹理,包含透射体的前表面,BGScene包含背景,Scene包含透射体场景/颜色。然后添加两个变量,一个包含用于计算吸收因子的距离因子,另一个包含透射体的稠度:
float Du = 1.0f;
float C = 12.0f;
因为这个一个post process shader,所以无需vertexshader只用到pixel shader:
float4PixelShader(float2 Tex:TEXCOORD0) : COLOR
{
float4 Color=tex2D(SceneSampler, Tex);
float4 BGColor=tex2D(BGSceneSampler, Tex);
float depth1=tex2D(D1MSampler, Tex).r;
float depth2=tex2D(D2MSampler, Tex).r;
}
没有新东西,我们从不同的纹理获取像素的颜色,深度纹理shader返回depth1和depth的r通道。再次看一下公式:
T=e-a′cd (公式1)
(公式2)
我们只计算了c变量和distance变量,让我们解决剩余的。变量d包含透射体的厚度,能够简单地使用depth1和depth2计算得出:
float distance =((depth2-depth1));
depth2 and depth1的差就是物体的厚度!
T变量用来找到公式2中的最大吸收因子,它包含最暗的颜色。这可以是一个颜色的硬编码,或作为一个参数传递到shader。在本教程中,我们使用Color(透射体,绘制到一个纹理)中的一个值,最后通过公式2得到最后的吸收因子a′:
float3 a;
a.r =(-log(Color.r))/Du;
a.g =(-log(Color.g))/Du;
a.b =(-log(Color.b))/Du;
这让我们获得了最终的变量T!
float4 T;
T.r = exp((-a.r)*C*distance)+0.000001;
T.g =exp((-a.g)*C*distance)+0.000001;
T.b =exp((-a.b)*C*distance)+0.000001;
T.w = 1;
我们使用公式1在每个颜色通道中计算透射值。要避免T值为零(使物体变得全黑),我们在每个通道加0.000001。完成这步后,我们就可以提取透射体背后(光线能穿过透射体)的像素并将它乘以T,最后从pixelshader中返回这个值:
return T*BGColor;
最后是technique:
techniquePostProcess
{
pass P0
{
// A post process shader only needs apixel shader.
PixelShader = compile ps_2_0PixelShader();
}
}
这个shader虽然长但不是非常复杂,但应该是教程至今最高级的一个了。如果你不理解,试着改变一下参数看看每个量是如何工作的。完整的shader如下:
// Global variables
float Du = 1.0f;
float C = 12.0f;
// This will use thetexture bound to the object( like from the sprite batch ).
samplerColorMapSampler : register(s0);
texture D1M;
sampler D1MSampler =sampler_state
{
Texture =
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
texture D2M;
sampler D2MSampler =sampler_state
{
Texture =
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
texture BGScene;
samplerBGSceneSampler = sampler_state
{
Texture =&kt;BGScene>;
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
texture Scene;
sampler SceneSampler= sampler_state
{
Texture =
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
// Transmittance
float4PixelShader(float2 Tex:TEXCOORD0) : COLOR
{
float4 Color=tex2D(SceneSampler, Tex);
float4 BGColor=tex2D(BGSceneSampler, Tex);
float depth1=tex2D(D1MSampler, Tex).r;
float depth2=tex2D(D2MSampler, Tex).r;
float distance = ((depth2-depth1));
float3 a;
a.r = (-log(Color.r))/Du;
a.g = (-log(Color.g))/Du;
a.b = (-log(Color.b))/Du;
float4 T;
T.r = exp((-a.r)*C*distance)+0.000001;
T.g = exp((-a.g)*C*distance)+0.000001;
T.b = exp((-a.b)*C*distance)+0.000001;
T.w = 1;
return T*BGColor;
}
techniquePostProcess
{
pass P0
{
// A post process shader only needs apixel shader.
PixelShader = compile ps_2_0PixelShader();
}
}
使用shader
最后,看看如何使用shader并设置深度缓冲。我们从定义渲染目标和渲染纹理开始:
RenderTarget2DdepthRT; DepthStencilBuffer depthSB;
RenderTarget2DdepthRT2;
DepthStencilBufferdepthSB2;
Texture2Ddepth1Texture;
Texture2Ddepth2Texture;
我们使用两个RenderTarget2D,两个DepthStencilBuffer和两个纹理存储深度纹理。如果想简化过程也可以只使用一个DepthStencilBuffer。在这个shader中,我们要设置两个变量设置使用哪个technique:
EffectTechniqueenvironmentShader;
EffectTechniquedepthMapShader;
我们还要设置距离因子用于计算吸收和透射体的稠度:
float Du = 1.0f;
float C = 12.0f;
现在可以制作场景和使用shader了。在LoadContent方法中我们需要初始化和创建渲染目标:
// Create our rendertargets
PresentationParameterspp = graphics.GraphicsDevice.PresentationParameters;
renderTarget = newRenderTarget2D(graphics.GraphicsDevice, pp.BackBufferWidth,pp.BackBufferHeight,
1,graphics.GraphicsDevice.DisplayMode.Format);
depthRT = newRenderTarget2D(graphics.GraphicsDevice, pp.BackBufferWidth,pp.BackBufferHeight,
1,SurfaceFormat.Single);// 32-bit float format using 32 bits for the red channel.
depthRT2 = newRenderTarget2D(graphics.GraphicsDevice, pp.BackBufferWidth,
pp.BackBufferHeight,1, SurfaceFormat.Single);// 32-bit float format using 32 bits for the redchannel.
还需要DepthStencilBuffers:
depthSB =CreateDepthStencil(depthRT, DepthFormat.Depth24Stencil8);
depthSB2 =CreateDepthStencil(depthRT2, DepthFormat.Depth24Stencil8);
上面的代码使用渲染目标创建两个DepthStencilBuffers,将深度格式设置为Depth24Stencil8,将DepthBuffer通道设置为24bit,模板缓冲(stencilbuffer)通道设置为8-bit。下面是DepthFormat的列表:
Depth15Stencil1 |
16-bit depth-buffer bit depth,其中15 bits保留给depth通道,1 bit用于stencil通道。 |
Depth16 |
16-bit depth-buffer bit depth |
Depth24 |
32-bit depth-buffer bit depth,使用24 bits的depth通道 |
Depth24Stencil4 |
32-bit depth-buffer bit depth使用24 bits的depth通道,4bits的stencil通道 |
Depth24Stencil8 |
non-lockable格式,包含24 bits depth(以24-bit 浮点数格式 − 20E4),8 bits的stencil |
Depth24Stencil8Single |
32-bit depth-buffer bit depth,使用24 bits的depth通道,8bits的stencil通道 |
Depth32 |
32-bit depth-buffer bit depth |
Unknown |
未知格式 |
我们使用了两个自定义函数创建深度缓冲,第一个函数CreateDepthStencil(RenderTarget2Dtarget) 使用传入的渲染目标创建DepthStencilBuffer:
privateDepthStencilBuffer CreateDepthStencil(RenderTarget2D target)
{
return new DepthStencilBuffer(target.GraphicsDevice,target.Width,target.Height,
target.GraphicsDevice.DepthStencilBuffer.Format, target.MultiSampleType,target.MultiSampleQuality);
}
第二个函数CreateDepthStencil(RenderTarget2Dtarget, DepthFormat depth)检查计算机支持格式并使用CreateDepthStencil(RenderTarget2D target) 创建DepthStencilBuffer:
privateDepthStencilBuffer CreateDepthStencil(RenderTarget2D target, DepthFormat depth)
{
if(GraphicsAdapter.DefaultAdapter.CheckDepthStencilMatch(DeviceType.Hardware,
GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Format,target.Format,depth))
{
return newDepthStencilBuffer(target.GraphicsDevice, target.Width,target.Height, depth,target.MultiSampleType,
target.MultiSampleQuality);
}
else
return CreateDepthStencil(target);
}
下一步需要将shader中的techniques存储在变量中:
// Get ourtechniques and store them in variables.
environmentShader =effect.Techniques["EnvironmentShader"];
depthMapShader =effect.Techniques["DepthMapShader"];
因为要多次用到渲染场景,所以我还将渲染场景的代码移至一个函数中:
void DrawScene(booltransmittance)
{
// Begin our effect
effect.Begin(SaveStateMode.SaveState);
// A shader can have multiple passes, besure to loop trough each of them.
foreach (EffectPass pass ineffect.CurrentTechnique.Passes)
{
// Begin current pass
pass.Begin();
foreach (ModelMesh mesh inm_Model.Meshes)
{
foreach (ModelMeshPart part inmesh.MeshParts)
{
// calculate our worldMatrix..
worldMatrix = bones[mesh.ParentBone.Index]* renderMatrix;
// Render our meshpart
graphics.GraphicsDevice.Vertices[0].SetSource(mesh.VertexBuffer,part.StreamOffset, part.VertexStride);
graphics.GraphicsDevice.Indices= mesh.IndexBuffer;
graphics.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList,
part.BaseVertex, 0,part.NumVertices,part.StartIndex, part.PrimitiveCount);
}
}
// Stop current pass
pass.End();
}
// Stop using this effect
effect.End();
}
我们使用镜面反射shader(EnvironementShader)绘制透射体,使用深度缓冲shader(DepthMapShader)进行深度缓冲:
// create depth-map1
effect.CurrentTechnique= depthMapShader;
GraphicsDevice.RenderState.CullMode= CullMode.CullClockwiseFace;
depth1Texture =RenderDepthMap(depthSB,depthRT);
// create depth-map2
effect.CurrentTechnique= depthMapShader;
GraphicsDevice.RenderState.CullMode= CullMode.CullCounterClockwiseFace;
depth2Texture =RenderDepthMap(depthSB2, depthRT2);
// render ourtrasmitting objects
graphics.GraphicsDevice.SetRenderTarget(0,renderTarget);
graphics.GraphicsDevice.Clear(Color.White);
effect.CurrentTechnique= environmentShader; DrawScene(true);
graphics.GraphicsDevice.SetRenderTarget(0,null);
SceneTexture =renderTarget.GetTexture();
现在我们已经有了所有的纹理,做好了进行post processtransmittance shader的准备。本教程中我只使用一张纹理存储场景背景。你可能注意到,我们使用了一个自定义函数RenderDepthMap绘制DepthMap。这个函数只是设置将渲染目标设置为传入的DepthStencilBuffer,绘制场景,恢复旧的渲染目标状态并将DepthBuffer 作为一张纹理返回:
private Texture2DRenderDepthMap(DepthStencilBuffer dsb, RenderTarget2D rt2D)
{
GraphicsDevice.RenderState.DepthBufferFunction= CompareFunction.LessEqual;
GraphicsDevice.SetRenderTarget(0,rt2D);
// Save ourDepthStencilBuffer, so we can restore it later DepthStencilBuffer
saveSB =GraphicsDevice.DepthStencilBuffer;
GraphicsDevice.DepthStencilBuffer= dsb;
GraphicsDevice.Clear(Color.Black);
DrawScene(true);
// restore old depth stencilbuffer
GraphicsDevice.SetRenderTarget(0,null);
GraphicsDevice.DepthStencilBuffer= saveSB;
return rt2D.GetTexture();
}
最后,使用transmittanceshader生成最后的场景:
spriteBatch.Begin(SpriteBlendMode.None,SpriteSortMode.Immediate, SaveStateMode.SaveState);
{
// Apply the post processshader
effectPost.Begin();
{
effectPost.CurrentTechnique.Passes[0].Begin();
{
effectPost.Parameters["D1M"].SetValue(depth1Texture);
effectPost.Parameters["D2M"].SetValue(depth2Texture);
effectPost.Parameters["BGScene"].SetValue(m_BGScene);
effectPost.Parameters["Scene"].SetValue(SceneTexture);
effectPost.Parameters["Du"].SetValue(Du);
effectPost.Parameters["C"].SetValue(C);
spriteBatch.Draw(SceneTexture,new Rectangle(0, 0, 800, 600), Color.White);
effectPost.CurrentTechnique.Passes[0].End();
}
}
effectPost.End();
}
spriteBatch.End();
以上代码不多,只是设置shader参数并绘制场景。下面是一些其他例子:
下次我们将在透射体上添加反射。
几个Buffer的区别
译者注:这是我自己补充的,如果有说错的地方请及时指正。
Buffer在计算机领域的意思就是缓冲区,也就是一块内存,通常是为了大量的信息计算与搬迁而用,而Buffer里的每笔数据通常都有着相同的规格,以利于大量且快速处理,在3D程序中,从效能上的考虑,通常将Buffer放在显存中。
计算机屏幕其实是由一个个(或一组组)光点组成的,这些光点叫做像素(Pixel),每个像素均可表现出千万种不同的颜色,由此才构成丰富多彩的图片。虽然3D程序在运算结构上都以三维坐标来思考,但最终还是要呈现在二维屏幕上,因此无论3D场景多复杂,最终都要将3D场景对应到窗口的像素上(这也是为什么在pixelshader中总是输出一个float4类型color的原因),换言之,就是要决定像素是什么颜色。这样我们会准备一块Buffer存储像素信息,这个Buffer被称之为ColorBuffer或PixelBuffer或FrameBuffer。
要达到快速反应与类似平行处理的机制,一般ColorBuffer有两块,当第一块ColorBuffer填充完信息并呈现在窗口上以后,第二块ColorBuffer就赶紧处理下一个画面的信息。等到第二块也填充完信息后,与第一块交换,同时呈现在窗口上,而原本第一块Buffer就继续处理下一个画面,两块ColorBuffer轮流替换。凡是正在呈现在窗口上的ColorBuffer叫做FrontBuffer,幕后做准备的叫做BackBuffer(后备缓冲)。如果不是全屏模式,ColorBuffer有三块,这也是非全屏模式比全屏模式慢的原因。今天不少新游戏采用的是三重缓冲,因为它没有Vsync(屏幕的垂直刷新频率)等待的时间,游戏也将更加流畅,当然这个三重缓冲和非全屏模式中的三个缓冲意思不同。
DepthBuffer(深度缓冲)用来判断3D场景中每个对象离观察者的距离,决定哪个该画,哪个不该画,你可以试着在XNA程序中关闭DepthBuffer,看看3D物体会有什么变化,就能更好地理解DepthBuffer的作用。在3D坐标系中,深度或远近用Z值表示,所以DepthBuffer又叫做ZBuffer。
而StencilBuffer翻译为模板缓冲,它都是与ColorBuffer搭配使用的,是提供ColorBuffer在运算时做记号用的。做记号为了什么?如何做记号?做什么记号?具体解释我认为要通过代码才能理解,StencilBuffer通常用在反射、阴影中,XNA的帮助文件中就有一个例子是使用StencilBuffer实现了阴影效果。
其他还有shader中的VertexBuffer和IndexBuffer,顾名思义,这两个缓冲分别用来存储顶点信息和索引信息。
XNA Shader编程教程15-动态环境映射
上个教程我们制作了一个透射post process shader,比起教程13的实现了一个更加真实的透明效果,本教程学习使用立方贴图实现动态环境映射。
动态环境映射
什么是动态环境贴图?一个动态环境贴图是一张表示周围环境的纹理,这个纹理每帧动态生成。这个纹理是一种特殊的纹理,叫做立方体纹理,包含六张2D纹理:如图15-1:
如图15.1所示,立方贴图是一个展开的立方体。每个面表示环境的一张图片,你需要将相机设置到正确的方向,这个例子中只是绘制了反射在物体上的立方纹理而不绘制立方贴图本身。
当我们获取了立方贴图后,我们就将它传递到shader中,shader会将一个反射向量作为查询表使用这个环境贴图。反射向量可以这样创建(在前面的教程中我们已经看到过很多次了):
R = 2 * N·L * N - L
图15.2 R是反射向量,L是指向光源的方向,N是法线。
有了反射向量,我们使用它作为立方贴图的查询纹理。向量中的最后那个分量决定使用那个面,另外两个对应被选择那个面的UV坐标。在现实世界中,只有100%反光的物体才会反射所有光线。通常,光线会在物体中散射和折射,最终穿过物体或被物体吸收。这次我们只实现反射,下个教程实现折射。
实现shader
这个shader需要获取立方纹理,计算反射向量。首先定义一个立方纹理:
textureReflectionCubeMap;
samplerCUBEReflectionCubeMapSampler = sampler_state
{
texture =
};
我们创建了一个普通的纹理和samplerCUBE,samplerCUBE可以使用一个3D向量作为纹理坐标代替我们以前用的2D向量。
然后计算反射向量,并将它作为samplerCUBE 中的查询向量。下面是代码:
float Diff =saturate(dot(L, N));
// Calculatereflection vector
float3 Reflect =normalize(2 * Diff * N - L);
然后使用Reflect在立方贴图中查询像素颜色:
float3 ReflectColor= texCUBE(ReflectionCubeMapSampler, Reflect);
好了,你如要100%反射,只需在pixelshader中返回ReflectColor就可以了。但我们还想加上环境光,漫反射和镜面反射,只需用ReflectColor分别乘以ambient,diffuse和specular:
returnColor*vAmbient*float4(ReflectColor,1) + Color*vDiffuseColor *
Diff*float4(ReflectColor,1)+ vSpecularColor * Specular*float4(ReflectColor,1);
使用shader
要使用shader,我们需要生成立方贴图纹理,将场景绘制到这个纹理中并将它传递到shader。幸运的是,XNA支持立方贴图,而且用起来很简单。
首先声明一个立方纹理和一个渲染目标。这个渲染目标用来绘制场景包含六个渲染目标,每个目标对应立方体的一个面。然后把这个渲染目标复制到纹理,再将这个纹理传递到shader中。首先声明两个变量:
RenderTargetCube RefCubeMap; TextureCubeEnvironmentMap;
然后初始化:
RefCubeMap = newRenderTargetCube(this.GraphicsDevice, 256, 1, SurfaceFormat.Color);
RenderTargetCube函数需要graphicsdevice对象,每个纹理的大小(这里是256x256),mipmap的级别和SurfaceFormat。
因为场景每帧绘制六次,所以我们在不损失质量的前提下尽量缩小贴图的大小。你只需设置一个面的纹理大小,立方纹理的必须是正方形(64x64,128x128,256x256……)。接下来我们将这个纹理传递到shader:
effect.Parameters["ReflectionCubeMap"].SetValue(EnvironmentMap);
最后将场景绘制到立方渲染目标的不同面上,并将这些渲染目标复制到EnvironmentMap纹理中。
for (int i = 0; i< 6; i++)
{
// render the scene to all cubemap faces
CubeMapFace cubeMapFace = (CubeMapFace)i;
switch (cubeMapFace)
{
case CubeMapFace.NegativeX:
{
viewMatrix = Matrix.CreateLookAt(Vector3.Zero,Vector3.Left, Vector3.Up);
break;
}
case CubeMapFace.NegativeY:
{
viewMatrix =Matrix.CreateLookAt(Vector3.Zero, Vector3.Down, Vector3.Forward);
break;
}
case CubeMapFace.NegativeZ:
{
viewMatrix =Matrix.CreateLookAt(Vector3.Zero, Vector3.Backward, Vector3.Up);
break;
}
case CubeMapFace.PositiveX:
{
viewMatrix =Matrix.CreateLookAt(Vector3.Zero, Vector3.Right, Vector3.Up);
break;
}
case CubeMapFace.PositiveY:
{
viewMatrix =Matrix.CreateLookAt(Vector3.Zero, Vector3.Up, Vector3.Backward);
break;
}
case CubeMapFace.PositiveZ:
{
viewMatrix =Matrix.CreateLookAt(Vector3.Zero, Vector3.Forward, Vector3.Up);
break;
}
}
effect.Parameters["matWorldViewProj"].SetValue(worldMatrix *viewMatrix * projMatrix);
// Set the cubemap render target, using theselected face
this.GraphicsDevice.SetRenderTarget(0,RefCubeMap, cubeMapFace);
this.GraphicsDevice.Clear(Color.White);this.DrawScene(false);
}
graphics.GraphicsDevice.SetRenderTarget(0,null);
this.EnvironmentMap= RefCubeMap.GetTexture();
上面的代码中我们首先进行一个循环,遍历立方贴图的每个面(见图15.1,看一下面对应的数字)。面存储在一个CubeMapFace变量中,这个变量包含数字[0,5]。
现在知道了正在使用哪个面,我们必须正确设置相机。只需使用Matrix.CreateLookAt(Position,Target, Up)方法。我们知道物体在0.0,这样使用Vector.Up,Vector.Down等设置目标就能让相机指向正确的方向。
下面就可以绘制场景了。注意在自定义的DrawScene方法中有一个bool变量,表示我们是绘制透射还是环境。当绘制环境时,我们并不想施加透射效果。我只是简单地加了一个if语句,如果是true则绘制透射,如果false则绘制环境。
绘制完六个面后就可以将渲染目标复制到纹理中去了。
XNA Shader编程教程16-折射
本教程我们将实现折射,首先要建立环境映射shader,所以请先理解教程15。
折射
折射是指当光线从一种介质进入另一种介质时发生传播方向发生改变的现象,改变量的大小是基于介质密度的(对应光在此介质中的传播速度),对应的系数叫做折射率。在现实世界中,当你将一支钢笔放入水中时,游泳时,透过玻璃宝石观察时就会看到折射现象。
每种介质的折射率不同,下面是常见介质的折射率表:
真空 |
1.00000 |
标准状态下的空气 |
1.00029 |
冰 |
1.31 |
20℃的水 |
1.33 |
丙酮 |
1.36 |
酒精 |
1.36 |
糖溶液(浓度30%) |
1.38 |
萤石 |
1.433 |
熔石英 |
1.46 |
甘油 |
1.473 |
斯涅耳定律( wikipedia) :
n1sinα1=n2sinα2
公式中n1和n2分别是两个介质的折射系数,α1 是L和N之间的夹角,α2是Q和N的夹角。
HLSL有一个内置函数refract,这个函数根据斯涅耳定律基于入射向量、法线和折射率的比值计算折射向量。
n1和n2的相对折射率可根据下式计算:
Ratio R=n1/n2
在本例中,我们的光线是从空气射入玻璃。如果看一下上面的表格,空气的折射率是1.00029,玻璃是1.52,相对折射率是0.66。
Refract函数会返回一个向量,作为立方映射的查询向量。
实现shader
首先是the vertex shader:
OUTVertexShaderRefract( float4 Pos: POSITION, float2 Tex : TEXCOORD, float3 N:NORMAL )
{
OUT Out = (OUT) 0;
Out.Pos = mul(Pos, matWorldViewProj);
Out.N = normalize(mul(matInverseWorld, N));
Out.V = vecEye - Pos; return Out;
}
我们需要法线和观察向量计算折射。也可以使用光线向量,但这里我想折射观察向量。下一步是pixelshader:
float4PixelShaderRefract(float2 Tex:TEXCOORD0,float3 L:TEXCOORD1, float3 N: TEXCOORD2, float3 V: TEXCOORD3) : COLOR
{
float3 ViewDir = normalize(V);
float3 Refract = refract(ViewDir, N, 0.66);
float3 RefractColor = texCUBE(ReflectionCubeMapSampler,Refract);
// return the color return
float4(RefractColor,1);
}
在pixel shader中,我们使用refract函数获取从空气射向水的折射向量,并将这个向量存储在Refract变量中,接着使用这个变量查询像素的颜色。
大多数情况中,你会将折射和其他shader一起使用,包括反射,颜色贴图,法线贴图等。在本教程中,我们在一个post-processshader中组合这些效果。我们已经知道,透射shader将一个包含环境的纹理作为透射体,将这个纹理传递到shader计算透射。我们现在可以在这个纹理传递到shader之前添加折射效果。
现在,我们可以使用这个背景纹理作为显示在透射网格后面的纹理,要做到这点方法很多,但为了保持简单并抓住重点,我们使用了以下方法。下面是technique:
techniqueRefractionMapShader
{
pass P0
{
VertexShader = compile vs_2_0VertexShaderRefract();
PixelShader = compile ps_2_0PixelShaderRefract();
}
}
这就是折射shader,很简单不是吗?这可以让你完全控制shader,让你可以折射RGB的不同分量。下面是绘制结果的区别。一个有反射透射,另一个除了有反射透射还有折射:
左图没有折射,右图有折射
使用shader
我们需要为折射贴图创建一个渲染目标和纹理,使用这个纹理作为透射shader的背景纹理。
// render refractionmap
graphics.GraphicsDevice.SetRenderTarget(0,RefractionRenderTarget);
graphics.GraphicsDevice.Clear(Color.White);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend,SpriteSortMode.Immediate, SaveStateMode.SaveState);
{
spriteBatch.Draw(BackGroundRenderTexture,new Rectangle(0, 0, 800, 600), Color.White);
}
spriteBatch.End();
GraphicsDevice.RenderState.CullMode= CullMode.None;
effect.CurrentTechnique= refractMapShader;
DrawScene(true);
graphics.GraphicsDevice.SetRenderTarget(0,null);
RefractionRenderTexture= RefractionRenderTarget.GetTexture();
我们创建了折射贴图,在背景纹理中添加折射效果并把它保存到一个叫做RefractionRenderTexture的新纹理中。现在,当使用透射post-processshader时,我们把这个纹理作为背景纹理:
effectPost.Parameters["BGScene"].SetValue(RefractionRenderTexture);
XNA Shader编程教程21-过渡:淡入淡出
今天的教程是一个过渡效果,先展示一个场景,然后使用一个简单的postprocess shader淡入淡出到另一个场景。
淡入淡出效果
淡入淡出效果可以在许多电影,游戏和power point演示中看到。先播放场景A,然后在场景A淡出的同时播放场景B,产生两个场景间平滑的淡入淡出效果。
这在许多场合是很有用的,例如游戏中的切换场景或从一个菜单切换到另一个菜单等。
实现shader
淡入淡出shader很容易实现,你可能会有几个解决方案。
在shader中需要两张纹理,每张对应一个场景,还需要一个变量决定淡入淡出的程度。这个变量必须在0和1之间,当为0时播放场景A,为1时播放场景B。
下面是shader代码:
samplerColorMapSampler : register(s0);
texture ColorMap2;
samplerColorMapSampler2 = sampler_state
{
Texture =
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
float fFadeAmount;
这里我们定义了两个纹理采样器,ColorMapSampler1和ColorMapSampler2,两者都包含一个场景,使用一个叫做fFadeAmount的变量实现两者的切换。纹理采样器可以使用两种不同的方式定义,这在前面的教程中已经解释过了。
现在我们已经做好了实现pixel shader的准备。因为这是一个postprocess shader,所以无需编写vertex shader。
// Transition
float4PixelShader(float2 Tex:TEXCOORD0) : COLOR
{
float4 Color = tex2D(ColorMapSampler, Tex);
float4 Color2 = tex2D(ColorMapSampler2, Tex);
float4 finalColor =lerp(Color,Color2,fFadeAmount);
// Set our alphachannel to fAlphaAmount.
finalColor.a = 1;
return finalColor;
}
这里我们做的就是从每个场景中提取颜色,并使用lerp函数混合这两个颜色。变量fFadeAmount控制渐淡的程度,0表示不减淡,显示场景SceneA,如果为0.3则减淡30%,同时显示场景A和B,其中场景A占70%,B占30%。(译者注:如果查看directXsdk,可知lerp函数的用法是:lerp(x,y, s),返回x + s(y - x))最后创建
technique:technique PostProcess
{
pass P0
{
// A post process shader only needs apixel shader.
PixelShader = compile ps_2_0 PixelShader();
}
}
使用shader
使用shader时,我们需要将两个场景绘制到一张纹理中,并将它传递到shader:
spriteBatch.Begin(SpriteBlendMode.AlphaBlend,SpriteSortMode.Immediate,SaveStateMode.SaveState);
{
// Apply the post process shader
float fadeBetweenScenes = ((float)Math.Sin(m_Timer)* 0.5f) + 0.5f;
effectPost.Parameters["fFadeAmount"].SetValue(fadeBetweenScenes);
effectPost.Parameters["ColorMap2"].SetValue(Scene2Texture);
effectPost.CommitChanges();effectPost.Begin();
{
effectPost.CurrentTechnique.Passes[0].Begin();
{
spriteBatch.Draw(SceneTexture, newRectangle(0, 0, 800, 600), Color.White);
effectPost.CurrentTechnique.Passes[0].End();
}
}
effectPost.End();
}
spriteBatch.End();
我们使用一张绘制场景A的纹理和另一张绘制场景B的纹理作为参数,并通过一个周期函数让变量fFadeAmount在0和1之间变化。
现在已经完成这个示例了,比起前面的教程其实并不难,对吗? ;)
XNA Shader编程教程22 – 过渡:左右揭开
这是第二个过渡效果的教程,使用一个简单的postprocess shader从一个场景切换到另一个场景。
今天的effect是一个基本的过渡效果,一个场景会左右揭开(cross,译者注:这里不知道如何翻译,就借用powerpoint中的说法了)至另一个场景。
Cross过渡
Cross过渡使用一根“线”分隔两个场景。在图22.1中,显示了场景B,而场景A正在向右移动。
实现shader
和教程21一样,我们需要两个纹理对应两个场景,你还需要一个变量指定淡入淡出的程度。这个变量必须在0和1之间,为0时显示场景A,为1时显示场景B。
下面是shader代码:
samplerColorMapSampler : register(s0);
texture ColorMap2;
samplerColorMapSampler2 = sampler_state
{
Texture =
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
float fFadeAmount;
我们定义了两个各纹理采样器,ColorMapSampler1和ColorMapSampler2,这两个采样器都包含一个场景,使用一个fFadeAmount变量确定淡入淡出的程度。下面是pixel shader的代码:
// Transition
float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR
{
float4 Color = tex2D(ColorMapSampler, Tex);
float4 Color2 = tex2D(ColorMapSampler2, Tex);
float4 finalColor =lerp(Color,Color2,smoothstep(fFadeAmount,fFadeAmount+fSmoothSize,Tex.x));
// Set our alphachannel to fAlphaAmount.
finalColor.a = 1;
return finalColor;
}
这个shader和教程21中的几乎是一样的,除了下列代码:
float4 finalColor =lerp(Color,Color2,smoothstep(fFadeAmount,fFadeAmount+fSmoothSize,Tex.x));
这里我们使用了一个叫做moothstep(a,b,x)的函数(译者注:这个函数的具体解释可见directXsdk,用法是smoothstep(min,max, x),如果x的范围是[min, max],则返回一个介于0和1之间的Hermite插值),这个函数会基于参数a,b,x返回一个介于0和1之间的值。当x小于a时返回0,当x大于b时返回1,当x介于a和b之间则返回一个介于0和1之间的值。
当使用smoothstep时,我们使用Tex.x作为x,fFadeAmount作为a,如果fFadeAmount (fSmoothSize设置为0)作为b,那么分割线会变得最清晰。你可以让fSmoothSize大于0使过渡更加平滑,这时b大于a。这个函数会基于Tex.x返回[0,1],可以将这个返回值作为lerp函数的控制参数,实现一个漂亮的过渡效果!
注意:当fSmoothSize大于0而fFadeAmount = 0时,我们仍能在fFadeAmount = 1时看到场景的fSmoothSize部分(译者注:此句好像不通,原文是WhenfSmoothSize is > 0.0, and fFadeAmount = 0, we will still be able to seefSmoothSize part of the scene displayed when fFadeAmount = 1, because our fadestarts at fFadeAmount and stops at fFadeAmount( 0 ) + fSmoothSize.),这是因为切换效果从 fFadeAmount 开始,fFadeAmount( 0 ) +fSmoothSize结束。
使用shader
使用shader时,我们需要将两个场景绘制到一张纹理中,并将它传递到shader:
spriteBatch.Begin(SpriteBlendMode.AlphaBlend,SpriteSortMode.Immediate, SaveStateMode.SaveState);
{
// Apply the post process shader
float fadeBetweenScenes =((float)Math.Sin(m_Timer) * 0.5f)+ 0.5f;
effectPost.Parameters["fFadeAmount"].SetValue(fadeBetweenScenes);
effectPost.Parameters["ColorMap2"].SetValue(Scene2Texture);
effectPost.CommitChanges();effectPost.Begin();
{
effectPost.CurrentTechnique.Passes[0].Begin();
{
spriteBatch.Draw(SceneTexture, newRectangle(0, 0, 800, 600), Color.White);
effectPost.CurrentTechnique.Passes[0].End();
}
}
effectPost.End();
}
spriteBatch.End();
我们使用一张绘制场景A的纹理和另一张绘制场景B的纹理作为参数,并通过一个周期函数让变量fFadeAmount在0和1之间变化。
译者注:你可以使用
float4 finalColor =lerp(Color,Color2,smoothstep(fFadeAmount,fFadeAmount+fSmoothSize,Tex.y));
让分割线上下移动;
使用
float4 finalColor =lerp(Color,Color2,smoothstep(fFadeAmount,fFadeAmount+fSmoothSize,Tex.x*0.5+Tex.y*0.5));
水面渲染教程
第一章 引言
计算机世界中创新或许是最重要的,尤其是对不同类型的图形卡来说。全新的渲染技术使得绘制电影效果的水面、电脑游戏和地形创建变成可能,最新的水面模拟使虚拟现实变得越来越逼真。要实现真实的水面效果需要解决三个部分:
·水的绘制
·光学特性
·波动
这三个部分互相依赖。第一次尝试水面渲染的时间以无从考定,以前CPU的计算能力有限,也没有显卡能支持3D绘图。随着GPU的进化,不仅加速而且改变了渲染方法。今天的CPU快得多,GPU的并行计算代替了CPU能实现更加真实的动画。水面渲染的一个关键文章是JerryTessendorf写的Simulating OceanWater (2001)一文。他提出的方法整合了一个复杂水面绘制和一个海浪的经验算法。后来这篇文章所提出的方法被别人多次引用,例如Jensen和Robert Golias写的Deep-Water Animation andRendering (2001)一文,他们还提出了另一种水面模拟的方法(基于快速傅立叶变换Fast Fourier Transformations,简称FFT)并讨论了自然界中水面上的更小的成分。新的方法由Damien Hinsinger,FagriceNeyret和Marie-Paule Cani写的Interactive Animation ofOcean Waves (2002)和ClaesJohanson写的Real-Time WaterRendering – Projected Grid Concept (2004)的文章中被提出,利用他们的技术可以减少计算量并加强视觉效果。最新的游戏和图形应用程序能创建非常逼真的水面场景,唯一的缺点在于,基于商业原因,他们没有公布技术的详情。在下面的章节中,我将讨论最新水面渲染方法的基础,并可以推广到多种条件下的真实水面渲染。
在第二章中,我会总结用到的主要科学规律,公式和方法。第三章介绍图形硬件的操作方法。第四章介绍水面渲染最常用的各种方法,从最基础的扩展到最复杂的。第五和第六章描述我的演示程序的步骤:一个是湖面shader,另一个是海面shader。
第二章 数学知识
l在生活中你可以看到许多不同种类的水并希望它们足够真实。自然界中不同类型的水有几个共同的特性,但有些特性截然不同。水的主要特性是:
·水表面的反射
·水面下的折射
·多次反射和折射
·反射和折射的比例:菲涅尔公式
·一些颜色的小变化(脏水),水雾
·水面的移动
·镜面反射
·深层水现象
根据维基百科的定义:“反射是指波在两个不同介质之间发生的方向改变。常见的例子包括光线,声音和波浪的反射。”从某种程度上说水面就像一面镜子。光会在水面发生反射。物理规律是:反射角等于入射角,我们根据表面的法线测量这两个角度,即α=β:
图2-1:反射定律,入射角α等于反射角β
只考虑水面的反射很容易计算水面像素的颜色。将相机的位置关于水表面镜像:只需确定通过水面的每个像素的物体颜色就可以了,原理如图2-2所示:
图2-2:从水面获取反射的颜色
如果相机在A点,水面的颜色就是物体通过B点的交点的颜色。B点离开水面的距离与A点相同,这两个距离用图中的字母“k”表示。
电磁波的速度在不同媒质中是不同的。光从一个介质通过另一个介质时速度会发生改变,这时会发生折射现象。根据斯涅耳定律(得名于荷兰数学家Willebrord Snellius),入射角和折射角之间的关系是:它们的比值是一个常数,取决于介质,更确切地说两者的正弦之比等于两介质中的波速之比:
sinθ1/sinθ2=v1/v2=n2/n1
或
n1sinθ1=n2sinθ2
这两个角度是相对于界面法线的说的。根据这个规律,入射光会发生偏折,偏折程度由两介质的相对折射率决定。如图2-3所示:
图2-3:斯涅耳定律,在此图中,第二个介质中的光速小(V2
空气中的折射率为1,而水中为4/3。
前面讨论的规律和例子只在2维平面中,但要描述现实世界,一切都需要三维。 [ MFGD ]提出了下列方程。s为入射光(向量),t是经过变换的光线,n为表面法线。转换的向量t有两个分量:一个平行于法线,另一个垂直于法线,可写成下列形式:
t = -n cosθ2 + msinθ2
要计算两个系数,我们需要用到这样一个关系:即只有沿表面的角会发生变化而不是整个方向都变化。m可以定义如下:
m = perpns / sinθ1 = s - (n s) n /sinθ1
由前面的方程[ MFGD ]结果如下:
该方程中有可能包含负的平方根,这意味着方程的每个系数并不确定,物理原因如下描述。
我要提及的一个重要现象与折射有关。如果光线从光密媒质射向光疏媒质,折射角会变大,当入射角增至某一数值时,折射角等于90度,这时,折射光线消失,这种现象称为全反射,对应的这个入射角叫做全反射临界角,水射向空气的临界角约为50度。见图2-4:
图2-4临界角
上面的图片来着维基百科和和
http://www.glenbrook.k12.il.us。
还有一个的Flash游戏可以理解反射和折射,在这个网址:
http://www.ps.missouri.edu/rickspage/refract/refraction.html
译者:以上知识可参见《新概念物理-光学》6至14页
光线在水面会发生反射和折射,但一定程度上光线会在表面再次反射和折射,图2-5说明了这种情况:
图2-5多次反射和折射
图像来自于[ TEoNIaRT ]。
译者注:对水面来说,当观察者和水面的角度越小时,反射效果越明显,角度越大时,折射效果越明显,称为菲涅尔效果。
本章的前两节介绍了反射和折射,它们在界面上是同时发生的,如图2-6所示:
图2-6在界面同时发生反射和折射
但是,如何获取准确的反射和折射的比例?Augustin-Jean Fresnel 在19世纪初得出了规律。他的方程用不同的强度定义了反射角和边界上的透光率,方程的导出过程已经超出本教程的范围。波有两个偏振分量:一个平行一个垂直。Ei是入射波振幅,Er和Et是反射波的振幅和透射波的振幅。
r = Er/Ei
t = Et/Ei
垂直部分的方程如下:
r = (nicosθi− ntcosθt)/(nicosθi+ ntcosθt)
t =[2nicosθi)/(nicosθi+ntcosθt)
下一个方程显示的平行部分:
r = (nicosθi− ntcosθi)/(nicosθt+ ntcosθi)
t = 2nicosθi/(nicosθt+ ntcosθi)
更优雅的版本有:
perpendicular r = −(sinθi−sinθt)/(sinθi+sinθt)
和
parallel r =(tanθi−tanθt)/(tanθi+tanθt)
如果光的偏振只有垂直部分,我们称之为S 极化。同样地,如果只有平行部分,称之为P极化。图2-7展示了取决于角度的系数在同时存在S 极化和P极化下的情况:
图2-7:由图可见,若从光密媒质射向光疏媒质(右图所示),反射系数为1大于临界角,这种现象叫做全反射。
http://en.wikipedia.org/wiki/Fresnel_term
http://hyperphysics.phy-astr.gsu.edu/Hbase/phyopt/freseq.html
http://www.inyourfacefotos.com/fresnel.htm
译者:以上知识可参见《新概念物理-光学》276至288页
描述海浪是一个巨大的挑战。它有几个不同的部分并组合在一起形成一个非常复杂的系统。有两种不同类型的机械波:横波和和纵波。横波的中质点的振动方向与波的传播方向垂直,如图2-8所示:
图2-8:横波
而纵波中质点的振动方向与波的传播方向在一直线,如图2-9所示:
图2-9:纵波
例如,声波在空气中的传播是纵波而弹吉他弦是横波。对于两种波来说,振幅是偏离平衡位置的最大位移,而波长是运动情况总相同的两质点间的最短距离。频率表示一秒内质点的振动次数,从以上数据很容易计算波速:波速等于频率乘以波长。
风和重力的共同作用构成了海浪沿表面的传播。这个复合系统既有横波又有纵波,使水粒子沿着一个圆形路径运动。越接近表面的粒子运动半径越大。这种波称为表面波,可用图2-10表示:
图2-10表面波
A点水较深,是圆形路径,B点水较浅,运动路径随着深度的增大变成椭圆并越来越扁。箭头#1表示波的传播方向。#2表示波峰,#3表示波谷。下面的动画显示了这个过程:
图像来自于Kettering University.的网页
以下公式描述了表面波的散射关系(来自scienceworld homepage):
ω是角频率,g是重力加速度,k是波数,γ是表面张力,ρ是密度。可用下式求ω:
先前讨论的理论足以描述海浪的运动,但我们需要使用更多具有不同振幅和波长的波获得更为真实的效果,如图2-11:
图2-11:不同振幅和波长的波
不同的波组合在一起形成如图2-12所示的波:
图2-12:波的叠加
在三维世界中波的不同组成部分不仅有不同的振幅和波长,还有不同的传播方向。占主导地位的方向是最大幅度和最长周期的那个波。图2-13展示了三个组成部分:
图2-13:三维波
三者的组合可以近似的模拟真实的海表,如图2-14:
图2-14:三维波的叠加图片
图片来自于 http://www.glenbrook.k12.il.us/gbssci/phys/Class/waves/u10l1c.html
和
http://www.carbontrust.co.uk/technology/technologyaccelerator/ME_guide2.htm
有用的链接:
http://oceanworld.tamu.edu/resources/ocng_textbook/chapter16/chapter16_01.htm
捷克科学家Jozef Gerstner在1802年第一次提出了精确描述水波任意幅度的解决方法。他的模型还介绍了表面波的摆线运动。在这个模型中,水深比波长大。这个曲线也称为摆线。偏移量是由下列方程决定的:
x = X0-(k/k0) * A sin( k* X0 - ωt)
y = A cos( k * X0 - ωt)
公式中X0是不受干扰的表面上的点,A是振幅,k是波矢,k0是其大小。如果振幅很小,Gerstner波接近正弦,但如果振幅很大波会破碎,见图2-15:
图2-15:不同振幅的Gerstner波
这些性质使Gerstner波可以用来描述不同情况下的波。更多细节可参见 [IAoOW] 或[GW]。
lNavier - Stokes方程(NSE)是非线性偏微分方程,用来描述的不可压缩的液体的运动。在NSE中有三个力:
·重力:Fg=ρG ,其中ρ是密度和G是引力(9.81 m/s2l)。
·压力:压力指向水面的法线。
·粘滞力:水之间的摩擦力,作用在水的各个方向上。
公式中的时间t依赖于混沌,流体的随机行为称为动荡,Navier - Stokes方程用来描述这个现象,但现在还没有解决这个方程的求解。谁能作出初步的数学理论将有助于理解这种现象,奖金是100万美元。Navier -Stokes方程如下:
uº(x)给定,C∞divergence-free矢量场Rn上,fi(x,t)是给定的分量,外加力(如重力),ν是粘滞系数,
是拉普拉斯算符。欲了解详情,请参阅[ NSEP ]或[ GPUGEMS ]。
译者:这个公式十分眼熟,原来在几个月前看的一本名叫《千年难题》书中见过。此书描述了七大“悬赏一百万美元的难题”,从简到难分别是:黎曼假设,杨-米尔斯理论和质量缺口假设,P对NP问题,纳维-斯托克斯方程,庞加莱猜想,戴尔猜想,霍奇猜想。纳维-斯托克斯方程排第四,纳维是法国当时最著名的工程师之一,他的名字被刻在了埃菲尔铁塔上。纳维改进了欧拉的方程,使之能适用于有一定黏性的流体这一更为实际的情况。纳维的数学推理是有缺陷的。但由于运气好(或者说由于工程师的过人直觉),他最后得出的方程是正确的。几年之后,爱尔兰数学家斯托克斯(在月亮和火星上都有以他名字命名的环形山)作出了正确的推导。但没有人能够找出一个解纳维-斯托克斯方程的公式。确切地说,没有人能够在原则上证明这个方程是否有解!(当然,每当一种真实的流体作了一次流动,大自然就“解”了一次这个方程)
具有一个平面的材料(如:皮革,玻璃和水)有一个有趣的现象,这点我前面没有提及。有几种不同的反射模式,其中一些模式使图像更加逼真,同时也有助于改近小的细节。其中一个是镜面反射。像沙子之类的材料有不规则,不平坦的表面,这使得入射光在每一个方向都会反射。图2-16表示了这个现象:
图2-16:反射光的方向
但是,如果材料具有光滑表面,入射光将会平行反射。由于这个性质,如果入射光从特定角度入射的话,会在皮革、不同的金属或水之类的物体上形成光泽和亮点。Phong光照模型是三维图形中最常用的方式。PhongBui Tong在1975年提出了这个模型至今仍使用得很普遍。根据这个模型,点的亮度有三个组成部分:一个环境光,一个漫反射光和一个镜面反射光。如果观察者远离反射光的方向,那么镜面反射将变弱。图2-17显示了相关的矢量:
图2-17:向量L指向光源,V指向观察者,N是表面法线,R是反射方向,而H为V和L的角平分线。
在Phong模型中,反射强度近似为角度余弦的幂,原始公式是:
kspec cosnβ
其中β是R和V的夹角,kspec是镜面反射系数。指数n可以影响锐度的大小,指数越大反射越强。两个向量的点乘等于它们之间的角度的余弦,所以公式可以写成:
kspec (V•N)n
这里的“• ”表示点乘。使用漫反射光照模型Phong光照模型如下表示(I是指强度):
I = kambient * Iambient+ (Ip /(d)) [kdiffuse * (N•L) + kspecular * (V•R)n]
k是环境、漫反射、镜面反射系数,“ •”指点乘,即点的光强等于三者的和。
http://www.dgp.toronto.edu/~karan/courses/csc418/fall_2002/notes/illum_local.html
http://www.mini.pw.edu.pl/~kotowski/Grafika/IlluminationModel/Index.html
水面还有刻蚀:光束从弯曲的表面反射或折射,因此只聚焦在受光面上的某些区域,于是就产生了刻蚀现象,如图2-18所示:
图2-18:刻蚀
刻蚀的原理如图2-19所示
图2-19:刻蚀是光线聚焦的明亮区域
引起刻蚀的相同物理效果也会形成Godrays(在[ DWAaR ]中提及)。不断变化的水面使光线不断聚焦和失焦。水中漂浮的小颗粒可以进入这些焦点中,并在短期内可见。由这些效果产生的光斑不断变化被称为Godrays,如果您从水下看光源就可以看到这种现象。图2-20是一个渲染的例子:
图2-20:Godrays– 这张图片是由Typhoon 引擎创建的[TYPHOON]
波浪的破碎会形成泡沫,堆积的部分叫做白浪。根据[ RNW ],白浪取决于水的温度和水面上空气的化学性质。他们使用一个经验公式来近似覆盖泡沫的水的光学特性:
f = 1.59 * 10-5 U2.55exp[0.086 * (Tw - Ta )]
其中f是小数部分,U是风速,TW和Ta是水和空气摄氏温度。
在开放的水域,移动的船只会产生波浪。这些波浪不能视为纵波。这种现象,称之为水楔,首先是由开尔文勋爵提出的。一个理想化的例子可见图2-21
图2-21:理想化的开尔文水楔
船只后面的复杂波形受到水的粘度、移动方向、重力的影响,如果振幅很大,还要产生非线性俄效果。Stern波和涡流互相叠加,and notinfrequently, other wave systems may be discerned originating from somewherebetween the bow and stern。事实上,与船的速度无关,该角有开尔文水楔封闭,解释这个超出了本文的范围。详细信息请参阅[ FDfP ]。
[MFGD] - Mathematics for Game Developers -Christopher Tremblay - Thomson course technology.
[DWAaR] - Deep-WaterAnimation and Rendering
[TEoNIaRT] - The elements of nature: interactive and realisic techniques
[RNW] - Rendering Natural Waters - Simon Premoze, Michael Ashikhmin
[TYPHOON] - Typhoon3D engine
[NSEP] - CHARLES L. FEFFERMAN: Existence and smoothness of theNavier-Stokes equation
[GPUGEMS] - Gpu Gems - Chapter 38. : Mark J. Harris: Fast Fluid DynamicsSimulation on the GPU
[FDfP] - T. E. Faber: Fluid Dynamics forPhysicists
[IAoOW] - Damien Hinsinger, Fabrice Neyret, Marie-Paule Cani: InteractiveAnimation of Ocean Waves
[GW] - Jefrey Alcantara: Gerstnerwaves
4.1导言
有各种不同的技术可以实现水面渲染,其中最重要的是Perlin噪点(Perlin noise),快速傅立叶合成(Fast Fourier syntheses)和纳维-斯托克斯(Navier Stokes)方程。不同的水面需要不同的计算量和真实程度,可以将不同的方法组合起来获得最好的效果。本章将介绍一些解决方案,涵盖最简单的到最复杂的计算。本章将讨论以下知识:
·水面建模:我们可以使用多种方法来描述水面,但无论使用哪种方法最终总要通过顶点绘制结果。网格和粒子系统是最流行的方法。
·水模拟方法:我们可以使用不同的方法描绘水面并使其动起来,不同的解决方法可用于不同的场合,对于复杂的系统可以组合这些方法。
·反射的渲染技术。
·菲涅尔方程近似。
·绘制各种水现象。例如,飞溅,刻蚀和开尔文水楔。
4.2水的建模
4.2.1 3D网格
用三维网格表示水可以让各种水行为的仿真成为可能。主要的想法很简单:我们确定受力情况并计算网格上所有元素的所受力的效果。虽然这些力很容易描述,但计算本身代价昂贵。物理模拟必须精确,但对于水表面来说,我们不必如此精确。如果不考虑计算的花销,那么三维网格可用于渲染的小面积的水,在这种情况下,例如, Navier - Stokes方程可以应用地很好。物理模拟超出了本文的范围,这里我只讨论实时渲染的解决方案。虽然三维网格在实时情况下只能表示少量的水,但预渲染的计算可以提高真实程度。绘制水下的纹理,刻蚀现象或水花溅起也能通过预渲染过程在实时动画中获得更高的性能。有几篇文章介绍了这种技术(如Deep-WaterAnimation and Rendering和Meshuggah Demo and Effect browser),但它们已超出了本文所讨论的范围。为了实现我们的目的,需要一个简单的描述水的方法。
4.2.2 2D网格– 高度图
三维网格水体比较精确的近似,但如果加上一些限制,我们可以使用一个简单的解决办法。为了绘制水面,我们只需要知道它的形状,在这种情况下,水面的高度对应某一给定的(x , y )坐标。这意味着,水体可以简化为一个高度场,该高度场使用一个有两个变量的函数返回给定二维空间中的点的高度。这种方法与三维网格相比会受到一些限制,因为函数只能返回一个值,一个高度场能返回给定(x,y)坐标的高度,这意味着重叠波可以无法通过这种方式加以描述,如图4-1所示:
图4-1 如右图所示一个高度场只存储对应坐标的一个高度值,所以水表面的重叠部分无法通过这种方式描述,如左图所示。
与三维网格相比,二维网格的主要优势是它更容易使用,具有更简单的数据结构。如果高度场储存在一个纹理中,它通常被称为高度图。同样,渲染过程称为位移映射(displacement-mapping),因为原始的几何数据会根据高度图中所储存数据的大小发生偏移。
不同的优化技术可以有更好的实时性能。例如,如果高度图是由一个连续函数定义的,那么就不需要计算出整个水面,只渲染可见部分就足够了。在其他情况下,例如,在使用Navier - Stokes方程的时候,高度图的每个元素都需要更新,即使它们在相机中是不可见的。下面的段落会讨论一些优化方法。
4.3 性能优化
因为是硬件处理三角形,所以无法回避表面镶嵌。真正的水面是持续不断的,但电脑绘图使用的是多边形,所以需要使表面离散。更多三角形可以描述更详细的虚拟世界,但也需要更多顶点。每个图形程序员都要在复杂性和性能之间找到平衡,即在真实度和速度之间找到平衡。
4.3.1经典LOD算法
根据Level Of Detail( LOD值)的概念,一个复杂的对象可以简化为不同程度的细节。屏幕上的物体越小,细节越少以获得更好的性能。图4-2展示了LOD:
图4-2兔子的LOD层次
如果兔子很小或距离较远,我们无法分辨高精度模型和低精度模型之间的区别,如果视觉上没有差别,要提高性能,我们可以只绘制较少三角形的兔子模型,如图4-3:
图4-3 LOD例子
还有一种LOD是连续LOD。这种LOD不是在运行前创建不同细节的模型,而是实时将模型简化到想要的细节。我们只需存储最高细节的兔子,应用程序会删除不必要的多边形以提升性能。这样一来, LOD值粒度好得多,还可以使用前面生成的模型层次,但应用程序变得更加复杂。细节可参考[LODf3DG] - Morgan Kaufmann: Level of Detail for 3D Graphics 。
4.3.2水面的LOD算法
LOD技术也可用于水面渲染。如果水面是由大量三角形构成的,例如,一个三角形带,这一三角形带可以比使用同等大小的三角形获得更加真实的优化结果。使用这一方法最主要的问题是应该如何排列三角形。
下面的方法是在[BMELAB2] - Gamedevelopment laboratory material 2, BME, AUT中提出的。使用30 × 30大小的网格(三角带)模拟水面的可见部分,这意味着总是在相机前方进行转换。可以在其他坐标(z = f(x,y))的连续函数的帮助下计算高度,这样顶点位置的放置可以没有任何限制。如果我们使用同等大小的三角形带,距离远的三角形会变得很小。
可以通过顶点置换优化性能。遥远的三角形太小,他们应转换为对相机来说大致相同的面积。还必须考虑近裁和远裁平面及三角形带的宽度使在相机中看见到整个水面。还有一个问题是将三角带的高度变成相同高度。下面的双曲函数给出了取决于行数的水平位置:
公式中的k是第几行,kmax是总行数。三角形的第一行(k=0)水平距离是0,最后一行(k= kmax-1) 收敛至无穷大。
因为我们不会在无穷远处绘制物体,上面的方程需要进行缩放。在渲染前我们应该已经有了近裁和远裁平面,这意味着,只有在这两个平面之间的三角形才会被渲染。如果Dmax是远裁平面距离,Dmin是近裁平面距离,下列方程可以置换和缩放点的坐标:
例如,如果在0至1000范围之内的物体要渲染,水面可以缩放到同样大小,只需把Dmax设置成1000,Dmin设置成0。网格行必须比从相机可见的相同大小宽得多。如图4-4所示:
图4-4远处的三角形边要比近处的长
在[BMELAB2] - Gamedevelopment laboratory material 2, BME, AUT一文中,使用如下公式让三角边可以缩放以匹配视角的两边:
公式中的i是实际列序号,imax是列总数,d是行间距(之前以计算),r是视角比例(例如4:3或16:9)有用链接: Terrain LOD: RuntimeRegular-Grid Algorithms
4.3.3使用投影网格(Projectedgrid)
LOD算法判断哪些顶点使用得更频繁,哪些应忽略,实现了很漂亮的实时水面渲染。但如果相机移动,那么屏幕上的顶点位置也会发生变化。Projected l将网格转换到世界空间。l将这个网格投影到对应平面。l在相机空间创建一个规则网格与相机正交。lgrid算法通过以下几步让顶点在相机空间中更加平滑: 渲染网格例如,如果你将一张带网点的纸放在投影灯的前面,网格会投影到桌面上,如图4-5所示:l设置偏移,波浪等。
图4-5:现实世界的投影网格
这些网格是规则和光滑的,这也是我们处理水面顶点想要达到的目标。
在[ IAoOW ] (译者:不知是哪篇文章)一文中还提到了自适应水面网格的使用。包含水面网格切换的相机移动让每个顶点都有近似相同的投影。但也带来了新的问题,因为不正确的表面近似,表面的法线无法利用有限差分技术估计。要确定法线需要使用分析方法。如图4-6所示:
图4-6 :不准确的有限差分方法。
如图所示,取决于顶点的细节程度,分析方法比有限差分方法能计算出更准确的法线。
自适应水面网格是优化水的渲染的一个好办法,但它们是复杂的,应用时需要认真考虑。
4.4 模拟水的方法
4.4.1生成连续噪点-Perlin噪点
实时渲染所需的随机噪点生成有几种不同的方法。Ken Perlin提出了一种产生连续噪点的方法可以比简单随机函数更类似与自然界的情况。图4-7显示了两者的区别:
图4-7 随机噪点和Perlin噪点的区别。左图是由一个简单随机函数生成的,而右图的Perlin噪点更接近自然界的随机现象。
基本Perlin噪点看起来不是很有趣,但如果把不同频率和振幅的噪点函数叠加在一起就能创建一个有趣地多的分形噪点,如图4-8所示:
图4-8不同频率和振幅的Perlin噪点层
组合结果如图4-9所示:
图4-9:不同频率和振幅的Perlin噪点层的叠加
每个层的频率是前者的两倍,这也是为什么层通常用8度表示(referred to as octaves)。要制作三维噪点动画使用二维噪点纹理也可以,更详细的解释可见[PN2]。
使用Perlin噪点作为水面的核心算法比以后要介绍的技术计算量小很多。Perlin噪点的主要问题是无法精确控制,只有频率和振幅可以很容易的改变,与外部物体的交互很难实现。
4.4.2快速傅立叶变换Fast FourierTransformations
物理模拟很耗费资源。要更有效率,可以基于统计模型作为水面动画的核心算法。在这个模型中,波的高度是位置(位置是指不包含高度Z的水平XY坐标)和时间的变量。高度可以由一个函数决定,这个函数是一组有不同振幅和相位的正弦波,要快速获得振幅之和,可以使用反快速傅立叶变换。波的顶部很圆滑,还有其他方法可以增加波的陡度,使波看起来更波涛汹涌。
http://www.finelightvisualtechnology.com/docs/coursenotes2004.pdf
http://www.gamasutra.com/gdce/2001/jensen/jensen_01.htm
4.4.3纳维-斯托克斯方程
在数学知识一章中提到的纳维-斯托克斯方程描述了不可压缩的流体的运动。作用力是重力、压力和粘滞力。这个方程很难解出,所以我们需要简化并离散方程以实现实时计算。一个有效的方法是使用高度图模拟相邻列的水体,用此方法,不需要明确指定海浪和其他表面失真,因为他们由物理状态自然生成。 [DSoSF]介绍了一种技术,可以通过连接相邻栏的虚拟管道模拟水体变换,如图4-10所示:
图4-10:相邻水体连接起来计算纳维-斯托克斯方程流体
垂直栏通过一组有方向的水平管道与周围连接。这些管道让水的压力作用到整个水面。网格上的控制点可以通过采用分离相邻列。要和外部物体发生交互,表面可以作为一个单独的子系统将外部压力传递到整个水体网格(或子系统)上。如图4-11:
图4-11:传递到网格上的压力底部箭头表示水流方向,竖直箭头表示竖直的速度,细箭头表示粒子发射的速度矢量。
这些不同的子系统相互交互形成复杂的流体系统,如图4-12所示:
图4-12:基于纳维-斯托克斯方程复杂水面渲染系统
虽然它们很真实,但纳维-斯托克斯方程的每一步都很消耗资源,即使是对最新的显卡而言,也要限制网格的尺寸才能满足实时渲染的需要。如果不是实时渲染,例如电影泰坦尼克中使用的就是2048 x 2048 大小的网格,但这个大小在实时情况中是无法工作的。纳维-斯托克斯方程可用来模拟小的水面,如水池或喷泉。也存在其他渲染方法可以做同样的事,例如,一个简单的顶点偏移技术可以和纳维-斯托克斯方程组合起来实现与外部物体的更深入的互动。纳维-斯托克斯方程需要使用世界空间进行计算,而其他解决方案需要不同的网格空间,比如前面提到的投影网格技术。
纳维-斯托克斯方程解决二维网格要简单得多。通常情况下,二维网格就足够了,但二维网格有缺点,只有一个竖直力可以插入到系统中,所有外力的模拟必须要在竖直方向上近似,这会影响结果,比如风力通常是水平的,二维网格就会出现错误。
4.4.4粒子系统
基于物理的方法最近非常受欢迎。硬件性能的提高使实时粒子系统成为可能。根据不同的情况,基于顶点和基于像素的解决方案都可以实现大量独立的粒子的运动。粒子系统技术可与其他水动画方法组合获得更为真实的结果。粒子系统需要解决的问题是:如何使粒子运动,以及粒子应作为怎样的物体。整个系统可以有速度向量,但速度向量无需通过整个水流。如图4-13所示:
图4-13:粒子系统中的速度流
第二个问题是:我们的粒子也可以忽略大小和质量。但他们可以携带其他类型的信息使其他类型的相互作用成为可能,例如,颜色,温度和压力,这取决于想要达到的效果。
粒子遵循物理规律运动,他们的运动可以在前面讨论过的速度向量贴图的帮助下计算得到,要在图形硬件上计算,一个纹理必须储存粒子的位置,其位置采样到纹理。这些纹理被称为粒子纹理贴图,如图4-14:
图4-14:粒子纹理贴图
要在下一时间步长获取粒子位置,我们追踪这些粒子,就像他们独自沿速度矢量图移动那样。这种做法被称为forward-mapping。如图4-15所示:
图4-15:追踪粒子的位置
这个技术有一些问题。首先,如果流速过小,有些粒子会永远停留在相同的网格单元中,因为每次循环中它们被认为是从网格单元的中心出发的,如果不能在一个时间步长中离开网格单元,它们会重新回到中心。第二,基于前面的同样理由,有些网格单元总是空的,这会导致粒子静止不动。
为了克服这些问题,可以用backward mapping代替forward mapping。对于每一个网格单元,我们计算粒子来自于哪个单元。然后,我们使用原始单元的颜色确定当前单元的颜色。如果使用插值,周围的颜色也可以考虑,这样我们就能够避免静止的粒子,如图4-16所示:
4-16:backward mapping
速度贴图和粒子贴图都存储在单独的纹理中,纹理有两个组成部分。一个标准的2D纹理通过这种方式表示,第三维是近似添加的以提升性能。Offset纹理是硬件支持的,所以沿速度场的移动可以通过硬件实现。Inflow 和outflow(粒子生成和清除)超出本文范围。更详尽的解释和源代码中可以在[SHADERX ]中找到。
粒子系统是实现外部物体与水面的实时交互的很好的解决方案。它们也可以有效地表现水面动画,但通常它们与其他技术一起应用。流动的水,水滴,喷溅,瀑布都可以通过粒子系统实现。
喷溅在[DSoSF]- James F. O’Brien and Jessica K. Hodgins: Dynamic Simulation of SplashingFluids一文中作为一个子系统,当表面的一个部分具有较高的上升速度时,粒子不会发生交互,它们只会在重力的作用下落回到水面,然后将这些粒子从系统中移除。
[DWAaR]- Deep-Water Animation and Rendering一文中使用了类似的粒子模型模拟喷溅。只考虑简单的牛顿动力学:喷溅位置的速度影响了初速度,它可以由重力、风力和其他可能的外力改变。渲染混合了alpha-transparency和additive-alpha精灵。欲了解更多细节和截图,请参阅Deep-WaterAnimation and Rendering。
4.5 反射
4.5.1静态立方体贴图
反射如果水面不需要反射所有东西,可以使用预生成的立方贴图计算反射的颜色。立方贴图是一种可以硬件加速的纹理贴图(其他还有sphere mapping 和dual paraboloid mapping),可以把它看出一个六个面都有图片的普通立方体,这些图片都是从立方体中心拍摄的照片,它们显示了从周围环境向对应方向观察的景象,图4-17是一个立方体贴图的例子:
图4-17立方体贴图
如图4-18所示,立方体贴图的六个面由三个坐标轴命名:x、 y、z和负x、 y、z 方向:
图4-18使用立方贴图
如果有了立方贴图和水的反射面,我们就可以计算从水面上的点指向反射物体的向量,使用这个三维向量(如图中红色箭头所示)可以确定从立方体中心出发的对应纹理坐标的位置,这个向量指向立方体上的一个点,而这个点具有与反射物体相同的颜色。为每个点计算全局光照要花费长得多的时间,但这个计算有效率得多,并可以使用硬件加速满足实时渲染的需要。使用立方贴图还有一个优点是:立方体的有些面相机是不可见的,但即使有些点在相机背后也能反射。另一方面,如果你想反射动态环境(比如移动的物体),立方贴图没必要一定要预渲染。使用这个技术,天空盒可以很容易地反射在水面,但要渲染水面上移动的船需要另一种方式。另外在立方贴图边缘会有错误,这很难避免。图片来自于:
http://www.cescg.org/CESCG-2002/GSchroecker/node14.html
和
http://www.modwiki.net/wiki/Cube_maps
4.5.2动态立方贴图
要反射动态环境需要更新立方贴图。因为立方贴图是每个面上六张图片的集合,所以要创建动态立方贴图需要依次绘制这些纹理。我们需要渲染场景6次,每次一个面,设置相机使它匹配对应每个面的视点。改变相机位置不难,但视场(FOV)需要调整到相同尺寸,正方形图片在场景中都是相同的,因为水面相对比环境大,不同的物体需要以相同的方向反射到水面的不同位置,这意味着仅有一个立方贴图不足以模拟水面的实时反射,每帧创建多个立方贴图代价昂贵,所以对今天的显卡来说,动态立方贴图不是一个好的选择。
虽然相当复杂,但还是有几个解决方案的。如在HalfLife 2中,从水面的不同位置使用多个动态立方贴图,而反射的数据是从已经存储的变量中插值得到的。要能实现实时渲染,可以每秒只生成几次立方贴图。
4.5.3将反射渲染到纹理
在第二章的数学知识中我介绍了如何获取水面上每个点的反射颜色,最关键的地方就是在水面的对称位置创建一个虚拟视点,并将场景渲染到一个纹理中用于后面的反射贴图。这意味着在渲染最终图片前需要添加一个预渲染过程,在这个过程中,相机位置和视向量是水面的镜像,能被水面反射的每个物体是从虚拟视点的纹理中获得的,让我们再次看一下图4-19:
图4-19渲染反射
要获得预期效果,应先计算B点位置,要做到这点,我们必须知道相机离开水面的距离即图中的k。如果水面是水平的,这个距离要减去水面高度以获得高度坐标,A点和B点的其他坐标是相同的。为了避免出错,在渲染到纹理前应先移去水下的物体。当最终图片生成后,这个纹理就可以作为反射贴图了,反射的颜色可以简单地通过相机和水面上对应点的向量采样得到,水的形状也可采用同样的思路得到。要获得更好的效果需要进行微调,例如在水面上调整高度或微调B点的位置。
参考可见:
http://developer.nvidia.com/object/cube_map_ogl_tutorial.html
http://www.mpinf.mpg.de/departments/irg3/ws0405/cg/rcomp/09/doc/aufgaben.pdf
http://forums.xna.com/thread/7619.aspx
//----------------------------------------------------
//----
//-- www.riemers.net --
//--Series 4: Advanced terrain--
//-- Shader code --
//-- --
//----------------------------------------------------
//------- Constants --------
float4x4 xView;
float4x4 xProjection;
float4x4 xWorld;
float3 xLightDirection;
float xAmbient;
bool xEnableLighting;
//------- Texture Samplers --------
Texture xTexture;
samplerTextureSampler = sampler_state ...{ texture =
samplerTextureSampler0 = sampler_state ...{ texture =
samplerTextureSampler1 = sampler_state ...{ texture =
samplerTextureSampler2 = sampler_state ...{ texture =
samplerTextureSampler3 = sampler_state ...{ texture =
//------- Technique: Textured --------
struct TVertexToPixel
...{
float4 Position : POSITION;
float4 Color : COLOR0;
float LightingFactor:TEXCOORD0;
float2 TextureCoords: TEXCOORD1;
};
struct TPixelToFrame
...{
float4 Color : COLOR0;
};
TVertexToPixel TexturedVS( float4 inPos :POSITION, float3 inNormal: NORMAL, float2 inTexCoords:TEXCOORD0)
...{
TVertexToPixel Output = (TVertexToPixel)0;
float4x4 preViewProjection = mul (xView, xProjection);
float4x4 preWorldViewProjection = mul (xWorld, preViewProjection);
Output.Position = mul(inPos, preWorldViewProjection);
Output.TextureCoords = inTexCoords;
float3 Normal= normalize(mul(normalize(inNormal), xWorld));
Output.LightingFactor = 1;
if(xEnableLighting)
Output.LightingFactor = saturate(dot(Normal, -xLightDirection));
return Output;
}
TPixelToFrame TexturedPS(TVertexToPixel PSIn)
...{
TPixelToFrame Output = (TPixelToFrame)0;
Output.Color = tex2D(TextureSampler, PSIn.TextureCoords);
Output.Color.rgb *= saturate(PSIn.LightingFactor + xAmbient);
return Output;
}
technique Textured_2_0
...{
pass Pass0
...{
VertexShader = compile vs_2_0 TexturedVS();
PixelShader = compile ps_2_0 TexturedPS();
}
}
technique Textured
...{
pass Pass0
...{
VertexShader = compile vs_1_1 TexturedVS();
PixelShader = compile ps_1_1 TexturedPS();
}
}
//------- Technique: Multitextured--------
struct MTVertexToPixel
...{
float4 Position :POSITION;
float4 Color : COLOR0;
float3 Normal : TEXCOORD0;
float2 TextureCoords :TEXCOORD1;
float4 LightDirection :TEXCOORD2;
float4 TextureWeights :TEXCOORD3;
};
struct MTPixelToFrame
...{
float4 Color : COLOR0;
};
MTVertexToPixel MultiTexturedVS( float4 inPos: POSITION, float3 inNormal: NORMAL, float2inTexCoords: TEXCOORD0, float4 inTexWeights: TEXCOORD1)
...{
MTVertexToPixel Output = (MTVertexToPixel)0;
float4x4 preViewProjection = mul (xView, xProjection);
float4x4 preWorldViewProjection = mul (xWorld, preViewProjection);
Output.Position = mul(inPos, preWorldViewProjection);
Output.Normal = mul(normalize(inNormal), xWorld);
Output.TextureCoords = inTexCoords;
Output.LightDirection.xyz = -xLightDirection;
Output.LightDirection.w = 1;
Output.TextureWeights = inTexWeights;
returnOutput;
}
MTPixelToFrame MultiTexturedPS(MTVertexToPixelPSIn)
...{
MTPixelToFrame Output = (MTPixelToFrame)0;
floatlightingFactor = 1;
if(xEnableLighting)
lightingFactor = saturate(saturate(dot(PSIn.Normal, PSIn.LightDirection))+ xAmbient);
Output.Color = tex2D(TextureSampler0,PSIn.TextureCoords)*PSIn.TextureWeights.x;
Output.Color += tex2D(TextureSampler1, PSIn.TextureCoords)*PSIn.TextureWeights.y;
Output.Color += tex2D(TextureSampler2,PSIn.TextureCoords)*PSIn.TextureWeights.z;
Output.Color += tex2D(TextureSampler3,PSIn.TextureCoords)*PSIn.TextureWeights.w;
Output.Color *= lightingFactor;
return Output;
}
technique MultiTextured
...{
pass Pass0
...{
VertexShader = compile vs_1_1 MultiTexturedVS();
PixelShader = compile ps_2_0 MultiTexturedPS();
}
}
//----------------------------------------------------
//----
//-- www.riemers.net --
//--Series 4: Advanced terrain--
//-- Shader code --
//----
//----------------------------------------------------
//------- Constants --------
float4x4 xView;
float4x4 xProjection;
float4x4 xWorld;
float3 xLightDirection;
float xAmbient;
bool xEnableLighting;
//------- Texture Samplers --------
Texture xTexture;
sampler TextureSampler = sampler_state ...{ texture =
sampler TextureSampler0 = sampler_state ...{ texture =
sampler TextureSampler1 = sampler_state ...{ texture =
sampler TextureSampler2 = sampler_state ...{ texture =
sampler TextureSampler3 = sampler_state ...{ texture =
//------- Technique: Textured --------
struct TVertexToPixel
...{
float4 Position : POSITION;
float4 Color : COLOR0;
float LightingFactor:TEXCOORD0;
float2 TextureCoords: TEXCOORD1;
};
struct TPixelToFrame
...{
float4 Color : COLOR0;
};
TVertexToPixel TexturedVS( float4 inPos :POSITION, float3 inNormal: NORMAL, float2 inTexCoords: TEXCOORD0)
...{
TVertexToPixel Output = (TVertexToPixel)0;
float4x4 preViewProjection = mul (xView, xProjection);
float4x4 preWorldViewProjection = mul (xWorld, preViewProjection);
Output.Position = mul(inPos, preWorldViewProjection);
Output.TextureCoords = inTexCoords;
float3 Normal = normalize(mul(normalize(inNormal), xWorld));
Output.LightingFactor = 1;
if(xEnableLighting)
Output.LightingFactor = saturate(dot(Normal, -xLightDirection));
return Output;
}
TPixelToFrame TexturedPS(TVertexToPixel PSIn)
...{
TPixelToFrame Output = (TPixelToFrame)0;
Output.Color = tex2D(TextureSampler, PSIn.TextureCoords);
Output.Color.rgb *= saturate(PSIn.LightingFactor + xAmbient);
return Output;
}
technique Textured_2_0
...{
pass Pass0
...{
VertexShader = compile vs_2_0 TexturedVS();
PixelShader = compile ps_2_0 TexturedPS();
}
}
technique Textured
...{
pass Pass0
...{
VertexShader = compile vs_1_1 TexturedVS();
PixelShader = compile ps_1_1 TexturedPS();
}
}
//------- Technique: Multitextured--------
struct MTVertexToPixel
...{
float4 Position :POSITION;
float4 Color : COLOR0;
float3 Normal :TEXCOORD0;
float2 TextureCoords :TEXCOORD1;
float4 LightDirection :TEXCOORD2;
float4 TextureWeights :TEXCOORD3;
};
struct MTPixelToFrame
...{
float4 Color : COLOR0;
};
MTVertexToPixel MultiTexturedVS( float4 inPos: POSITION, float3 inNormal: NORMAL, float2 inTexCoords: TEXCOORD0, float4inTexWeights: TEXCOORD1)
...{
MTVertexToPixel Output = (MTVertexToPixel)0;
float4x4 preViewProjection = mul (xView, xProjection);
float4x4 preWorldViewProjection = mul (xWorld, preViewProjection);
Output.Position = mul(inPos, preWorldViewProjection);
Output.Normal = mul(normalize(inNormal), xWorld);
Output.TextureCoords= inTexCoords;
Output.LightDirection.xyz = -xLightDirection;
Output.LightDirection.w = 1;
Output.TextureWeights = inTexWeights;
returnOutput;
}
MTPixelToFrameMultiTexturedPS(MTVertexToPixel PSIn)
...{
MTPixelToFrame Output = (MTPixelToFrame)0;
floatlightingFactor = 1;
if(xEnableLighting)
lightingFactor = saturate(saturate(dot(PSIn.Normal,PSIn.LightDirection)) + xAmbient);
Output.Color = tex2D(TextureSampler0,PSIn.TextureCoords)*PSIn.TextureWeights.x;
Output.Color += tex2D(TextureSampler1,PSIn.TextureCoords)*PSIn.TextureWeights.y;
Output.Color += tex2D(TextureSampler2,PSIn.TextureCoords)*PSIn.TextureWeights.z;
Output.Color += tex2D(TextureSampler3,PSIn.TextureCoords)*PSIn.TextureWeights.w;
Output.Color *= lightingFactor;
return Output;
}
technique MultiTextured
...{
pass Pass0
...{
VertexShader = compile vs_1_1 MultiTexturedVS();
PixelShader = compile ps_2_0 MultiTexturedPS();
}
}