使用 XNA/3D 开发/着色器和效果创建游戏
存在像素着色器和顶点着色器。首先,您需要了解它们之间的区别、工作原理以及它们能为您做什么。然后,您需要学习着色器语言 HLSL、其语法以及如何使用它。特别是如何从程序中调用它。最后,您还将学习一个名为 FXComposer 的程序,它将向您展示如何加载效果、它们的 HLSL 代码是什么、如何修改它以及如何导出并在游戏中使用完成的着色器。
在过去,计算机生成的图形是由视频硬件中的所谓固定功能管道 (FFP) 生成的。此管道仅提供以特定顺序执行的一组有限的操作。这对于像游戏这样的图形应用程序日益增长的复杂性来说不够灵活。
这就是为什么引入了一种新的 图形管道 来替代这种硬编码方法。新模型仍然具有一些固定组件,但它引入了所谓的着色器。着色器在渲染屏幕上的场景中发挥主要作用,并且可以轻松地交换、编程和适应程序员的需求。这种方法提供了完全的创造力,但也对图形程序员提出了更多责任。
着色器有两种类型:顶点着色器 和 像素着色器(在 OpenGL 中称为片段着色器)。随着 DirectX 10 和 OpenGL 3.2 的推出,第三种类型的着色器也出现了:几何着色器,它通过基于现有顶点创建额外的、新的顶点来提供更广泛的可能性。
着色器描述和计算顶点或像素的属性。顶点着色器处理顶点及其属性:它们在屏幕上的位置、每个顶点的纹理坐标、它的颜色等等。
像素着色器处理顶点着色器(光栅化的片段)的结果,并描述像素的属性:它的颜色、与屏幕上其他像素相比的深度(z 深度)及其 alpha 值。
如今,有三种类型的着色器以特定顺序执行以渲染最终图像。该方案显示了每种着色器在将数据从 XNA 发送到 GPU 并最终渲染图像的过程中所扮演的角色及其顺序。此过程称为 GPU 工作流
顶点着色器是用于通过使用数学运算来操作顶点数据的特殊函数。为此,顶点着色器将 XNA 中的顶点数据作为输入。该数据包含顶点在三维世界中的位置、它的颜色(如果它有颜色)、它的法线向量及其纹理坐标。使用顶点着色器,可以操作这些数据,但只会更改值,不会更改数据存储方式。
每个顶点着色器的最基本功能是将每个顶点的位置从虚拟空间中的三维位置转换为屏幕上的二维位置。这通过使用视图、世界和投影矩阵进行矩阵乘法来完成。
顶点着色器还计算顶点在二维屏幕上的深度(z 缓冲区深度),以便不会丢失有关对象深度原始三维信息,并且更靠近查看者的顶点显示在位于其他顶点后面的顶点前面。顶点着色器可以操作所有输入属性,例如位置、颜色、法线向量和纹理坐标,但它不能创建新的顶点。但顶点着色器可用于更改查看对象的方式。雾、运动模糊和热浪效果都可以使用顶点着色器进行模拟。
管道中的下一步是新的但可选的几何着色器。几何着色器可以根据已发送到 GPU 的顶点向网格添加新的顶点。使用此方法的一种方法称为几何 细分,它是在特定程序的基础上向现有表面添加更多三角形的过程,以使其更详细,更美观。
使用几何着色器而不是高多边形模型可以节省大量的 CPU 时间,因为不必由 CPU 处理并发送到 GPU 所有应该在屏幕上显示的顶点。在某些情况下,多边形数量可以减少一半或四分之一。
如果未使用几何着色器,则顶点着色器的输出将直接发送到光栅化器。如果使用了几何着色器,则输出在添加新顶点后也会发送到光栅化器。
光栅化器获取处理后的顶点,并将它们转换为片段(多边形的像素大小部分)。无论是点、线还是多边形基元,此阶段都会生成片段以“填充”多边形并插值所有颜色和纹理坐标,以便为每个片段分配适当的值。
之后,像素着色器(DirectX 使用术语“像素着色器”,而 OpenGL 使用术语“片段着色器”)将针对每个片段进行调用。像素着色器计算单个像素的颜色,并用于漫射光照(场景照明)、凹凸贴图、法线贴图、镜面光照和模拟反射。像素着色器通常用于为表面提供它们在现实生活中具有的效果。
像素着色器的结果是具有特定颜色的像素,该像素将传递到输出合并器,最后绘制到屏幕上。
顶点着色器和像素着色器之间的主要区别在于,顶点着色器用于更改几何体(顶点)的属性并将其转换为 2D 屏幕。相反,像素着色器用于更改生成像素的外观,目的是创建表面效果。
在 XNA 中使用 BasicEffect 类进行编程
[edit | edit source]如果您想为模型制作简单的效果和照明,Basic Class XNA 非常有用且有效。它的工作方式类似于固定功能管道 (FFP),它提供了有限且不灵活的操作。
要使用 BasicEffect 类,我们首先需要在游戏类的顶部声明 BasicEffect 的实例。
BasicEffect basicEffect;
此实例应在 Initiliaze() 方法中初始化,因为我们希望在程序启动时初始化它。如果我们在其他地方执行此操作,可能会导致性能问题。
basicEffect =
new BasicEffect(graphics.GraphicsDevice, null);
接下来,我们在游戏类中实现一些方法来使用 BasicEffect 类绘制模型。使用 BasicEffect 类,我们不必为每个变量创建 EffectParameter 对象。相反,我们可以直接将这些值分配给 BasicEffect 的属性。
private void DrawWithBasicEffect
(Model model, Matrix world, Matrix view, Matrix proj){
basicEffect.World = world;
basicEffect.View = view;
basicEffect.Projection = proj;
basicEffect.LightingEnabled = true;
basicEffect.DiffuseColor = new Vector3(1.0f, 1.0f, 1.0f);
basicEffect.SpecularColor = new Vector3(0.2f, 0.2f, 0.2f);
basicEffect.SpecularPower = 5.0f;
basicEffect.AmbientLightColor =
new Vector3(0.5f, 0.5f, 0.5f);
basicEffect.DirectionalLight0.Enabled = true;
basicEffect.DirectionalLight0.DiffuseColor = Vector3.One;
basicEffect.DirectionalLight0.Direction =
Vector3.Normalize(new Vector3(1.0f, 1.0f, -1.0f));
basicEffect.DirectionalLight0.SpecularColor = Vector3.One;
basicEffect.DirectionalLight1.Enabled = true;
basicEffect.DirectionalLight1.DiffuseColor =
new Vector3(0.5f, 0.5f, 0.5f);
basicEffect.DirectionalLight1.Direction =
Vector3.Normalize(new Vector3(-1.0f, -1.0f, 1.0f));
basicEffect.DirectionalLight1.SpecularColor =
new Vector3(0.5f, 0.5f, 0.5f);
}
在所有必要的属性都分配后。现在我们的模型应该使用 BasicEffect 类绘制。由于模型中可能包含多个网格,因此我们使用 foreach 循环迭代模型的每个网格。
private void DrawWithBasicEffect
(Model model, Matrix world, Matrix view, Matrix proj){
....
foreach (ModelMesh meshes in model.Meshes)
{
foreach (ModelMeshPart parts in meshes.MeshParts)
parts.Effect = basicEffect;
meshes.Draw();
}
}
要在 XNA 中查看我们的模型,我们只需在 Draw() 方法中调用我们的方法。
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
DrawWithBasicEffect(myModel, world, view, proj);
base.Draw(gameTime);
}
使用 BasicEffect 类绘制纹理
[edit | edit source]要使用 BasicEffect 类绘制纹理,我们必须启用 alpha 属性。之后,我们可以将纹理分配给模型。
basicEffect.TextureEnabled = true;
basicEffect.Texture = myTexture;
使用 BasicEffect 类创建透明度
[edit | edit source]首先,我们将透明度值分配给 basicEffect 属性。
basicEffect.Alpha = 0.5f;
然后,我们必须告诉 GraphicsDevice 使用以下代码在 Draw() 方法中启用透明度。
protected void Draw(){
.....
GraphicsDevice.RenderState.AlphaBlendEnable = true;
GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
DrawWithBasicEffect(model,world,view,projection)
GraphicsDevice.RenderState.AlphaBlendEnable = false;
.....
}
在 XNA 中编程您自己的 HLSL 着色器
[edit | edit source]着色语言
[edit | edit source]着色器是可编程的,为此,已经开发了 C 类高级编程语言的几种变体。
高级着色语言 (HLSL) 由 Microsoft 为 Microsoft Direct3D API 开发。它使用 C 语法,我们将它与 XNA 框架一起使用。
其他着色语言包括自 OpenGL 2.0 起提供的 GLSL(OpenGL 着色语言)和 Cg(C for Graphics),这是由 Nvidia 与 Microsoft 合作开发的另一种高级着色语言,它与 HLSL 非常相似。Cg 受 FX Composer 支持,FX Composer 将在本文稍后部分介绍。
高级着色语言 (HLSL) 及其在 XNA 中的使用
[edit | edit source]XNA 中的着色器是用 HLSL 编写的,并存储在所谓的 effect 文件中,其文件扩展名为 .fx。最好将所有着色器保存在一个单独的文件夹中。因此,在 Visual C# 的解决方案资源管理器中,在内容节点中创建一个名为“Shaders”的新文件夹。要创建新的 Effect fx 文件,只需右键单击新的“Shaders”文件夹,然后选择添加→新建项。在新项目对话框中,选择“Effect 文件”,并为文件指定适当的名称。
新的 effect 文件将已经包含一些应该可以工作的基本着色器代码,但是在这章中,我们将从头开始编写着色器,因此可以删除已生成的代码。
HLSL Effect 文件的结构 (*.fx)
[edit | edit source]如前所述,HLSL 使用 C 语法,可以通过声明变量、结构体和编写函数来进行编程。HLSL 中的着色器通常由四个不同的部分组成。
变量声明
[edit | edit source]包含参数和固定常量的变量声明。这些变量可以从使用着色器的 XNA 应用程序设置。
示例
float4 AmbienceColor = float4(0.5f, 0.5f, 0.5f, 1.0f);
使用此语句,将声明一个新的全局变量并对其进行赋值。HLSL 提供标准的 c 数据类型,如 float、string 和 struct,但也提供其他特定于着色器的用于向量、矩阵、采样器、纹理等的数据类型。官方参考:MSDN
在示例中,我们声明了一个四维向量,它用于定义颜色。颜色由代表 4 个通道(红色、绿色、蓝色、alpha)的 4 个值表示,范围为 0.0 到 1.0。变量可以具有任意名称。
数据结构
[edit | edit source]着色器将用来输入和输出数据的结构体。通常,这两种结构体是:一种用于输入顶点着色器,另一种用于输出顶点着色器。顶点着色器的输出随后将用作像素着色器的输入。通常,像素着色器的输出不需要结构体,因为它已经是最终结果。如果包含几何着色器,则需要其他结构体,但我们只查看由顶点着色器和像素着色器组成的最基本示例。结构体可以具有任意名称。
示例
struct VertexShaderInput
{
float4 Position : POSITION0;
};
此数据结构包含一个名为 Position(或任何其他名称)的四维向量类型变量。
变量名后面的 POSITION0 是一个所谓的语义。输入和输出结构体中的所有变量都必须通过语义进行标识。可以在 HLSL 官方参考中找到列表:MSDN
着色器函数
[edit | edit source]着色器函数的实现及其背后的逻辑。通常,这包括一个用于顶点着色器的函数和一个用于像素着色器的函数。
示例
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return AmbienceColor;
}
函数与 C 中的函数一样:它们可以具有参数和返回值。在本例中,我们有一个名为 PixelShaderFunction(名称可以任意)的函数,它以 VertexShaderOutput 对象作为输入,并返回语义 COLOR0 类型为 float4(代表 4 个颜色通道的四维向量)的值。
技巧
[edit | edit source]技巧类似于着色器的 main() 方法,它告诉显卡何时使用哪个着色器函数。技巧可以有多个通道,它们使用不同的着色器函数,因此屏幕上的最终图像可以由多个通道组成。
示例
technique Ambient
{
pass Pass1
{
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
这个示例技术名为 Ambient,只有一个 pass。在这个 pass 中,顶点和像素着色器函数被分配,并且着色器版本(在本例中为 1.1)被指定。
第一次尝试:一个简单的环境着色器
[edit | edit source]最简单的着色器是所谓的环境着色器,它只是将一个固定颜色分配给物体的每个像素,所以只能看到它的轮廓。让我们尝试实现一个环境着色器作为第一个尝试。
我们从一个空的 .fx 文件开始,它可以有任意文件名。顶点着色器需要三个场景矩阵来根据三维坐标计算屏幕上某个顶点的二维位置。所以我们需要在 fx 文件中定义三个矩阵作为变量
float4x4 WorldMatrix;
float4x4 ViewMatrix;
float4x4 ProjectionMatrix;
float4 AmbienceColor = float4(0.5f, 0.5f, 0.5f, 1.0f);
类型为 float4x4 的变量是一个四维矩阵。另一个变量是一个四维向量,用来确定环境光颜色(在本例中为灰色)。环境色的颜色值为浮点值,表示 RGBA 通道,其中最小值为 0,最大值为 1。
接下来我们需要顶点着色器的输入和输出结构
struct VertexShaderInput
{
float4 Position : POSITION0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
};
因为它是一个非常简单的着色器,所以目前它们唯一包含的数据是虚拟三维空间中顶点的坐标(VertexShaderInput)和屏幕上二维空间中顶点的变换后的坐标(VertexShaderOutput)。POSITION0 是两个位置的语义类型。
现在我们需要添加着色器计算本身。这在两个函数中完成。首先是顶点着色器函数
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, World);
float4 viewPosition = mul(worldPosition, View);
output.Position = mul(viewPosition, Projection);
return output;
}
这是最基本的顶点着色器函数,每个顶点着色器都应该看起来类似。保存在 input 中的位置通过与三个场景矩阵相乘进行变换,然后作为结果返回。输入类型为 VertexShaderInput,输出类型为 VertexShaderOutput。所使用的矩阵乘法函数(mul)是 HLSL 语言的一部分。
现在我们只需要给像素着色器提供顶点着色器计算出来的位置,并用环境色(基于环境强度)对它进行着色。像素着色器在另一个函数中实现,该函数返回最终像素颜色,数据类型为 float4,语义类型为 COLOR0
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return AmbienceColor;
}
所以应该清楚为什么最终结果中物体的每个像素都具有相同的颜色:因为我们在着色器中还没有任何灯光,并且所有三维信息都丢失了。
为了使我们的着色器完整,我们需要一个所谓的技术,它类似于着色器的 main() 方法,也是 XNA 在使用着色器渲染物体时调用的函数
technique Ambient
{
pass Pass1
{
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
一个技术有一个名称(在本例中为 Ambient),可以从 XNA 中直接调用。一个技术也可以有多个 pass,但在这种简单的情况下,我们只需要一个 pass。在一个 pass 中,精确地定义了我们的着色器文件中哪个函数是顶点着色器,哪个函数是像素着色器。我们在这里不使用几何着色器,因为与顶点和像素着色器相比,它只是可选的。此外,还确定了要使用的着色器版本,因为着色器模型在不断发展,并且添加了新功能。可能的版本是:1.0 到 1.3、1.4、2.0、2.0a、2.0b、3.0、4.0。
对于简单的环境光照,我们只需要版本 1.1,但对于反射和其他更高级的效果,需要像素着色器版本 2.0。
完整的着色器代码
float4x4 WorldMatrix;
float4x4 ViewMatrix;
float4x4 ProjectionMatrix;
float4 AmbienceColor = float4(0.5f, 0.5f, 0.5f, 1.0f);
struct VertexShaderInput
{
float4 Position : POSITION0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, WorldMatrix);
float4 viewPosition = mul(worldPosition, ViewMatrix);
output.Position = mul(viewPosition, ProjectionMatrix);
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return AmbienceColor;
}
technique Ambient
{
pass Pass1
{
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
现在着色器文件已完成,可以保存,我们只需要让我们的 XNA 应用程序使用它来渲染物体。
首先,必须定义一个类型为 Effect 的新的全局变量。每个 Effect 对象都用于引用一个位于 fx 文件中的着色器。
Effect myEffect;
在用于从内容文件夹中加载内容(如模型、纹理等)的方法中,也需要加载着色器文件(在本例中,它是文件夹 Shaders 中的 Ambient.fx 文件)
myEffect = Content.Load<Effect>("Shaders/Ambient");
现在 Effect 已准备好使用。要使用我们自己的着色器绘制模型,我们需要实现一个用于该目的的方法
private void DrawModelWithEffect(Model model, Matrix world, Matrix view, Matrix projection)
{
foreach (ModelMesh mesh in model.Meshes)
{
foreach (ModelMeshPart part in mesh.MeshParts)
{
part.Effect = myEffect;
myEffect.Parameters["World"].SetValue(world * mesh.ParentBone.Transform);
myEffect.Parameters["View"].SetValue(view);
myEffect.Parameters["Projection"].SetValue(projection);
}
mesh.Draw();
}
}
该方法将模型和用于描述场景的三个矩阵作为参数。它遍历模型中的网格,然后遍历网格中的网格部分。对于每个部分,它将我们的新 myEffect 对象分配给名为“Effect”的属性。
但在着色器准备好使用之前,我们需要为它提供必需的参数。通过使用 myEffect 对象的 Parameters 集合,我们可以访问之前在着色器文件中定义的变量,并为它们提供一个值。我们通过使用 SetValue() 方法将三个主矩阵分配给着色器中等效的变量。之后,网格就可以使用 ModelMesh 类的 Draw() 方法进行绘制。
因此,新的方法 DrawModelWithEffect() 现在可以用于每个类型为 Model 的模型,以使用我们自定义的着色器在屏幕上绘制它!结果可以在图片中看到。如你所见,模型的每个像素都具有相同的颜色,因为我们还没有使用任何灯光、纹理或效果。
也可以通过使用 Parameters 集合和 SetValue() 方法直接在 XNA 中更改着色器的固定变量。例如,要更改 XNA 应用程序中着色器中的环境色,需要使用以下语句
myEffect.Parameters["AmbienceColor"].SetValue(Color.White.ToVector4());
漫射着色
[edit | edit source]漫射着色以来自光发射器的光渲染物体,并从物体表面向所有方向反射(它扩散)。这就是赋予大多数物体阴影的原因,使其具有明亮的亮部和较暗的暗部,从而产生简单的环境着色器中丢失的三维效果。现在,我们将修改之前环境着色器,使其也支持漫射着色。实现漫射着色的方法有两种,一种方法使用顶点着色器,另一种方法使用像素着色器。我们将看看顶点着色器变体。
我们需要在之前的环境着色器文件中添加三个新变量
float4x4 WorldInverseTransposeMatrix;
float3 DiffuseLightDirection = float3(-1.0f, 0.0f, 0.0f);
float4 DiffuseColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
变量 WorldInverseTransposeMatrix 是另一个矩阵,用于计算。它是世界矩阵逆矩阵的转置。在只有环境光照的情况下,我们不必关心顶点的法线向量,但随着漫射光照的加入,这个矩阵变得必要,它用于变换顶点的法线以进行光照计算。
另外两个变量用于定义漫射光来自的方向(第一个值为 X,第二个值为 Y,第三个值为 Z,位于三维空间中)以及从渲染物体表面反射出来的漫射光的颜色。在本例中,我们简单地使用白色,并且灯光在虚拟空间中沿 x 轴方向发射。
VertexShaderInput 和 VertexShaderOutput 的结构也需要一些小的修改。我们必须将以下变量添加到结构 VertexShaderInput 中,以便在顶点着色器输入中获取当前顶点的法线向量
float4 NormalVector : NORMAL0;
并且我们在结构 VertexShaderOutput 中添加了一个用于颜色的变量,因为我们将在顶点着色器中计算漫射着色,这将导致需要传递给像素着色器的颜色
float4 VertexColor : COLOR0;
要在顶点着色器中进行漫射光照,我们必须在 VertexShaderFunction 中添加一些代码
float4 normal = normalize(mul(input.NormalVector, WorldInverseTransposeMatrix));
float lightIntensity = dot(normal, DiffuseLightDirection);
output.VertexColor = saturate(DiffuseColor * lightIntensity);
使用这段代码,我们变换顶点的法线,使其相对于物体在世界中的位置(第一行新代码)。在第二行中,计算表面法线向量与照射它的灯光之间的夹角。HLSL 语言提供了一个 dot() 函数,用于计算两个向量的点积,这可以用来测量两个向量之间的夹角。在本例中,角度等于表面上顶点的光强。最后,通过将漫射颜色乘以强度来计算当前顶点的颜色。这种颜色存储在 VertexShaderOutput 结构的 VertexColor 属性中,它稍后会传递给像素着色器。
最后,我们必须更改 PixelShaderFunction 返回的值
return saturate(input.VertexColor + AmbienceColor);
它简单地获取我们在顶点着色器中计算出来的颜色,并将环境分量添加到其中。函数 saturate 由 HLSL 提供,以确保颜色在 0 到 1 之间的范围内。
你可能希望使 AmbienceColor 分量稍微暗一些,这样它对最终颜色的影响就不会那么大。这也可以通过定义一个强度变量来调节颜色的强度来实现。但现在我们将保持简单明了,并在以后讨论这个问题。
完整的着色器代码
float4x4 WorldMatrix;
float4x4 ViewMatrix;
float4x4 ProjectionMatrix;
float4 AmbienceColor = float4(0.2f, 0.2f, 0.2f, 1.0f);
// For Diffuse Lightning
float4x4 WorldInverseTransposeMatrix;
float3 DiffuseLightDirection = float3(-1.0f, 0.0f, 0.0f);
float4 DiffuseColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
struct VertexShaderInput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 NormalVector : NORMAL0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 VertexColor : COLOR0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, WorldMatrix);
float4 viewPosition = mul(worldPosition, ViewMatrix);
output.Position = mul(viewPosition, ProjectionMatrix);
// For Diffuse Lightning
float4 normal = normalize(mul(input.NormalVector, WorldInverseTransposeMatrix));
float lightIntensity = dot(normal, DiffuseLightDirection);
output.VertexColor = saturate(DiffuseColor * lightIntensity);
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return saturate(input.VertexColor + AmbienceColor);
}
technique Diffuse
{
pass Pass1
{
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
着色器文件就这些。要在 XNA 中使用新的着色器,我们必须对使用着色器渲染物体的 XNA 应用程序进行一个补充
我们必须在 XNA 中设置着色器的 WorldInverseTransposeMatrix 变量。因此,在 DrawModelWithEffect 方法中,在使用 SetValue() 设置 myEffect 对象的其他参数的部分,我们必须设置 WorldInverseTransposeMatrix。但在设置它之前,需要计算它。为此,我们将反转然后转置我们应用程序的世界矩阵(它首先与物体的变换相乘,所以一切都处于正确的位置)。
Matrix worldInverseTransposeMatrix = Matrix.Transpose(Matrix.Invert(mesh.ParentBone.Transform * world));
myEffect.Parameters["WorldInverseTransposeMatrix"].SetValue(worldInverseTransposeMatrix);
这就是 XNA 代码中需要更改的所有内容。现在你应该有不错的漫射光照。你可以在图片中看到结果。请记住,这个着色器已经使用了漫射和环境光照,这就是为什么模型的暗部只是灰色而不是黑色。
如果我们将像素着色器修改为只返回顶点颜色而不添加环境光,则场景看起来不同(第二张图片)
return saturate(input.VertexColor);
模型中没有光照的暗部现在完全是黑色,因为它们不再添加环境分量。
纹理着色器
[edit | edit source]根据纹理坐标在物体上应用和渲染纹理也是通过着色器完成的。为了使之前的漫射着色器适应纹理,我们必须添加以下变量
texture ModelTexture;
sampler2D TextureSampler = sampler_state {
Texture = (ModelTexture);
MagFilter = Linear;
MinFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
ModelTexture 是 HLSL 数据类型 texture,它存储应该在模型上渲染的纹理。类型为 sampler2D 的另一个变量与纹理相关联。采样器告诉显卡如何从纹理文件中提取一个像素的颜色。采样器包含五个属性
- 纹理:要使用的纹理文件。
- MagFilter + MinFilter: 用于缩放纹理的过滤器。一些过滤器比其他过滤器更快,其他过滤器看起来更好。可能的值包括:Linear、None、Point、Anisotropic。
- AddressU + AddressV: 确定当 U 或 V 坐标不在正常范围内(介于 0 和 1 之间)时该怎么做。可能的值包括:Clamp、BorderColor、Wrap、Mirror。
我们使用 Linear 过滤器,它速度快且 Clamp,如果 U/V 值小于 0,它只使用值 0;如果 U/V 值大于 1,它只使用值 1。
接下来,我们在顶点着色器的输出和输入结构体中添加纹理坐标,以便顶点着色器可以收集这种信息并转发给像素着色器。
添加到结构体 VertexShaderInput 中
float2 TextureCoordinate : TEXCOORD0;
并添加到结构体 VertexShaderOutput 中
float2 TextureCoordinate : TEXCOORD0;
两者都是 float2 类型(二维向量),因为我们只需要存储两个分量:U 和 V。这两个变量也具有语义类型 TEXCOORD0。
将纹理的颜色应用于对象的过程发生在像素着色器中,而不是在顶点着色器中。因此,在 VertexShaderFunction 中,我们只需将纹理坐标从输入中取出并放入输出中
output.TextureCoordinate = input.TextureCoordinate;
在 PixelShaderFunction 中,我们执行以下操作
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float4 VertexTextureColor = tex2D(TextureSampler, input.TextureCoordinate);
VertexTextureColor.a = 1;
return saturate(VertexTextureColor * input.VertexColor + AmbienceColor);
}
该函数现在根据纹理计算像素的颜色。此外,颜色的 alpha 值在第二行单独设置,因为 TextureSampler 没有从纹理中获取 alpha 值。
最后,在 return 语句中,顶点的纹理颜色乘以漫射颜色(将漫射阴影添加到纹理颜色),并像往常一样添加环境颜色。
我们还需要在 technique 函数中进行更改。新的 PixelShaderFunction 现在对像素着色器版本 1.1 来说太复杂了,因此需要将其设置为版本 2.0
PixelShader = compile ps_2_0 PixelShaderFunction();
纹理着色器的完整着色器代码
float4x4 WorldMatrix;
float4x4 ViewMatrix;
float4x4 ProjectionMatrix;
float4 AmbienceColor = float4(0.1f, 0.1f, 0.1f, 1.0f);
// For Diffuse Lightning
float4x4 WorldInverseTransposeMatrix;
float3 DiffuseLightDirection = float3(-1.0f, 0.0f, 0.0f);
float4 DiffuseColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
// For Texture
texture ModelTexture;
sampler2D TextureSampler = sampler_state {
Texture = (ModelTexture);
MagFilter = Linear;
MinFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
struct VertexShaderInput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 NormalVector : NORMAL0;
// For Texture
float2 TextureCoordinate : TEXCOORD0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 VertexColor : COLOR0;
// For Texture
float2 TextureCoordinate : TEXCOORD0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, WorldMatrix);
float4 viewPosition = mul(worldPosition, ViewMatrix);
output.Position = mul(viewPosition, ProjectionMatrix);
// For Diffuse Lightning
float4 normal = normalize(mul(input.NormalVector, WorldInverseTransposeMatrix));
float lightIntensity = dot(normal, DiffuseLightDirection);
output.VertexColor = saturate(DiffuseColor * lightIntensity);
// For Texture
output.TextureCoordinate = input.TextureCoordinate;
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
// For Texture
float4 VertexTextureColor = tex2D(TextureSampler, input.TextureCoordinate);
VertexTextureColor.a = 1;
return saturate(VertexTextureColor * input.VertexColor + AmbienceColor);
}
technique Texture
{
pass Pass1
{
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_2_0 PixelShaderFunction();
}
}
XNA 中的更改
在 XNA 代码中,我们必须通过声明一个 Texture2D 对象来添加一个新的纹理
Texture2D planeTexture;
通过加载内容节点中先前添加的图像来加载纹理(在本例中,加载内容节点解决方案资源管理器中“Images”文件夹中的名为“planetextur.png”的文件)
planeTexture = Content.Load<Texture2D>("Images/planetextur");
最后,在通常的绘制方法中将新纹理分配给着色器变量 ModelTexture
myEffect.Parameters["ModelTexture"].SetValue(planeTexture);
然后,对象应该具有纹理、漫射阴影和环境阴影,如示例图像所示。
带有镜面光和反射的高级阴影
[edit | edit source]现在让我们创建一个新的更复杂的特效,它看起来非常漂亮逼真,可以用来模拟金属等光亮表面。我们将结合纹理着色器、镜面着色器和反射着色器。反射着色器将反射预定义的环境
镜面光在模型表面上添加光亮斑点以模拟光滑度。它们具有照射在表面上的光的颜色。
镜面光与我们之前使用过的着色器的区别在于,它不仅受光线来自的方向的影响,还受观看者观察对象的方向的影响。因此,当相机在场景中移动时,镜面光会在表面上移动。
反射着色器也是如此,根据观看者的位置,物体表面的反射会发生变化。
像真实世界一样计算反射意味着计算光线从表面反弹的单个光线(一种称为光线追踪的技术)。这需要大量的计算能力,这就是我们在 XNA 等实时计算机图形中使用更简单方法的原因。我们使用的技术称为环境映射,并将环境图像映射到物体表面。当观看者的位置发生变化时,这种环境映射会移动,从而产生反射的错觉。这有一些局限性,例如,物体只反射预定义的环境图像,而不是真实的场景。因此,玩家和其他所有移动模型都不会被反射。这有一些局限性,但在实时应用程序中并不太明显。
环境贴图可以与场景的背景天空贴图相同。有关背景天空贴图的更多信息,请参阅另一篇文章:Game Creation with XNA/3D Development/Skybox。如果环境贴图与背景天空贴图相同,它将适合场景并看起来准确,但是您可以使用任何看起来适合场景中模型的环境映射。
以下更改的基础是之前开发的纹理着色器。对于镜面光,需要添加以下变量
float ShininessFactor = 10.0f;
float4 SpecularColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
float3 ViewVector = float3(1.0f, 0.0f, 0.0f);
ShininessFactor 定义了表面的光亮度。低值代表具有宽阔表面高光的表面,应该用于不太光亮的表面。高值代表更光亮的表面,如具有小而非常强烈的表面高光的金属。理论上,镜子将具有无限的值。
SpecularColor 指定镜面光的颜色。在本例中,我们使用白光。
ViewVector 是一个变量,它将在运行时从 XNA 应用程序计算和设置。它告诉着色器观看者正在看哪个方向。
对于反射着色器,我们需要添加环境纹理和采样器作为变量
Texture EnvironmentTexture;
samplerCUBE EnvironmentSampler = sampler_state
{
texture = <EnvironmentTexture>;
magfilter = LINEAR;
minfilter = LINEAR;
mipfilter = LINEAR;
AddressU = Mirror;
AddressV = Mirror;
};
EnvironmentTexture 是环境图像,它将作为反射映射到我们的物体上。这次使用的是立方体采样器,它与之前使用的二维采样器略有不同。它假定提供的纹理是为在立方体上渲染而创建的。
VertexShaderInput 结构体不需要进行任何更改,但需要在 VertexShaderOutput 结构体中添加两个新变量
float3 NormalVector : TEXCOORD1;
float3 ReflectionVector : TEXCOORD2;
NormalVector 只是单个顶点的法线向量,它直接来自输入。反射向量是在顶点着色器中计算的,并在像素着色器中使用,以将环境贴图的正确部分分配到表面。两者都具有语义类型 TEXCOORD。已经存在一个类型为 TEXCOORD0(TextureCoordinate)的变量,因此我们继续计数到 1 和 2。
在 VertexShaderFunction 中,我们必须添加以下命令
// For Specular Lighting
output.NormalVector = normal;
// For Reflection
float4 VertexPosition = mul(input.Position, WorldMatrix);
float3 ViewDirection = ViewVector - VertexPosition;
output.ReflectionVector = reflect(-normalize(ViewDirection), normalize(normal));
首先,将先前计算的当前顶点的法线向量写入输出,因为它将在后面的像素着色器中用于镜面阴影。
对于反射,世界中的顶点位置以及观看者看向顶点的方向将被计算出来。然后使用 HLSL 函数 reflect() 计算反射向量,该函数使用先前计算的法线向量和 ViewDirection 向量的归一化值。
在 PixelShaderFunction 中,我们为镜面值添加以下计算
float3 light = normalize(DiffuseLightDirection);
float3 normal = normalize(input.NormalVector);
float3 r = normalize(2 * dot(light, normal) * normal - light);
float3 v = normalize(mul(normalize(ViewVector), WorldMatrix));
float dotProduct = dot(r, v);
float4 specular = SpecularColor * max(pow(dotProduct, ShininessFactor), 0) * length(input.VertexColor);
因此,要计算镜面高光,需要漫射光方向、法线、视角向量和亮度。最终结果是包含镜面分量的另一个向量。
这个镜面分量与反射一起添加到 PixelShaderFunction 末尾的 return 语句中
return saturate(VertexTextureColor * texCUBE(EnvironmentSampler, normalize(input.ReflectionVector)) + specular * 2);
在本例中,我们去掉了漫射和环境分量,因为在本例中,它们对于演示来说没有必要,而且没有它们看起来更好。没有漫射光分量,看起来光线来自四面八方,并在光亮的金属上反射。
因此,在 return 语句中,使用纹理颜色以及反射和镜面高光(乘以 2 以使其更强烈)。
完成的着色器代码
float4x4 WorldMatrix;
float4x4 ViewMatrix;
float4x4 ProjectionMatrix;
float4 AmbienceColor = float4(0.1f, 0.1f, 0.1f, 1.0f);
// For Diffuse Lightning
float4x4 WorldInverseTransposeMatrix;
float3 DiffuseLightDirection = float3(-1.0f, 0.0f, 0.0f);
float4 DiffuseColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
// For Texture
texture ModelTexture;
sampler2D TextureSampler = sampler_state {
Texture = (ModelTexture);
MagFilter = Linear;
MinFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
// For Specular Lighting
float ShininessFactor = 10.0f;
float4 SpecularColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
float3 ViewVector = float3(1.0f, 0.0f, 0.0f);
// For Reflection Lighting
Texture EnvironmentTexture;
samplerCUBE EnvironmentSampler = sampler_state
{
texture = <EnvironmentTexture>;
magfilter = LINEAR;
minfilter = LINEAR;
mipfilter = LINEAR;
AddressU = Mirror;
AddressV = Mirror;
};
struct VertexShaderInput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 NormalVector : NORMAL0;
// For Texture
float2 TextureCoordinate : TEXCOORD0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 VertexColor : COLOR0;
// For Texture
float2 TextureCoordinate : TEXCOORD0;
// For Specular Shading
float3 NormalVector : TEXCOORD1;
// For Reflection
float3 ReflectionVector : TEXCOORD2;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, WorldMatrix);
float4 viewPosition = mul(worldPosition, ViewMatrix);
output.Position = mul(viewPosition, ProjectionMatrix);
// For Diffuse Lighting
float4 normal = normalize(mul(input.NormalVector, WorldInverseTransposeMatrix));
float lightIntensity = dot(normal, DiffuseLightDirection);
output.VertexColor = saturate(DiffuseColor * lightIntensity);
// For Texture
output.TextureCoordinate = input.TextureCoordinate;
// For Specular Lighting
output.NormalVector = normal;
// For Reflection
float4 VertexPosition = mul(input.Position, WorldMatrix);
float3 ViewDirection = ViewVector - VertexPosition;
output.ReflectionVector = reflect(-normalize(ViewDirection), normalize(normal));
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
// For Texture
float4 VertexTextureColor = tex2D(TextureSampler, input.TextureCoordinate);
VertexTextureColor.a = 1;
// For Specular Lighting
float3 light = normalize(DiffuseLightDirection);
float3 normal = normalize(input.NormalVector);
float3 r = normalize(2 * dot(light, normal) * normal - light);
float3 v = normalize(mul(normalize(ViewVector), WorldMatrix));
float dotProduct = dot(r, v);
float4 specular = SpecularColor * max(pow(dotProduct, ShininessFactor), 0) * length(input.VertexColor);
return saturate(VertexTextureColor * texCUBE(EnvironmentSampler, normalize(input.ReflectionVector)) + specular * 2);
}
technique Reflection
{
pass Pass1
{
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_2_0 PixelShaderFunction();
}
}
要在 XNA 中使用新的着色器,我们需要在绘制方法中从 XNA 设置 2 个额外的着色器变量
myEffect.Parameters["ViewVector"].SetValue(viewDirectionVector);
myEffect.Parameters["EnvironmentTexture"].SetValue(environmentTexture);
但首先,应该先声明和加载对象的 environmentTexture(与往常一样)
TextureCube environmentTexture;
environmentTexture = Content.Load<TextureCube>("Images/Skybox");
与模型纹理不同,这种纹理不是 Texture2D 类型,而是 TextureCube 类型,因为在本例中,我们使用背景天空贴图作为环境贴图。背景天空贴图不仅包含一张图像(就像普通纹理一样),而是六张不同的图像,这些图像映射到立方体的每个侧面。图像必须以正确的角度配合在一起,并且必须无缝连接。您可以在此处找到一些背景天空贴图:RB Whitaker 背景天空贴图
其次,用于在反射着色器中设置 ViewVector 变量的 viewDirectionVector 应该在类中声明为字段
Vector3 viewDirectionVector = new Vector3(0, 0, 0);
它可以用这种方式计算
viewDirectionVector = cameraPositionVector – cameraTargetVector;
其中 cameraPositionVector 是一个包含相机当前位置的 3D 向量,cameraTargetVector 是另一个包含相机目标坐标的向量。例如,如果相机只是看着虚拟空间中的 0,0,0 点,计算将更短
viewDirectionVector = cameraPositionVector;
//or
viewDirectionVector = new Vector3(eyePositionX, eyePositionY, eyePositionZ);
在 XNA 游戏中进行所有这些更改后,反射应该像图片中那样。但外观在很大程度上取决于使用的环境贴图。
其他参数
[edit | edit source]另一个好主意是为着色器的强度引入参数。例如,而不是在上面的漫射着色器中像素着色器函数的 return 语句中简单地返回环境颜色
return saturate(input.VertexColor + AmbienceColor);
可以返回
return saturate(input.VertexColor + AmbienceColor * AmbienceIntensity);
其中 AmbienceIntensity 是介于 0.0 和 1.0 之间的浮点数。这样就可以轻松地调整颜色的强度。这可以对我们到目前为止计算的每个组件(环境、漫射、纹理颜色、镜面强度、反射分量)进行。
到目前为止,我们一直在使用 3D 着色器,但 2D 着色器也是可能的。2D 图像可以通过 Photoshop 等图片编辑软件进行修改和处理,以调整其对比度、颜色并应用滤镜。同样的效果可以通过 2D 着色器来实现,这些着色器应用于整个输出图像,该图像是渲染场景的结果。
可实现的效果类型的示例
- 简单的颜色修改,例如使场景变为黑白,反转颜色通道,使场景呈现棕褐色等。
- 调整颜色,在场景中营造温暖或冷色调。
- 使用模糊滤镜模糊屏幕,以创建特殊效果。
- Bloom 效应:一种流行的效应,它在图像中非常明亮的物体周围产生光晕,模拟摄影中已知的效应。
因此,我们首先在 Visual Studio 中创建一个新的着色器文件(将其命名为 Postprocessing .fx)并插入以下代码以进行后处理
texture ScreenTexture;
sampler TextureSampler = sampler_state
{
Texture = <ScreenTexture>;
};
float4 PixelShaderFunction(float2 TextureCoordinate : TEXCOORD0) : COLOR0
{
float4 pixelColor = tex2D(TextureSampler, TextureCoordinate);
pixelColor.g = 0;
pixelColor.b = 0;
return pixelColor;
}
technique Grayscale
{
pass Pass1
{
PixelShader = compile ps_2_0 PixelShaderFunction();
}
}
如您所见,对于后处理,我们只需要一个像素着色器。后处理是通过将场景的渲染图像作为纹理提供来处理的,然后像素着色器将该纹理用作输入信息,进行处理并返回。
该函数只有一个输入参数(纹理坐标)并返回语义类型 COLOR0 的颜色向量。在这个例子中,我们只是读取当前纹理坐标(也就是屏幕坐标)处的像素颜色,并将绿色和蓝色通道设置为 0,这样就只剩下红色通道了。然后我们返回颜色值。
现在,在 XNA 中使用这个 2D 着色器有点棘手。首先,我们需要在 Game 类中创建以下对象
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
RenderTarget2D renderTarget;
Effect postProcessingEffect;
GraphicsDeviceManager 和 SpriteBatch 对象很可能已经在一个现有项目中创建了。但是,RenderTarget2D 和 Effect 对象必须声明。
检查 GraphicsDeviceManager 对象是否在构造函数中初始化
graphics = new GraphicsDeviceManager(this);
并且 SpriteBatch 对象在 LoadContent() 方法中初始化。我们刚刚创建的新着色器文件也应该在此方法中加载
spriteBatch = new SpriteBatch(GraphicsDevice);
postProcessingEffect = Content.Load<Effect>("Shaders/Postprocessing");
最后,确保 RenderTarget2D 对象在 Initialize() 方法中初始化
renderTarget = new RenderTarget2D(
GraphicsDevice,
GraphicsDevice.PresentationParameters.BackBufferWidth,
GraphicsDevice.PresentationParameters.BackBufferHeight,
1,
GraphicsDevice.PresentationParameters.BackBufferFormat
);
现在,我们需要一个方法将当前场景绘制到纹理(以渲染目标的形式)而不是屏幕上
protected Texture2D DrawSceneToTexture(RenderTarget2D currentRenderTarget) {
// Set the render target
GraphicsDevice.SetRenderTarget(0, currentRenderTarget);
// Draw the scene
GraphicsDevice.Clear(Color.Black);
drawModelWithTexture(model, world, view, projection);
// Drop the render target
GraphicsDevice.SetRenderTarget(0, null);
// Return the texture in the render target
return currentRenderTarget.GetTexture();
}
在这个方法中,我们使用的是使用 3D 着色器(在本例中为:drawModelWithTexture())的绘制函数。所以我们仍然使用所有 3D 着色器来先渲染场景,但是我们不是直接显示这个结果,而是将它渲染到一个纹理上,并在 Draw() 方法中对其进行一些后处理。之后,处理过的纹理将显示在屏幕上。所以用这个扩展 Draw() 方法
protected override void Draw(GameTime gameTime)
{
Texture2D texture = DrawSceneToTexture(renderTarget);
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
postProcessingEffect.Begin();
postProcessingEffect.CurrentTechnique.Passes[0].Begin();
spriteBatch.Draw(texture, new Rectangle(0, 0, 1024, 768), Color.White);
postProcessingEffect.CurrentTechnique.Passes[0].End();
postProcessingEffect.End();
spriteBatch.End();
base.Draw(gameTime);
}
首先,正常的场景被渲染到一个名为 texture 的纹理中。然后,一个精灵批处理程序与包含我们新的后处理着色器的 postProcessing 效应一起启动。然后,纹理在精灵批处理程序上渲染,应用了 postProcessing 效应。
效果应该像图片中那样。
另一个可以通过后处理着色器实现的简单效果是将彩色图像转换为灰度图像,然后将其缩减为 4 种颜色,从而产生卡通效果。为此,我们着色器文件中的 PixelShaderFunction 应该如下所示
float4 PixelShaderFunction(float2 TextureCoordinate : TEXCOORD0) : COLOR0
{
float4 pixelColor = tex2D(TextureSampler, TextureCoordinate);
float average = (pixelColor.r + pixelColor.g + pixelColor.b) / 3;
if (average > 0.95){
average = 1.0;
} else if (average > 0.5){
average = 0.7;
} else if (average > 0.2){
average = 0.35;
} else{
average = 0.1;
}
pixelColor.r = average;
pixelColor.g = average;
pixelColor.b = average;
return pixelColor;
}
灰度图像通过计算红色、绿色和蓝色通道的平均值并使用此值作为所有三个通道的值来生成。之后,平均值还被缩减为 4 个不同的值之一。最后,输出的红色、绿色和蓝色通道被设置为缩减后的值。图像为灰度,因为红色、绿色和蓝色通道的值相同。
创建透明度着色器很简单。我们可以从上面的漫射着色器示例开始。首先,我们需要一个名为 alpha 的变量来确定透明度。该值应在 1(不透明)到 0(完全透明)之间。为了实现透明度着色器,我们只需要对 PixelShaderFunction 进行一些修改。在完成所有光照计算后,我们必须将 alpha 值分配给结果颜色属性。
float alpha = 0.5f;
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float4 color = saturate(input.VertexColor + AmbienceColor);
color.a = alpha;
return color;
}
要启用 alpha 混合,我们必须在技术中添加一些代码
technique Tranparency {
pass p0 {
AlphaBlendEnable = TRUE;
DestBlend = INVSRCALPHA;
SrcBlend = SRCALPHA;
VertexShader = compile vs_2_0 std_VS();
PixelShader = compile ps_2_0 std_PS();
}
}
完整的透明度着色器
float4x4 WorldMatrix;
float4x4 ViewMatrix;
float4x4 ProjectionMatrix;
float4 AmbienceColor = float4(0.2f, 0.2f, 0.2f, 1.0f);
// For Diffuse Lightning
float4x4 WorldInverseTransposeMatrix;
float3 DiffuseLightDirection = float3(-1.0f, 0.0f, 0.0f);
float4 DiffuseColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
struct VertexShaderInput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 NormalVector : NORMAL0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 VertexColor : COLOR0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, WorldMatrix);
float4 viewPosition = mul(worldPosition, ViewMatrix);
output.Position = mul(viewPosition, ProjectionMatrix);
// For Diffuse Lightning
float4 normal = normalize(mul(input.NormalVector, WorldInverseTransposeMatrix));
float lightIntensity = dot(normal, DiffuseLightDirection);
output.VertexColor = saturate(DiffuseColor * lightIntensity);
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float4 color = saturate(input.VertexColor + AmbienceColor);
color.a = alpha;
return color;
}
technique Diffuse
{
pass Pass1
{
AlphaBlendEnable = TRUE;
DestBlend = INVSRCALPHA;
SrcBlend = SRCALPHA;
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
一些其他流行的着色器及其简短描述。
-
应用于球体的均匀表面的凹凸贴图
-
通过使用法线贴图将细节添加到低多边形模型
-
卡通着色器
凹凸贴图用于模拟原本均匀的多边形表面的凹凸,使表面看起来更真实,并除了纹理之外还赋予其一些结构。凹凸贴图是通过加载包含凹凸信息的另一张纹理并使用此信息扰乱表面法线来实现的。表面的原始法线会根据凹凸贴图中的偏移值而改变。凹凸贴图是灰度图像。
凹凸贴图现在已被法线贴图取代。法线贴图也用于在原本均匀的多边形表面上创建凹凸和结构。但与凹凸贴图相比,法线贴图可以更好地处理法线的急剧变化。
法线贴图与凹凸贴图的思路类似:加载另一张纹理并使用它来改变法线。但法线贴图不只是用偏移值来改变法线,而是使用多通道(RGB)贴图来完全替换现有的法线。法线贴图中每个像素的 R、G 和 B 值对应于顶点法向量的 X、Y、Z 坐标。
法线贴图的进一步发展称为视差贴图。
卡通着色器用于以卡通风格渲染 3D 场景,使其看起来像是手绘的。卡通着色可以通过使用多通道着色器在 XNA 中实现,该着色器在多个通道中构建结果图像。
要创建卡通着色器,我们可以从漫射着色器开始。卡通着色器背后的基本思想是,光强将被划分为多个级别。在这个例子中,我们将强度创建为 5 个级别。为了表示亮度级别,我们需要一个名为 toonthresholds 的数组变量,并且为了确定级别之间的边界,我们使用数组 toonBrightnessLevels。
float ToonThresholds[4] = {0.95,0.5, 0.2, 0.03 };
float ToonBrightnessLevels[5] = { 1.0, 0.8, 0.6, 0.35, 0.01 };
现在,我们在 PixelShader 中实现光强度的分类,并将它们分配到相应的颜色中。
float4 std_PS(VertexShaderOutput input) : COLOR0 {
float lightIntensity = dot(normalize(DiffuseLightDirection),
input.normal);
if(lightIntensity < 0)
lightIntensity = 0;
float4 color = tex2D(colorSampler, input.uv) *
DiffuseLightColor * DiffuseIntensity;
color.a = 1;
if (lightIntensity > ToonThresholds[0])
color *= ToonBrightnessLevels[0];
else if ( lightIntensity > ToonThresholds[1])
color *= ToonBrightnessLevels[1];
else if ( lightIntensity > ToonThresholds[2])
color *= ToonBrightnessLevels[2];
else if ( lightIntensity > ToonThresholds[3])
color *= ToonBrightnessLevels[3];
else
color *= ToonBrightnessLevels[4];
return color;
}
完整的卡通着色器
float4x4 World : World < string UIWidget="None"; >;
float4x4 View : View < string UIWidget="None"; >;
float4x4 Projection : Projection < string UIWidget="None"; >;
texture colorTexture : DIFFUSE <
string UIName = "Diffuse Texture";
string ResourceType = "2D";
>;
float3 DiffuseLightDirection = float3(1, 0, 0);
float4 DiffuseLightColor = float4(1, 1, 1, 1);
float DiffuseIntensity = 1.0;
float ToonThresholds[4] = {0.95,0.5, 0.2, 0.03 };
float ToonBrightnessLevels[5] = { 1.0, 0.8, 0.6, 0.35, 0.01 };
sampler2D colorSampler = sampler_state {
Texture = <colorTexture>;
FILTER = MIN_MAG_MIP_LINEAR;
AddressU = Wrap;
AddressV = Wrap;
};
struct VertexShaderInput {
float4 position : POSITION0;
float3 normal :NORMAL0;
float2 uv : TEXCOORD0;
};
struct VertexShaderOutput {
float4 position : POSITION0;
float3 normal : TEXCOORD1;
float2 uv : TEXCOORD0;
};
VertexShaderOutput std_VS(VertexShaderInput input) {
VertexShaderOutput output;
float4 worldPosition = mul(input.position, World);
float4 viewPosition = mul(worldPosition, View);
output.position = mul(viewPosition, Projection);
output.normal = normalize(mul(input.normal, World));
output.uv = input.uv;
return output;
}
float4 std_PS(VertexShaderOutput input) : COLOR0 {
float lightIntensity = dot(normalize(DiffuseLightDirection),
input.normal);
if(lightIntensity < 0)
lightIntensity = 0;
float4 color = tex2D(colorSampler, input.uv) *
DiffuseLightColor * DiffuseIntensity;
color.a = 1;
if (lightIntensity > ToonThresholds[0])
color *= ToonBrightnessLevels[0];
else if ( lightIntensity > ToonThresholds[1])
color *= ToonBrightnessLevels[1];
else if ( lightIntensity > ToonThresholds[2])
color *= ToonBrightnessLevels[2];
else if ( lightIntensity > ToonThresholds[3])
color *= ToonBrightnessLevels[3];
else
color *= ToonBrightnessLevels[4];
return color;
}
technique Toon {
pass p0 {
VertexShader = compile vs_2_0 std_VS();
PixelShader = compile ps_2_0 std_PS();
}
}
FX Composer 是一个用于着色器创作的集成开发环境。使用 FX Composer 创建我们自己的着色器非常有用。使用 Fx Composer,我们可以很快看到结果,并且对着色器进行一些实验非常有效。
在这个例子中,我使用的是 FX Composer 2.5 版。将 FX Composer 库用于你自己的 XNA 中是一项非常简单的任务。让我们以一个例子开始。打开 FX Composer 并创建一个新项目。在材质上右键单击,选择“从文件添加材质”,然后选择 metal.fx。
您只需要将 metal.fx 中的所有代码复制并创建一个新的效果在您的 XNA 项目中,并将所有内容替换为 metal fx 中的代码。您也可以将 metal.fx 文件复制到您的 XNA 项目中。
从这里开始,我们只需要根据 metal.fx 中的变量对 XNA 类进行一些修改。
在 metal.fx 中,您可以看到以下代码
// transform object vertices to world-space:
float4x4 gWorldXf : World < string UIWidget="None"; >;
// transform object normals, tangents, & binormals to world-space:
float4x4 gWorldITXf : WorldInverseTranspose < string UIWidget="None"; >;
// transform object vertices to view space and project them in perspective:
float4x4 gWvpXf : WorldViewProjection < string UIWidget="None"; >;
// provide transform from "view" or "eye" coords back to world-space:
float4x4 gViewIXf : ViewInverse < string UIWidget="None"; >;
在我们的 XNA 类中,我们必须更改 ParameterEffect 的名称。
Matrix InverseWorldMatrix = Matrix.Invert(world);
Matrix ViewInverse = Matrix.Invert(view);
effect.Parameters["gWorldXf"].SetValue(world);
effect.Parameters["gWorldITXf"].SetValue(InverseWorldMatrix);
effect.Parameters["gWvpXf"].SetValue(world*view*proj);
effect.Parameters["gViewIXf"].SetValue(ViewInverse);
我们还必须更改 XNA 类中的技术名称。因为 XNA 使用 directX9,所以我们选择“技术简单”。
effect.CurrentTechnique = effect.Techniques["Simple"];
现在,您可以使用金属效果运行代码。
完整的函数
private void DrawWithMetalEffect(Model model, Matrix world, Matrix view, Matrix proj){
Matrix InverseWorldMatrix = Matrix.Invert(world);
Matrix ViewInverse = Matrix.Invert(view);
effect.CurrentTechnique = effect.Techniques["Simple"];
effect.Parameters["gWorldXf"].SetValue(world);
effect.Parameters["gWorldITXf"].SetValue(InverseWorldMatrix);
effect.Parameters["gWvpXf"].SetValue(world*view*proj);
effect.Parameters["gViewIXf"].SetValue(ViewInverse);
foreach (ModelMesh meshes in model.Meshes)
{
foreach (ModelMeshPart parts in meshes.MeshParts)
parts.Effect = basicEffect;
meshes.Draw();
}
}
为了在 XNA 中创建粒子效果,我们使用点精灵。点精灵是一个可调整大小的纹理顶点,它始终面向相机。我们使用点精灵渲染粒子的原因有很多。
- 点精灵只使用一个顶点。对于数千个粒子,它可以减少大量的顶点。
- 无需存储或设置映射 UV 坐标。它会自动完成。
- 点精灵始终面向相机。因此,我们无需担心角度和视图。
创建点精灵着色器非常简单,我们只需要在像素着色器中进行一些实现来定义纹理坐标。
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float2 uv;
uv = input.uv.xy;
return tex2D(Sampler, uv);
}
在顶点着色器中,我们只需要为顶点返回 POSITION0。
float4 VertexShader(float4 pos : POSITION0) : POSITION0
{
return mul(pos, WVPMatrix);
}
为了启用点精灵并设置点精灵的属性,我们在技术中执行此操作。
technique Technique1
{
pass Pass1
{
sampler[0] = (Sampler);
PointSpriteEnable = true;
PointSize = 16.0f;
AlphaBlendEnable = true;
SrcBlend = SrcAlpha;
DestBlend = One;
ZWriteEnable = false;
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
完整的点精灵着色器
float4x4 World;
float4x4 View;
float4x4 Projection;
float4x4 WVPMatrix;
texture spriteTexture;
sampler Sampler = sampler_state
{
Texture = <spriteTexture>;
magfilter = LINEAR;
minfilter = LINEAR;
mipfilter = LINEAR;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
float2 uv :TEXCOORD0;
};
float4 VertexShaderFunction(float4 pos : POSITION0) : POSITION0
{
return mul(pos, WVPMatrix);
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float2 uv;
uv = input.uv.xy;
return tex2D(Sampler, uv);
}
technique Technique1
{
pass Pass1
{
sampler[0] = (Sampler);
PointSpriteEnable = true;
PointSize = 32.0f;
AlphaBlendEnable = true;
SrcBlend = SrcAlpha;
DestBlend = One;
ZWriteEnable = false;
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
现在让我们转到 game1.cs 文件。首先,我们需要声明并加载效果和纹理。为了存储位置顶点,我们使用 VertexPositionColor 元素数组。顶点的位置应使用随机数初始化。
Effect pointSpriteEffect;
VertexPositionColor[] positionColor;
VertexDeclaration vertexType;
Texture2D textureSprite;
Random rand;
const int NUM = 50;
....
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
textureSprite = Content.Load<Texture2D>
("Images//texture_particle");
pointSpriteEffect = Content.Load<Effect>
("Effect//PointSprite");
pointSpriteEffect.Parameters
["spriteTexture"].SetValue(textureSprite);
positionColor = new VertexPositionColor[NUM];
vertexType = new VertexDeclaration(graphics.GraphicsDevice,
VertexPositionColor.VertexElements);
rand = new Random();
for (int i = 0; i < NUM; i++) {
positionColor[i].Position =
new Vector3(rand.Next(400) / 10f,
rand.Next(400) / 10f, rand.Next(400) / 10f);
positionColor[i].Color = Color.BlueViolet;
}
}
下一步,我们创建 DrawPointsprite 方法来绘制粒子。
public void DrawPointsprite() {
Matrix world = Matrix.Identity;
pointSpriteEffect.Parameters
["WVPMatrix"].SetValue(world*view*projection);
graphics.GraphicsDevice.VertexDeclaration = vertexType;
pointSpriteEffect.Begin();
foreach (EffectPass pass in
pointSpriteEffect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives
<VertexPositionColor>(
PrimitiveType.PointList,
positionColor,
0,
positionColor.Length);
pass.End();
}
pointSpriteEffect.End();
}
我们在 Draw() 方法中调用 DrawPointSprite() 方法。
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
DrawPointsprite();
base.Draw(gameTime);
}
为了使位置动态化,我们在 Update() 方法中进行了一些实现。
protected override void Update(GameTime gameTime)
{
positionColor[rand.Next(0, NUM)].Position =
new Vector3(rand.Next(400) / 10f,
rand.Next(400) / 10f, rand.Next(400) / 10f);
positionColor[rand.Next(0, NUM)].Color = Color.White;
base.Update(gameTime);
}
这是一个非常简单的点精灵着色器。您可以使用动态大小和颜色创建更复杂的点精灵。
完整的 game1.cs
namespace MyPointSprite
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Matrix view, projection;
Effect pointSpriteEffect;
VertexPositionColor[] positionColor;
VertexDeclaration vertexType;
Texture2D textureSprite;
Random rand;
const int NUM = 50;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{
view =Matrix.CreateLookAt
(Vector3.One * 40, Vector3.Zero, Vector3.Up);
projection =
Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,
4.0f / 3.0f, 1.0f, 10000f);
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
textureSprite =
Content.Load<Texture2D>("Images//texture_particle");
pointSpriteEffect =
Content.Load<Effect>("Effect//PointSprite");
pointSpriteEffect.Parameters
["spriteTexture"].SetValue(textureSprite);
positionColor = new VertexPositionColor[NUM];
vertexType = new VertexDeclaration
(graphics.GraphicsDevice, VertexPositionColor.VertexElements);
rand = new Random();
for (int i = 0; i < NUM; i++) {
positionColor[i].Position =
new Vector3(rand.Next(400) / 10f,
rand.Next(400) / 10f, rand.Next(400) / 10f);
positionColor[i].Color = Color.BlueViolet;
}
}
protected override void Update(GameTime gameTime)
{
positionColor[rand.Next(0, NUM)].Position =
new Vector3(rand.Next(400) / 10f,
rand.Next(400) / 10f, rand.Next(400) / 10f);
positionColor[rand.Next(0, NUM)].Color = Color.Chocolate;
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
DrawPointsprite();
base.Draw(gameTime);
}
public void DrawPointsprite() {
Matrix world = Matrix.Identity;
pointSpriteEffect.Parameters
["WVPMatrix"].SetValue(world*view*projection);
graphics.GraphicsDevice.VertexDeclaration = vertexType;
pointSpriteEffect.Begin();
foreach (EffectPass pass in
pointSpriteEffect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives
<VertexPositionColor>(
PrimitiveType.PointList,
positionColor,
0,
positionColor.Length);
pass.End();
}
pointSpriteEffect.End();
}
}
}
HLSL 简介和一些更高级的示例 最后访问时间:2011 年 6 月 9 日
另一个 HLSL 简介 最后访问时间:2011 年 6 月 9 日
关于如何在 XNA 中使用着色器的非常出色且详细的教程 最后访问时间:2012 年 1 月 15 日
微软官方 HLSL 参考 最后访问时间:2011 年 6 月 9 日
- Leonhard Palm:基础、GPU 管道、像素和顶点着色器、HLSL、XNA 示例
- DR 212:BasicEffect 类、透明度着色器、卡通着色器、FX Composer、粒子效果